4264 changed files with 1833330 additions and 0 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
dit/cmd/ceh-cs-portal/__debug_bin |
||||
web/**/node_modules |
||||
dump.rdb |
||||
__debug_bin |
||||
data/ |
||||
testdata.sqlite |
||||
*.sqlite |
||||
dit/cmd/coupon-service/coupon/debug.test |
||||
*.a |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
cd ..\src\loreal.com\dit\cmd\coupon-service |
||||
make windows |
||||
cd ..\..\..\..\..\bin |
||||
|
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash |
||||
cd ../src/loreal.com/dit/cmd/coupon-service |
||||
make linux |
||||
cd ../../../../../bin |
||||
|
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
cd ..\src\loreal.com\dit\cmd\coupon-service |
||||
make test |
||||
cd ..\..\..\..\..\bin |
||||
|
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash |
||||
cd ../src/loreal.com/dit/cmd/coupon-service |
||||
make test |
||||
cd ../../../../../bin |
||||
|
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
dit/cmd/ceh-cs-portal/__debug_bin |
||||
web/**/node_modules |
||||
dump.rdb |
||||
__debug_bin |
||||
data/ |
||||
testdata.sqlite |
||||
*.sqlite |
||||
dit/cmd/coupon-service/coupon/debug.test |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
{ |
||||
// Use IntelliSense to learn about possible attributes. |
||||
// Hover to view descriptions of existing attributes. |
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
||||
"version": "0.2.0", |
||||
"configurations": [ |
||||
|
||||
{ |
||||
"name": "Launch ces", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "debug", |
||||
"program": "C:\\Users\\larry.yu2\\go\\src\\loreal.com\\dit\\cmd\\ceh-cs-portal\\main.go" |
||||
}, |
||||
{ |
||||
"name": "Debug CCS", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "debug", |
||||
"program": "${workspaceFolder}\\dit\\cmd\\coupon-service\\main.go" |
||||
} |
||||
|
||||
] |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
# editorconfig.org |
||||
root = true |
||||
|
||||
[*] |
||||
indent_style = space |
||||
indent_size = 4 |
||||
end_of_line = lf |
||||
charset = utf-8 |
||||
trim_trailing_whitespace = true |
||||
insert_final_newline = true |
||||
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
################################################ |
||||
############### .gitignore ################## |
||||
################################################ |
||||
# Common files generated by Node, NPM, and the |
||||
# related ecosystem. |
||||
################################################ |
||||
*.seed |
||||
*.log |
||||
*.out |
||||
*.pid |
||||
|
||||
|
||||
|
||||
|
||||
################################################ |
||||
# Miscellaneous |
||||
# |
||||
# Common files generated by text editors, |
||||
# operating systems, file systems, etc. |
||||
################################################ |
||||
|
||||
*~ |
||||
*# |
||||
.idea |
||||
*.db |
||||
*.exe |
||||
config/*.json |
||||
.vscode/* |
||||
.vscode\\* |
||||
.vscode\\launch.json |
||||
.vscode/launch.json |
||||
.DS_Store |
||||
out/ |
||||
debug |
||||
.debug/ |
||||
src/ |
||||
.DS_Store |
||||
*.code-workspace |
||||
**/config/*.json |
||||
@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"loreal.com/dit/module" |
||||
"loreal.com/dit/module/modules/root" |
||||
"loreal.com/dit/endpoint" |
||||
"loreal.com/dit/middlewares" |
||||
"loreal.com/dit/utils" |
||||
"loreal.com/dit/utils/task" |
||||
"log" |
||||
"net/http" |
||||
"strings" |
||||
"sync" |
||||
|
||||
"github.com/robfig/cron" |
||||
) |
||||
|
||||
//App - data struct for App & configuration file
|
||||
type App struct { |
||||
Name string |
||||
Description string |
||||
Config *Config |
||||
Root *root.Module |
||||
Endpoints map[string]EndpointEntry |
||||
MessageHandlers map[string]func(*module.Message) bool |
||||
AuthProvider middlewares.RoleVerifier |
||||
Scheduler *cron.Cron |
||||
TaskManager *task.Manager |
||||
wg *sync.WaitGroup |
||||
mutex *sync.RWMutex |
||||
Runtime map[string]*RuntimeEnv |
||||
} |
||||
|
||||
//RuntimeEnv - runtime env
|
||||
type RuntimeEnv struct { |
||||
Config *Env |
||||
stmts map[string]*sql.Stmt |
||||
db *sql.DB |
||||
KVStore map[string]interface{} |
||||
mutex *sync.RWMutex |
||||
} |
||||
|
||||
//Get - get value from kvstore in memory
|
||||
func (rt *RuntimeEnv) Get(key string) (value interface{}, ok bool) { |
||||
rt.mutex.RLock() |
||||
defer rt.mutex.RUnlock() |
||||
value, ok = rt.KVStore[key] |
||||
return |
||||
} |
||||
|
||||
//Retrive - get value from kvstore in memory, and delete it
|
||||
func (rt *RuntimeEnv) Retrive(key string) (value interface{}, ok bool) { |
||||
rt.mutex.Lock() |
||||
defer rt.mutex.Unlock() |
||||
value, ok = rt.KVStore[key] |
||||
if ok { |
||||
delete(rt.KVStore, key) |
||||
} |
||||
return |
||||
} |
||||
|
||||
//Set - set value to kvstore in memory
|
||||
func (rt *RuntimeEnv) Set(key string, value interface{}) { |
||||
rt.mutex.Lock() |
||||
defer rt.mutex.Unlock() |
||||
rt.KVStore[key] = value |
||||
} |
||||
|
||||
//EndpointEntry - endpoint registry entry
|
||||
type EndpointEntry struct { |
||||
Handler func(http.ResponseWriter, *http.Request) |
||||
Middlewares []endpoint.ServerMiddleware |
||||
} |
||||
|
||||
//NewApp - create new app
|
||||
func NewApp(name, description string, config *Config) *App { |
||||
if config == nil { |
||||
log.Println("Missing configuration data") |
||||
return nil |
||||
} |
||||
endpoint.SetPrometheus(strings.Replace(name, "-", "_", -1)) |
||||
app := &App{ |
||||
Name: name, |
||||
Description: description, |
||||
Config: config, |
||||
Root: root.NewModule(name, description, config.Prefix), |
||||
Endpoints: make(map[string]EndpointEntry, 0), |
||||
MessageHandlers: make(map[string]func(*module.Message) bool, 0), |
||||
Scheduler: cron.New(), |
||||
wg: &sync.WaitGroup{}, |
||||
mutex: &sync.RWMutex{}, |
||||
Runtime: make(map[string]*RuntimeEnv), |
||||
} |
||||
app.TaskManager = task.NewManager(app, 100) |
||||
return app |
||||
} |
||||
|
||||
//Init - app initialization
|
||||
func (a *App) Init() { |
||||
if a.Config != nil { |
||||
a.Config.fixPrefix() |
||||
for _, env := range a.Config.Envs { |
||||
utils.MakeFolder(env.DataFolder) |
||||
a.Runtime[env.Name] = &RuntimeEnv{ |
||||
Config: env, |
||||
KVStore: make(map[string]interface{}, 1024), |
||||
mutex: &sync.RWMutex{}, |
||||
} |
||||
} |
||||
a.InitDB() |
||||
} |
||||
a.registerEndpoints() |
||||
a.registerMessageHandlers() |
||||
a.registerTasks() |
||||
// utils.LoadOrCreateJSON("./saved_status.json", &a.Status)
|
||||
a.Root.OnStop = func(p *module.Module) { |
||||
a.TaskManager.SendAll("stop") |
||||
a.wg.Wait() |
||||
} |
||||
a.Root.OnDispose = func(p *module.Module) { |
||||
for _, env := range a.Runtime { |
||||
if env.db != nil { |
||||
log.Println("Close sqlite for", env.Config.Name) |
||||
env.db.Close() |
||||
} |
||||
} |
||||
// utils.SaveJSON(a.Status, "./saved_status.json")
|
||||
} |
||||
} |
||||
|
||||
//registerEndpoints - Register Endpoints
|
||||
func (a *App) registerEndpoints() { |
||||
a.initEndpoints() |
||||
for path, entry := range a.Endpoints { |
||||
if entry.Middlewares == nil { |
||||
entry.Middlewares = a.getDefaultMiddlewares(path) |
||||
} |
||||
a.Root.MountingPoints[path] = endpoint.DecorateServer( |
||||
endpoint.Impl(entry.Handler), |
||||
entry.Middlewares..., |
||||
) |
||||
} |
||||
} |
||||
|
||||
//registerMessageHandlers - Register Message Handlers
|
||||
func (a *App) registerMessageHandlers() { |
||||
a.initMessageHandlers() |
||||
for path, handler := range a.MessageHandlers { |
||||
a.Root.AddMessageHandler(path, handler) |
||||
} |
||||
} |
||||
|
||||
//StartScheduler - register and start the scheduled tasks
|
||||
func (a *App) StartScheduler() { |
||||
if a.Scheduler == nil { |
||||
a.Scheduler = cron.New() |
||||
} else { |
||||
a.Scheduler.Stop() |
||||
a.Scheduler = cron.New() |
||||
} |
||||
for _, item := range a.Config.ScheduledTasks { |
||||
log.Println("[INFO] - Adding task:", item.Task) |
||||
func() { |
||||
s := item.Schedule |
||||
t := item.Task |
||||
a.Scheduler.AddFunc(s, func() { |
||||
a.TaskManager.RunTask(t, item.DefaultArgs...) |
||||
}) |
||||
}() |
||||
} |
||||
a.Scheduler.Start() |
||||
} |
||||
|
||||
//ListenAndServe - Start app
|
||||
func (a *App) ListenAndServe() { |
||||
a.Init() |
||||
a.StartScheduler() |
||||
a.Root.ListenAndServe(a.Config.Address) |
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
package main |
||||
|
||||
//GORoutingNumberForWechat - Total GO Routing # for send process
|
||||
const GORoutingNumberForWechat = 10 |
||||
|
||||
//ScheduledTask - command and parameters to run
|
||||
/* Schedule Spec: |
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
*** |
||||
*** corn example ***: |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
*/ |
||||
var cfg = Config{ |
||||
AppID: "myapp", |
||||
Address: ":1501", |
||||
Prefix: "/", |
||||
RedisServerStr: "localhost:6379", |
||||
Envs: []*Env{ |
||||
{ |
||||
Name: "prod", |
||||
SqliteDB: "prod.db", |
||||
DataFolder: "./data/", |
||||
}, |
||||
{ |
||||
Name: "pp", |
||||
SqliteDB: "pp.db", |
||||
DataFolder: "./data/", |
||||
}, |
||||
}, |
||||
ScheduledTasks: []*ScheduledTask{ |
||||
{Schedule: "0 0 0 * * *", Task: "daily-maintenance", DefaultArgs: []string{}}, |
||||
{Schedule: "0 10 0 * * *", Task: "daily-maintenance-pp", DefaultArgs: []string{}}, |
||||
}, |
||||
} |
||||
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
package main |
||||
|
||||
import "strings" |
||||
|
||||
//Config - data struct for configuration file
|
||||
type Config struct { |
||||
AppID string `json:"appid"` |
||||
Address string `json:"address"` |
||||
Prefix string `json:"prefix"` |
||||
RedisServerStr string `json:"redis-server"` |
||||
AppDomainName string `json:"app-domain-name"` |
||||
TokenServiceURL string `json:"token-service-url"` |
||||
TokenServiceUsername string `json:"token-service-user"` |
||||
TokenServicePassword string `json:"token-service-password"` |
||||
Envs []*Env `json:"envs,omitempty"` |
||||
ScheduledTasks []*ScheduledTask `json:"scheduled-tasks,omitempty"` |
||||
} |
||||
|
||||
func (c *Config) fixPrefix() { |
||||
if !strings.HasPrefix(c.Prefix, "/") { |
||||
c.Prefix = "/" + c.Prefix |
||||
} |
||||
if !strings.HasSuffix(c.Prefix, "/") { |
||||
c.Prefix = c.Prefix + "/" |
||||
} |
||||
} |
||||
|
||||
//Env - env configuration
|
||||
type Env struct { |
||||
Name string `json:"name,omitempty"` |
||||
SqliteDB string `json:"sqlite-db,omitempty"` |
||||
DataFolder string `json:"data,omitempty"` |
||||
} |
||||
|
||||
//ScheduledTask - command and parameters to run
|
||||
/* Schedule Spec: |
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
*** |
||||
*** corn example ***: |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
*/ |
||||
//ScheduledTask - Scheduled Task
|
||||
type ScheduledTask struct { |
||||
Schedule string `json:"schedule,omitempty"` |
||||
Task string `json:"task,omitempty"` |
||||
DefaultArgs []string `json:"default-args,omitempty"` |
||||
} |
||||
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"log" |
||||
|
||||
_ "github.com/mattn/go-sqlite3" |
||||
) |
||||
|
||||
//InitDB - initialized database
|
||||
func (a *App) InitDB() { |
||||
//init database tables
|
||||
sqlStmts := []string{ |
||||
//PV计数
|
||||
`CREATE TABLE IF NOT EXISTS visit ( |
||||
openid TEXT DEFAULT '', |
||||
pageid TEXT DEFAULT '', |
||||
scene TEXT DEFAULT '', |
||||
state TEXT INTEGER DEFAULT 0, |
||||
pv INTEGER DEFAULT 0, |
||||
createat DATETIME, |
||||
recent DATETIME |
||||
);`, |
||||
"CREATE INDEX IF NOT EXISTS idx_visit_openid ON visit(openid);", |
||||
"CREATE INDEX IF NOT EXISTS idx_visit_pageid ON visit(pageid);", |
||||
"CREATE INDEX IF NOT EXISTS idx_visit_scene ON visit(scene);", |
||||
"CREATE INDEX IF NOT EXISTS idx_visit_state ON visit(state);", |
||||
"CREATE INDEX IF NOT EXISTS idx_visit_createat ON visit(createat);", |
||||
} |
||||
|
||||
var err error |
||||
for _, env := range a.Runtime { |
||||
env.db, err = sql.Open("sqlite3", fmt.Sprintf("%s%s?cache=shared&mode=rwc", env.Config.DataFolder, env.Config.SqliteDB)) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
log.Printf("[INFO] - Initialization DB for [%s]...\n", env.Config.Name) |
||||
for _, sqlStmt := range sqlStmts { |
||||
_, err := env.db.Exec(sqlStmt) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [InitDB] %q: %s\n", err, sqlStmt) |
||||
return |
||||
} |
||||
} |
||||
env.stmts = make(map[string]*sql.Stmt, 0) |
||||
log.Printf("[INFO] - DB for [%s] ready!\n", env.Config.Name) |
||||
} |
||||
} |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"loreal.com/dit/utils" |
||||
"log" |
||||
"net/http" |
||||
) |
||||
|
||||
/* 以下为具体 Endpoint 实现代码 */ |
||||
|
||||
//entryHandler - entry point for frontend web pages, to get initData in cookie
|
||||
//endpoint: entry.html
|
||||
//method: GET
|
||||
func (a *App) entryHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
showError(w, r, "", "无效的方法") |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
// var userid int64
|
||||
// userid = -1
|
||||
appid := sanitizePolicy.Sanitize(q.Get("appid")) |
||||
env := a.getEnv(appid) |
||||
rt := a.getRuntime(env) |
||||
// states := parseState(sanitizePolicy.Sanitize(q.Get("state")))
|
||||
scene := sanitizePolicy.Sanitize(q.Get("state")) |
||||
_ = sanitizePolicy.Sanitize(q.Get("token")) |
||||
|
||||
openid := sanitizePolicy.Sanitize(q.Get("openid")) |
||||
|
||||
dataObject := map[string]interface{}{ |
||||
"appid": appid, |
||||
"scene": scene, |
||||
"openid": openid, |
||||
} |
||||
// follower, nickname := a.wxUserKeyInfo(openid)
|
||||
// dataObject["nickname"] = nickname
|
||||
if rt != nil { |
||||
// if err := a.recordUser(
|
||||
// rt,
|
||||
// openid,
|
||||
// scene,
|
||||
// "0",
|
||||
// &userid,
|
||||
// ); err != nil {
|
||||
// log.Println("[ERR] - [EP][entry.html], err:", err)
|
||||
// }
|
||||
} |
||||
if q.Get("debug") == "1" { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
encoder := json.NewEncoder(w) |
||||
encoder.SetIndent("", " ") |
||||
if err := encoder.Encode(dataObject); err != nil { |
||||
log.Println("[ERR] - JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
} |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8") |
||||
// cookieValue := url.PathEscape(utils.MarshalJSON(dataObject))
|
||||
if DEBUG { |
||||
log.Println("[DEBUG] - set-cookie:", utils.MarshalJSON(dataObject)) |
||||
} |
||||
// http.SetCookie(w, &http.Cookie{
|
||||
// Name: "initdata",
|
||||
// Value: cookieValue,
|
||||
// HttpOnly: false,
|
||||
// Secure: false,
|
||||
// MaxAge: 0,
|
||||
// })
|
||||
http.ServeFile(w, r, "./public/index.html") |
||||
} |
||||
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"loreal.com/dit/endpoint" |
||||
"loreal.com/dit/loreal/webservice" |
||||
"loreal.com/dit/middlewares" |
||||
"html/template" |
||||
"log" |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"github.com/microcosm-cc/bluemonday" |
||||
) |
||||
|
||||
// var seededRand *rand.Rand
|
||||
var sanitizePolicy *bluemonday.Policy |
||||
|
||||
var errorTemplate *template.Template |
||||
|
||||
func init() { |
||||
// seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sanitizePolicy = bluemonday.UGCPolicy() |
||||
|
||||
var err error |
||||
errorTemplate, _ = template.ParseFiles("./template/error.tpl") |
||||
if err != nil { |
||||
log.Panic("[ERR] - Parsing error template", err) |
||||
} |
||||
} |
||||
|
||||
func (a *App) initEndpoints() { |
||||
a.Endpoints = map[string]EndpointEntry{ |
||||
"entry.html": {Handler: a.entryHandler, Middlewares: a.noAuthMiddlewares("entry.html")}, |
||||
"api/kvstore": {Handler: a.kvstoreHandler, Middlewares: a.noAuthMiddlewares("api/kvstore")}, |
||||
"api/visit": {Handler: a.pvHandler, Middlewares: a.noAuthMiddlewares("api/visit")}, |
||||
"error": {Handler: a.errorHandler, Middlewares: a.noAuthMiddlewares("error")}, |
||||
"report/visit": {Handler: a.reportVisitHandler}, |
||||
} |
||||
} |
||||
|
||||
//noAuthMiddlewares - middlewares without auth
|
||||
func (a *App) noAuthMiddlewares(path string) []endpoint.ServerMiddleware { |
||||
return []endpoint.ServerMiddleware{ |
||||
middlewares.NoCache(), |
||||
middlewares.ServerInstrumentation(path, endpoint.RequestCounter, endpoint.LatencyHistogram, endpoint.DurationsSummary), |
||||
} |
||||
} |
||||
|
||||
//tokenAuthMiddlewares - middlewares auth by token
|
||||
// func (a *App) tokenAuthMiddlewares(path string) []endpoint.ServerMiddleware {
|
||||
// return []endpoint.ServerMiddleware{
|
||||
// middlewares.NoCache(),
|
||||
// middlewares.ServerInstrumentation(path, endpoint.RequestCounter, endpoint.LatencyHistogram, endpoint.DurationsSummary),
|
||||
// a.signatureVerifier(),
|
||||
// }
|
||||
// }
|
||||
|
||||
//getDefaultMiddlewares - middlewares installed by defaults
|
||||
func (a *App) getDefaultMiddlewares(path string) []endpoint.ServerMiddleware { |
||||
return []endpoint.ServerMiddleware{ |
||||
middlewares.NoCache(), |
||||
middlewares.BasicAuthOrTokenAuthWithRole(a.AuthProvider, "", "user,admin"), |
||||
middlewares.ServerInstrumentation( |
||||
path, |
||||
endpoint.RequestCounter, |
||||
endpoint.LatencyHistogram, |
||||
endpoint.DurationsSummary, |
||||
), |
||||
} |
||||
} |
||||
|
||||
func (a *App) getEnv(appid string) string { |
||||
if appid == a.Config.AppID { |
||||
return "prod" |
||||
} |
||||
return "pp" |
||||
} |
||||
|
||||
/* 以下为具体 Endpoint 实现代码 */ |
||||
|
||||
//errorHandler - query error info
|
||||
//endpoint: error
|
||||
//method: GET
|
||||
func (a *App) errorHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
http.Error(w, "Not Acceptable", http.StatusNotAcceptable) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
title := sanitizePolicy.Sanitize(q.Get("title")) |
||||
errmsg := sanitizePolicy.Sanitize(q.Get("errmsg")) |
||||
|
||||
if err := errorTemplate.Execute(w, map[string]interface{}{ |
||||
"title": title, |
||||
"errmsg": errmsg, |
||||
}); err != nil { |
||||
log.Println("[ERR] - errorTemplate error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
} |
||||
} |
||||
|
||||
/* 以下为具体 Endpoint 实现代码 */ |
||||
|
||||
//kvstoreHandler - get value from kvstore in runtime
|
||||
//endpoint: /api/kvstore
|
||||
//method: GET
|
||||
func (a *App) kvstoreHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
outputJSON(w, webservice.APIStatus{ |
||||
ErrCode: -100, |
||||
ErrMessage: "Method not acceptable", |
||||
}) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
ticket := q.Get("ticket") |
||||
env := a.getEnv(q.Get("appid")) |
||||
rt := a.getRuntime(env) |
||||
if rt == nil { |
||||
outputJSON(w, webservice.APIStatus{ |
||||
ErrCode: -1, |
||||
ErrMessage: "invalid appid", |
||||
}) |
||||
return |
||||
} |
||||
var result struct { |
||||
Value interface{} `json:"value"` |
||||
} |
||||
var ok bool |
||||
var v interface{} |
||||
v, ok = rt.Retrive(ticket) |
||||
if !ok { |
||||
outputJSON(w, webservice.APIStatus{ |
||||
ErrCode: -2, |
||||
ErrMessage: "invalid ticket", |
||||
}) |
||||
return |
||||
} |
||||
switch val := v.(type) { |
||||
case chan interface{}: |
||||
// log.Println("[Hu Bin] - Get Value Chan:", val)
|
||||
result.Value = <-val |
||||
// log.Println("[Hu Bin] - Get Value from Chan:", result.Value)
|
||||
default: |
||||
// log.Println("[Hu Bin] - Get Value:", val)
|
||||
result.Value = val |
||||
} |
||||
outputJSON(w, result) |
||||
} |
||||
|
||||
//pvHandler - record PV/UV
|
||||
//endpoint: /api/visit
|
||||
//method: GET
|
||||
func (a *App) pvHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": -1, |
||||
"msg": "Not support", |
||||
}) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
appid := q.Get("appid") |
||||
env := a.getEnv(appid) |
||||
rt := a.getRuntime(env) |
||||
if rt == nil { |
||||
log.Println("[ERR] - Invalid appid:", appid) |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": -2, |
||||
"msg": "Invalid APPID", |
||||
}) |
||||
return |
||||
} |
||||
openid := sanitizePolicy.Sanitize(q.Get("openid")) |
||||
pageid := sanitizePolicy.Sanitize(q.Get("pageid")) |
||||
scene := sanitizePolicy.Sanitize(q.Get("scene")) |
||||
visitState, _ := strconv.Atoi(sanitizePolicy.Sanitize(q.Get("type"))) |
||||
|
||||
if err := a.recordPV( |
||||
rt, |
||||
openid, |
||||
pageid, |
||||
scene, |
||||
visitState, |
||||
); err != nil { |
||||
log.Println("[ERR] - [EP][api/visit], err:", err) |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": -3, |
||||
"msg": "internal error", |
||||
}) |
||||
return |
||||
} |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": 0, |
||||
"msg": "ok", |
||||
}) |
||||
} |
||||
|
||||
//CSV BOM
|
||||
//file.Write([]byte{0xef, 0xbb, 0xbf})
|
||||
|
||||
func outputExcel(w http.ResponseWriter, b []byte, filename string) { |
||||
w.Header().Add("Content-Disposition", "attachment; filename="+filename) |
||||
//w.Header().Add("Content-Type", "application/vnd.ms-excel")
|
||||
w.Header().Add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") |
||||
// w.Header().Add("Content-Transfer-Encoding", "binary")
|
||||
w.Write(b) |
||||
} |
||||
|
||||
func outputText(w http.ResponseWriter, b []byte) { |
||||
w.Header().Add("Content-Type", "text/plain;charset=utf-8") |
||||
w.Write(b) |
||||
} |
||||
|
||||
func showError(w http.ResponseWriter, r *http.Request, title, message string) { |
||||
if err := errorTemplate.Execute(w, map[string]interface{}{ |
||||
"title": title, |
||||
"errmsg": message, |
||||
}); err != nil { |
||||
log.Println("[ERR] - errorTemplate error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
} |
||||
} |
||||
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"net/http" |
||||
"time" |
||||
) |
||||
|
||||
//reportVisitHandler - export visit detail report in excel format
|
||||
//endpoint: /report/visit
|
||||
//method: GET
|
||||
func (a *App) reportVisitHandler(w http.ResponseWriter, r *http.Request) { |
||||
const dateFormat = "2006-01-02" |
||||
if r.Method != "GET" { |
||||
showError(w, r, "明细报表下载", "调用方法不正确") |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
env := a.getEnv(q.Get("appid")) |
||||
rt := a.getRuntime(env) |
||||
if rt == nil { |
||||
showError(w, r, "明细报表下载", "参数错误, APPID不正确") |
||||
return |
||||
} |
||||
var err error |
||||
from, err := time.ParseInLocation(dateFormat, sanitizePolicy.Sanitize(q.Get("from")), time.Local) |
||||
if err != nil { |
||||
showError(w, r, "明细报表下载", "参数错误, 开始时间‘from’格式不正确") |
||||
return |
||||
} |
||||
to, err := time.ParseInLocation(dateFormat, sanitizePolicy.Sanitize(q.Get("to")), time.Local) |
||||
if err != nil { |
||||
showError(w, r, "明细报表下载", "参数错误, 结束时间‘to’格式不正确") |
||||
return |
||||
} |
||||
to = to.Add(time.Second*(60*60*24-1) + time.Millisecond*999) //23:59:59.999
|
||||
xlsxFile, err := a.genVisitReportXlsx(rt, from, to) |
||||
if err != nil { |
||||
log.Println("[ERR] - [ep][report/download], err:", err) |
||||
showError(w, r, "明细报表下载", "查询报表时发生错误, 请联系管理员查看日志。") |
||||
return |
||||
} |
||||
fileName := fmt.Sprintf("visit-report-%s-%s.xlsx", from.Format("0102"), to.Format("0102")) |
||||
w.Header().Add("Content-Disposition", "attachment; filename="+fileName) |
||||
w.Header().Add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") |
||||
if err := xlsxFile.Write(w); err != nil { |
||||
showError(w, r, "明细报表下载", "查询报表时发生错误, 无法生存Xlsx文件。") |
||||
return |
||||
} |
||||
} |
||||
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
package main |
||||
|
||||
func (a *App) recordPV( |
||||
runtime *RuntimeEnv, |
||||
openid, pageid, scene string, |
||||
visitState int, |
||||
) (err error) { |
||||
const stmtNameNewPV = "insert-visit" |
||||
const stmtSQLNewPV = "INSERT INTO visit (openid,pageid,scene,createAt,recent,pv,state) VALUES (?,?,?,datetime('now','localtime'),datetime('now','localtime'),1,?);" |
||||
const stmtNamePV = "update-pv" |
||||
const stmtSQLPV = "UPDATE visit SET pv=pv+1,recent=datetime('now','localtime') WHERE openid=? AND pageid=? AND scene=? AND state=?;" |
||||
stmtPV := a.getStmt(runtime, stmtNamePV) |
||||
if stmtPV == nil { |
||||
if stmtPV, err = a.setStmt(runtime, stmtNamePV, stmtSQLPV); err != nil { |
||||
return |
||||
} |
||||
} |
||||
stmtNewPV := a.getStmt(runtime, stmtNameNewPV) |
||||
if stmtNewPV == nil { |
||||
if stmtNewPV, err = a.setStmt(runtime, stmtNameNewPV, stmtSQLNewPV); err != nil { |
||||
return |
||||
} |
||||
} |
||||
runtime.mutex.Lock() |
||||
defer runtime.mutex.Unlock() |
||||
tx, err := runtime.db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmtPV = tx.Stmt(stmtPV) |
||||
pvResult, err := stmtPV.Exec( |
||||
openid, |
||||
pageid, |
||||
scene, |
||||
visitState, |
||||
) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
cnt, err := pvResult.RowsAffected() |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
if cnt > 0 { |
||||
tx.Commit() |
||||
return |
||||
} |
||||
stmtNewPV = tx.Stmt(stmtNewPV) |
||||
_, err = stmtNewPV.Exec( |
||||
openid, |
||||
pageid, |
||||
scene, |
||||
visitState, |
||||
) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
tx.Commit() |
||||
return |
||||
} |
||||
|
||||
// func (a *App) recordQRScan(
|
||||
// openid string,
|
||||
// ) (err error) {
|
||||
// for _, env := range a.Config.Envs {
|
||||
// rt := a.getRuntime(env.Name)
|
||||
// if rt == nil {
|
||||
// continue
|
||||
// }
|
||||
// a.doRecordQRScan(rt, openid)
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func (a *App) doRecordQRScan(
|
||||
// runtime *RuntimeEnv,
|
||||
// openid string,
|
||||
// ) (err error) {
|
||||
// const stmtNameAdd = "add-openid"
|
||||
// const stmtSQLAdd = "INSERT INTO qrscan (openid,createat) VALUES (?,datetime('now','localtime'));"
|
||||
// const stmtNameRecord = "record-scan"
|
||||
// const stmtSQLRecord = "UPDATE qrscan SET scanCnt=scanCnt+1,recent=datetime('now','localtime') WHERE openid=?;"
|
||||
// stmtAdd := a.getStmt(runtime, stmtNameAdd)
|
||||
// if stmtAdd == nil {
|
||||
// //lazy setup for stmt
|
||||
// if stmtAdd, err = a.setStmt(runtime, stmtNameAdd, stmtSQLAdd); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// stmtRecord := a.getStmt(runtime, stmtSQLRecord)
|
||||
// if stmtRecord == nil {
|
||||
// if stmtRecord, err = a.setStmt(runtime, stmtNameRecord, stmtSQLRecord); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// runtime.mutex.Lock()
|
||||
// defer runtime.mutex.Unlock()
|
||||
// tx, err := runtime.db.Begin()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// //Add scan
|
||||
// stmtAdd = tx.Stmt(stmtAdd)
|
||||
// _, err = stmtAdd.Exec(
|
||||
// openid,
|
||||
// )
|
||||
// if err != nil && !strings.HasPrefix(err.Error(), "UNIQUE") {
|
||||
// tx.Rollback()
|
||||
// return err
|
||||
// }
|
||||
// //record scan
|
||||
// stmtRecord = tx.Stmt(stmtRecord)
|
||||
// _, err = stmtRecord.Exec(openid)
|
||||
// if err != nil {
|
||||
// tx.Rollback()
|
||||
// return err
|
||||
// }
|
||||
// tx.Commit()
|
||||
// return
|
||||
// }
|
||||
@ -0,0 +1,188 @@
@@ -0,0 +1,188 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"encoding/json" |
||||
"loreal.com/dit/module/modules/loreal" |
||||
"loreal.com/dit/wechat" |
||||
"fmt" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
//DEBUG - whether in debug mode
|
||||
var DEBUG bool |
||||
|
||||
//INFOLEVEL - info level for debug mode
|
||||
var INFOLEVEL int |
||||
|
||||
//LOGLEVEL - info level for logs
|
||||
var LOGLEVEL int |
||||
|
||||
var wxAccount *wechat.Account |
||||
|
||||
func init() { |
||||
wxAccount = wechat.Accounts["default"] |
||||
if wxAccount.Token.Requester == nil { |
||||
wxAccount.Token.Requester = loreal.NewWechatTokenService(wxAccount) |
||||
} |
||||
if os.Getenv("EV_DEBUG") != "" { |
||||
DEBUG = true |
||||
} |
||||
INFOLEVEL = 1 |
||||
LOGLEVEL = 1 |
||||
} |
||||
|
||||
//var wxAccount = wechat.Accounts["default"]
|
||||
|
||||
func lorealCardValid(cardNo string) bool { |
||||
if len(cardNo) != 22 { |
||||
return false |
||||
} |
||||
re := regexp.MustCompile("\\d{22}") |
||||
if !re.MatchString(cardNo) { |
||||
return false |
||||
} |
||||
return (cardNo == lorealCardCheckSum(cardNo[:20])) |
||||
} |
||||
|
||||
//lorealCardCheckSum calculate 2 check sum bit for loreal card
|
||||
func lorealCardCheckSum(cardNo string) string { |
||||
firstLineWeight := []int{1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2} |
||||
secondLineWeight := []int{4, 3, 2, 7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2} |
||||
cardRunes := []rune(cardNo) |
||||
|
||||
var firstDigital, secondDigital int |
||||
|
||||
var temp, result int |
||||
for i := 0; i <= 19; i++ { |
||||
cardBit, _ := strconv.Atoi(string(cardRunes[i])) |
||||
temp = cardBit * firstLineWeight[i] |
||||
if temp > 9 { |
||||
temp -= 9 |
||||
} |
||||
result += temp |
||||
} |
||||
|
||||
firstDigital = result % 10 |
||||
if firstDigital != 0 { |
||||
firstDigital = 10 - firstDigital |
||||
} |
||||
|
||||
cardNo = fmt.Sprintf("%s%d", cardNo, firstDigital) |
||||
cardRunes = []rune(cardNo) |
||||
|
||||
result = 0 |
||||
|
||||
for i := 0; i <= 20; i++ { |
||||
cardBit, _ := strconv.Atoi(string(cardRunes[i])) |
||||
temp = cardBit * secondLineWeight[i] |
||||
result += temp |
||||
} |
||||
|
||||
secondDigital = 11 - result%11 |
||||
if secondDigital > 9 { |
||||
secondDigital = 0 |
||||
} |
||||
return fmt.Sprintf("%s%d", cardNo, secondDigital) |
||||
} |
||||
|
||||
func retry(count int, fn func() error) error { |
||||
total := count |
||||
retry: |
||||
err := fn() |
||||
if err != nil { |
||||
count-- |
||||
log.Println("[INFO] - Retry: ", total-count) |
||||
if count > 0 { |
||||
goto retry |
||||
} |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func parseState(state string) map[string]string { |
||||
result := make(map[string]string, 2) |
||||
var err error |
||||
state, err = url.PathUnescape(state) |
||||
if err != nil { |
||||
log.Println("[ERR] - parseState", err) |
||||
return result |
||||
} |
||||
if DEBUG { |
||||
log.Println("[DEBUG] - PathUnescape state:", state) |
||||
} |
||||
states := strings.Split(state, ";") |
||||
for _, kv := range states { |
||||
sp := strings.Index(kv, ":") |
||||
if sp < 0 { |
||||
//empty value
|
||||
result[kv] = "" |
||||
continue |
||||
} |
||||
result[kv[:sp]] = kv[sp+1:] |
||||
} |
||||
return result |
||||
} |
||||
|
||||
func (a *App) getRuntime(env string) *RuntimeEnv { |
||||
runtime, ok := a.Runtime[env] |
||||
if !ok { |
||||
return nil |
||||
} |
||||
return runtime |
||||
} |
||||
|
||||
//getStmt - get stmt from app safely
|
||||
func (a *App) getStmt(runtime *RuntimeEnv, name string) *sql.Stmt { |
||||
runtime.mutex.RLock() |
||||
defer runtime.mutex.RUnlock() |
||||
if stmt, ok := runtime.stmts[name]; ok { |
||||
return stmt |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
//getStmt - get stmt from app safely
|
||||
func (a *App) setStmt(runtime *RuntimeEnv, name, query string) (stmt *sql.Stmt, err error) { |
||||
stmt, err = runtime.db.Prepare(query) |
||||
if err != nil { |
||||
logError(err, name) |
||||
return nil, err |
||||
} |
||||
runtime.mutex.Lock() |
||||
runtime.stmts[name] = stmt |
||||
runtime.mutex.Unlock() |
||||
return stmt, nil |
||||
} |
||||
|
||||
//outputJSON - output json for http response
|
||||
func outputJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
enc := json.NewEncoder(w) |
||||
if DEBUG { |
||||
enc.SetIndent("", " ") |
||||
} |
||||
if err := enc.Encode(data); err != nil { |
||||
log.Println("[ERR] - JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func logError(err error, msg string) { |
||||
if err != nil { |
||||
log.Printf("[ERR] - %s, err: %v\n", msg, err) |
||||
} |
||||
} |
||||
|
||||
func debugInfo(source, msg string, level int) { |
||||
if DEBUG && INFOLEVEL >= level { |
||||
log.Printf("[DEBUG] - [%s]%s\n", source, msg) |
||||
} |
||||
} |
||||
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/tealeg/xlsx" |
||||
) |
||||
|
||||
/* |
||||
openid TEXT DEFAULT '', |
||||
pageid TEXT DEFAULT '', |
||||
scene TEXT DEFAULT '', |
||||
state TEXT INTEGER DEFAULT 0, |
||||
pv INTEGER DEFAULT 0, |
||||
createat DATETIME, |
||||
recent DATETIME |
||||
*/ |
||||
|
||||
type visitRecord struct { |
||||
OpenID string |
||||
PageID string |
||||
Scene string |
||||
PV int |
||||
CreateAt time.Time |
||||
Recent time.Time |
||||
} |
||||
|
||||
func (a *App) genVisitReportXlsx( |
||||
runtime *RuntimeEnv, |
||||
from time.Time, |
||||
to time.Time, |
||||
) (f *xlsx.File, err error) { |
||||
const stmtName = "visit-report" |
||||
const stmtSQL = "SELECT openid,pageid,scene,pv,createat,recent FROM visit WHERE createat>=? AND createat<=?;" |
||||
stmt := a.getStmt(runtime, stmtName) |
||||
if stmt == nil { |
||||
//lazy setup for stmt
|
||||
if stmt, err = a.setStmt(runtime, stmtName, stmtSQL); err != nil { |
||||
return |
||||
} |
||||
} |
||||
runtime.mutex.Lock() |
||||
defer runtime.mutex.Unlock() |
||||
rows, err := stmt.Query(from, to) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
var headerRow *xlsx.Row |
||||
|
||||
f = xlsx.NewFile() |
||||
sheet, err := f.AddSheet(fmt.Sprintf("PV details %s-%s", from.Format("0102"), to.Format("0102"))) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
headerRow = sheet.AddRow() |
||||
headerRow.AddCell().SetString("OpenID") |
||||
headerRow.AddCell().SetString("页面号") |
||||
headerRow.AddCell().SetString("入口场景") |
||||
headerRow.AddCell().SetString("PV") |
||||
headerRow.AddCell().SetString("首次进入时间") |
||||
headerRow.AddCell().SetString("最后进入时间") |
||||
|
||||
for rows.Next() { |
||||
r := sheet.AddRow() |
||||
vr := visitRecord{} |
||||
if err = rows.Scan( |
||||
&vr.OpenID, |
||||
&vr.PageID, |
||||
&vr.Scene, |
||||
&vr.PV, |
||||
&vr.CreateAt, |
||||
&vr.Recent, |
||||
); err != nil { |
||||
return |
||||
} |
||||
r.AddCell().SetString(vr.OpenID) |
||||
r.AddCell().SetString(vr.PageID) |
||||
r.AddCell().SetString(vr.Scene) |
||||
r.AddCell().SetInt(vr.PV) |
||||
r.AddCell().SetDateTime(vr.CreateAt) |
||||
r.AddCell().SetDateTime(vr.Recent) |
||||
} |
||||
return f, nil |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"loreal.com/dit/utils/task" |
||||
) |
||||
|
||||
//DailyMaintenance - task to do daily maintenance
|
||||
func (a *App) DailyMaintenance(t *task.Task) (err error) { |
||||
// const stmtName = "dm-clean-vehicle"
|
||||
// const stmtSQL = "DELETE FROM vehicle_left WHERE enter<=?;"
|
||||
// env := getEnv(t.Context)
|
||||
// runtime := a.getRuntime(env)
|
||||
// if runtime == nil {
|
||||
// return ErrMissingRuntime
|
||||
// }
|
||||
// stmt := a.getStmt(runtime, stmtName)
|
||||
// if stmt == nil {
|
||||
// //lazy setup for stmt
|
||||
// if stmt, err = a.setStmt(runtime, stmtName, stmtSQL); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// runtime.mutex.Lock()
|
||||
// defer runtime.mutex.Unlock()
|
||||
// _, err = stmt.Exec(int(time.Now().Add(time.Hour * -168).Unix())) /* 7*24Hours = 168*/
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
//General Wechat WebAPP Host
|
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"log" |
||||
"math/rand" |
||||
"time" |
||||
|
||||
"loreal.com/dit/module/modules/account" |
||||
"loreal.com/dit/module/modules/wechat" |
||||
"loreal.com/dit/utils" |
||||
) |
||||
|
||||
//Version - generate on build time by makefile
|
||||
var Version = "v0.1" |
||||
|
||||
//CommitID - generate on build time by makefile
|
||||
var CommitID = "" |
||||
|
||||
func main() { |
||||
rand.Seed(time.Now().UnixNano()) |
||||
const serviceName = "wxAppHost" |
||||
const serviceDescription = "Wechat WebAPP Host" |
||||
log.Println("[INFO] -", serviceName, Version+"-"+CommitID) |
||||
log.Println("[INFO] -", serviceDescription) |
||||
|
||||
utils.LoadOrCreateJSON("./config/config.json", &cfg) //cfg initialized in config.go
|
||||
|
||||
flag.StringVar(&cfg.Address, "addr", cfg.Address, "host:port of the service") |
||||
flag.StringVar(&cfg.Prefix, "prefix", cfg.Prefix, "/path/ prefixe to service") |
||||
flag.StringVar(&cfg.RedisServerStr, "redis", cfg.RedisServerStr, "Redis connection string") |
||||
flag.StringVar(&cfg.AppDomainName, "app-domain", cfg.AppDomainName, "app domain name") |
||||
flag.Parse() |
||||
|
||||
//Create Main service
|
||||
var app = NewApp(serviceName, serviceDescription, &cfg) |
||||
uas := account.NewModule("account", |
||||
app.Config.RedisServerStr, /*Redis server address*/ |
||||
3, /*Numbe of faild logins to lock the account */ |
||||
60*time.Second, /*How long the account will stay locked*/ |
||||
7200*time.Second, /*How long the token will be valid*/ |
||||
) |
||||
app.Root.Install( |
||||
uas, |
||||
wechat.NewModuleWithCEHTokenService( |
||||
"wx", |
||||
app.Config.AppDomainName, |
||||
app.Config.TokenServiceURL, |
||||
app.Config.TokenServiceUsername, |
||||
app.Config.TokenServicePassword, |
||||
), |
||||
) |
||||
app.AuthProvider = uas |
||||
app.ListenAndServe() |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
|
||||
"loreal.com/dit/module" |
||||
"loreal.com/dit/utils" |
||||
) |
||||
|
||||
func (a *App) initMessageHandlers() { |
||||
a.MessageHandlers = map[string]func(*module.Message) bool{ |
||||
"reload": a.reloadMessageHandler, |
||||
} |
||||
|
||||
} |
||||
|
||||
//reloadMessageHandler - handle reload message
|
||||
func (a *App) reloadMessageHandler(msgPtr *module.Message) (handled bool) { |
||||
//reload configuration
|
||||
utils.LoadOrCreateJSON("./config/config.json", &a.Config) |
||||
a.Config.fixPrefix() |
||||
a.StartScheduler() |
||||
log.Println("[INFO] - Configuration reloaded!") |
||||
return true |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
package main |
||||
|
||||
import "loreal.com/dit/utils/task" |
||||
|
||||
func (a *App) registerTasks() { |
||||
a.TaskManager.RegisterWithContext("daily-maintenance-pp", "xmillion-test", a.dailyMaintenanceTaskHandler, 1) |
||||
a.TaskManager.RegisterWithContext("daily-maintenance", "xmillion", a.dailyMaintenanceTaskHandler, 1) |
||||
} |
||||
|
||||
//dailyMaintenanceTaskHandler - run daily maintenance task
|
||||
func (a *App) dailyMaintenanceTaskHandler(t *task.Task, args ...string) { |
||||
//a.DailyMaintenance(t, task.GetArgs(args, 0))
|
||||
a.DailyMaintenance(t) |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
{ |
||||
// Use IntelliSense to learn about possible attributes. |
||||
// Hover to view descriptions of existing attributes. |
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
||||
"version": "0.2.0", |
||||
"configurations": [ |
||||
{ |
||||
"name": "api test", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "debug", |
||||
// "backend": "native", |
||||
"program": "${workspaceFolder}/main.go" |
||||
} |
||||
] |
||||
} |
||||
Binary file not shown.
@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
|
||||
package base |
||||
|
||||
import ( |
||||
// "bytes"
|
||||
"encoding/json" |
||||
// "errors"
|
||||
"fmt" |
||||
"log" |
||||
"math/rand" |
||||
|
||||
// "mime/multipart"
|
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/google/uuid" |
||||
) |
||||
|
||||
var r *rand.Rand = rand.New(rand.NewSource(time.Now().Unix())) |
||||
|
||||
// IsBlankString 判断是否为空的ID,ID不能都是空白.
|
||||
func IsBlankString(str string) bool { |
||||
return len(strings.TrimSpace(str)) == 0 |
||||
} |
||||
|
||||
// IsEmptyString 判断是否为空的字符串.
|
||||
func IsEmptyString(str string) bool { |
||||
return len(str) == 0 |
||||
} |
||||
|
||||
// IsValidUUID
|
||||
func IsValidUUID(u string) bool { |
||||
_, err := uuid.Parse(u) |
||||
return err == nil |
||||
} |
||||
|
||||
// SetResponseHeader 一个快捷设置status code 和content type的方法
|
||||
func SetResponseHeader(w http.ResponseWriter, statusCode int, contentType string) { |
||||
w.Header().Set("Content-Type", contentType) |
||||
w.WriteHeader(statusCode) |
||||
} |
||||
|
||||
// WriteErrorResponse 一个快捷设置包含错误body的response
|
||||
func WriteErrorResponse(w http.ResponseWriter, statusCode int, contentType string, a interface{}) { |
||||
SetResponseHeader(w, statusCode, contentType) |
||||
switch vv := a.(type) { |
||||
case error: |
||||
{ |
||||
fmt.Fprintf(w, vv.Error()) |
||||
} |
||||
case map[string][]error: |
||||
{ |
||||
jsonBytes, err := json.Marshal(vv) |
||||
if nil != err { |
||||
log.Println(err) |
||||
fmt.Fprintf(w, err.Error()) |
||||
} |
||||
var str = string(jsonBytes) |
||||
fmt.Fprintf(w, str) |
||||
} |
||||
case []error: |
||||
{ |
||||
jsonBytes, err := json.Marshal(vv) |
||||
if nil != err { |
||||
log.Println(err) |
||||
fmt.Fprintf(w, err.Error()) |
||||
} |
||||
var str = string(jsonBytes) |
||||
fmt.Fprintf(w, str) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// trimURIPrefix 将一个uri拆分为若干node,根据ndoe取得一些动态参数。
|
||||
func TrimURIPrefix(uri string, stopTag string) []string { |
||||
params := strings.Split(strings.TrimPrefix(strings.TrimSuffix(uri, "/"), "/"), "/") |
||||
last := len(params) - 1 |
||||
for i := last; i >= 0; i-- { |
||||
if params[i] == stopTag { |
||||
return params[i+1:] |
||||
} |
||||
} |
||||
return params |
||||
} |
||||
|
||||
//outputJSON - output json for http response
|
||||
func outputJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
enc := json.NewEncoder(w) |
||||
if err := enc.Encode(data); err != nil { |
||||
log.Println("[ERR] - [outputJSON] JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
|
||||
// RandString 生成随机字符串
|
||||
func RandString(len int) string { |
||||
bytes := make([]byte, len) |
||||
for i := 0; i < len; i++ { |
||||
b := r.Intn(26) + 65 |
||||
bytes[i] = byte(b) |
||||
} |
||||
return string(bytes) |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
module api-tests-for-coupon-service |
||||
|
||||
go 1.13 |
||||
|
||||
require ( |
||||
github.com/ajg/form v1.5.1 // indirect |
||||
github.com/chenhg5/collection v0.0.0-20191118032303-cb21bccce4c3 |
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible |
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect |
||||
github.com/fatih/structs v1.1.0 // indirect |
||||
github.com/gavv/httpexpect v2.0.0+incompatible |
||||
github.com/google/go-querystring v1.0.0 // indirect |
||||
github.com/google/uuid v1.1.1 |
||||
github.com/gorilla/websocket v1.4.1 // indirect |
||||
github.com/imkira/go-interpol v1.1.0 // indirect |
||||
github.com/jinzhu/now v1.1.1 |
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible |
||||
github.com/moul/http2curl v1.0.0 // indirect |
||||
github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3 |
||||
github.com/sergi/go-diff v1.1.0 // indirect |
||||
github.com/smartystreets/goconvey v1.6.4 |
||||
github.com/valyala/fasthttp v1.9.0 // indirect |
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect |
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect |
||||
github.com/yudai/gojsondiff v1.0.0 // indirect |
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect |
||||
) |
||||
@ -0,0 +1,181 @@
@@ -0,0 +1,181 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= |
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= |
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= |
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= |
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= |
||||
github.com/chenhg5/collection v0.0.0-20191118032303-cb21bccce4c3 h1:77Y9kzo3hVaRyipak3XyNVVUfJXyofQeUWoxaiEgPwk= |
||||
github.com/chenhg5/collection v0.0.0-20191118032303-cb21bccce4c3/go.mod h1:RE3lB6QNf4YUL8Jl/OONdlltQuN9LfZD8eR3nZZdBLA= |
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= |
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= |
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= |
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= |
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= |
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= |
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= |
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= |
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= |
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= |
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= |
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= |
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= |
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= |
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= |
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= |
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= |
||||
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= |
||||
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= |
||||
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= |
||||
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= |
||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= |
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= |
||||
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= |
||||
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= |
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= |
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= |
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= |
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= |
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= |
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= |
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= |
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= |
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= |
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= |
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= |
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= |
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= |
||||
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= |
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= |
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= |
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= |
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= |
||||
github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= |
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= |
||||
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= |
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= |
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= |
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= |
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= |
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= |
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= |
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= |
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= |
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= |
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= |
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= |
||||
github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= |
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= |
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= |
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= |
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= |
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= |
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= |
||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= |
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= |
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= |
||||
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= |
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= |
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= |
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= |
||||
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= |
||||
github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY= |
||||
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= |
||||
github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3 h1:xkBtI5JktwbW/vf4vopBbhYsRFTGfQWHYXzC0/qYwxI= |
||||
github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlpHw+eR6UrqaS3kX1VYeaCxzCVdimDS7g5Ln4pPc= |
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= |
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= |
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= |
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= |
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= |
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= |
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= |
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= |
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= |
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= |
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= |
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= |
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= |
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= |
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= |
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= |
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= |
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= |
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= |
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= |
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= |
||||
github.com/valyala/fasthttp v1.9.0 h1:hNpmUdy/+ZXYpGy0OBfm7K0UQTzb73W0T0U4iJIVrMw= |
||||
github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= |
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= |
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= |
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= |
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= |
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= |
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= |
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= |
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= |
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= |
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= |
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= |
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= |
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= |
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= |
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= |
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= |
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= |
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= |
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= |
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= |
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438 h1:khxRGsvPk4n2y8I/mLLjp7e5dMTJmH75wvqS6nMwUtY= |
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
||||
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= |
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= |
||||
gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= |
||||
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= |
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= |
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= |
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= |
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
func main() { |
||||
fmt.Println("this project only for api test") |
||||
} |
||||
@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"api-tests-for-coupon-service/base" |
||||
"fmt" |
||||
"net/http" |
||||
"strings" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/gavv/httpexpect" |
||||
"github.com/google/uuid" |
||||
"github.com/jinzhu/now" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
func Test_APPLY_TIMES(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("coupon A only can apply one time", t, func() { |
||||
u := uuid.New().String() |
||||
_post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
_post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusBadRequest) |
||||
}) |
||||
|
||||
Convey("coupon B can apply upto 2 times", t, func() { |
||||
u := uuid.New().String() |
||||
_post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeB, channelID, http.StatusOK) |
||||
_post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeB, channelID, http.StatusOK) |
||||
_post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeB, channelID, http.StatusBadRequest) |
||||
}) |
||||
} |
||||
|
||||
func Test_REDEEM_TIMES(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("coupon A only can redeem one time", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusBadRequest) |
||||
}) |
||||
|
||||
Convey("coupon B can redeem upto 2 times", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeB, channelID, http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusBadRequest) |
||||
}) |
||||
} |
||||
|
||||
func Test_REDEEM_BY_SAME_BRAND(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("coupon issued by lancome can't be redeemed by parise", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1CouponWithToken(e, u, strings.Join([]string{u, "xx"}, ""), typeB, channelID, http.StatusOK, jwttony5000d) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusBadRequest) |
||||
}) |
||||
} |
||||
|
||||
func Test_REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("coupon A can not redeem in next month", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
ct := time.Now().AddDate(0, 0, -31) |
||||
sql := fmt.Sprintf(`UPDATE coupons SET createdTime = "%s" WHERE id = "%s"`, ct, cid) |
||||
_, _ = dbConnection.Exec(sql) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusBadRequest) |
||||
}) |
||||
|
||||
Convey("coupon C can not redeem since expired", t, func() { |
||||
|
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeC, channelID, http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusBadRequest) |
||||
}) |
||||
|
||||
Convey("coupon B can redeem in next month", t, func() { |
||||
|
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeB, channelID, http.StatusOK) |
||||
|
||||
ct := now.BeginningOfMonth().AddDate(0, 0, -5).Local() // coupon b 延长了31天。
|
||||
sql := fmt.Sprintf(`UPDATE coupons SET createdTime = %d WHERE id = "%s"`, ct.Unix(), cid) |
||||
_, _ = dbConnection.Exec(sql) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
}) |
||||
|
||||
Convey("coupon D can not redeem in the month after next month", t, func() { |
||||
|
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeD, channelID, http.StatusOK) |
||||
|
||||
ct := now.BeginningOfMonth().AddDate(0, 0, -35) // 因为月份天数不一致,如果今天是1号或者2号可能会失败。
|
||||
sql := fmt.Sprintf(`UPDATE coupons SET createdTime = %d WHERE id = "%s"`, ct.Unix(), cid) |
||||
_, _ = dbConnection.Exec(sql) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusBadRequest) |
||||
}) |
||||
} |
||||
|
||||
func Test_REDEEM_PERIOD_WITH_OFFSET(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("coupon E can redeem after issued", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeE, channelID, http.StatusOK) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
}) |
||||
|
||||
Convey("coupon E can redeem after 100 days", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeE, channelID, http.StatusOK) |
||||
ct := time.Now().AddDate(0, 0, -100) |
||||
sql := fmt.Sprintf(`UPDATE coupons SET createdTime = "%s" WHERE id = "%s"`, ct, cid) |
||||
_, _ = dbConnection.Exec(sql) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
}) |
||||
|
||||
Convey("coupon E can not redeem after 365 days", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeE, channelID, http.StatusOK) |
||||
ct := time.Now().AddDate(0, 0, -365) |
||||
sql := fmt.Sprintf(`UPDATE coupons SET createdTime = "%s" WHERE id = "%s"`, ct, cid) |
||||
_, _ = dbConnection.Exec(sql) |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
}) |
||||
} |
||||
@ -0,0 +1,213 @@
@@ -0,0 +1,213 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"api-tests-for-coupon-service/base" |
||||
// "database/sql"
|
||||
// "fmt"
|
||||
"net/http" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/gavv/httpexpect" |
||||
"github.com/google/uuid" |
||||
_ "github.com/mattn/go-sqlite3" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
var baseurl string = "http://127.0.0.1:1503" |
||||
var jwtyhl5000d string = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJvbXZRTEFoTWxVcUsxSjItQmhFYl82QlNkTFpmNkhSVVgtUlhXcHRINElFIn0.eyJqdGkiOiI5NWE1N2E0NS1lOGU3LTRhYzEtOTNhYi00NDI0M2ExNTg3YWMiLCJleHAiOjIwMTMyMjUwMTYsIm5iZiI6MCwiaWF0IjoxNTgxMjI1MDE2LCJpc3MiOiJodHRwOi8vNTIuMTMwLjczLjE4MC9hdXRoL3JlYWxtcy9Mb3JlYWwiLCJzdWIiOiIzNzk1MzdkYy1mYzdkLTRmMzAtOWExMy0wOWFjNmY4OGZhYjYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzb21lYXBwIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiY2I2NDllOGQtZDkwNC00M2E3LWE2NDMtNGY1YTNmMDhmMmVkIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjb3Vwb25fcmVkZWVtZXIiLCJjb3Vwb25faXNzdWVyIl19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InlobDEwMDAwIiwiYnJhbmQiOiJMQU5DT01FIn0.jiRcTomtwvCnyFg2PxZA2MfK36UmD0tsx9kz3PNjlo4tNoxVPax-ocdln5qQfrJg6yzASwsg_-gNFgLhUqCCUV0iGLXcf69fBxvuxcQyBszmcByPda55u_zvLrv-91mI0a167ipjaIWqL2uOo_lSPm44JpwBcex2nqjz1FFBk1g3-nAHPuceh4-c0cF0y51QlS4fSCexorYDlIhUtcym6YQn9hmrM6Xdtjdtgf4iG_srnlH3gIAckT-Ihq_7rueNRE6cniabXg5AkzluEIwDwxY9KbPjSQ6Y1mxAleZ_dIvLFXzjxbnXn1vm8jRt3MAtvxG5yQ0sKjyb9j7h8jGzPA" |
||||
var jwttony5000d string = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJvbXZRTEFoTWxVcUsxSjItQmhFYl82QlNkTFpmNkhSVVgtUlhXcHRINElFIn0.eyJqdGkiOiJiYjUxNWYyZC01ZDA0LTQxYzctYWM0My00ZGRiMTQxODBiNDMiLCJleHAiOjIwMTQwNzg2OTAsIm5iZiI6MCwiaWF0IjoxNTgyMDc4NjkwLCJpc3MiOiJodHRwOi8vNTIuMTMwLjczLjE4MC9hdXRoL3JlYWxtcy9Mb3JlYWwiLCJzdWIiOiI5ZDU3ZGFmMi0xN2Q3LTRiYjMtYjk1Mi00ODY1MTMzNjkwMzgiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzb21lYXBwIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiZjAwNDhhMWItOGFjOC00ZTA1LWI2Y2QtN2RhZTNmYTU4ZjFhIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjb3Vwb25fcmVkZWVtZXIiLCJjb3Vwb25faXNzdWVyIl19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InRvbnkiLCJicmFuZCI6IlBBUklTX0xPUkVBTCJ9.Z0-ZKZXcqcATnjDpfVYFelq4scCBMP1l9LuCC8zUYmzJwBgo56JwdhVaQO-_sQeaqU__a7gGz8P2DKxv_Y6mbc5jTh5BWcc7AC9LR5axZFzHVTgp_ZE7FCBEHkmYpF72W6BKI73e-0_Qwn1FVRxWAF7KkuSnV5_hdfi6-CStfREikE5_Wr0VGS9mn_fcmuVbGchE1yzHhDGKmVa2RiypcMWGHcSY6iF9FTkbF9TbZ-Lu-ASJRFQ8U-k7Q4wtd8laMQTctEJHMVXQ0GcQ362J5l42OiCZjjTleMghx05gvbjhaCF8FI0YdgaBkPUQ-hw_C4IO2fJdc6x4CkdTolgHQg" |
||||
var jwt1s string = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJvbXZRTEFoTWxVcUsxSjItQmhFYl82QlNkTFpmNkhSVVgtUlhXcHRINElFIn0.eyJqdGkiOiJmZWExMDE3NS1jM2Q4LTQ5ZDEtYjI1NS1kODcwYTJiNWFmYmQiLCJleHAiOjE1ODEyMjUxNjgsIm5iZiI6MCwiaWF0IjoxNTgxMjI1MTA4LCJpc3MiOiJodHRwOi8vNTIuMTMwLjczLjE4MC9hdXRoL3JlYWxtcy9Mb3JlYWwiLCJzdWIiOiIzNzk1MzdkYy1mYzdkLTRmMzAtOWExMy0wOWFjNmY4OGZhYjYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzb21lYXBwIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiYjJkODdmMjYtMGEyNC00MTQxLWI1MmItMWJhNTFiOGQ4NmYxIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjb3Vwb25fcmVkZWVtZXIiLCJjb3Vwb25faXNzdWVyIl19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InlobDEwMDAwIiwiYnJhbmQiOiJMQU5DT01FIn0.wACD6pdlnGEZdgMmToCQnp29L5wTxjwsCBB0qPNhULZ3ZSIXAIoC7bb3Rjzomk_FpJjCpBlcmfm83kU1UbDQuiKSkI6DOemZMbccAfHfnqEu3V7195-pBTIWsEpVpKNCFI4lXAKBo7IsHBJwWfMrdkSdUljYWIC_7LgH3vVmY6LheEszRcl9P3NbXaDcmWYtjuywIS7Aph5wse8671aJ7w2ahyDLsr7prlUNs0K9rJMgGy1DNJhzVaGXQnEmg2IMWQihJT1YGzWTzTE24YieQ0BYzvHPfwPsDxyiDZS6qj3z9qBugmFAgJulU7AnoAmEdEFSdMHN_0QwqlvzhGLlrg" |
||||
var jwtnoissueredeemrole string = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJvbXZRTEFoTWxVcUsxSjItQmhFYl82QlNkTFpmNkhSVVgtUlhXcHRINElFIn0.eyJqdGkiOiI5YTUxNDU0My02YWQ3LTRmMjMtOGJiYS1lZGFmNzI5NDRlZDEiLCJleHAiOjIwMTMzMDQ4MTYsIm5iZiI6MCwiaWF0IjoxNTgxMzA0ODE2LCJpc3MiOiJodHRwOi8vNTIuMTMwLjczLjE4MC9hdXRoL3JlYWxtcy9Mb3JlYWwiLCJzdWIiOiIzNzk1MzdkYy1mYzdkLTRmMzAtOWExMy0wOWFjNmY4OGZhYjYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzb21lYXBwIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiOGU2NzVhYTUtZDMxZi00YTFkLWI3OGItYjllYjBhOTYyODg1IiwiYWNyIjoiMSIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoieWhsMTAwMDAiLCJicmFuZCI6IkxBTkNPTUUifQ.PwCTxuLcMwz4-XyoBd750rk7T2qXU6zBdLtSEUa7pMGPI5Eo7Ayp0Yub15EraIoZP0Kphv9MdFtKkNMuV4Ua9otSDkPe2CSn4be3Ez-gvhk7Gbylh1atyrSpdaW-QJNqCek3C8jvGnC4c3_9o4Bduj6pnrRtrttM-oEFGrtauahIr73vmBuRalokw7OMm2dfq3ot8hTb1oT5RkPt_IILxTIorxMKUSoetKUM9b87KHv7EMZQz0sZne8EQ6DrpZmZDAsxU4RL5osKpgYH6p7XACG8RznZtDDfN0uC87nRiUyRbHYHetRwyu_AlnxpRfyCtFCrOFYn00hdrQYO7wi0FA" |
||||
var typeA string = "63f9f1ce-2ad0-462a-b798-4ead5e5ab3a5" |
||||
var typeB string = "58b388ff-689e-445a-8bbd-8d707dbe70ef" |
||||
var typeC string = "abd73dbe-cc91-4b61-b10c-c6532d7a7770" |
||||
var typeD string = "ca0ff68f-dc05-488d-b185-660b101a1068" |
||||
var typeE string = "7c0fb6b2-7362-4933-ad15-cd4ad9fccfec" |
||||
var channelID string = "defaultChannel" |
||||
|
||||
// TODO: 需要直接在DB插入数据然后做一些验证,因为现在无法测试一些和时间有关的case
|
||||
|
||||
func Test_Authorization(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("Given a request without Authorization", t, func() { |
||||
e.GET("/api/coupontypes"). |
||||
Expect().Status(http.StatusUnauthorized) |
||||
}) |
||||
|
||||
Convey("Given a request with a expired jwt", t, func() { |
||||
e.GET("/api/coupontypes"). |
||||
WithHeader("Authorization", jwt1s). |
||||
Expect().Status(http.StatusBadRequest). |
||||
JSON().Object().ContainsKey("error-code").ValueEqual("error-code", 1501) |
||||
}) |
||||
} |
||||
|
||||
func Test_GET_coupontypes(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("Make sure api only support GET", t, func() { |
||||
e.POST("/api/coupontypes").WithHeader("Authorization", jwtyhl5000d). |
||||
Expect().Status(http.StatusMethodNotAllowed) |
||||
e.PUT("/api/coupontypes").WithHeader("Authorization", jwtyhl5000d). |
||||
Expect().Status(http.StatusMethodNotAllowed) |
||||
e.DELETE("/api/coupontypes").WithHeader("Authorization", jwtyhl5000d). |
||||
Expect().Status(http.StatusMethodNotAllowed) |
||||
e.PATCH("/api/coupontypes").WithHeader("Authorization", jwtyhl5000d). |
||||
Expect().Status(http.StatusMethodNotAllowed) |
||||
}) |
||||
|
||||
Convey("Given a person with no required roles", t, func() { |
||||
e.GET("/api/coupontypes"). |
||||
WithHeader("Authorization", jwtnoissueredeemrole). |
||||
Expect().Status(http.StatusForbidden) |
||||
}) |
||||
|
||||
Convey("Given a right person to get the coupon types", t, func() { |
||||
arr := e.GET("/api/coupontypes"). |
||||
WithHeader("Authorization", jwtyhl5000d). |
||||
Expect().Status(http.StatusOK). |
||||
JSON().Array() |
||||
arr.Length().Gt(0) |
||||
obj := arr.Element(0).Object() |
||||
obj.ContainsKey("name") |
||||
obj.ContainsKey("id") |
||||
obj.ContainsKey("template_id") |
||||
obj.ContainsKey("description") |
||||
obj.ContainsKey("internal_description") |
||||
obj.ContainsKey("state") |
||||
obj.ContainsKey("publisher") |
||||
obj.ContainsKey("visible_start_time") |
||||
obj.ContainsKey("visible_end_time") |
||||
obj.ContainsKey("created_time") |
||||
obj.ContainsKey("deleted_time") |
||||
obj.ContainsKey("rules") |
||||
}) |
||||
} |
||||
|
||||
func Test_POST_coupons(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("Issue coupon A for one consumer", t, func() { |
||||
u := uuid.New().String() |
||||
c, _ := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
_validateCouponWithTypeA(c, u, strings.Join([]string{u, "xx"}, ""), channelID, 0) |
||||
}) |
||||
|
||||
Convey("Issue coupon of type A for multi consumers", t, func() { |
||||
u1 := uuid.New().String() |
||||
u2 := uuid.New().String() |
||||
c1, c2, _, _ := _post2Coupons(e, u1, u2, strings.Join([]string{u1, "xx"}, ""), strings.Join([]string{u2, "yy"}, ""), typeA, channelID) |
||||
|
||||
Convey("the coupons should be same as created", func() { |
||||
_validateCouponWithTypeA(c1, u1, strings.Join([]string{u1, "xx"}, ""), channelID, 0) |
||||
_validateCouponWithTypeA(c2, u2, strings.Join([]string{u2, "yy"}, ""), channelID, 0) |
||||
}) |
||||
|
||||
}) |
||||
} |
||||
|
||||
func Test_Get_coupon(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("Issue coupon A for one consumer", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
// cid := oc.Value("ID").String().Raw()
|
||||
|
||||
Convey("Get the coupon and validate", func() { |
||||
c := _getCouponByID(e, cid) |
||||
_validateCouponWithTypeA(c, u, strings.Join([]string{u, "xx"}, ""), channelID, 0) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Issue coupon of type A for multi consumers", t, func() { |
||||
u1 := uuid.New().String() |
||||
u2 := uuid.New().String() |
||||
_, _, cid1, cid2 := _post2Coupons(e, u1, u2, strings.Join([]string{u1, "xx"}, ""), strings.Join([]string{u2, "yy"}, ""), typeA, channelID) |
||||
|
||||
Convey("Get the coupon c1", func() { |
||||
c1 := _getCouponByID(e, cid1) |
||||
_validateCouponWithTypeA(c1, u1, strings.Join([]string{u1, "xx"}, ""), channelID, 0) |
||||
}) |
||||
Convey("Get the coupon c2", func() { |
||||
c2 := _getCouponByID(e, cid2) |
||||
_validateCouponWithTypeA(c2, u2, strings.Join([]string{u2, "yy"}, ""), channelID, 0) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_Get_coupons(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("Issue coupon A, B for one consumer", t, func() { |
||||
u := uuid.New().String() |
||||
uref := strings.Join([]string{u, "xx"}, "") |
||||
_, cid1 := _post1Coupon(e, u, uref, typeA, channelID, http.StatusOK) |
||||
_post1Coupon(e, u, uref, typeB, channelID, http.StatusOK) |
||||
// _, _, cid1, _ := _post2Coupons(e, u, u, uref, uref, typeA, typeB, channelID)
|
||||
|
||||
Convey("Get the coupons", func() { |
||||
arr := _getCoupons(e, u) |
||||
arr.Length().Equal(2) |
||||
var c1, c2 *httpexpect.Object |
||||
if arr.Element(0).Object().Value("ID").String().Raw() == cid1 { |
||||
c1 = arr.Element(0).Object() |
||||
c2 = arr.Element(1).Object() |
||||
} else { |
||||
c1 = arr.Element(1).Object() |
||||
c2 = arr.Element(0).Object() |
||||
} |
||||
|
||||
Convey("the coupons should be same as created", func() { |
||||
_validateCouponWithTypeA(c1, u, strings.Join([]string{u, "xx"}, ""), channelID, 0) |
||||
_validateCouponWithTypeB(c2, u, strings.Join([]string{u, "xx"}, ""), channelID, 0) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_POST_redemptions(t *testing.T) { |
||||
e := httpexpect.New(t, baseurl) |
||||
|
||||
Convey("Given a coupon for redeem by coupon id", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
|
||||
Convey("Redeem the coupon", func() { |
||||
_redeemCouponByID(e, u, cid, base.RandString(4), http.StatusOK) |
||||
_getCouponByID(e, cid).ContainsKey("State").ValueEqual("State", 2) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given 2 coupons to redeem by coupon type", t, func() { |
||||
u1 := uuid.New().String() |
||||
u2 := uuid.New().String() |
||||
_, _, cid1, cid2 := _post2Coupons(e, u1, u2, strings.Join([]string{u1, "xx"}, ""), strings.Join([]string{u2, "yy"}, ""), typeA, channelID) |
||||
|
||||
Convey("Redeem the coupons", func() { |
||||
extraInfo := base.RandString(4) |
||||
e.POST("/api/redemptions").WithHeader("Authorization", jwtyhl5000d). |
||||
WithForm(map[string]interface{}{ |
||||
"consumerIDs": strings.Join([]string{u1, u2}, ","), |
||||
// "couponID": cid,
|
||||
"couponTypeID": typeA, |
||||
"extraInfo": extraInfo, |
||||
}).Expect().Status(http.StatusOK) |
||||
|
||||
_getCouponByID(e, cid1).ContainsKey("State").ValueEqual("State", 2) |
||||
_getCouponByID(e, cid2).ContainsKey("State").ValueEqual("State", 2) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Redeem coupon should bind correct consumer", t, func() { |
||||
u := uuid.New().String() |
||||
_, cid := _post1Coupon(e, u, strings.Join([]string{u, "xx"}, ""), typeA, channelID, http.StatusOK) |
||||
|
||||
Convey("Redeem the coupon by another one should failed", func() { |
||||
_redeemCouponByID(e, "hacker", cid, base.RandString(4), http.StatusBadRequest) |
||||
_getCouponByID(e, cid).ContainsKey("State").ValueEqual("State", 0) |
||||
}) |
||||
}) |
||||
} |
||||
@ -0,0 +1,232 @@
@@ -0,0 +1,232 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"net/http" |
||||
"strings" |
||||
|
||||
// "fmt"
|
||||
"math/rand" |
||||
|
||||
// "reflect"
|
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/gavv/httpexpect" |
||||
) |
||||
|
||||
var r *rand.Rand = rand.New(rand.NewSource(time.Now().Unix())) |
||||
|
||||
const defaultCouponTypeID string = "678719f5-44a8-4ac8-afd0-288d2f14daf8" |
||||
const anotherCouponTypeID string = "dff0710e-f5af-4ecf-a4b5-cc5599d98030" |
||||
|
||||
var dbConnection *sql.DB |
||||
|
||||
func TestMain(m *testing.M) { |
||||
_setUp() |
||||
m.Run() |
||||
_tearDown() |
||||
} |
||||
|
||||
func _setUp() { |
||||
client := &http.Client{} |
||||
// 激活测试数据,未来会重构
|
||||
req, err := http.NewRequest("GET", baseurl+"/api/apitester", strings.NewReader("name=cjb")) |
||||
if err != nil { |
||||
fmt.Println(err.Error()) |
||||
} |
||||
req.Header.Set("Authorization", jwtyhl5000d) |
||||
resp, err := client.Do(req) |
||||
if err != nil { |
||||
fmt.Println(err.Error()) |
||||
} |
||||
fmt.Println(resp.Status) |
||||
|
||||
dbConnection, err = sql.Open("sqlite3", "../coupon-service/data/data.db?cache=shared&mode=rwc") |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
func _tearDown() { |
||||
} |
||||
|
||||
// func _aCoupon(consumerID string, consumerRefID string, channelID string, couponTypeID string, state State, p map[string]interface{}) *Coupon {
|
||||
// lt := time.Now().Local()
|
||||
|
||||
// var c = Coupon{
|
||||
// ID: uuid.New().String(),
|
||||
// CouponTypeID: couponTypeID,
|
||||
// ConsumerID: consumerID,
|
||||
// ConsumerRefID: consumerRefID,
|
||||
// ChannelID: channelID,
|
||||
// State: state,
|
||||
// Properties: p,
|
||||
// CreatedTime: <,
|
||||
// }
|
||||
// return &c
|
||||
// }
|
||||
|
||||
// func _someCoupons(consumerID string, consumerRefID string, channelID string, couponTypeID string) []*Coupon {
|
||||
// count := r.Intn(10) + 1
|
||||
// cs := make([]*Coupon, 0, count)
|
||||
// for i := 0; i < count; i++ {
|
||||
// state := r.Intn(int(SUnknown))
|
||||
// var p map[string]interface{}
|
||||
// p = make(map[string]interface{}, 1)
|
||||
// p["the_key"] = "the value"
|
||||
// cs = append(cs, _aCoupon(consumerID, consumerRefID, channelID, couponTypeID, State(state), p))
|
||||
// }
|
||||
// return cs
|
||||
// }
|
||||
|
||||
// func _aTransaction(actorID string, couponID string, tt TransType, extraInfo string) *Transaction {
|
||||
// var t = Transaction{
|
||||
// ID: uuid.New().String(),
|
||||
// CouponID: couponID,
|
||||
// ActorID: actorID,
|
||||
// TransType: tt,
|
||||
// ExtraInfo: extraInfo,
|
||||
// CreatedTime: time.Now().Local(),
|
||||
// }
|
||||
// return &t
|
||||
// }
|
||||
|
||||
// func _someTransaction(actorID string, couponID string, extraInfo string) []*Transaction {
|
||||
// count := r.Intn(10) + 1
|
||||
// ts := make([]*Transaction, 0, count)
|
||||
// for i := 0; i < count; i++ {
|
||||
// tt := r.Intn(int(TTUnknownTransaction))
|
||||
// ts = append(ts, _aTransaction(actorID, couponID, TransType(tt), extraInfo))
|
||||
// }
|
||||
// return ts
|
||||
// }
|
||||
|
||||
// func _aRequester(userID string, roles []string, brand string) *base.Requester {
|
||||
// var requester base.Requester
|
||||
// requester.UserID = userID
|
||||
// requester.Roles = map[string]([]string){
|
||||
// "roles": roles,
|
||||
// }
|
||||
// requester.Brand = brand
|
||||
// return &requester
|
||||
// }
|
||||
|
||||
func _validateCouponWithTypeA(c *httpexpect.Object, cid string, cref string, channelID string, state int) { |
||||
__validateBasicCoupon(c, cid, cref, channelID, state) |
||||
c.ContainsKey("CouponTypeID").ValueEqual("CouponTypeID", typeA) |
||||
ps := c.ContainsKey("Properties").Value("Properties").Object() |
||||
bind := ps.ContainsKey("binding_rule_properties").Value("binding_rule_properties").Object() |
||||
nature := bind.ContainsKey("REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR").Value("REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR").Object() |
||||
nature.ContainsKey("unit").ValueEqual("unit", "MONTH") |
||||
nature.ContainsKey("endInAdvance").ValueEqual("endInAdvance", 0) |
||||
rdtimes := bind.ContainsKey("REDEEM_TIMES").Value("REDEEM_TIMES").Object() |
||||
rdtimes.ContainsKey("times").ValueEqual("times", 1) |
||||
} |
||||
|
||||
func _validateCouponWithTypeB(c *httpexpect.Object, cid string, cref string, channelID string, state int) { |
||||
__validateBasicCoupon(c, cid, cref, channelID, state) |
||||
c.ContainsKey("CouponTypeID").ValueEqual("CouponTypeID", typeB) |
||||
|
||||
// ps := c.ContainsKey("Properties").Value("Properties").Object()
|
||||
// bind := ps.ContainsKey("binding_rule_properties").Value("binding_rule_properties").Object()
|
||||
// nature := bind.ContainsKey("REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR").Value("REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR").Object()
|
||||
// nature.ContainsKey("unit").ValueEqual("unit", "MONTH")
|
||||
// nature.ContainsKey("endInAdvance").ValueEqual("endInAdvance", 0)
|
||||
// rdtimes := bind.ContainsKey("REDEEM_TIMES").Value("REDEEM_TIMES").Object()
|
||||
// rdtimes.ContainsKey("times").ValueEqual("times", 1)
|
||||
} |
||||
|
||||
func __validateBasicCoupon(c *httpexpect.Object, cid string, cref string, channelID string, state int) { |
||||
c.ContainsKey("ID").NotEmpty() |
||||
c.ContainsKey("ConsumerID").ValueEqual("ConsumerID", cid) |
||||
c.ContainsKey("ConsumerRefID").ValueEqual("ConsumerRefID", cref) |
||||
c.ContainsKey("ChannelID").ValueEqual("ChannelID", channelID) |
||||
c.ContainsKey("State").ValueEqual("State", state) |
||||
c.ContainsKey("CreatedTime") |
||||
} |
||||
|
||||
func _post1CouponWithToken(e *httpexpect.Expect, u string, uref string, t string, channelID string, statusCode int, token string) (*httpexpect.Object, string) { |
||||
r := e.POST("/api/coupons/").WithHeader("Authorization", token). |
||||
WithForm(map[string]interface{}{ |
||||
"consumerIDs": u, |
||||
"couponTypeID": t, |
||||
"consumerRefIDs": uref, |
||||
"channelID": channelID, |
||||
}).Expect().Status(statusCode) |
||||
|
||||
var arr *httpexpect.Array |
||||
if http.StatusOK == r.Raw().StatusCode { |
||||
arr = r.JSON().Array() |
||||
} else { |
||||
return nil, "" |
||||
} |
||||
|
||||
arr.Length().Equal(1) |
||||
o := arr.Element(0).Object() |
||||
return o, o.Value("ID").String().Raw() |
||||
} |
||||
|
||||
func _post1Coupon(e *httpexpect.Expect, u string, uref string, t string, channelID string, statusCode int) (*httpexpect.Object, string) { |
||||
return _post1CouponWithToken(e, u, uref, t, channelID, statusCode, jwtyhl5000d) |
||||
} |
||||
|
||||
func _post2Coupons(e *httpexpect.Expect, u1 string, u2 string, u1ref string, u2ref string, t string, channelID string) (*httpexpect.Object, *httpexpect.Object, string, string) { |
||||
arr := e.POST("/api/coupons/").WithHeader("Authorization", jwtyhl5000d). |
||||
WithForm(map[string]interface{}{ |
||||
"consumerIDs": strings.Join([]string{u1, u2}, ","), |
||||
"couponTypeID": t, |
||||
"consumerRefIDs": strings.Join([]string{u1ref, u2ref}, ","), |
||||
"channelID": channelID, |
||||
}). |
||||
Expect().Status(http.StatusOK). |
||||
JSON().Array() |
||||
|
||||
arr.Length().Equal(2) |
||||
var c1, c2 *httpexpect.Object |
||||
if arr.Element(0).Object().Value("ConsumerID").String().Raw() == u1 { |
||||
c1 = arr.Element(0).Object() |
||||
c2 = arr.Element(1).Object() |
||||
} else { |
||||
c1 = arr.Element(1).Object() |
||||
c2 = arr.Element(0).Object() |
||||
} |
||||
return c1, c2, c1.Value("ID").String().Raw(), c2.Value("ID").String().Raw() |
||||
} |
||||
|
||||
func _getCouponByID(e *httpexpect.Expect, cid string) *httpexpect.Object { |
||||
return e.GET(strings.Join([]string{"/api/coupons/", cid}, "")).WithHeader("Authorization", jwtyhl5000d). |
||||
Expect().Status(http.StatusOK). |
||||
JSON().Object() |
||||
} |
||||
|
||||
func _getCoupons(e *httpexpect.Expect, u string) *httpexpect.Array { |
||||
arr := e.GET("/api/coupons/").WithHeader("Authorization", jwtyhl5000d). |
||||
WithHeader("consumerID", u). |
||||
Expect().Status(http.StatusOK). |
||||
JSON().Array() |
||||
return arr |
||||
} |
||||
|
||||
func _redeemCouponByID(e *httpexpect.Expect, u string, cid string, extraInfo string, statusCode int) { |
||||
e.POST("/api/redemptions").WithHeader("Authorization", jwtyhl5000d). |
||||
WithForm(map[string]interface{}{ |
||||
"consumerIDs": u, |
||||
"couponID": cid, |
||||
"extraInfo": extraInfo, |
||||
}).Expect().Status(statusCode) |
||||
// v := r.JSON()
|
||||
// fmt.Println(v.String().Raw())
|
||||
} |
||||
|
||||
// func _prepareARandomCouponInDB(consumerID string, consumerRefID string, channelID string, couponType string, propties string) *Coupon {
|
||||
// state := r.Intn(int(SUnknown))
|
||||
// var p map[string]interface{}
|
||||
// p = make(map[string]interface{}, 1)
|
||||
// p["the_key"] = "the value"
|
||||
// c := _aCoupon(consumerID, consumerRefID, channelID, couponType, State(state), p)
|
||||
// sql := fmt.Sprintf(`INSERT INTO coupons VALUES ("%s","%s","%s","%s","%s",%d,"%s","%s")`, c.ID, c.CouponTypeID, c.ConsumerID, c.ConsumerRefID, c.ChannelID, c.State, propties, c.CreatedTime)
|
||||
// _, _ = dbConnection.Exec(sql)
|
||||
// return c
|
||||
// }
|
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
vendor |
||||
web/**/node_modules |
||||
dump.rdb |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
{ |
||||
"javascript.format.insertSpaceBeforeFunctionParenthesis": true, |
||||
"vetur.format.defaultFormatter.js": "vscode-typescript", |
||||
"vetur.format.defaultFormatter.ts": "vscode-typescript" |
||||
} |
||||
@ -0,0 +1,182 @@
@@ -0,0 +1,182 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"log" |
||||
"net/http" |
||||
"strings" |
||||
"sync" |
||||
|
||||
"loreal.com/dit/endpoint" |
||||
"loreal.com/dit/middlewares" |
||||
"loreal.com/dit/module" |
||||
"loreal.com/dit/module/modules/root" |
||||
"loreal.com/dit/utils" |
||||
"loreal.com/dit/utils/task" |
||||
|
||||
"github.com/robfig/cron" |
||||
) |
||||
|
||||
//App - data struct for App & configuration file
|
||||
type App struct { |
||||
Name string |
||||
Description string |
||||
Config *Configuration |
||||
Root *root.Module |
||||
Endpoints map[string]EndpointEntry |
||||
MessageHandlers map[string]func(*module.Message) bool |
||||
AuthProvider middlewares.RoleVerifier |
||||
WebTokenAuthProvider middlewares.WebTokenVerifier |
||||
Scheduler *cron.Cron |
||||
TaskManager *task.Manager |
||||
wg *sync.WaitGroup |
||||
mutex *sync.RWMutex |
||||
Runtime map[string]*RuntimeEnv |
||||
} |
||||
|
||||
//RuntimeEnv - runtime env
|
||||
type RuntimeEnv struct { |
||||
Config *Env |
||||
stmts map[string]*sql.Stmt |
||||
db *sql.DB |
||||
KVStore map[string]interface{} |
||||
mutex *sync.RWMutex |
||||
} |
||||
|
||||
//Get - get value from kvstore in memory
|
||||
func (rt *RuntimeEnv) Get(key string) (value interface{}, ok bool) { |
||||
rt.mutex.RLock() |
||||
defer rt.mutex.RUnlock() |
||||
value, ok = rt.KVStore[key] |
||||
return |
||||
} |
||||
|
||||
//Retrive - get value from kvstore in memory, and delete it
|
||||
func (rt *RuntimeEnv) Retrive(key string) (value interface{}, ok bool) { |
||||
rt.mutex.Lock() |
||||
defer rt.mutex.Unlock() |
||||
value, ok = rt.KVStore[key] |
||||
if ok { |
||||
delete(rt.KVStore, key) |
||||
} |
||||
return |
||||
} |
||||
|
||||
//Set - set value to kvstore in memory
|
||||
func (rt *RuntimeEnv) Set(key string, value interface{}) { |
||||
rt.mutex.Lock() |
||||
defer rt.mutex.Unlock() |
||||
rt.KVStore[key] = value |
||||
} |
||||
|
||||
//EndpointEntry - endpoint registry entry
|
||||
type EndpointEntry struct { |
||||
Handler func(http.ResponseWriter, *http.Request) |
||||
Middlewares []endpoint.ServerMiddleware |
||||
} |
||||
|
||||
//NewApp - create new app
|
||||
func NewApp(name, description string, config *Configuration) *App { |
||||
if config == nil { |
||||
log.Println("Missing configuration data") |
||||
return nil |
||||
} |
||||
endpoint.SetPrometheus(strings.Replace(name, "-", "_", -1)) |
||||
app := &App{ |
||||
Name: name, |
||||
Description: description, |
||||
Config: config, |
||||
Root: root.NewModule(name, description, config.Prefix), |
||||
Endpoints: make(map[string]EndpointEntry, 0), |
||||
MessageHandlers: make(map[string]func(*module.Message) bool, 0), |
||||
Scheduler: cron.New(), |
||||
wg: &sync.WaitGroup{}, |
||||
mutex: &sync.RWMutex{}, |
||||
Runtime: make(map[string]*RuntimeEnv), |
||||
} |
||||
app.TaskManager = task.NewManager(app, 100) |
||||
return app |
||||
} |
||||
|
||||
//Init - app initialization
|
||||
func (a *App) Init() { |
||||
if a.Config != nil { |
||||
a.Config.fixPrefix() |
||||
for _, env := range a.Config.Envs { |
||||
utils.MakeFolder(env.DataFolder) |
||||
a.Runtime[env.Name] = &RuntimeEnv{ |
||||
Config: env, |
||||
KVStore: make(map[string]interface{}, 1024), |
||||
mutex: &sync.RWMutex{}, |
||||
} |
||||
} |
||||
a.InitDB() |
||||
} |
||||
a.registerEndpoints() |
||||
a.registerMessageHandlers() |
||||
a.registerTasks() |
||||
// utils.LoadOrCreateJSON("./saved_status.json", &a.Status)
|
||||
a.Root.OnStop = func(p *module.Module) { |
||||
a.TaskManager.SendAll("stop") |
||||
a.wg.Wait() |
||||
} |
||||
a.Root.OnDispose = func(p *module.Module) { |
||||
for _, env := range a.Runtime { |
||||
if env.db != nil { |
||||
log.Println("Close sqlite for", env.Config.Name) |
||||
env.db.Close() |
||||
} |
||||
} |
||||
// utils.SaveJSON(a.Status, "./saved_status.json")
|
||||
} |
||||
} |
||||
|
||||
//registerEndpoints - Register Endpoints
|
||||
func (a *App) registerEndpoints() { |
||||
a.initEndpoints() |
||||
for path, entry := range a.Endpoints { |
||||
if entry.Middlewares == nil { |
||||
entry.Middlewares = a.getDefaultMiddlewares(path) |
||||
} |
||||
a.Root.MountingPoints[path] = endpoint.DecorateServer( |
||||
endpoint.Impl(entry.Handler), |
||||
entry.Middlewares..., |
||||
) |
||||
} |
||||
} |
||||
|
||||
//registerMessageHandlers - Register Message Handlers
|
||||
func (a *App) registerMessageHandlers() { |
||||
a.initMessageHandlers() |
||||
for path, handler := range a.MessageHandlers { |
||||
a.Root.AddMessageHandler(path, handler) |
||||
} |
||||
} |
||||
|
||||
//StartScheduler - register and start the scheduled tasks
|
||||
func (a *App) StartScheduler() { |
||||
if a.Scheduler == nil { |
||||
a.Scheduler = cron.New() |
||||
} else { |
||||
a.Scheduler.Stop() |
||||
a.Scheduler = cron.New() |
||||
} |
||||
for _, item := range a.Config.ScheduledTasks { |
||||
log.Println("[INFO] - Adding task:", item.Task) |
||||
func() { |
||||
s := item.Schedule |
||||
t := item.Task |
||||
a.Scheduler.AddFunc(s, func() { |
||||
a.TaskManager.RunTask(t, item.DefaultArgs...) |
||||
}) |
||||
}() |
||||
} |
||||
a.Scheduler.Start() |
||||
} |
||||
|
||||
//ListenAndServe - Start app
|
||||
func (a *App) ListenAndServe() { |
||||
a.Init() |
||||
a.StartScheduler() |
||||
a.Root.ListenAndServe(a.Config.Address) |
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
package main |
||||
|
||||
//GORoutingNumberForWechat - Total GO Routing # for send process
|
||||
const GORoutingNumberForWechat = 10 |
||||
|
||||
//ScheduledTask - command and parameters to run
|
||||
/* Schedule Spec: |
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
*** |
||||
*** corn example ***: |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
*/ |
||||
var cfg = Configuration{ |
||||
Address: ":1501", |
||||
Prefix: "/", |
||||
JwtKey: "3efe5d7d2b4c477db53a5fba0a31a6c5", |
||||
AppTitle: "客服查询系统", |
||||
UpstreamURL: "https://dl-api.lorealchina.com/api/interface/", |
||||
UpstreamClientID: "buycool", |
||||
UpstreamClientSecret: "7h48!D!M", |
||||
UpstreamUserName: "buycoolcs", |
||||
UpstreamPassword: "B0^WRnJZCByrJMMk", |
||||
Production: false, |
||||
Envs: []*Env{ |
||||
{ |
||||
Name: "prod", |
||||
SqliteDB: "data.db", |
||||
DataFolder: "./data/", |
||||
}, |
||||
{ |
||||
Name: "pp", |
||||
SqliteDB: "ppdata.db", |
||||
DataFolder: "./data/", |
||||
}, |
||||
}, |
||||
ScheduledTasks: []*ScheduledTask{ |
||||
{Schedule: "0 0 0 * * *", Task: "daily-maintenance", DefaultArgs: []string{}}, |
||||
{Schedule: "0 10 0 * * *", Task: "daily-maintenance-pp", DefaultArgs: []string{}}, |
||||
}, |
||||
} |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"strings" |
||||
) |
||||
|
||||
//Configuration - app configuration
|
||||
type Configuration struct { |
||||
Address string `json:"address,omitempty"` |
||||
Prefix string `json:"prefix,omitempty"` |
||||
AppTitle string `json:"app-title"` |
||||
JwtKey string `json:"jwt-key,omitempty"` |
||||
UpstreamURL string `json:"upstream-url"` |
||||
UpstreamClientID string `json:"upstream-client-id"` |
||||
UpstreamClientSecret string `json:"upstream-client-secret"` |
||||
UpstreamUserName string `json:"upstream-username"` |
||||
UpstreamPassword string `json:"upstream-password"` |
||||
Production bool `json:"production,omitempty"` |
||||
Envs []*Env `json:"envs,omitempty"` |
||||
ScheduledTasks []*ScheduledTask `json:"scheduled-tasks,omitempty"` |
||||
} |
||||
|
||||
//Env - env configuration
|
||||
type Env struct { |
||||
Name string `json:"name,omitempty"` |
||||
SqliteDB string `json:"sqlite-db,omitempty"` |
||||
DataFolder string `json:"data,omitempty"` |
||||
} |
||||
|
||||
//ScheduledTask - command and parameters to run
|
||||
/* Schedule Spec: |
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
*** |
||||
*** corn example ***: |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
*/ |
||||
//ScheduledTask - Scheduled Task
|
||||
type ScheduledTask struct { |
||||
Schedule string `json:"schedule,omitempty"` |
||||
Task string `json:"task,omitempty"` |
||||
DefaultArgs []string `json:"default-args,omitempty"` |
||||
} |
||||
|
||||
func (c *Configuration) fixPrefix() { |
||||
if !strings.HasPrefix(c.Prefix, "/") { |
||||
c.Prefix = "/" + c.Prefix |
||||
} |
||||
if !strings.HasSuffix(c.Prefix, "/") { |
||||
c.Prefix = c.Prefix + "/" |
||||
} |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"log" |
||||
// _ "github.com/mattn/go-sqlite3"
|
||||
) |
||||
|
||||
//InitDB - initialized database
|
||||
func (a *App) InitDB() { |
||||
//init database tables
|
||||
sqlStmts := []string{ |
||||
//PV计数
|
||||
`CREATE TABLE IF NOT EXISTS Visit ( |
||||
UserID INTEGER DEFAULT 0, |
||||
PageID TEXT DEFAULT '', |
||||
Scene TEXT DEFAULT '', |
||||
State TEXT INTEGER DEFAULT 0, |
||||
PV INTEGER DEFAULT 0, |
||||
CreateAt DATETIME, |
||||
Recent DATETIME |
||||
);`, |
||||
"CREATE INDEX IF NOT EXISTS idxVisitUserID ON Visit(UserID);", |
||||
"CREATE INDEX IF NOT EXISTS idxVisitPageID ON Visit(PageID);", |
||||
"CREATE INDEX IF NOT EXISTS idxVisitScene ON Visit(Scene);", |
||||
"CREATE INDEX IF NOT EXISTS idxVisitState ON Visit(State);", |
||||
"CREATE INDEX IF NOT EXISTS idxVisitCreateAt ON Visit(CreateAt);", |
||||
} |
||||
|
||||
var err error |
||||
for _, env := range a.Runtime { |
||||
env.db, err = sql.Open("sqlite3", fmt.Sprintf("%s%s?cache=shared&mode=rwc", env.Config.DataFolder, env.Config.SqliteDB)) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
log.Printf("[INFO] - Initialization DB for [%s]...\n", env.Config.Name) |
||||
for _, sqlStmt := range sqlStmts { |
||||
_, err := env.db.Exec(sqlStmt) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [InitDB] %q: %s\n", err, sqlStmt) |
||||
return |
||||
} |
||||
} |
||||
env.stmts = make(map[string]*sql.Stmt, 0) |
||||
log.Printf("[INFO] - DB for [%s] ready!\n", env.Config.Name) |
||||
} |
||||
} |
||||
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"os/exec" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
//debugHandler - vehicle plate management
|
||||
//endpoint: debug
|
||||
//method: GET
|
||||
func (a *App) debugHandler(w http.ResponseWriter, r *http.Request) { |
||||
switch r.Method { |
||||
case "GET": |
||||
q := r.URL.Query() |
||||
module := sanitizePolicy.Sanitize(q.Get("module")) |
||||
if module == "" { |
||||
module = "*" |
||||
} |
||||
DEBUG = "" != sanitizePolicy.Sanitize(q.Get("debug")) |
||||
INFOLEVEL, _ = strconv.Atoi(sanitizePolicy.Sanitize(q.Get("level"))) |
||||
LOGLEVEL, _ = strconv.Atoi(sanitizePolicy.Sanitize(q.Get("log-level"))) |
||||
var result struct { |
||||
Module string `json:"module"` |
||||
DebugFlag bool `json:"debug-flag"` |
||||
InfoLevel int `json:"info-level"` |
||||
LogLevel int `json:"log-level"` |
||||
} |
||||
switch module { |
||||
case "*": |
||||
} |
||||
result.Module = module |
||||
result.DebugFlag = DEBUG |
||||
result.InfoLevel = INFOLEVEL |
||||
result.LogLevel = LOGLEVEL |
||||
outputJSON(w, result) |
||||
default: |
||||
outputJSON(w, map[string]interface{}{ |
||||
"errcode": -100, |
||||
"errmsg": "Method not acceptable", |
||||
}) |
||||
} |
||||
} |
||||
|
||||
//feUpgradeHandler - upgrade fe
|
||||
//endpoint: maintenance/fe/upgrade
|
||||
//method: GET
|
||||
func (a *App) feUpgradeHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
http.Error(w, "Not Acceptable", http.StatusNotAcceptable) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
_ = q |
||||
username := sanitizePolicy.Sanitize(q.Get("u")) |
||||
password := q.Get("p") |
||||
q.Encode() |
||||
var resp struct { |
||||
ErrCode int `json:"errcode,omitempty"` |
||||
ErrMessage string `json:"errmsg,omitempty"` |
||||
Data string `json:"data,omitempty"` |
||||
} |
||||
const feFolder = "./fe/" |
||||
var err error |
||||
var strPullURL string |
||||
if strPullURL, err = getGitURL(feFolder); err != nil { |
||||
log.Println(err) |
||||
} |
||||
log.Println(strPullURL) |
||||
if strPullURL == "" { |
||||
resp.Data = "empty pull url" |
||||
outputJSON(w, resp) |
||||
return |
||||
} |
||||
buffer := bytes.NewBuffer(nil) |
||||
if pullURL, err := url.Parse(strPullURL); err == nil { |
||||
pullURL.User = url.UserPassword(username, password) |
||||
strPullURL = pullURL.String() |
||||
resp.Data += pullURL.String() |
||||
} |
||||
|
||||
if err := runShellCmd(nil, buffer, buffer, feFolder, "git", "pull", strPullURL); err != nil { |
||||
logError(err, "git pull") |
||||
} |
||||
if err := runShellCmd(nil, buffer, buffer, feFolder, "npm", "run", "build"); err != nil { |
||||
logError(err, "git pull") |
||||
} |
||||
resp.Data = buffer.String() |
||||
outputText(w, buffer.Bytes()) |
||||
} |
||||
|
||||
func runShellCmd(stdin io.Reader, stdout, stderr io.Writer, workingDir, cmd string, args ...string) error { |
||||
di, err := os.Stat(workingDir) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !di.IsDir() { |
||||
return fmt.Errorf("invalid working dir") |
||||
} |
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) |
||||
defer cancel() |
||||
c := exec.CommandContext(ctx, cmd, args...) |
||||
c.Stdin = stdin |
||||
c.Stdout = stdout |
||||
c.Stderr = stderr |
||||
c.Dir = workingDir |
||||
if err := c.Run(); err != nil { |
||||
log.Println("[ERR] - run", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func getGitURL(path string) (string, error) { |
||||
fi, err := os.Stat(path + ".git/") |
||||
if os.IsNotExist(err) || !fi.IsDir() { |
||||
return "", err |
||||
} |
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) |
||||
cmd := exec.CommandContext(ctx, "git", "remote", "get-url", "--push", "origin") |
||||
cmd.Dir = path |
||||
rc, err := cmd.StdoutPipe() |
||||
if err != nil { |
||||
cancel() |
||||
log.Println(err) |
||||
return "", err |
||||
} |
||||
go func(cancel context.CancelFunc) { |
||||
if err := cmd.Run(); err != nil { |
||||
log.Println("[ERR] - run", err) |
||||
} |
||||
defer cancel() |
||||
}(cancel) |
||||
|
||||
data, err := ioutil.ReadAll(rc) |
||||
if err := rc.Close(); err != nil { |
||||
return strings.TrimRight(string(data), "\r\n"), err |
||||
} |
||||
return strings.TrimRight(string(data), "\r\n"), nil |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"log" |
||||
"net/http" |
||||
"strings" |
||||
) |
||||
|
||||
/* 以下为具体 Endpoint 实现代码 */ |
||||
|
||||
//kvstoreHandler - get value from kvstore in runtime
|
||||
//endpoint: /api/kvstore
|
||||
//method: GET
|
||||
func (a *App) gatewayHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodPost { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -1, |
||||
ErrMessage: "not acceptable", |
||||
}) |
||||
return |
||||
} |
||||
if err := r.ParseForm(); err != nil { |
||||
log.Printf("[ERR] - [gatewayHandler][ParseForm], err: %v", err) |
||||
} |
||||
path := sanitizePolicy.Sanitize(r.PostFormValue("path")) |
||||
r.PostForm.Del("path") |
||||
|
||||
payload := map[string]interface{}{} |
||||
//prepare payload
|
||||
for k, v := range r.PostForm { |
||||
if v == nil || len(v) == 0 { |
||||
continue |
||||
} |
||||
value := sanitizePolicy.Sanitize(v[0]) |
||||
r.PostForm.Set(k, value) |
||||
payload[k] = value |
||||
} |
||||
bodyBuffer := bytes.NewBuffer(nil) |
||||
enc := json.NewEncoder(bodyBuffer) |
||||
if err := enc.Encode(payload); err != nil { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -2, |
||||
ErrMessage: "500", |
||||
}) |
||||
return |
||||
} |
||||
|
||||
req, err := http.NewRequest(http.MethodPost, a.Config.UpstreamURL+path, bytes.NewReader(bodyBuffer.Bytes())) |
||||
if err != nil { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -3, |
||||
ErrMessage: "500", |
||||
}) |
||||
return |
||||
} |
||||
account := a.getAPIAccount() |
||||
req.Header.Add("Content-Type", "application/json;charset=utf-8") |
||||
accessToken := account.GetToken(false) |
||||
retry: |
||||
if accessToken == "" { |
||||
log.Println("[ERR] - [gatewayHandler] Cannot get token") |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -4, |
||||
ErrMessage: "500", |
||||
}) |
||||
return |
||||
} |
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) |
||||
resp, err := httpClient.Do(req) |
||||
if err != nil { |
||||
log.Println("[ERR] - [gatewayHandler] http.do err:", err) |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -5, |
||||
ErrMessage: "502", |
||||
}) |
||||
return |
||||
} |
||||
defer resp.Body.Close() |
||||
bodyObject := map[string]interface{}{} |
||||
dec := json.NewDecoder(resp.Body) |
||||
if err := dec.Decode(&bodyObject); err != nil { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -6, |
||||
ErrMessage: "500", |
||||
}) |
||||
return |
||||
} |
||||
if errCode, ok := bodyObject["error"]; ok { |
||||
strErr, _ := errCode.(string) |
||||
switch strings.ToLower(strErr) { |
||||
case "invalid_token": |
||||
accessToken = account.GetToken(true) |
||||
goto retry |
||||
case "not found": |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -7, |
||||
ErrMessage: "404", |
||||
}) |
||||
return |
||||
default: |
||||
msg, _ := bodyObject["error_description"] |
||||
log.Printf("[ERR] - [interface][wg] errcode: %s, msg: %v", strErr, msg) |
||||
} |
||||
} |
||||
//log.Println("[OUTPUT] - ", bodyObject)
|
||||
outputJSON(w, bodyObject) |
||||
} |
||||
@ -0,0 +1,302 @@
@@ -0,0 +1,302 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"html/template" |
||||
"log" |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"loreal.com/dit/endpoint" |
||||
"loreal.com/dit/middlewares" |
||||
|
||||
"loreal.com/dit/cmd/ceh-cs-portal/restful" |
||||
|
||||
"github.com/microcosm-cc/bluemonday" |
||||
) |
||||
|
||||
// var seededRand *rand.Rand
|
||||
var sanitizePolicy *bluemonday.Policy |
||||
|
||||
var errorTemplate *template.Template |
||||
|
||||
func init() { |
||||
// seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sanitizePolicy = bluemonday.UGCPolicy() |
||||
|
||||
var err error |
||||
errorTemplate, _ = template.ParseFiles("./template/error.tpl") |
||||
if err != nil { |
||||
log.Panic("[ERR] - Parsing error template", err) |
||||
} |
||||
} |
||||
|
||||
func brandFilter(r *http.Request, item *map[string]interface{}) bool { |
||||
roles := r.Header.Get("roles") |
||||
if roles == "admin" { |
||||
return false |
||||
} |
||||
loginBrand := r.Header.Get("brand") |
||||
if loginBrand == "" { |
||||
return false |
||||
} |
||||
targetBrand, _ := ((*item)["brand"]).(*string) |
||||
return *targetBrand != "" && *targetBrand != loginBrand |
||||
} |
||||
|
||||
func (a *App) initEndpoints() { |
||||
rt := a.getRuntime("prod") |
||||
a.Endpoints = map[string]EndpointEntry{ |
||||
"api/kvstore": {Handler: a.kvstoreHandler, Middlewares: a.noAuthMiddlewares("api/kvstore")}, |
||||
"api/visit": {Handler: a.pvHandler}, |
||||
"error": {Handler: a.errorHandler, Middlewares: a.noAuthMiddlewares("error")}, |
||||
"debug": {Handler: a.debugHandler}, |
||||
"maintenance/fe/upgrade": {Handler: a.feUpgradeHandler}, |
||||
"api/gw": {Handler: a.gatewayHandler}, |
||||
"api/brand/": { |
||||
Handler: restful.NewHandler( |
||||
"brand", |
||||
restful.NewSQLiteAdapter(rt.db, |
||||
rt.mutex, |
||||
"Brand", |
||||
Brand{}, |
||||
), |
||||
).ServeHTTP, |
||||
}, |
||||
// "api/customer/": {
|
||||
// Handler: restful.NewHandler(
|
||||
// "customer",
|
||||
// restful.NewSQLiteAdapter(rt.db,
|
||||
// rt.mutex,
|
||||
// "Customer",
|
||||
// Customer{},
|
||||
// ),
|
||||
// ).SetFilter(storeFilter).ServeHTTP,
|
||||
// },
|
||||
} |
||||
|
||||
postPrepareDB(rt) |
||||
} |
||||
|
||||
//noAuthMiddlewares - middlewares without auth
|
||||
func (a *App) noAuthMiddlewares(path string) []endpoint.ServerMiddleware { |
||||
return []endpoint.ServerMiddleware{ |
||||
middlewares.NoCache(), |
||||
middlewares.ServerInstrumentation(path, endpoint.RequestCounter, endpoint.LatencyHistogram, endpoint.DurationsSummary), |
||||
} |
||||
} |
||||
|
||||
// //webTokenAuthMiddlewares - middlewares auth by token
|
||||
// func (a *App) webTokenAuthMiddlewares(path string) []endpoint.ServerMiddleware {
|
||||
// return []endpoint.ServerMiddleware{
|
||||
// middlewares.NoCache(),
|
||||
// middlewares.WebTokenAuth(a.WebTokenAuthProvider),
|
||||
// middlewares.ServerInstrumentation(path, endpoint.RequestCounter, endpoint.LatencyHistogram, endpoint.DurationsSummary),
|
||||
// }
|
||||
// }
|
||||
|
||||
//getDefaultMiddlewares - middlewares installed by defaults
|
||||
func (a *App) getDefaultMiddlewares(path string) []endpoint.ServerMiddleware { |
||||
return []endpoint.ServerMiddleware{ |
||||
middlewares.NoCache(), |
||||
middlewares.WebTokenAuth(a.WebTokenAuthProvider), |
||||
// middlewares.BasicAuthOrTokenAuthWithRole(a.AuthProvider, "", "user,admin"),
|
||||
middlewares.ServerInstrumentation( |
||||
path, |
||||
endpoint.RequestCounter, |
||||
endpoint.LatencyHistogram, |
||||
endpoint.DurationsSummary, |
||||
), |
||||
} |
||||
} |
||||
|
||||
func (a *App) getEnv(appid string) string { |
||||
if appid == "" { |
||||
if a.Config.Production { |
||||
return "prod" |
||||
} |
||||
return "pp" |
||||
} |
||||
if appid == "ceh" { |
||||
return "prod" |
||||
} |
||||
return "pp" |
||||
} |
||||
|
||||
/* 以下为具体 Endpoint 实现代码 */ |
||||
|
||||
//errorHandler - query error info
|
||||
//endpoint: error
|
||||
//method: GET
|
||||
func (a *App) errorHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
http.Error(w, "Not Acceptable", http.StatusNotAcceptable) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
title := sanitizePolicy.Sanitize(q.Get("title")) |
||||
errmsg := sanitizePolicy.Sanitize(q.Get("errmsg")) |
||||
|
||||
if err := errorTemplate.Execute(w, map[string]interface{}{ |
||||
"title": title, |
||||
"errmsg": errmsg, |
||||
}); err != nil { |
||||
log.Println("[ERR] - errorTemplate error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
} |
||||
} |
||||
|
||||
/* 以下为具体 Endpoint 实现代码 */ |
||||
|
||||
//kvstoreHandler - get value from kvstore in runtime
|
||||
//endpoint: /api/kvstore
|
||||
//method: GET
|
||||
func (a *App) kvstoreHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -100, |
||||
ErrMessage: "Method not acceptable", |
||||
}) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
ticket := q.Get("ticket") |
||||
env := a.getEnv(q.Get("appid")) |
||||
rt := a.getRuntime(env) |
||||
if rt == nil { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -1, |
||||
ErrMessage: "invalid appid", |
||||
}) |
||||
return |
||||
} |
||||
var result struct { |
||||
Value interface{} `json:"value"` |
||||
} |
||||
var ok bool |
||||
var v interface{} |
||||
v, ok = rt.Retrive(ticket) |
||||
if !ok { |
||||
outputJSON(w, APIStatus{ |
||||
ErrCode: -2, |
||||
ErrMessage: "invalid ticket", |
||||
}) |
||||
return |
||||
} |
||||
switch val := v.(type) { |
||||
case chan interface{}: |
||||
// log.Println("[Hu Bin] - Get Value Chan:", val)
|
||||
result.Value = <-val |
||||
// log.Println("[Hu Bin] - Get Value from Chan:", result.Value)
|
||||
default: |
||||
// log.Println("[Hu Bin] - Get Value:", val)
|
||||
result.Value = val |
||||
} |
||||
outputJSON(w, result) |
||||
} |
||||
|
||||
//pvHandler - record PV/UV
|
||||
//endpoint: /api/visit
|
||||
//method: GET
|
||||
func (a *App) pvHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": -1, |
||||
"msg": "Not support", |
||||
}) |
||||
return |
||||
} |
||||
q := r.URL.Query() |
||||
rt := a.getRuntime(r.PostForm.Get("env")) |
||||
if rt == nil { |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": -2, |
||||
"msg": "Invalid APPID", |
||||
}) |
||||
return |
||||
} |
||||
userid, _ := strconv.ParseInt(sanitizePolicy.Sanitize(r.PostForm.Get("userid")), 10, 64) |
||||
pageid := sanitizePolicy.Sanitize(q.Get("pageid")) |
||||
scene := sanitizePolicy.Sanitize(q.Get("scene")) |
||||
visitState, _ := strconv.Atoi(sanitizePolicy.Sanitize(q.Get("type"))) |
||||
|
||||
if err := a.recordPV( |
||||
rt, |
||||
userid, |
||||
pageid, |
||||
scene, |
||||
visitState, |
||||
); err != nil { |
||||
log.Println("[ERR] - [EP][api/visit], err:", err) |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": -3, |
||||
"msg": "internal error", |
||||
}) |
||||
return |
||||
} |
||||
outputJSON(w, map[string]interface{}{ |
||||
"code": 0, |
||||
"msg": "ok", |
||||
}) |
||||
} |
||||
|
||||
//CSV BOM
|
||||
//file.Write([]byte{0xef, 0xbb, 0xbf})
|
||||
|
||||
func outputExcel(w http.ResponseWriter, b []byte, filename string) { |
||||
w.Header().Add("Content-Disposition", "attachment; filename="+filename) |
||||
//w.Header().Add("Content-Type", "application/vnd.ms-excel")
|
||||
w.Header().Add("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") |
||||
// w.Header().Add("Content-Transfer-Encoding", "binary")
|
||||
w.Write(b) |
||||
} |
||||
|
||||
func outputText(w http.ResponseWriter, b []byte) { |
||||
w.Header().Add("Content-Type", "text/plain;charset=utf-8") |
||||
w.Write(b) |
||||
} |
||||
|
||||
func showError(w http.ResponseWriter, r *http.Request, title, message string) { |
||||
if err := errorTemplate.Execute(w, map[string]interface{}{ |
||||
"title": title, |
||||
"errmsg": message, |
||||
}); err != nil { |
||||
log.Println("[ERR] - errorTemplate error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
} |
||||
} |
||||
|
||||
//postPrepareDB - initialized database after init endpoints
|
||||
func postPrepareDB(rt *RuntimeEnv) { |
||||
//init database tables
|
||||
sqlStmts := []string{ |
||||
// `CREATE TRIGGER IF NOT EXISTS insert_fulfill INSERT ON fulfillment
|
||||
// BEGIN
|
||||
// UPDATE CustomerOrder SET qtyfulfilled=qtyfulfilled+new.quantity WHERE id=new.orderid;
|
||||
// END;`,
|
||||
// `CREATE TRIGGER IF NOT EXISTS delete_fulfill DELETE ON fulfillment
|
||||
// BEGIN
|
||||
// UPDATE CustomerOrder SET qtyfulfilled=qtyfulfilled-old.quantity WHERE id=old.orderid;
|
||||
// END;`,
|
||||
// `CREATE TRIGGER IF NOT EXISTS before_update_fulfill BEFORE UPDATE ON fulfillment
|
||||
// BEGIN
|
||||
// UPDATE CustomerOrder SET qtyfulfilled=qtyfulfilled-old.quantity WHERE id=old.orderid;
|
||||
// END;`,
|
||||
// `CREATE TRIGGER IF NOT EXISTS after_update_fulfill AFTER UPDATE ON fulfillment
|
||||
// BEGIN
|
||||
// UPDATE CustomerOrder SET qtyfulfilled=qtyfulfilled+new.quantity WHERE id=new.orderid;
|
||||
// END;`,
|
||||
// "CREATE UNIQUE INDEX IF NOT EXISTS uidxOpenID ON WxUser(OpenID);",
|
||||
} |
||||
|
||||
log.Printf("[INFO] - Post Prepare DB for [%s]...\n", rt.Config.Name) |
||||
for _, sqlStmt := range sqlStmts { |
||||
_, err := rt.db.Exec(sqlStmt) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [PrepareDB] %q: %s\n", err, sqlStmt) |
||||
return |
||||
} |
||||
} |
||||
rt.stmts = make(map[string]*sql.Stmt, 0) |
||||
log.Printf("[INFO] - DB for [%s] prepared!\n", rt.Config.Name) |
||||
} |
||||
@ -0,0 +1,218 @@
@@ -0,0 +1,218 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"strings" |
||||
) |
||||
|
||||
func (a *App) getUserID( |
||||
runtime *RuntimeEnv, |
||||
UID string, |
||||
userID *int64, |
||||
) (err error) { |
||||
*userID = -1 |
||||
const stmtNameGet = "GetUserID" |
||||
const stmtSQLGet = "SELECT ID FROM User WHERE UID=?;" |
||||
stmtGet := a.getStmt(runtime, stmtNameGet) |
||||
if stmtGet == nil { |
||||
//lazy setup for stmt
|
||||
if stmtGet, err = a.setStmt(runtime, stmtNameGet, stmtSQLGet); err != nil { |
||||
return |
||||
} |
||||
} |
||||
runtime.mutex.Lock() |
||||
defer runtime.mutex.Unlock() |
||||
//get customerID
|
||||
if err = stmtGet.QueryRow( |
||||
UID, |
||||
).Scan(userID); err != nil { |
||||
return err |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (a *App) recordWxUser( |
||||
runtime *RuntimeEnv, |
||||
openID, nickName, avatar, scene, pageID string, |
||||
customerID *int64, |
||||
) (err error) { |
||||
*customerID = -1 |
||||
const stmtNameAdd = "AddUser" |
||||
const stmtSQLAdd = "INSERT INTO User (OpenID,NickName,Avatar,Scene,CreateAt) VALUES (?,?,?,?,datetime('now','localtime'));" |
||||
const stmtNameGet = "GetWxUserID" |
||||
const stmtSQLGet = "SELECT ID FROM User WHERE OpenID=?;" |
||||
const stmtNameRecord = "RecordVisit" |
||||
const stmtSQLRecord = "UPDATE User SET PV=PV+1 WHERE ID=?;" |
||||
const stmtNameNewPV = "InsertVisit" |
||||
const stmtSQLNewPV = "INSERT INTO Visit (UserID,PageID,Scene,CreateAt,Recent,PV,State) VALUES (?,?,?,datetime('now','localtime'),datetime('now','localtime'),1,1);" |
||||
const stmtNamePV = "RecordPV" |
||||
const stmtSQLPV = "UPDATE Visit SET PV=PV+1,Recent=datetime('now','localtime'),State=1 WHERE WxUserID=? AND PageID=? AND Scene=?;" |
||||
stmtAdd := a.getStmt(runtime, stmtNameAdd) |
||||
if stmtAdd == nil { |
||||
//lazy setup for stmt
|
||||
if stmtAdd, err = a.setStmt(runtime, stmtNameAdd, stmtSQLAdd); err != nil { |
||||
return |
||||
} |
||||
} |
||||
stmtGet := a.getStmt(runtime, stmtNameGet) |
||||
if stmtGet == nil { |
||||
//lazy setup for stmt
|
||||
if stmtGet, err = a.setStmt(runtime, stmtNameGet, stmtSQLGet); err != nil { |
||||
return |
||||
} |
||||
} |
||||
stmtRecord := a.getStmt(runtime, stmtNameRecord) |
||||
if stmtRecord == nil { |
||||
if stmtRecord, err = a.setStmt(runtime, stmtNameRecord, stmtSQLRecord); err != nil { |
||||
return |
||||
} |
||||
} |
||||
stmtNewPV := a.getStmt(runtime, stmtNameNewPV) |
||||
if stmtNewPV == nil { |
||||
if stmtNewPV, err = a.setStmt(runtime, stmtNameNewPV, stmtSQLNewPV); err != nil { |
||||
return |
||||
} |
||||
} |
||||
stmtPV := a.getStmt(runtime, stmtNamePV) |
||||
if stmtPV == nil { |
||||
if stmtPV, err = a.setStmt(runtime, stmtNamePV, stmtSQLPV); err != nil { |
||||
return |
||||
} |
||||
} |
||||
runtime.mutex.Lock() |
||||
defer runtime.mutex.Unlock() |
||||
tx, err := runtime.db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
//Add user
|
||||
stmtAdd = tx.Stmt(stmtAdd) |
||||
result, err := stmtAdd.Exec( |
||||
openID, |
||||
nickName, |
||||
avatar, |
||||
scene, |
||||
) |
||||
if err != nil && !strings.HasPrefix(err.Error(), "UNIQUE") { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
//get customerID
|
||||
if result == nil { |
||||
//find user
|
||||
stmtGet = tx.Stmt(stmtGet) |
||||
if err = stmtGet.QueryRow( |
||||
openID, |
||||
).Scan(customerID); err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
} else { |
||||
*customerID, err = result.LastInsertId() |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
} |
||||
//record customer visit
|
||||
//log.Println("[INFO] - Add user:", *customerID)
|
||||
stmtRecord = tx.Stmt(stmtRecord) |
||||
_, err = stmtRecord.Exec(*customerID) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
stmtPV = tx.Stmt(stmtPV) |
||||
pvResult, err := stmtPV.Exec( |
||||
*customerID, |
||||
pageID, |
||||
scene, |
||||
) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
cnt, err := pvResult.RowsAffected() |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
if cnt > 0 { |
||||
tx.Commit() |
||||
return |
||||
} |
||||
stmtNewPV = tx.Stmt(stmtNewPV) |
||||
_, err = stmtNewPV.Exec( |
||||
*customerID, |
||||
pageID, |
||||
scene, |
||||
) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
tx.Commit() |
||||
return |
||||
} |
||||
|
||||
func (a *App) recordPV( |
||||
runtime *RuntimeEnv, |
||||
customerID int64, |
||||
pageID, scene string, |
||||
visitState int, |
||||
) (err error) { |
||||
const stmtNameNewPV = "InsertVisit1" |
||||
const stmtSQLNewPV = "INSERT INTO Visit (CustomerID,PageID,Scene,CreateAt,Recent,PV,State) VALUES (?,?,?,datetime('now','localtime'),datetime('now','localtime'),1,?);" |
||||
const stmtNamePV = "UpdatePV" |
||||
const stmtSQLPV = "UPDATE Visit SET PV=PV+1,Recent=datetime('now','localtime') WHERE CustomerID=? AND PageID=? AND Scene=? AND State=?;" |
||||
stmtPV := a.getStmt(runtime, stmtNamePV) |
||||
if stmtPV == nil { |
||||
if stmtPV, err = a.setStmt(runtime, stmtNamePV, stmtSQLPV); err != nil { |
||||
return |
||||
} |
||||
} |
||||
stmtNewPV := a.getStmt(runtime, stmtNameNewPV) |
||||
if stmtNewPV == nil { |
||||
if stmtNewPV, err = a.setStmt(runtime, stmtNameNewPV, stmtSQLNewPV); err != nil { |
||||
return |
||||
} |
||||
} |
||||
runtime.mutex.Lock() |
||||
defer runtime.mutex.Unlock() |
||||
tx, err := runtime.db.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
stmtPV = tx.Stmt(stmtPV) |
||||
pvResult, err := stmtPV.Exec( |
||||
customerID, |
||||
pageID, |
||||
scene, |
||||
visitState, |
||||
) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
cnt, err := pvResult.RowsAffected() |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
if cnt > 0 { |
||||
tx.Commit() |
||||
return |
||||
} |
||||
stmtNewPV = tx.Stmt(stmtNewPV) |
||||
_, err = stmtNewPV.Exec( |
||||
customerID, |
||||
pageID, |
||||
scene, |
||||
visitState, |
||||
) |
||||
if err != nil { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
tx.Commit() |
||||
return |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
package main |
||||
|
||||
import "strings" |
||||
|
||||
/* jsonBodyfilter - 用于过滤混淆bodyObject里的内容 |
||||
bodyObject: 存放需混淆的 JSON 对象; |
||||
mixupFn: 混淆函数, 如果为nil则会删除相应的键值; |
||||
keys: 需要混淆或过滤的键; |
||||
*/ |
||||
func jsonBodyfilter( |
||||
bodyObject *map[string]interface{}, |
||||
mixupFn func(interface{}) interface{}, |
||||
keys ...string, |
||||
) { |
||||
for k, v := range *bodyObject { |
||||
switch val := v.(type) { |
||||
case map[string]interface{}: |
||||
jsonBodyfilter(&val, mixupFn, keys...) |
||||
default: |
||||
for _, hotKey := range keys { |
||||
if k == hotKey { |
||||
if mixupFn == nil { |
||||
delete(*bodyObject, k) |
||||
continue |
||||
} |
||||
(*bodyObject)[k] = mixupFn(val) |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
func mixupString(s interface{}) interface{} { |
||||
switch value := s.(type) { |
||||
case string: |
||||
return strings.Repeat("*", len(value)) |
||||
default: |
||||
return value |
||||
} |
||||
} |
||||
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
//APIAccount - Upstream API Account
|
||||
var APIAccount *UpstreamAccount |
||||
|
||||
func (a *App) getAPIAccount() *UpstreamAccount { |
||||
if APIAccount != nil { |
||||
return APIAccount |
||||
} |
||||
APIAccount = a.NewUpstreamAccount() |
||||
return APIAccount |
||||
} |
||||
|
||||
//UpstreamTokenStore - token store for upstream system
|
||||
type UpstreamTokenStore struct { |
||||
Scope string `json:"scope,omitempty"` |
||||
TokenType string `json:"token_type,omitempty"` |
||||
AccessToken string `json:"access_token,omitempty"` |
||||
RefreshToken string `json:"refresh_token,omitempty"` |
||||
ExpiresIn int `json:"expires_in,omitempty"` |
||||
JTI string `json:"jti,omitempty"` |
||||
Error string `json:"error,omitempty"` |
||||
ErrorDescription string `json:"error_description,omitempty"` |
||||
RefreshAt time.Time `json:"-"` |
||||
} |
||||
|
||||
//Reset - reset store
|
||||
func (s *UpstreamTokenStore) Reset() { |
||||
s.Scope = "" |
||||
s.TokenType = "" |
||||
s.AccessToken = "" |
||||
s.RefreshToken = "" |
||||
s.ExpiresIn = 0 |
||||
s.JTI = "" |
||||
s.Error = "" |
||||
s.ErrorDescription = "" |
||||
} |
||||
|
||||
//UpstreamAccount - account for upstream system
|
||||
type UpstreamAccount struct { |
||||
TokenURL string |
||||
ClientID string |
||||
ClientSecret string |
||||
UserName string |
||||
Password string |
||||
store *UpstreamTokenStore |
||||
mutex *sync.Mutex |
||||
} |
||||
|
||||
//NewUpstreamAccount - create upstream account from config
|
||||
func (a *App) NewUpstreamAccount() *UpstreamAccount { |
||||
const oauthPath = "oauth/token" |
||||
return &UpstreamAccount{ |
||||
TokenURL: a.Config.UpstreamURL + oauthPath, |
||||
ClientID: a.Config.UpstreamClientID, |
||||
ClientSecret: a.Config.UpstreamClientSecret, |
||||
UserName: a.Config.UpstreamUserName, |
||||
Password: a.Config.UpstreamPassword, |
||||
store: &UpstreamTokenStore{}, |
||||
mutex: &sync.Mutex{}, |
||||
} |
||||
} |
||||
|
||||
//GetToken - get or refresh Token
|
||||
func (a *UpstreamAccount) GetToken(forced bool) string { |
||||
a.mutex.Lock() |
||||
defer a.mutex.Unlock() |
||||
if a.store.RefreshToken == "" { |
||||
return a.getToken() |
||||
} |
||||
expiresAt := a.store.RefreshAt.Add(time.Second * time.Duration(a.store.ExpiresIn-10)) |
||||
if !forced && expiresAt.After(time.Now()) { |
||||
return a.store.AccessToken |
||||
} |
||||
token := a.refreshToken() |
||||
if token != "" { |
||||
return token |
||||
} |
||||
return a.getToken() |
||||
} |
||||
|
||||
func (a *UpstreamAccount) getToken() string { |
||||
payload := url.Values{} |
||||
payload.Add("grant_type", "password") |
||||
payload.Add("username", a.UserName) |
||||
payload.Add("password", a.Password) |
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, a.TokenURL, strings.NewReader(payload.Encode())) |
||||
req.SetBasicAuth(a.ClientID, a.ClientSecret) |
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") |
||||
|
||||
resp, err := httpClient.Do(req) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [UpstreamTokenStore][getToken], http.do err: %v", err) |
||||
return "" |
||||
} |
||||
defer resp.Body.Close() |
||||
dec := json.NewDecoder(resp.Body) |
||||
if err := dec.Decode(&a.store); err != nil { |
||||
log.Printf("[ERR] - [UpstreamTokenStore][getToken], decode err: %v", err) |
||||
return "" |
||||
} |
||||
if a.store.Error != "" { |
||||
log.Printf("[ERR] - [UpstreamTokenStore][getToken], token err: %s, %s", a.store.Error, a.store.ErrorDescription) |
||||
//a.store.Reset()
|
||||
return "" |
||||
} |
||||
a.store.RefreshAt = time.Now() |
||||
return a.store.AccessToken |
||||
} |
||||
|
||||
func (a *UpstreamAccount) refreshToken() string { |
||||
payload := url.Values{} |
||||
payload.Add("grant_type", "refresh_token") |
||||
payload.Add("refresh_token", a.store.RefreshToken) |
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, a.TokenURL, strings.NewReader(payload.Encode())) |
||||
req.SetBasicAuth(a.ClientID, a.ClientSecret) |
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") |
||||
|
||||
resp, err := httpClient.Do(req) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [UpstreamTokenStore][refreshToken], http.do err: %v", err) |
||||
return "" |
||||
} |
||||
defer resp.Body.Close() |
||||
dec := json.NewDecoder(resp.Body) |
||||
if err := dec.Decode(&a.store); err != nil { |
||||
log.Printf("[ERR] - [UpstreamTokenStore][refreshToken], decode err: %v", err) |
||||
a.store.Reset() |
||||
return "" |
||||
} |
||||
if a.store.Error != "" { |
||||
log.Printf("[ERR] - [UpstreamTokenStore][refreshToken], token err: %s, %s", a.store.Error, a.store.ErrorDescription) |
||||
a.store.Reset() |
||||
return "" |
||||
} |
||||
a.store.RefreshAt = time.Now() |
||||
return a.store.AccessToken |
||||
} |
||||
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"reflect" |
||||
"testing" |
||||
|
||||
"loreal.com/dit/utils" |
||||
) |
||||
|
||||
func Test_jsonBodyfilter(t *testing.T) { |
||||
type args struct { |
||||
bodyObject *map[string]interface{} |
||||
mixupFn func(interface{}) interface{} |
||||
keys []string |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
args args |
||||
want map[string]interface{} |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
{ |
||||
name: "case1", |
||||
args: args{ |
||||
bodyObject: &map[string]interface{}{ |
||||
"kt": "a", |
||||
"k1": map[string]interface{}{ |
||||
"kt": "aa", |
||||
}, |
||||
"k2": map[string]interface{}{ |
||||
"k21": map[string]interface{}{ |
||||
"kt": "aaa", |
||||
"kt1": "aaaa", |
||||
}, |
||||
}, |
||||
}, |
||||
mixupFn: mixupString, |
||||
keys: []string{"kt", "kt1"}, |
||||
}, |
||||
want: map[string]interface{}{ |
||||
"kt": "*", |
||||
"k1": map[string]interface{}{ |
||||
"kt": "**", |
||||
}, |
||||
"k2": map[string]interface{}{ |
||||
"k21": map[string]interface{}{ |
||||
"kt": "***", |
||||
"kt1": "****", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "case2", |
||||
args: args{ |
||||
bodyObject: &map[string]interface{}{ |
||||
"kt": "a", |
||||
"k1": map[string]interface{}{ |
||||
"kt": "aa", |
||||
}, |
||||
"k2": map[string]interface{}{ |
||||
"k21": map[string]interface{}{ |
||||
"kt": "aaa", |
||||
"kt1": "aaaa", |
||||
}, |
||||
}, |
||||
}, |
||||
mixupFn: nil, |
||||
keys: []string{"kt", "kt1"}, |
||||
}, |
||||
want: map[string]interface{}{ |
||||
"k1": map[string]interface{}{}, |
||||
"k2": map[string]interface{}{ |
||||
"k21": map[string]interface{}{}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
jsonBodyfilter(tt.args.bodyObject, tt.args.mixupFn, tt.args.keys...) |
||||
if !reflect.DeepEqual(*tt.args.bodyObject, tt.want) { |
||||
t.Error("\n\nwant:\n", utils.MarshalJSON(tt.want, true), "\ngot:\n", utils.MarshalJSON(*tt.args.bodyObject, true)) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"encoding/json" |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
"strings" |
||||
) |
||||
|
||||
//DEBUG - whether in debug mode
|
||||
var DEBUG bool |
||||
|
||||
//INFOLEVEL - info level for debug mode
|
||||
var INFOLEVEL int |
||||
|
||||
//LOGLEVEL - info level for logs
|
||||
var LOGLEVEL int |
||||
|
||||
func init() { |
||||
if os.Getenv("EV_DEBUG") != "" { |
||||
DEBUG = true |
||||
} |
||||
INFOLEVEL = 1 |
||||
LOGLEVEL = 1 |
||||
} |
||||
|
||||
func retry(count int, fn func() error) error { |
||||
total := count |
||||
retry: |
||||
err := fn() |
||||
if err != nil { |
||||
count-- |
||||
log.Println("[INFO] - Retry: ", total-count) |
||||
if count > 0 { |
||||
goto retry |
||||
} |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func parseState(state string) map[string]string { |
||||
result := make(map[string]string, 2) |
||||
var err error |
||||
state, err = url.PathUnescape(state) |
||||
if err != nil { |
||||
log.Println("[ERR] - parseState", err) |
||||
return result |
||||
} |
||||
if DEBUG { |
||||
log.Println("[DEBUG] - PathUnescape state:", state) |
||||
} |
||||
states := strings.Split(state, ";") |
||||
for _, kv := range states { |
||||
sp := strings.Index(kv, ":") |
||||
if sp < 0 { |
||||
//empty value
|
||||
result[kv] = "" |
||||
continue |
||||
} |
||||
result[kv[:sp]] = kv[sp+1:] |
||||
} |
||||
return result |
||||
} |
||||
|
||||
func (a *App) getRuntime(env string) *RuntimeEnv { |
||||
runtime, ok := a.Runtime[env] |
||||
if !ok { |
||||
return nil |
||||
} |
||||
return runtime |
||||
} |
||||
|
||||
//getStmt - get stmt from app safely
|
||||
func (a *App) getStmt(runtime *RuntimeEnv, name string) *sql.Stmt { |
||||
runtime.mutex.RLock() |
||||
defer runtime.mutex.RUnlock() |
||||
if stmt, ok := runtime.stmts[name]; ok { |
||||
return stmt |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
//getStmt - get stmt from app safely
|
||||
func (a *App) setStmt(runtime *RuntimeEnv, name, query string) (stmt *sql.Stmt, err error) { |
||||
stmt, err = runtime.db.Prepare(query) |
||||
if err != nil { |
||||
logError(err, name) |
||||
return nil, err |
||||
} |
||||
runtime.mutex.Lock() |
||||
runtime.stmts[name] = stmt |
||||
runtime.mutex.Unlock() |
||||
return stmt, nil |
||||
} |
||||
|
||||
//outputJSON - output json for http response
|
||||
func outputJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
enc := json.NewEncoder(w) |
||||
if DEBUG { |
||||
enc.SetIndent("", " ") |
||||
} |
||||
if err := enc.Encode(data); err != nil { |
||||
log.Println("[ERR] - [outputJSON] JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func logError(err error, msg string) { |
||||
if err != nil { |
||||
log.Printf("[ERR] - %s, err: %v\n", msg, err) |
||||
} |
||||
} |
||||
|
||||
func debugInfo(source, msg string, level int) { |
||||
if DEBUG && INFOLEVEL >= level { |
||||
log.Printf("[DEBUG] - [%s]%s\n", source, msg) |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"loreal.com/dit/utils/task" |
||||
) |
||||
|
||||
//DailyMaintenance - task to do daily maintenance
|
||||
func (a *App) DailyMaintenance(t *task.Task) (err error) { |
||||
// const stmtName = "dm-clean-vehicle"
|
||||
// const stmtSQL = "DELETE FROM vehicle_left WHERE enter<=?;"
|
||||
// env := getEnv(t.Context)
|
||||
// runtime := a.getRuntime(env)
|
||||
// if runtime == nil {
|
||||
// return ErrMissingRuntime
|
||||
// }
|
||||
// stmt := a.getStmt(runtime, stmtName)
|
||||
// if stmt == nil {
|
||||
// //lazy setup for stmt
|
||||
// if stmt, err = a.setStmt(runtime, stmtName, stmtSQL); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// runtime.mutex.Lock()
|
||||
// defer runtime.mutex.Unlock()
|
||||
// _, err = stmt.Exec(int(time.Now().Add(time.Hour * -168).Unix())) /* 7*24Hours = 168*/
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
//Smartfix
|
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"log" |
||||
"math/rand" |
||||
"time" |
||||
|
||||
"loreal.com/dit/module/modules/account" |
||||
"loreal.com/dit/utils" |
||||
) |
||||
|
||||
//Version - generate on build time by makefile
|
||||
var Version = "v0.1" |
||||
|
||||
//CommitID - generate on build time by makefile
|
||||
var CommitID = "" |
||||
|
||||
const serviceName = "CS-PORTAL" |
||||
const serviceDescription = "CEH-CS-PORTAL" |
||||
|
||||
var app *App |
||||
|
||||
func init() { |
||||
|
||||
} |
||||
|
||||
func main() { |
||||
rand.Seed(time.Now().UnixNano()) |
||||
log.Println("[INFO] -", serviceName, Version+"-"+CommitID) |
||||
log.Println("[INFO] -", serviceDescription) |
||||
|
||||
utils.LoadOrCreateJSON("./config/config.json", &cfg) //cfg initialized in config.go
|
||||
|
||||
flag.StringVar(&cfg.Address, "addr", cfg.Address, "host:port of the service") |
||||
flag.StringVar(&cfg.Prefix, "prefix", cfg.Prefix, "/path/ prefixe to service") |
||||
flag.Parse() |
||||
|
||||
//Create Main service
|
||||
var app = NewApp(serviceName, serviceDescription, &cfg) |
||||
uas := account.NewModule("account", |
||||
serviceName, /*Token Issuer*/ |
||||
[]byte(cfg.JwtKey), /*Json Web Token Sign Key*/ |
||||
10, /*Numbe of faild logins to lock the account */ |
||||
60*time.Second, /*How long the account will stay locked*/ |
||||
7200*time.Second, /*How long the token will be valid*/ |
||||
) |
||||
app.Root.Install( |
||||
uas, |
||||
) |
||||
app.AuthProvider = uas |
||||
app.WebTokenAuthProvider = uas |
||||
app.ListenAndServe() |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
NAME = ceh-cs-portal |
||||
BUILDPATH = `go env GOPATH` |
||||
OUTPUT = ${BUILDPATH}/bin/loreal.com/${NAME} |
||||
PACKAGES = ${BUILDPATH}/src/loreal.com/dit/cmd/${NAME} |
||||
GIT_COMMIT = `git rev-parse HEAD | cut -c1-7` |
||||
DT = `date +'%Y%m%d-%H%M%S'` |
||||
VERSION = V0.5 |
||||
BUILD_OPTIONS = -ldflags "-X main.Version=$(VERSION) -X main.CommitID=$(DT)" |
||||
|
||||
default: |
||||
mkdir -p ${OUTPUT} |
||||
go build ${BUILD_OPTIONS} -o ${OUTPUT}/${NAME} ${PACKAGES} |
||||
|
||||
windows: |
||||
mkdir -p ${OUTPUT} |
||||
go build ${BUILD_OPTIONS} -o ${OUTPUT}/${NAME}.exe ${PACKAGES} |
||||
|
||||
race: |
||||
mkdir -p ${OUTPUT} |
||||
go build ${BUILD_OPTIONS} -race -o ${OUTPUT}/${NAME}-race ${PACKAGES} |
||||
|
||||
health: |
||||
curl http://localhost:1521/health |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
|
||||
"loreal.com/dit/module" |
||||
"loreal.com/dit/utils" |
||||
) |
||||
|
||||
func (a *App) initMessageHandlers() { |
||||
a.MessageHandlers = map[string]func(*module.Message) bool{ |
||||
"reload": a.reloadMessageHandler, |
||||
} |
||||
|
||||
} |
||||
|
||||
//reloadMessageHandler - handle reload message
|
||||
func (a *App) reloadMessageHandler(msgPtr *module.Message) (handled bool) { |
||||
//reload configuration
|
||||
utils.LoadOrCreateJSON("./config/config.json", &a.Config) |
||||
a.Config.fixPrefix() |
||||
a.StartScheduler() |
||||
log.Println("[INFO] - Configuration reloaded!") |
||||
return true |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
package main |
||||
|
||||
import "time" |
||||
|
||||
//Brand - Loreal Brand
|
||||
type Brand struct { |
||||
ID int64 `name:"id" type:"INTEGER"` |
||||
Code string `type:"TEXT" index:"asc"` |
||||
Name string `type:"TEXT" index:"asc"` |
||||
CreateAt time.Time `type:"DATETIME" default:"datetime('now','localtime')"` |
||||
Modified time.Time `type:"DATETIME"` |
||||
CreateBy string `type:"TEXT"` |
||||
ModifiedBy string `type:"TEXT"` |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
package main |
||||
|
||||
import "fmt" |
||||
|
||||
//ErrMissingRuntime - cannot found runtime by name
|
||||
var ErrMissingRuntime = fmt.Errorf("missing runtime") |
||||
|
||||
//ErrUnfollow - 用户未关注
|
||||
var ErrUnfollow = fmt.Errorf("用户未关注") |
||||
|
||||
//ErrMsgFailed -消息发送失败
|
||||
var ErrMsgFailed = fmt.Errorf("消息发送失败") |
||||
|
||||
//MsgState - 消息发送状态
|
||||
type MsgState int |
||||
|
||||
const ( |
||||
//MsgStateNew - Initial state
|
||||
MsgStateNew MsgState = iota |
||||
//MsgStateSent - 消息已发送
|
||||
MsgStateSent |
||||
//MsgStateUnfollow - 用户未关注
|
||||
MsgStateUnfollow |
||||
//MsgStateFailed - 发送失败
|
||||
MsgStateFailed |
||||
) |
||||
|
||||
func (ms MsgState) String() string { |
||||
switch ms { |
||||
case MsgStateNew: |
||||
return "消息未发送" |
||||
case MsgStateSent: |
||||
return "消息已发送" |
||||
case MsgStateUnfollow: |
||||
return "用户未关注" |
||||
case MsgStateFailed: |
||||
return "发送失败" |
||||
default: |
||||
return "" |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package main |
||||
|
||||
//APIStatus - general api result
|
||||
type APIStatus struct { |
||||
ErrCode int `json:"code,omitempty"` |
||||
ErrMessage string `json:"msg,omitempty"` |
||||
} |
||||
|
||||
//GatewayRequest - requst encoding struct for gateway
|
||||
type GatewayRequest struct { |
||||
Path string `json:"path,omitempty"` |
||||
Payload map[string]interface{} `json:"payload,omitempty"` |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"net" |
||||
"net/http" |
||||
"time" |
||||
|
||||
"loreal.com/dit/endpoint" |
||||
"loreal.com/dit/middlewares" |
||||
) |
||||
|
||||
var httpClient endpoint.HTTPClient |
||||
|
||||
func init() { |
||||
tr := &http.Transport{ |
||||
Dial: (&net.Dialer{ |
||||
Timeout: 10 * time.Second, |
||||
KeepAlive: 30 * time.Second, |
||||
}).Dial, |
||||
Proxy: nil, |
||||
ResponseHeaderTimeout: 5 * time.Second, |
||||
ExpectContinueTimeout: 1 * time.Second, |
||||
MaxIdleConns: 50, |
||||
IdleConnTimeout: 30 * time.Second, |
||||
} |
||||
endpoint.DefaultHTTPClient = &http.Client{Transport: tr} |
||||
|
||||
httpClient = endpoint.DecorateClient(endpoint.DefaultHTTPClient, |
||||
middlewares.FaultTolerance(3, 5*time.Second, nil), |
||||
//middlewares.ClientInstrumentation("wx-msg-backend", endpoint.RequestCounter, endpoint.LatencyHistogram, endpoint.DurationsSummary),
|
||||
) |
||||
} |
||||
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"log" |
||||
"sync" |
||||
) |
||||
|
||||
//SQLiteAdapter - SQLite Restful Adapter
|
||||
type SQLiteAdapter struct { |
||||
DB *sql.DB |
||||
Mutex *sync.RWMutex |
||||
TableName string |
||||
tags []FieldTag |
||||
sqls map[string]string |
||||
stmts map[string]*sql.Stmt |
||||
sample interface{} |
||||
} |
||||
|
||||
//NewSQLiteAdapter - create new instance from a model template
|
||||
func NewSQLiteAdapter(db *sql.DB, mutex *sync.RWMutex, tableName string, modelTemplate interface{}) *SQLiteAdapter { |
||||
if db == nil { |
||||
log.Fatal("[ERR] - [NewSQLiteAdapter] nil db") |
||||
} |
||||
if mutex == nil { |
||||
mutex = &sync.RWMutex{} |
||||
} |
||||
adapter := &SQLiteAdapter{ |
||||
DB: db, |
||||
Mutex: mutex, |
||||
TableName: tableName, |
||||
tags: ParseTags(modelTemplate), |
||||
sqls: make(map[string]string), |
||||
stmts: make(map[string]*sql.Stmt), |
||||
sample: modelTemplate, |
||||
} |
||||
if len(adapter.tags) == 0 { |
||||
log.Fatalln("Invalid ModelTemplate:", modelTemplate) |
||||
} |
||||
adapter.init() |
||||
return adapter |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) prepareStmt(key, sql string) { |
||||
a.Mutex.Lock() |
||||
defer a.Mutex.Unlock() |
||||
var err error |
||||
if a.stmts[key], err = a.DB.Prepare(sql); err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) init() { |
||||
if _, err := a.DB.Exec(a.createTableSQL()); err != nil { |
||||
log.Printf("[ERR] - [SQLiteAdapter] Can not create table: [%s], err: %v\n", a.TableName, err) |
||||
log.Println("[ERR] - [INFO]", a.createTableSQL()) |
||||
} |
||||
createIdxSqls := a.createIndexSQLs() |
||||
for _, cmd := range createIdxSqls { |
||||
_, err := a.DB.Exec(cmd) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [CreateIndex] %v: %s\n", err, cmd) |
||||
return |
||||
} |
||||
} |
||||
|
||||
a.sqls["set"] = a.setSQL(a.getFields(false /*Do not include ID*/)) |
||||
a.sqls["delete"] = a.deleteSQL() |
||||
a.sqls["one"] = a.selectOneSQL() |
||||
for key, sql := range a.sqls { |
||||
if DEBUG { |
||||
log.Printf("[DEBUG] - Prepare [%s]:\n", key) |
||||
fmt.Println("------") |
||||
fmt.Println(sql) |
||||
fmt.Println("------") |
||||
fmt.Println() |
||||
} |
||||
a.prepareStmt(key, sql) |
||||
} |
||||
log.Printf("[INFO] - Table [%s] prepared\n", a.TableName) |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"log" |
||||
"net/url" |
||||
"strconv" |
||||
) |
||||
|
||||
//Find - implementation of restful model interface
|
||||
func (a *SQLiteAdapter) Find(query url.Values) (total int64, records []*map[string]interface{}) { |
||||
orderby := query.Get("orderby") |
||||
if orderby == "" { |
||||
orderby = "id" |
||||
} |
||||
limit, _ := strconv.Atoi(query.Get("limit")) |
||||
offset, _ := strconv.Atoi(query.Get("offset")) |
||||
paged := limit > 0 |
||||
|
||||
keys, values := a.parseParams(query) |
||||
countSQL := a.buildCountSQL(keys) |
||||
sql := a.buildQuerySQL(keys, orderby, paged) |
||||
if DEBUG { |
||||
log.Printf("[DEBUG] - [INFO] Count SQL: %s\n", countSQL) |
||||
log.Printf("[DEBUG] - [INFO] SQL: %s\n", sql) |
||||
} |
||||
if paged { |
||||
values = append(values, limit, offset) |
||||
} |
||||
a.Mutex.Lock() |
||||
defer a.Mutex.Unlock() |
||||
if err := a.DB.QueryRow(countSQL, values...).Scan(&total); err != nil { |
||||
log.Printf("[ERR] - [Count SQL]: %s, err: %v\n", countSQL, err) |
||||
} |
||||
records = a.scanRows(a.DB.Query(sql, values...)) |
||||
return |
||||
} |
||||
|
||||
//FindByID - implementation of restful model interface
|
||||
func (a *SQLiteAdapter) FindByID(id int64) map[string]interface{} { |
||||
buffer := make([]interface{}, 0, len(a.tags)) |
||||
record := a.newRecord(&buffer) |
||||
a.Mutex.Lock() |
||||
defer a.Mutex.Unlock() |
||||
row := a.stmts["one"].QueryRow(id) |
||||
if err := row.Scan(buffer...); err != nil { |
||||
log.Println(err) |
||||
return nil |
||||
} |
||||
return record |
||||
} |
||||
|
||||
//Insert - implementation of restful model interface
|
||||
func (a *SQLiteAdapter) Insert(data url.Values) (id int64, err error) { |
||||
keys, values := a.parseParams(data) |
||||
for i, key := range keys { |
||||
if key == "id" { |
||||
values[i] = sql.NullInt64{Valid: false} |
||||
} |
||||
} |
||||
r, err := a.DB.Exec(a.insertSQL(keys), values...) |
||||
if err != nil { |
||||
return -1, err |
||||
} |
||||
newID, err := r.LastInsertId() |
||||
if err != nil { |
||||
return -1, err |
||||
} |
||||
return newID, nil |
||||
} |
||||
|
||||
//Set - implementation of restful model interface
|
||||
func (a *SQLiteAdapter) Set(id int64, data url.Values) error { |
||||
keys, values := a.parseParams(data) |
||||
values = append(values, id) |
||||
sql := a.setSQL(keys) |
||||
if DEBUG { |
||||
log.Printf("[DEBUG] - [INFO] SQL: %s\n", sql) |
||||
} |
||||
_, err := a.DB.Exec(a.setSQL(keys), values...) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
//Update - implementation of restful model interface
|
||||
func (a *SQLiteAdapter) Update(data url.Values, where url.Values) (rowsAffected int64, err error) { |
||||
keys, values := a.parseParams(data) |
||||
whereKeys, whereValues := a.parseParams(where) |
||||
values = append(values, whereValues...) |
||||
sql := a.updateSQL(keys, whereKeys) |
||||
if DEBUG { |
||||
log.Printf("[DEBUG] - [INFO] SQL: %s\n", sql) |
||||
} |
||||
r, err := a.DB.Exec(sql, values...) |
||||
if err != nil || r == nil { |
||||
return 0, err |
||||
} |
||||
return r.RowsAffected() |
||||
} |
||||
|
||||
//Delete - implementation of restful model interface
|
||||
func (a *SQLiteAdapter) Delete(id int64) (rowsAffected int64, err error) { |
||||
r, err := a.stmts["delete"].Exec(id) |
||||
if err != nil || r == nil { |
||||
return 0, err |
||||
} |
||||
return r.RowsAffected() |
||||
} |
||||
@ -0,0 +1,261 @@
@@ -0,0 +1,261 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"reflect" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
func defaultForSQLNull(tag FieldTag) string { |
||||
switch tag.GoType.Kind() { |
||||
case reflect.Bool: |
||||
return "false" |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return "-1" |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
return "0" |
||||
case reflect.Float32, reflect.Float64: |
||||
return "-1" |
||||
case reflect.String: |
||||
return "''" |
||||
case reflect.Struct: |
||||
switch strings.ToLower(tag.DataType) { |
||||
case "date", "datetime": |
||||
return "'" + time.Time{}.Local().Format(time.RFC3339) + "'" |
||||
default: |
||||
return "''" |
||||
} |
||||
// case reflect.Uintptr:
|
||||
// case reflect.Complex64:
|
||||
// case reflect.Complex128:
|
||||
// case reflect.Array:
|
||||
// case reflect.Chan:
|
||||
// case reflect.Func:
|
||||
// case reflect.Interface:
|
||||
// case reflect.Map:
|
||||
// case reflect.Ptr:
|
||||
// case reflect.Slice:
|
||||
// case reflect.UnsafePointer:
|
||||
default: |
||||
return "''" |
||||
} |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) createTableSQL() string { |
||||
const space = ' ' |
||||
const tab = '\t' |
||||
b := strings.Builder{} |
||||
b.WriteString("CREATE TABLE IF NOT EXISTS ") |
||||
b.WriteString(a.TableName) |
||||
b.WriteString(" (\r\n") |
||||
// lastIdx := len(a.tags) - 1
|
||||
for idx, tag := range a.tags { |
||||
if idx > 0 { |
||||
b.Write([]byte{',', '\n'}) |
||||
} |
||||
b.WriteByte(tab) |
||||
b.WriteString(tag.FieldName) |
||||
b.WriteByte(space) |
||||
b.WriteString(tag.DataType) |
||||
if strings.ToLower(tag.FieldName) == "id" { |
||||
b.WriteString(" PRIMARY KEY ASC") |
||||
continue |
||||
} |
||||
b.WriteString(" DEFAULT ") |
||||
if tag.Default != "" { |
||||
b.WriteByte('(') |
||||
b.WriteString(tag.Default) |
||||
b.WriteByte(')') |
||||
} else { |
||||
b.WriteString(defaultForSQLNull(tag)) |
||||
} |
||||
// b.Write(ret)
|
||||
// if idx == lastIdx {
|
||||
// b.Write(ret)
|
||||
// } else {
|
||||
// b.Write([]byte{',', '\r', '\n'})
|
||||
// }
|
||||
} |
||||
b.Write([]byte{')', ';'}) |
||||
return b.String() |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) createIndexSQLs() []string { |
||||
sqlcmds := make([]string, 0, len(a.tags)) |
||||
const space = ' ' |
||||
b := strings.Builder{} |
||||
for _, tag := range a.tags { |
||||
if tag.Index == "" { |
||||
continue |
||||
} |
||||
b.Reset() |
||||
b.WriteString("CREATE INDEX IF NOT EXISTS Idx") |
||||
b.WriteString(a.TableName) |
||||
b.WriteString(strings.ToTitle(tag.FieldName)) |
||||
b.WriteString(" ON ") |
||||
b.WriteString(a.TableName) |
||||
b.WriteByte('(') |
||||
b.WriteString(tag.FieldName) |
||||
if strings.ToLower(tag.Index) == "desc" { |
||||
b.WriteString(" DESC") |
||||
} |
||||
b.WriteByte(')') |
||||
sqlcmds = append(sqlcmds, b.String()) |
||||
} |
||||
return sqlcmds |
||||
} |
||||
|
||||
//buildQuerySQL - generate query sql, keys => list of field names in where term
|
||||
func (a *SQLiteAdapter) buildQuerySQL(keys []string, orderby string, paged bool) string { |
||||
var lastIdx int |
||||
var lastKeyIdx int |
||||
b1 := strings.Builder{} |
||||
b1.WriteString("SELECT ") |
||||
lastIdx = len(a.tags) - 1 |
||||
for idx, tag := range a.tags { |
||||
b1.WriteString(tag.FieldName) |
||||
if idx != lastIdx { |
||||
b1.WriteByte(',') |
||||
} |
||||
} |
||||
b1.WriteString(" FROM ") |
||||
b1.WriteString(a.TableName) |
||||
if len(keys) == 0 { |
||||
goto orderby |
||||
} |
||||
b1.WriteString(" WHERE ") |
||||
lastKeyIdx = len(keys) - 1 |
||||
for idx, key := range keys { |
||||
b1.WriteString(key) |
||||
b1.Write([]byte{'=', '?'}) |
||||
if idx != lastKeyIdx { |
||||
b1.WriteString(" AND ") |
||||
} |
||||
} |
||||
|
||||
orderby: |
||||
if orderby != "" { |
||||
b1.WriteString(" ORDER BY ") |
||||
b1.WriteString(orderby) |
||||
} |
||||
if !paged { |
||||
b1.WriteByte(';') |
||||
return b1.String() |
||||
} |
||||
b1.WriteString(" LIMIT ? OFFSET ?;") |
||||
return b1.String() |
||||
} |
||||
|
||||
//buildCountSQL - generate query sql, keys => list of field names in where term
|
||||
func (a *SQLiteAdapter) buildCountSQL(keys []string) string { |
||||
b1 := strings.Builder{} |
||||
b1.WriteString("SELECT count(*) FROM ") |
||||
b1.WriteString(a.TableName) |
||||
if len(keys) == 0 { |
||||
b1.WriteByte(';') |
||||
return b1.String() |
||||
} |
||||
b1.WriteString(" WHERE ") |
||||
lastKeyIdx := len(keys) - 1 |
||||
for idx, key := range keys { |
||||
b1.WriteString(key) |
||||
b1.Write([]byte{'=', '?'}) |
||||
if idx != lastKeyIdx { |
||||
b1.WriteString(" AND ") |
||||
} |
||||
} |
||||
b1.WriteByte(';') |
||||
return b1.String() |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) selectOneSQL() string { |
||||
b1 := strings.Builder{} |
||||
b1.WriteString("SELECT ") |
||||
lastIdx := len(a.tags) - 1 |
||||
for idx, tag := range a.tags { |
||||
b1.WriteString(tag.FieldName) |
||||
if idx != lastIdx { |
||||
b1.WriteByte(',') |
||||
} |
||||
} |
||||
b1.WriteString(" FROM ") |
||||
b1.WriteString(a.TableName) |
||||
b1.WriteString(" WHERE id=?;") |
||||
return b1.String() |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) insertSQL(fields []string) string { |
||||
b1 := strings.Builder{} |
||||
b2 := strings.Builder{} |
||||
b1.WriteString("INSERT INTO ") |
||||
b2.WriteString(" VALUES (") |
||||
b1.WriteString(a.TableName) |
||||
b1.WriteString(" (") |
||||
lastIdx := len(fields) - 1 |
||||
for idx, field := range fields { |
||||
b1.WriteString(field) |
||||
b2.WriteByte('?') |
||||
if idx != lastIdx { |
||||
b1.WriteByte(',') |
||||
b2.WriteByte(',') |
||||
} |
||||
} |
||||
b1.Write([]byte{')'}) |
||||
b2.Write([]byte{')', ';'}) |
||||
return b1.String() + b2.String() |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) setSQL(fields []string) string { |
||||
b1 := strings.Builder{} |
||||
b1.WriteString("UPDATE ") |
||||
b1.WriteString(a.TableName) |
||||
b1.WriteString(" SET ") |
||||
l1 := len(fields) - 1 |
||||
for i, f := range fields { |
||||
b1.WriteString(f) |
||||
b1.Write([]byte{'=', '?'}) |
||||
if i != l1 { |
||||
b1.WriteByte(',') |
||||
} |
||||
} |
||||
b1.WriteString(" WHERE id=?;") |
||||
return b1.String() |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) updateSQL(fields, where []string) string { |
||||
b1 := strings.Builder{} |
||||
b1.WriteString("UPDATE ") |
||||
b1.WriteString(a.TableName) |
||||
b1.WriteString(" SET ") |
||||
l1 := len(fields) - 1 |
||||
for i, f := range fields { |
||||
b1.WriteString(f) |
||||
b1.Write([]byte{'=', '?'}) |
||||
if i != l1 { |
||||
b1.WriteByte(',') |
||||
} |
||||
} |
||||
if len(where) == 0 { |
||||
b1.WriteByte(';') |
||||
return b1.String() |
||||
} |
||||
l2 := len(where) - 1 |
||||
b1.WriteString(" WHERE ") |
||||
for i, f := range where { |
||||
b1.WriteString(f) |
||||
b1.Write([]byte{'=', '?'}) |
||||
if i != l2 { |
||||
b1.WriteString(" AND ") |
||||
} |
||||
} |
||||
b1.WriteByte(';') |
||||
return b1.String() |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) deleteSQL() string { |
||||
b1 := strings.Builder{} |
||||
b1.WriteString("DELETE FROM ") |
||||
b1.WriteString(a.TableName) |
||||
b1.WriteString(" WHERE id=?;") |
||||
return b1.String() |
||||
} |
||||
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"log" |
||||
"net/url" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
func (a *SQLiteAdapter) parseParams(params url.Values) (keys []string, values []interface{}) { |
||||
var ( |
||||
err error |
||||
val interface{} |
||||
) |
||||
keys = make([]string, 0, len(a.tags)) |
||||
values = make([]interface{}, 0, len(a.tags)) |
||||
for _, tag := range a.tags { |
||||
fname := strings.ToLower(tag.FieldName) |
||||
s := params.Get(fname) |
||||
if s == "" { |
||||
continue |
||||
} |
||||
switch strings.ToLower(tag.DataType) { |
||||
case "int", "integer": |
||||
val, err = strconv.Atoi(s) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [parseParams][int]: s='%s', err=%v\n", s, err) |
||||
} |
||||
case "real", "float", "double": |
||||
val, err = strconv.ParseFloat(s, 64) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [parseParams][float]: s='%s', err=%v\n", s, err) |
||||
} |
||||
case "bool", "boolean": |
||||
val, err = strconv.ParseBool(s) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [parseParams][bool]: s='%s', err=%v\n", s, err) |
||||
} |
||||
case "date", "datetime": |
||||
switch s { |
||||
case "now": |
||||
val = time.Now().Local() |
||||
default: |
||||
val, err = time.ParseInLocation(time.RFC3339, s, time.Local) |
||||
if err != nil { |
||||
val, err = time.ParseInLocation("2006-01-02", s, time.Local) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [parseParams][datetime]: s='%s', err=%v\n", s, err) |
||||
} |
||||
} |
||||
} |
||||
default: |
||||
val = s |
||||
} |
||||
keys = append(keys, fname) |
||||
values = append(values, val) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) getFields(includeID bool) (fields []string) { |
||||
fields = make([]string, 0, len(a.tags)) |
||||
for _, tag := range a.tags { |
||||
fname := strings.ToLower(tag.FieldName) |
||||
if fname == "id" && !includeID { |
||||
continue |
||||
} |
||||
fields = append(fields, fname) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) newRecord(buffer *[]interface{}) (record map[string]interface{}) { |
||||
record = make(map[string]interface{}, len(a.tags)) |
||||
*buffer = (*buffer)[:0] |
||||
for _, tag := range a.tags { |
||||
// var ptr interface{}
|
||||
// switch tag.GoType.Kind() {
|
||||
// case reflect.Bool:
|
||||
// ptr = &sql.NullBool{}
|
||||
// case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
// ptr = &sql.NullInt64{}
|
||||
// case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
// ptr = &sql.NullInt64{}
|
||||
// case reflect.Float32, reflect.Float64:
|
||||
// ptr = &sql.NullFloat64{}
|
||||
// case reflect.String:
|
||||
// ptr = &sql.NullString{}
|
||||
// case reflect.Struct:
|
||||
// switch strings.ToLower(tag.DataType) {
|
||||
// case "date", "datetime":
|
||||
// ptr = &sql.NullString{}
|
||||
// default:
|
||||
// ptr = &sql.NullString{}
|
||||
// }
|
||||
// // case reflect.Uintptr:
|
||||
// // case reflect.Complex64:
|
||||
// // case reflect.Complex128:
|
||||
// // case reflect.Array:
|
||||
// // case reflect.Chan:
|
||||
// // case reflect.Func:
|
||||
// // case reflect.Interface:
|
||||
// // case reflect.Map:
|
||||
// // case reflect.Ptr:
|
||||
// // case reflect.Slice:
|
||||
// // case reflect.UnsafePointer:
|
||||
// default:
|
||||
// ptr = reflect.New(tag.GoType).Interface()
|
||||
// }
|
||||
ptr := reflect.New(tag.GoType).Interface() |
||||
record[strings.ToLower(tag.Name)] = ptr |
||||
*buffer = append(*buffer, ptr) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (a *SQLiteAdapter) scanRows(rows *sql.Rows, err error) (records []*map[string]interface{}) { |
||||
records = make([]*map[string]interface{}, 0, 0) |
||||
if err != nil { |
||||
log.Printf("[ERR] - [scanRows] err: %v\n", err) |
||||
return |
||||
} |
||||
if rows == nil { |
||||
return |
||||
} |
||||
defer rows.Close() |
||||
buffer := make([]interface{}, 0, len(a.tags)) |
||||
for rows.Next() { |
||||
record := a.newRecord(&buffer) |
||||
if err := rows.Scan(buffer...); err != nil { |
||||
log.Printf("[ERR] - [scanRows] err: %v\n", err) |
||||
} |
||||
records = append(records, &record) |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"reflect" |
||||
"sync" |
||||
"testing" |
||||
) |
||||
|
||||
func TestNewSQLiteAdapter(t *testing.T) { |
||||
type modelTemplateStruct struct { |
||||
ID int `name:"id" type:"INTEGER"` |
||||
Name string `type:"TEXT"` |
||||
Phone string `type:"TEXT"` |
||||
Address string `type:"TEXT"` |
||||
} |
||||
template := modelTemplateStruct{} |
||||
type args struct { |
||||
db *sql.DB |
||||
mutex *sync.RWMutex |
||||
tableName string |
||||
modelTemplate interface{} |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
args args |
||||
want *SQLiteAdapter |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
{ |
||||
name: "case1", |
||||
args: args{ |
||||
db: nil, |
||||
mutex: nil, |
||||
tableName: "TestTable", |
||||
modelTemplate: template, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
if got := NewSQLiteAdapter(tt.args.db, tt.args.mutex, tt.args.tableName, tt.args.modelTemplate); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("NewSQLiteAdapter() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,262 @@
@@ -0,0 +1,262 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"log" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/microcosm-cc/bluemonday" |
||||
) |
||||
|
||||
// var seededRand *rand.Rand
|
||||
var sanitizePolicy *bluemonday.Policy |
||||
|
||||
var jsonInvalidMethod = map[string]interface{}{ |
||||
"errcode": -1, |
||||
"message": "Invalid method", |
||||
} |
||||
var jsonInvalidData = map[string]interface{}{ |
||||
"errcode": -2, |
||||
"message": "Invalid Data", |
||||
} |
||||
|
||||
var jsonInvalidID = map[string]interface{}{ |
||||
"errcode": -3, |
||||
"message": "Invalid ID", |
||||
} |
||||
|
||||
var jsonOPError = map[string]interface{}{ |
||||
"errcode": -4, |
||||
"message": "Operation Error", |
||||
} |
||||
|
||||
var jsonOK = map[string]interface{}{ |
||||
"message": "OK", |
||||
} |
||||
|
||||
func init() { |
||||
// seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sanitizePolicy = bluemonday.UGCPolicy() |
||||
} |
||||
|
||||
//Handler - http handler for a restful endpoint
|
||||
type Handler struct { |
||||
Name string /*Endpoint name*/ |
||||
Model interface{} |
||||
Filter func(*http.Request, *map[string]interface{}) bool |
||||
} |
||||
|
||||
//NewHandler - create a new instance of RestfulHandler
|
||||
func NewHandler(name string, model interface{}) *Handler { |
||||
handler := &Handler{ |
||||
Name: name, |
||||
Model: model, |
||||
} |
||||
return handler |
||||
} |
||||
|
||||
//SetFilter - set filter
|
||||
func (h *Handler) SetFilter(filter func(*http.Request, *map[string]interface{}) bool) *Handler { |
||||
if filter != nil { |
||||
h.Filter = filter |
||||
} |
||||
return h |
||||
} |
||||
|
||||
//sanitize parameters
|
||||
func sanitize(params *url.Values) { |
||||
for key := range *params { |
||||
(*params).Set(key, sanitizePolicy.Sanitize((*params).Get(key))) |
||||
} |
||||
} |
||||
|
||||
func trimURIPrefix(uri string, stopTag string) []string { |
||||
params := strings.Split(strings.TrimPrefix(strings.TrimSuffix(uri, "/"), "/"), "/") |
||||
last := len(params) - 1 |
||||
for i := last; i >= 0; i-- { |
||||
if params[i] == stopTag { |
||||
return params[i+1:] |
||||
} |
||||
} |
||||
return params |
||||
} |
||||
|
||||
func parseID(s string) int64 { |
||||
id, err := strconv.ParseInt(s, 10, 64) |
||||
if err != nil { |
||||
return -1 |
||||
} |
||||
return id |
||||
} |
||||
|
||||
func (h *Handler) httpGet(w http.ResponseWriter, r *http.Request, id int64) { |
||||
m, ok := h.Model.(Querier) |
||||
if !ok { |
||||
outputGzipJSON(w, jsonInvalidMethod) |
||||
return |
||||
} |
||||
if id != -1 { |
||||
outputGzipJSON(w, map[string]interface{}{ |
||||
"message": "ok", |
||||
"method": "one", |
||||
"payload": m.FindByID(id), |
||||
}) |
||||
return |
||||
} |
||||
query := r.URL.Query() |
||||
sanitize(&query) |
||||
total, records := m.Find(query) |
||||
if h.Filter == nil { |
||||
outputGzipJSON(w, map[string]interface{}{ |
||||
"message": "ok", |
||||
"method": "query", |
||||
"total": total, |
||||
"payload": records, |
||||
}) |
||||
return |
||||
} |
||||
finalRecords := make([]*map[string]interface{}, 0, len(records)) |
||||
for _, record := range records { |
||||
if !h.Filter(r, record) { |
||||
finalRecords = append(finalRecords, record) |
||||
} |
||||
} |
||||
outputGzipJSON(w, map[string]interface{}{ |
||||
"message": "ok", |
||||
"method": "query", |
||||
"total": len(finalRecords), |
||||
"payload": finalRecords, |
||||
}) |
||||
} |
||||
|
||||
func (h *Handler) httpPost(w http.ResponseWriter, r *http.Request, id int64) { |
||||
m, ok := h.Model.(Inserter) |
||||
if !ok { |
||||
outputGzipJSON(w, jsonInvalidMethod) |
||||
return |
||||
} |
||||
if err := r.ParseForm(); err != nil { |
||||
log.Println("[ERR] - [RestfulHandler][POST][ParseForm] err:", err) |
||||
outputGzipJSON(w, jsonInvalidData) |
||||
return |
||||
} |
||||
sanitize(&r.PostForm) |
||||
newID, err := m.Insert(r.PostForm) |
||||
if err != nil { |
||||
log.Println("[ERR] - [RestfulHandler][POST] err:", err) |
||||
outputGzipJSON(w, jsonOPError) |
||||
return |
||||
} |
||||
outputGzipJSON(w, map[string]interface{}{ |
||||
"message": "ok", |
||||
"method": "insert", |
||||
"id": newID, |
||||
}) |
||||
} |
||||
|
||||
func (h *Handler) httpPut(w http.ResponseWriter, r *http.Request, id int64) { |
||||
if err := r.ParseForm(); err != nil { |
||||
log.Println("[ERR] - [RestfulHandler][PUT][ParseForm] err:", err) |
||||
outputGzipJSON(w, jsonInvalidData) |
||||
return |
||||
} |
||||
sanitize(&r.PostForm) |
||||
|
||||
switch id { |
||||
case -1 /*update by query condition*/ : |
||||
// m, ok := h.Model.(Updater)
|
||||
// if !ok {
|
||||
// outputGzipJSON(w, jsonInvalidMethod)
|
||||
// return
|
||||
// }
|
||||
// query := r.URL.Query()
|
||||
// sanitize(&query)
|
||||
// rowsAffected, err := m.Update(r.PostForm, query)
|
||||
// if err != nil {
|
||||
// log.Println("[ERR] - [RestfulHandler][PUT-Update] err:", err)
|
||||
// outputGzipJSON(w, jsonOPError)
|
||||
// return
|
||||
// }
|
||||
// outputGzipJSON(w, map[string]interface{}{
|
||||
// "message": "ok",
|
||||
// "method": "update",
|
||||
// "count": rowsAffected,
|
||||
// })
|
||||
outputGzipJSON(w, jsonInvalidID) |
||||
return |
||||
default /*update by ID*/ : |
||||
m, ok := h.Model.(Setter) |
||||
if !ok { |
||||
outputGzipJSON(w, jsonInvalidMethod) |
||||
return |
||||
} |
||||
if err := m.Set(id, r.PostForm); err != nil { |
||||
log.Println("[ERR] - [RestfulHandler][PUT-Set] err:", err) |
||||
outputGzipJSON(w, jsonOPError) |
||||
return |
||||
} |
||||
outputGzipJSON(w, map[string]interface{}{ |
||||
"message": "ok", |
||||
"method": "set", |
||||
}) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func (h *Handler) httpDelete(w http.ResponseWriter, r *http.Request, id int64) { |
||||
m, ok := h.Model.(Deleter) |
||||
if !ok { |
||||
outputGzipJSON(w, jsonInvalidMethod) |
||||
return |
||||
} |
||||
switch id { |
||||
case -1: |
||||
outputGzipJSON(w, jsonInvalidID) |
||||
return |
||||
} |
||||
rowsAffected, err := m.Delete(id) |
||||
if err != nil { |
||||
log.Println("[ERR] - [RestfulHandler][DELETE] err:", err) |
||||
outputGzipJSON(w, jsonOPError) |
||||
return |
||||
} |
||||
outputGzipJSON(w, map[string]interface{}{ |
||||
"message": "ok", |
||||
"method": "delete", |
||||
"count": rowsAffected, |
||||
}) |
||||
} |
||||
|
||||
//ServeHTTP - implementation of http.handler
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
if h.Model == nil { |
||||
outputGzipJSON(w, jsonInvalidMethod) |
||||
return |
||||
} |
||||
if DEBUG { |
||||
log.Println("[DEBUG] - [r.RequestURI]:", r.RequestURI) |
||||
} |
||||
params := trimURIPrefix(r.RequestURI, h.Name) |
||||
var id int64 = -1 |
||||
if len(params) > 0 { |
||||
id = parseID(sanitizePolicy.Sanitize(params[0])) |
||||
} |
||||
switch r.Method { |
||||
case "GET": |
||||
h.httpGet(w, r, id) |
||||
return |
||||
case "POST": |
||||
h.httpPost(w, r, id) |
||||
return |
||||
case "PUT": |
||||
h.httpPut(w, r, id) |
||||
return |
||||
case "DELETE": |
||||
h.httpDelete(w, r, id) |
||||
return |
||||
default: |
||||
outputGzipJSON(w, jsonInvalidMethod) |
||||
} |
||||
} |
||||
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func Test_trimURIPrefix(t *testing.T) { |
||||
type args struct { |
||||
uri string |
||||
stopTag string |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
args args |
||||
want []string |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
{ |
||||
name: "case1", |
||||
args: args{ |
||||
uri: "/crm/api/store/", |
||||
stopTag: "store", |
||||
}, |
||||
want: []string{}, |
||||
}, |
||||
{ |
||||
name: "case2", |
||||
args: args{ |
||||
uri: "/crm/api/store/332/", |
||||
stopTag: "store", |
||||
}, |
||||
want: []string{"332"}, |
||||
}, |
||||
{ |
||||
name: "case3", |
||||
args: args{ |
||||
uri: "/crm/api/store/332/1222/11", |
||||
stopTag: "store", |
||||
}, |
||||
want: []string{"332", "1222", "11"}, |
||||
}, |
||||
{ |
||||
name: "case4", |
||||
args: args{ |
||||
uri: "/crm/api/store", |
||||
stopTag: "store", |
||||
}, |
||||
want: []string{}, |
||||
}, |
||||
{ |
||||
name: "case5", |
||||
args: args{ |
||||
uri: "/crm/api/store", |
||||
stopTag: "store1", |
||||
}, |
||||
want: []string{"crm", "api", "store"}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
if got := trimURIPrefix(tt.args.uri, tt.args.stopTag); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("trimURIPrefix() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
module restful |
||||
|
||||
go 1.13 |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
package restful |
||||
|
||||
import "os" |
||||
|
||||
//DEBUG - whether in debug mode
|
||||
var DEBUG bool |
||||
|
||||
//INFOLEVEL - info level for debug mode
|
||||
var INFOLEVEL int |
||||
|
||||
//LOGLEVEL - info level for logs
|
||||
var LOGLEVEL int |
||||
|
||||
func init() { |
||||
if os.Getenv("EV_DEBUG") != "" { |
||||
DEBUG = true |
||||
} |
||||
INFOLEVEL = 1 |
||||
LOGLEVEL = 1 |
||||
} |
||||
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"net/url" |
||||
) |
||||
|
||||
//Inserter - can insert
|
||||
type Inserter interface { |
||||
Insert(data url.Values) (id int64, err error) |
||||
} |
||||
|
||||
//Deleter - can delete
|
||||
type Deleter interface { |
||||
Delete(id int64) (rowsAffected int64, err error) |
||||
} |
||||
|
||||
//Querier - can run query
|
||||
type Querier interface { |
||||
Find(query url.Values) (total int64, records []*map[string]interface{}) |
||||
FindByID(id int64) map[string]interface{} |
||||
} |
||||
|
||||
//Setter - can update by ID
|
||||
type Setter interface { |
||||
Set(id int64, data url.Values) error |
||||
} |
||||
|
||||
//Updater - can update by condition
|
||||
type Updater interface { |
||||
Update(data url.Values, where url.Values) (rowsAffected int64, err error) |
||||
} |
||||
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"log" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
//FieldTag - table field tag for restful models
|
||||
type FieldTag struct { |
||||
Name string |
||||
FieldName string |
||||
DataType string |
||||
Default string |
||||
Index string |
||||
GoType reflect.Type |
||||
} |
||||
|
||||
//ParseTags - Parse field tags from source struct
|
||||
func ParseTags(source interface{}) []FieldTag { |
||||
t := reflect.TypeOf(source) |
||||
nFields := t.NumField() |
||||
r := make([]FieldTag, 0, nFields) |
||||
for i := 0; i < nFields; i++ { |
||||
field := t.Field(i) |
||||
r = append(r, FieldTag{ |
||||
Name: field.Name, |
||||
FieldName: getTagStr(&field, "name", strings.ToLower(field.Name)), |
||||
DataType: getTagStr(&field, "type", "text"), |
||||
Default: getTagStr(&field, "default", ""), |
||||
Index: getTagStr(&field, "index", ""), |
||||
GoType: field.Type, |
||||
}) |
||||
} |
||||
return r |
||||
} |
||||
|
||||
func getTagInt(field *reflect.StructField, tagName string, defaultValue int) int { |
||||
v := field.Tag.Get(tagName) |
||||
if v == "" { |
||||
return defaultValue |
||||
} |
||||
intValue, err := strconv.Atoi(v) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
return defaultValue |
||||
} |
||||
return intValue |
||||
} |
||||
|
||||
func getTagStr(field *reflect.StructField, tagName string, defaultValue string) string { |
||||
v := field.Tag.Get(tagName) |
||||
if v == "" { |
||||
return defaultValue |
||||
} |
||||
return v |
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
package restful |
||||
|
||||
import ( |
||||
"compress/gzip" |
||||
"encoding/json" |
||||
"log" |
||||
"net/http" |
||||
) |
||||
|
||||
//outputJSON - output json for http response
|
||||
func outputJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
enc := json.NewEncoder(w) |
||||
if DEBUG { |
||||
enc.SetIndent("", " ") |
||||
} |
||||
if err := enc.Encode(data); err != nil { |
||||
log.Println("[ERR] - JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
|
||||
//outputGzipJSON - output json for http response
|
||||
func outputGzipJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
w.Header().Set("Content-Encoding", "gzip") |
||||
// zw, _ := gzip.NewWriterLevel(w, gzip.BestCompression)
|
||||
zw := gzip.NewWriter(w) |
||||
defer func(zw *gzip.Writer) { |
||||
zw.Flush() |
||||
zw.Close() |
||||
}(zw) |
||||
enc := json.NewEncoder(zw) |
||||
if DEBUG { |
||||
enc.SetIndent("", " ") |
||||
} |
||||
if err := enc.Encode(data); err != nil { |
||||
log.Println("[ERR] - JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
package main |
||||
|
||||
import "loreal.com/dit/utils/task" |
||||
|
||||
func (a *App) registerTasks() { |
||||
a.TaskManager.RegisterWithContext("daily-maintenance-pp", "ceh-cs-test", a.dailyMaintenanceTaskHandler, 1) |
||||
a.TaskManager.RegisterWithContext("daily-maintenance", "ceh-cs", a.dailyMaintenanceTaskHandler, 1) |
||||
} |
||||
|
||||
//dailyMaintenanceTaskHandler - run daily maintenance task
|
||||
func (a *App) dailyMaintenanceTaskHandler(t *task.Task, args ...string) { |
||||
//a.DailyMaintenance(t, task.GetArgs(args, 0))
|
||||
a.DailyMaintenance(t) |
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
{ |
||||
// Use IntelliSense to learn about possible attributes. |
||||
// Hover to view descriptions of existing attributes. |
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
||||
"version": "0.2.0", |
||||
"configurations": [ |
||||
{ |
||||
"name": "test coupon@mac", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "test", |
||||
"program": "${workspaceFolder}/coupon", |
||||
}, |
||||
{ |
||||
"name": "Debug Coupon-service@MAC", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "debug", |
||||
"program": "${workspaceFolder}/main.go", |
||||
"args": [ |
||||
"-apitest=1" |
||||
] |
||||
}, |
||||
{ |
||||
"name": "run Coupon-service@MAC", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "exec", |
||||
"program": "${workspaceFolder}/main.go" |
||||
}, |
||||
|
||||
|
||||
{ |
||||
"name": "Debug CCS", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "debug", |
||||
"program": "${workspaceFolder}\\main.go", |
||||
"args": [ |
||||
"-apitest=1" |
||||
] |
||||
} |
||||
|
||||
] |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
{ |
||||
"javascript.format.insertSpaceBeforeFunctionParenthesis": true, |
||||
"vetur.format.defaultFormatter.js": "vscode-typescript", |
||||
"vetur.format.defaultFormatter.ts": "vscode-typescript", |
||||
"files.autoSave": "afterDelay", |
||||
"window.zoomLevel": 0, |
||||
"go.autocompleteUnimportedPackages": true, |
||||
"go.gocodePackageLookupMode": "go", |
||||
"go.gotoSymbol.includeImports": true, |
||||
"go.useCodeSnippetsOnFunctionSuggest": true, |
||||
"go.inferGopath": true, |
||||
"go.gopath":"C:\\GoPath", |
||||
"go.useCodeSnippetsOnFunctionSuggestWithoutType": true, |
||||
|
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
FROM centos:7 |
||||
|
||||
LABEL MAINTAINER="[email protected]" |
||||
|
||||
RUN yum -y update && yum clean all |
||||
|
||||
RUN mkdir -p /go && chmod -R 777 /go && \ |
||||
yum install -y centos-release-scl && \ |
||||
yum -y install git go-toolset-1.12 && yum clean all |
||||
|
||||
ENV GOPATH=/go \ |
||||
BASH_ENV=/opt/rh/go-toolset-1.12/enable \ |
||||
ENV=/opt/rh/go-toolset-1.12/enable |
||||
# PROMPT_COMMAND=". /opt/rh/go-toolset-1.12/enable" |
||||
|
||||
WORKDIR /go |
||||
@ -0,0 +1,191 @@
@@ -0,0 +1,191 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"log" |
||||
"net/http" |
||||
"strings" |
||||
"sync" |
||||
|
||||
"loreal.com/dit/endpoint" |
||||
"loreal.com/dit/middlewares" |
||||
"loreal.com/dit/module" |
||||
"loreal.com/dit/module/modules/root" |
||||
"loreal.com/dit/utils" |
||||
"loreal.com/dit/utils/task" |
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
|
||||
"github.com/robfig/cron" |
||||
"github.com/dgrijalva/jwt-go" |
||||
) |
||||
|
||||
//App - data struct for App & configuration file
|
||||
type App struct { |
||||
Name string |
||||
Description string |
||||
Config *base.Configuration |
||||
Root *root.Module |
||||
Endpoints map[string]EndpointEntry |
||||
MessageHandlers map[string]func(*module.Message) bool |
||||
AuthProvider middlewares.RoleVerifier |
||||
WebTokenAuthProvider middlewares.WebTokenVerifier |
||||
Scheduler *cron.Cron |
||||
TaskManager *task.Manager |
||||
wg *sync.WaitGroup |
||||
mutex *sync.RWMutex |
||||
Runtime map[string]*RuntimeEnv |
||||
} |
||||
|
||||
//RuntimeEnv - runtime env
|
||||
type RuntimeEnv struct { |
||||
Config *base.Env |
||||
stmts map[string]*sql.Stmt |
||||
db *sql.DB |
||||
KVStore map[string]interface{} |
||||
mutex *sync.RWMutex |
||||
} |
||||
|
||||
//Get - get value from kvstore in memory
|
||||
func (rt *RuntimeEnv) Get(key string) (value interface{}, ok bool) { |
||||
rt.mutex.RLock() |
||||
defer rt.mutex.RUnlock() |
||||
value, ok = rt.KVStore[key] |
||||
return |
||||
} |
||||
|
||||
//Retrive - get value from kvstore in memory, and delete it
|
||||
func (rt *RuntimeEnv) Retrive(key string) (value interface{}, ok bool) { |
||||
rt.mutex.Lock() |
||||
defer rt.mutex.Unlock() |
||||
value, ok = rt.KVStore[key] |
||||
if ok { |
||||
delete(rt.KVStore, key) |
||||
} |
||||
return |
||||
} |
||||
|
||||
//Set - set value to kvstore in memory
|
||||
func (rt *RuntimeEnv) Set(key string, value interface{}) { |
||||
rt.mutex.Lock() |
||||
defer rt.mutex.Unlock() |
||||
rt.KVStore[key] = value |
||||
} |
||||
|
||||
//EndpointEntry - endpoint registry entry
|
||||
type EndpointEntry struct { |
||||
Handler func(http.ResponseWriter, *http.Request) |
||||
Middlewares []endpoint.ServerMiddleware |
||||
} |
||||
|
||||
//NewApp - create new app
|
||||
func NewApp(name, description string, config *base.Configuration) *App { |
||||
if config == nil { |
||||
log.Println("Missing configuration data") |
||||
return nil |
||||
} |
||||
endpoint.SetPrometheus(strings.Replace(name, "-", "_", -1)) |
||||
app := &App{ |
||||
Name: name, |
||||
Description: description, |
||||
Config: config, |
||||
Root: root.NewModule(name, description, config.Prefix), |
||||
Endpoints: make(map[string]EndpointEntry, 0), |
||||
MessageHandlers: make(map[string]func(*module.Message) bool, 0), |
||||
Scheduler: cron.New(), |
||||
wg: &sync.WaitGroup{}, |
||||
mutex: &sync.RWMutex{}, |
||||
Runtime: make(map[string]*RuntimeEnv), |
||||
} |
||||
app.TaskManager = task.NewManager(app, 100) |
||||
return app |
||||
} |
||||
|
||||
//Init - app initialization
|
||||
func (a *App) Init() { |
||||
if a.Config != nil { |
||||
a.Config.FixPrefix() |
||||
for _, env := range a.Config.Envs { |
||||
utils.MakeFolder(env.DataFolder) |
||||
a.Runtime[env.Name] = &RuntimeEnv{ |
||||
Config: env, |
||||
KVStore: make(map[string]interface{}, 1024), |
||||
mutex: &sync.RWMutex{}, |
||||
} |
||||
} |
||||
a.InitDB() |
||||
} |
||||
var err error |
||||
base.Pubkey, err = jwt.ParseRSAPublicKeyFromPEM([]byte(base.Cfg.AuthPubKey)) //解析公钥
|
||||
if err != nil { |
||||
log.Println("ParseRSAPublicKeyFromPEM:", err.Error()) |
||||
panic(err) |
||||
} |
||||
|
||||
a.registerEndpoints() |
||||
a.registerMessageHandlers() |
||||
a.registerTasks() |
||||
// utils.LoadOrCreateJSON("./saved_status.json", &a.Status)
|
||||
a.Root.OnStop = func(p *module.Module) { |
||||
a.TaskManager.SendAll("stop") |
||||
a.wg.Wait() |
||||
} |
||||
a.Root.OnDispose = func(p *module.Module) { |
||||
for _, env := range a.Runtime { |
||||
if env.db != nil { |
||||
log.Println("Close sqlite for", env.Config.Name) |
||||
env.db.Close() |
||||
} |
||||
} |
||||
// utils.SaveJSON(a.Status, "./saved_status.json")
|
||||
} |
||||
} |
||||
|
||||
//registerEndpoints - Register Endpoints
|
||||
func (a *App) registerEndpoints() { |
||||
a.initEndpoints() |
||||
for path, entry := range a.Endpoints { |
||||
if entry.Middlewares == nil { |
||||
entry.Middlewares = a.getDefaultMiddlewares(path) |
||||
} |
||||
a.Root.MountingPoints[path] = endpoint.DecorateServer( |
||||
endpoint.Impl(entry.Handler), |
||||
entry.Middlewares..., |
||||
) |
||||
} |
||||
} |
||||
|
||||
//registerMessageHandlers - Register Message Handlers
|
||||
func (a *App) registerMessageHandlers() { |
||||
a.initMessageHandlers() |
||||
for path, handler := range a.MessageHandlers { |
||||
a.Root.AddMessageHandler(path, handler) |
||||
} |
||||
} |
||||
|
||||
//StartScheduler - register and start the scheduled tasks
|
||||
func (a *App) StartScheduler() { |
||||
if a.Scheduler == nil { |
||||
a.Scheduler = cron.New() |
||||
} else { |
||||
a.Scheduler.Stop() |
||||
a.Scheduler = cron.New() |
||||
} |
||||
for _, item := range a.Config.ScheduledTasks { |
||||
log.Println("[INFO] - Adding task:", item.Task) |
||||
func() { |
||||
s := item.Schedule |
||||
t := item.Task |
||||
a.Scheduler.AddFunc(s, func() { |
||||
a.TaskManager.RunTask(t, item.DefaultArgs...) |
||||
}) |
||||
}() |
||||
} |
||||
a.Scheduler.Start() |
||||
} |
||||
|
||||
//ListenAndServe - Start app
|
||||
func (a *App) ListenAndServe() { |
||||
// a.Init()
|
||||
a.StartScheduler() |
||||
a.Root.ListenAndServe(a.Config.Address) |
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package base |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// ErrorWithCode 作为统一返回给调用端的错误结构。
|
||||
// TODO: 确认下面的注释在godoc里面
|
||||
type ErrorWithCode struct { |
||||
// Code:错误编码, 每个code对应一个业务错误。
|
||||
Code int |
||||
// Message:错误描述, 可以用于显示给用户。
|
||||
Message string |
||||
} |
||||
|
||||
func (e *ErrorWithCode) Error() string { |
||||
return fmt.Sprintf( |
||||
`{ |
||||
"error-code" : %d, |
||||
"error-message" : "%s" |
||||
}`, e.Code, e.Message) |
||||
} |
||||
|
||||
//ErrTokenValidateFailed - 找不到一个规则的校验。
|
||||
var ErrTokenValidateFailed = ErrorWithCode{ |
||||
Code: 1500, |
||||
Message: "validate token failed", |
||||
} |
||||
|
||||
//ErrTokenExpired - 找不到一个规则的校验。
|
||||
var ErrTokenExpired = ErrorWithCode{ |
||||
Code: 1501, |
||||
Message: "the token is expired", |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
package base |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"testing" |
||||
|
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
type errorBody struct { |
||||
Code int `json:"error-code"` |
||||
Msg string `json:"error-message"` |
||||
} |
||||
|
||||
const sampleCode int = 100 |
||||
const sampleMsg string = "Hello world" |
||||
|
||||
func TestBaseerror(t *testing.T) { |
||||
Convey("Given an ErrorWithCode object", t, func() { |
||||
var e = ErrorWithCode { |
||||
Code : sampleCode, |
||||
Message : sampleMsg, |
||||
} |
||||
Convey("The error message Unmarshal by json", func() { |
||||
var js = e.Error() |
||||
var eb errorBody |
||||
err := json.Unmarshal([]byte(js), &eb) |
||||
Convey("The value should not be changed", func() { |
||||
So(err, ShouldBeNil) |
||||
So(eb.Code, ShouldEqual, sampleCode ) |
||||
So(eb.Msg, ShouldEqual, sampleMsg ) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
package base |
||||
|
||||
import( |
||||
"crypto/rsa" |
||||
) |
||||
|
||||
//GORoutingNumberForWechat - Total GO Routing # for send process
|
||||
const GORoutingNumberForWechat = 10 |
||||
|
||||
//ScheduledTask - command and parameters to run
|
||||
/* Schedule Spec: |
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
*** |
||||
*** corn example ***: |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
*/ |
||||
var Cfg = Configuration{ |
||||
Address: ":1503", |
||||
Prefix: "/", |
||||
JwtKey: "a9ac231b0f2a4f448b8846fd1f57814a", |
||||
AuthPubKey: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxWt9gzvtKVZhN9Xt/t1S5xApkSVeKRDbUGbto1NhWIqZCSgY1bmYVDFgnFLGT9tWdZDR4NMYwJxIpdpqjW/w4Q/4H9ummQE57C/AVQ/d4dJrF6MNyz67TL6kmHnrWCNYdHG9I4buTNCUL2y3DRutZ2nhNED/fDFkvQfWjj0ihqa6+Z4ZVTo0i1pX6u/IAjkHSdFRlzluM9EatuSyPo7T83hYqEjwoXkARLjm9jxPBU9jKOcL/1a3pE1QpTisxiQeIsmcbzRH/DPOhbJUwueQ3ux1CGu9RDZ8AX8eZvTrvXF41/b7N4cOi5jUvmV2H02NQh7WLp60Ln/hYmf5+nV5UwIDAQAB\n-----END PUBLIC KEY-----", |
||||
AppTitle: "Loreal coupon service", |
||||
Production: false, |
||||
Envs: []*Env{ |
||||
{ |
||||
Name: "prod", |
||||
SqliteDB: "data.db", |
||||
DataFolder: "./data/", |
||||
}, |
||||
}, |
||||
ScheduledTasks: []*ScheduledTask{ |
||||
{Schedule: "0 0 0 * * *", Task: "daily-maintenance", DefaultArgs: []string{}}, |
||||
{Schedule: "0 10 0 * * *", Task: "daily-maintenance-pp", DefaultArgs: []string{}}, |
||||
}, |
||||
} |
||||
|
||||
var Pubkey *rsa.PublicKey |
||||
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
package base |
||||
|
||||
import ( |
||||
"strings" |
||||
) |
||||
|
||||
//Configuration - app configuration
|
||||
type Configuration struct { |
||||
Address string `json:"address,omitempty"` |
||||
Prefix string `json:"prefix,omitempty"` |
||||
AppTitle string `json:"app-title"` |
||||
JwtKey string `json:"jwt-key,omitempty"` |
||||
AuthPubKey string `json:"auth-pubkey"` |
||||
UpstreamURL string `json:"upstream-url"` |
||||
UpstreamClientID string `json:"upstream-client-id"` |
||||
UpstreamClientSecret string `json:"upstream-client-secret"` |
||||
UpstreamUserName string `json:"upstream-username"` |
||||
UpstreamPassword string `json:"upstream-password"` |
||||
Production bool `json:"production,omitempty"` |
||||
Envs []*Env `json:"envs,omitempty"` |
||||
ScheduledTasks []*ScheduledTask `json:"scheduled-tasks,omitempty"` |
||||
} |
||||
|
||||
//Env - env configuration
|
||||
type Env struct { |
||||
Name string `json:"name,omitempty"` |
||||
SqliteDB string `json:"sqlite-db,omitempty"` |
||||
DataFolder string `json:"data,omitempty"` |
||||
} |
||||
|
||||
//ScheduledTask - command and parameters to run
|
||||
/* Schedule Spec: |
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Seconds | Yes | 0-59 | * / , - |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * * |
||||
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 0 * * * * |
||||
|
||||
*** |
||||
*** corn example ***: |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
*/ |
||||
//ScheduledTask - Scheduled Task
|
||||
type ScheduledTask struct { |
||||
Schedule string `json:"schedule,omitempty"` |
||||
Task string `json:"task,omitempty"` |
||||
DefaultArgs []string `json:"default-args,omitempty"` |
||||
} |
||||
|
||||
func (c *Configuration) FixPrefix() { |
||||
if !strings.HasPrefix(c.Prefix, "/") { |
||||
c.Prefix = "/" + c.Prefix |
||||
} |
||||
if !strings.HasSuffix(c.Prefix, "/") { |
||||
c.Prefix = c.Prefix + "/" |
||||
} |
||||
} |
||||
@ -0,0 +1,142 @@
@@ -0,0 +1,142 @@
|
||||
package base |
||||
|
||||
import ( |
||||
// "bytes"
|
||||
"encoding/json" |
||||
// "errors"
|
||||
"fmt" |
||||
"log" |
||||
"math/rand" |
||||
// "mime/multipart"
|
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/google/uuid" |
||||
"github.com/dgrijalva/jwt-go" |
||||
) |
||||
|
||||
var r *rand.Rand = rand.New(rand.NewSource(time.Now().Unix())) |
||||
|
||||
// IsBlankString 判断是否为空的ID,ID不能都是空白.
|
||||
func IsBlankString(str string) bool { |
||||
return len(strings.TrimSpace(str)) == 0 |
||||
} |
||||
|
||||
// IsEmptyString 判断是否为空的字符串.
|
||||
func IsEmptyString(str string) bool { |
||||
return len(str) == 0 |
||||
} |
||||
|
||||
// IsValidUUID
|
||||
func IsValidUUID(u string) bool { |
||||
_, err := uuid.Parse(u) |
||||
return err == nil |
||||
} |
||||
// SetResponseHeader 一个快捷设置status code 和content type的方法
|
||||
func SetResponseHeader(w http.ResponseWriter, statusCode int, contentType string) { |
||||
w.Header().Set("Content-Type", contentType) |
||||
w.WriteHeader(statusCode) |
||||
} |
||||
|
||||
// WriteErrorResponse 一个快捷设置包含错误body的response
|
||||
func WriteErrorResponse(w http.ResponseWriter, statusCode int, contentType string, a interface{}) { |
||||
SetResponseHeader(w, statusCode, contentType) |
||||
switch vv := a.(type) { |
||||
case error: { |
||||
fmt.Fprintf(w, vv.Error()) |
||||
} |
||||
case map[string][]error: { |
||||
jsonBytes, err := json.Marshal(vv) |
||||
if nil != err { |
||||
log.Println(err) |
||||
fmt.Fprintf(w, err.Error()) |
||||
} |
||||
var str = string(jsonBytes) |
||||
fmt.Fprintf(w, str) |
||||
} |
||||
case []error: { |
||||
jsonBytes, err := json.Marshal(vv) |
||||
if nil != err { |
||||
log.Println(err) |
||||
fmt.Fprintf(w, err.Error()) |
||||
} |
||||
var str = string(jsonBytes) |
||||
fmt.Fprintf(w, str) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// trimURIPrefix 将一个uri拆分为若干node,根据ndoe取得一些动态参数。
|
||||
func TrimURIPrefix(uri string, stopTag string) []string { |
||||
params := strings.Split(strings.TrimPrefix(strings.TrimSuffix(uri, "/"), "/"), "/") |
||||
last := len(params) - 1 |
||||
for i := last; i >= 0; i-- { |
||||
if params[i] == stopTag { |
||||
return params[i+1:] |
||||
} |
||||
} |
||||
return params |
||||
} |
||||
|
||||
//outputJSON - output json for http response
|
||||
func outputJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json;charset=utf-8") |
||||
enc := json.NewEncoder(w) |
||||
if err := enc.Encode(data); err != nil { |
||||
log.Println("[ERR] - [outputJSON] JSON encode error:", err) |
||||
http.Error(w, "500", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
|
||||
// GetMapClaim 获取JWT 里面的map claims
|
||||
func GetMapClaim(token string) (interface{}) { |
||||
if IsBlankString(token) { |
||||
return nil |
||||
} |
||||
ret, b := ParesToken(token, Pubkey) |
||||
if nil != b { |
||||
return nil |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// ParesToken 检查toekn
|
||||
func ParesToken(tokenString string, key interface{}) (interface{}, error) { |
||||
token, err := jwt.ParseWithClaims(tokenString, &Requester{}, func(token *jwt.Token) (interface{}, error) { |
||||
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { |
||||
return nil, fmt.Errorf("不支持的加密方式: %v", token.Header["alg"]) |
||||
} |
||||
return key, nil |
||||
}) |
||||
if nil != err { |
||||
if ve, ok := err.(*jwt.ValidationError); ok { |
||||
if ve.Errors&(jwt.ValidationErrorExpired) != 0 { |
||||
return nil, &ErrTokenExpired |
||||
} |
||||
} |
||||
|
||||
log.Println("解析token失败:", err) |
||||
return nil, &ErrTokenValidateFailed |
||||
} |
||||
|
||||
// if claims, ok := token.Claims.(jwt.StandardClaims); ok && token.Valid {
|
||||
if claims, ok := token.Claims.(*Requester); ok && token.Valid { |
||||
return claims, nil |
||||
} else { |
||||
fmt.Println("======pares:", err) |
||||
return "", &ErrTokenValidateFailed |
||||
} |
||||
} |
||||
|
||||
// RandString 生成随机字符串
|
||||
func RandString(len int) string { |
||||
bytes := make([]byte, len) |
||||
for i := 0; i < len; i++ { |
||||
b := r.Intn(26) + 65 |
||||
bytes[i] = byte(b) |
||||
} |
||||
return string(bytes) |
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
package base |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
func TestIsBlankString(t *testing.T) { |
||||
Convey("Given a hello world string", t, func() { |
||||
str := "Hello world" |
||||
Convey("The str should not be blank", func() { |
||||
So(IsBlankString(str), ShouldBeFalse) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a string with nothing", t, func() { |
||||
str := "" |
||||
Convey("The str should be blank", func() { |
||||
So(IsBlankString(str), ShouldBeTrue) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a string with some blank characters", t, func() { |
||||
str := " " |
||||
Convey("The str should be blank", func() { |
||||
So(IsBlankString(str), ShouldBeTrue) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func TestIsEmptyString(t *testing.T) { |
||||
Convey("Given a hello world string", t, func() { |
||||
str := "Hello world" |
||||
Convey("The str should not be empty", func() { |
||||
So(IsEmptyString(str), ShouldBeFalse) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a string with nothing", t, func() { |
||||
str := "" |
||||
Convey("The str should be empty", func() { |
||||
So(IsEmptyString(str), ShouldBeTrue) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a string with some blank characters", t, func() { |
||||
str := " " |
||||
Convey("The str should not be empty", func() { |
||||
So(IsEmptyString(str), ShouldBeFalse) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func TestIsValidUUID(t *testing.T) { |
||||
Convey("Given an UUID string", t, func() { |
||||
str := "66e382c4-b859-4e46-9a88-d875fbdaf366" |
||||
Convey("The str should be UUID", func() { |
||||
So(IsValidUUID(str), ShouldBeTrue) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a hello world string", t, func() { |
||||
str := "Hello world" |
||||
Convey("The str should not be UUID", func() { |
||||
So(IsValidUUID(str), ShouldBeFalse) |
||||
}) |
||||
}) |
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package base |
||||
|
||||
import ( |
||||
"github.com/chenhg5/collection" |
||||
"github.com/dgrijalva/jwt-go" |
||||
) |
||||
|
||||
const ROLE_COUPON_ISSUER string = "coupon_issuer" |
||||
const ROLE_COUPON_REDEEMER string = "coupon_redeemer" |
||||
const ROLE_COUPON_LISTENER string = "coupon_listener" |
||||
|
||||
// Requester 解析数据
|
||||
type Requester struct { // token里面添加用户信息,验证token后可能会用到用户信息
|
||||
jwt.StandardClaims |
||||
UserID string `json:"preferred_username"` //TODO: 能否改成username?
|
||||
Roles map[string]([]string) `json:"realm_access"` |
||||
Brand string `json:"brand"` |
||||
} |
||||
|
||||
// HasRole 检查请求者是否有某个角色
|
||||
func (r *Requester) HasRole(role string) bool { |
||||
// if !collection.Collect(requester.Roles).Has("roles") {
|
||||
// return false
|
||||
// }
|
||||
roles := r.Roles["roles"] |
||||
if nil == roles { |
||||
return false |
||||
} |
||||
if !collection.Collect(roles).Contains(role) { |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
@ -0,0 +1,315 @@
@@ -0,0 +1,315 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"log" |
||||
"strings" |
||||
"sync" |
||||
|
||||
// "time"
|
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
// "loreal.com/dit/cmd/coupon-service/rule"
|
||||
) |
||||
|
||||
var stmtQueryWithType *sql.Stmt |
||||
var stmtQueryCoupon *sql.Stmt |
||||
var stmtQueryCoupons *sql.Stmt |
||||
var stmtInsertCoupon *sql.Stmt |
||||
var stmtUpdateCouponState *sql.Stmt |
||||
var stmtInsertCouponTransaction *sql.Stmt |
||||
var stmtQueryCouponTransactionsWithType *sql.Stmt |
||||
var stmtQueryCouponTransactionCountWithType *sql.Stmt |
||||
var dbMutex *sync.RWMutex |
||||
|
||||
func dbInit() { |
||||
var err error |
||||
dbMutex = new(sync.RWMutex) |
||||
stmtQueryWithType, err = dbConnection.Prepare("SELECT id, couponTypeID, consumerRefID, channelID, state, properties, createdTime FROM coupons WHERE consumerID = (?) AND couponTypeID = (?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtQueryCoupon, err = dbConnection.Prepare("SELECT id, consumerID, consumerRefID, channelID, couponTypeID, state, properties, createdTime FROM coupons WHERE id = (?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtQueryCoupons, err = dbConnection.Prepare("SELECT id, couponTypeID, consumerRefID, channelID, state, properties, createdTime FROM coupons WHERE consumerID = (?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtInsertCoupon, err = dbConnection.Prepare("INSERT INTO coupons VALUES (?,?,?,?,?,?,?,?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtUpdateCouponState, err = dbConnection.Prepare("UPDATE coupons SET state = (?) WHERE id = (?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtInsertCouponTransaction, err = dbConnection.Prepare("INSERT INTO couponTransactions VALUES (?,?,?,?,?,?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtQueryCouponTransactionsWithType, err = dbConnection.Prepare("SELECT id, actorID, transType, extraInfo, createdTime FROM couponTransactions WHERE couponID = (?) and transType = (?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
|
||||
stmtQueryCouponTransactionCountWithType, err = dbConnection.Prepare("SELECT count(1) FROM couponTransactions WHERE couponID = (?) and transType = (?)") |
||||
if err != nil { |
||||
log.Println(err) |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
// getCoupons
|
||||
// TODO: 增加各种过滤条件
|
||||
func getCoupons(consumerID string, couponTypeID string) ([]*Coupon, error) { |
||||
dbMutex.RLock() |
||||
defer dbMutex.RUnlock() |
||||
var rows *sql.Rows |
||||
var err error |
||||
if base.IsBlankString(couponTypeID) { |
||||
rows, err = stmtQueryCoupons.Query(consumerID) |
||||
} else { |
||||
rows, err = stmtQueryWithType.Query(consumerID, couponTypeID) |
||||
} |
||||
|
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
return getCouponsFromRows(consumerID, rows) |
||||
} |
||||
|
||||
func getCouponsFromRows(consumerID string, rows *sql.Rows) ([]*Coupon, error) { |
||||
var coupons []*Coupon = make([]*Coupon, 0) |
||||
for rows.Next() { |
||||
var c Coupon |
||||
var pstr string |
||||
err := rows.Scan(&c.ID, &c.CouponTypeID, &c.ConsumerRefID, &c.ChannelID, &c.State, &pstr, &c.CreatedTime) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
c.SetPropertiesFromString(pstr) |
||||
c.CreatedTimeToLocal() |
||||
c.ConsumerID = consumerID |
||||
c.Transactions = make([]*Transaction, 0) |
||||
coupons = append(coupons, &c) |
||||
} |
||||
err := rows.Err() |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
return coupons, nil |
||||
} |
||||
|
||||
// getCoupon 获取某一卡券
|
||||
func getCoupon(couponID string) (*Coupon, error) { |
||||
dbMutex.RLock() |
||||
defer dbMutex.RUnlock() |
||||
row := stmtQueryCoupon.QueryRow(couponID) |
||||
|
||||
var c Coupon |
||||
var pstr string |
||||
err := row.Scan(&c.ID, &c.ConsumerID, &c.ConsumerRefID, &c.ChannelID, &c.CouponTypeID, &c.State, &pstr, &c.CreatedTime) |
||||
if err != nil { |
||||
if sql.ErrNoRows == err { |
||||
return nil, nil |
||||
} |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
|
||||
c.SetPropertiesFromString(pstr) |
||||
c.CreatedTimeToLocal() |
||||
return &c, nil |
||||
} |
||||
|
||||
// CreateCoupon 保存卡券到数据库
|
||||
func createCoupon(c *Coupon) error { |
||||
if nil == c { |
||||
return nil |
||||
} |
||||
dbMutex.Lock() |
||||
defer dbMutex.Unlock() |
||||
pstr, err := c.GetPropertiesString() |
||||
if nil != err { |
||||
return err |
||||
} |
||||
res, err := stmtInsertCoupon.Exec(c.ID, c.CouponTypeID, c.ConsumerID, c.ConsumerRefID, c.ChannelID, c.State, pstr, c.CreatedTime.Unix()) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return err |
||||
} |
||||
|
||||
log.Println(res) |
||||
return nil |
||||
} |
||||
|
||||
// CreateCoupon 保存卡券到数据库
|
||||
func createCoupons(cs []*Coupon) error { |
||||
if len(cs) == 0 { |
||||
return nil |
||||
} |
||||
dbMutex.Lock() |
||||
defer dbMutex.Unlock() |
||||
|
||||
valueStrings := make([]string, 0, len(cs)) |
||||
valueArgs := make([]interface{}, 0, len(cs)*8) |
||||
for _, c := range cs { |
||||
valueStrings = append(valueStrings, "(?,?,?,?,?,?,?,?)") |
||||
valueArgs = append(valueArgs, c.ID) |
||||
valueArgs = append(valueArgs, c.CouponTypeID) |
||||
valueArgs = append(valueArgs, c.ConsumerID) |
||||
valueArgs = append(valueArgs, c.ConsumerRefID) |
||||
valueArgs = append(valueArgs, c.ChannelID) |
||||
valueArgs = append(valueArgs, c.State) |
||||
pstr, err := c.GetPropertiesString() |
||||
if nil != err { |
||||
return err |
||||
} |
||||
valueArgs = append(valueArgs, pstr) |
||||
valueArgs = append(valueArgs, c.CreatedTime.Unix()) |
||||
} |
||||
stmt := fmt.Sprintf("INSERT INTO coupons VALUES %s", strings.Join(valueStrings, ",")) |
||||
res, err := dbConnection.Exec(stmt, valueArgs...) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return err |
||||
} |
||||
log.Println(res) |
||||
return nil |
||||
} |
||||
|
||||
// updateCouponState 更新卡券状态
|
||||
func updateCouponState(c *Coupon) error { |
||||
if nil == c { |
||||
return nil |
||||
} |
||||
dbMutex.Lock() |
||||
defer dbMutex.Unlock() |
||||
|
||||
res, err := stmtUpdateCouponState.Exec(c.State, c.ID) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return err |
||||
} |
||||
log.Println(res) |
||||
return nil |
||||
} |
||||
|
||||
// CreateCouponTransaction 保存卡券操作log到数据库
|
||||
func createCouponTransaction(t *Transaction) error { |
||||
if nil == t { |
||||
return nil |
||||
} |
||||
dbMutex.Lock() |
||||
defer dbMutex.Unlock() |
||||
res, err := stmtInsertCouponTransaction.Exec(t.ID, t.CouponID, t.ActorID, t.TransType, t.EncryptExtraInfo(), t.CreatedTime.Unix()) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return err |
||||
} |
||||
log.Println(res) |
||||
return nil |
||||
} |
||||
|
||||
// createCouponTransactions 保存卡券操作log到数据库
|
||||
func createCouponTransactions(ts []*Transaction) error { |
||||
if len(ts) == 0 { |
||||
return nil |
||||
} |
||||
dbMutex.Lock() |
||||
defer dbMutex.Unlock() |
||||
|
||||
valueStrings := make([]string, 0, len(ts)) |
||||
valueArgs := make([]interface{}, 0, len(ts)*6) |
||||
for _, t := range ts { |
||||
valueStrings = append(valueStrings, "(?,?,?,?,?,?)") |
||||
valueArgs = append(valueArgs, t.ID) |
||||
valueArgs = append(valueArgs, t.CouponID) |
||||
valueArgs = append(valueArgs, t.ActorID) |
||||
valueArgs = append(valueArgs, t.TransType) |
||||
valueArgs = append(valueArgs, t.EncryptExtraInfo()) |
||||
valueArgs = append(valueArgs, t.CreatedTime.Unix()) |
||||
} |
||||
stmt := fmt.Sprintf("INSERT INTO couponTransactions VALUES %s", strings.Join(valueStrings, ",")) |
||||
res, err := dbConnection.Exec(stmt, valueArgs...) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return err |
||||
} |
||||
log.Println(res) |
||||
return nil |
||||
} |
||||
|
||||
// getCouponTransactionsWithType 获取某一卡券某类型业务的记录
|
||||
func getCouponTransactionsWithType(couponID string, ttype TransType) ([]*Transaction, error) { |
||||
dbMutex.RLock() |
||||
defer dbMutex.RUnlock() |
||||
rows, err := stmtQueryCouponTransactionsWithType.Query(couponID, ttype) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
|
||||
var ts []*Transaction = make([]*Transaction, 0) |
||||
for rows.Next() { |
||||
var t Transaction |
||||
var ei string |
||||
err := rows.Scan(&t.ID, &t.ActorID, &t.TransType, &ei, &t.CreatedTime) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
t.CreatedTimeToLocal() |
||||
err = t.DecryptExtraInfo(ei) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
|
||||
t.CouponID = couponID |
||||
ts = append(ts, &t) |
||||
} |
||||
err = rows.Err() |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
return ts, nil |
||||
} |
||||
|
||||
// getCouponTransactionCountWithType 获取某一卡券某一类型业务的次数,比如查询兑换多少次这样的场景
|
||||
func getCouponTransactionCountWithType(couponID string, ttype TransType) (uint, error) { |
||||
dbMutex.RLock() |
||||
defer dbMutex.RUnlock() |
||||
var count uint |
||||
err := stmtQueryCouponTransactionCountWithType.QueryRow(couponID, ttype).Scan(&count) |
||||
if err != nil { |
||||
if sql.ErrNoRows == err { |
||||
return 0, nil |
||||
} |
||||
log.Println(err) |
||||
return 0, err |
||||
} |
||||
return count, nil |
||||
} |
||||
@ -0,0 +1,419 @@
@@ -0,0 +1,419 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"fmt" |
||||
// "time"
|
||||
|
||||
// "reflect"
|
||||
"testing" |
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
// "loreal.com/dit/utils"
|
||||
|
||||
"github.com/google/uuid" |
||||
_ "github.com/mattn/go-sqlite3" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
func Test_GetCoupons(t *testing.T) { |
||||
Convey("Given a coupon in an empty table", t, func() { |
||||
c := _prepareARandomCouponInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, true) |
||||
Convey("Get the coupon and should contain the coupon just created", func() { |
||||
cs, _ := getCoupons(c.ConsumerID, defaultCouponTypeID) |
||||
So(len(cs), ShouldEqual, 1) |
||||
c2 := cs[0] |
||||
So(base.IsValidUUID(c2.ID), ShouldBeTrue) |
||||
So(c.ConsumerID, ShouldEqual, c2.ConsumerID) |
||||
So(c.ConsumerRefID, ShouldEqual, c2.ConsumerRefID) |
||||
So(c.ChannelID, ShouldEqual, c2.ChannelID) |
||||
So(defaultCouponTypeID, ShouldEqual, c2.CouponTypeID) |
||||
So(c.State, ShouldEqual, c2.State) |
||||
}) |
||||
}) |
||||
|
||||
// Convey("Given several coupons for 2 consumers in an empty table", t, func() {
|
||||
// cs := _prepareSeveralCouponsInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, true)
|
||||
// _ = _prepareSeveralCouponsInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, false)
|
||||
// Convey("Get for the first consumer and should contain all the coupons just created for first consumer", func() {
|
||||
// cs3, _ := getCoupons(cs[0].ConsumerID, defaultCouponTypeID)
|
||||
// So(len(cs3), ShouldEqual, len(cs))
|
||||
// c := cs[r.Intn(len(cs))]
|
||||
// for _, c2 := range cs3 {
|
||||
// if c.ID == c2.ID {
|
||||
// So(c.ConsumerID, ShouldEqual, c2.ConsumerID)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
// Convey("Given several coupons with different coupon type in an empty table", t, func() {
|
||||
// consumerID := base.RandString(4)
|
||||
// cs := _prepareSeveralCouponsInDB(consumerID, base.RandString(4), base.RandString(4), defaultCouponTypeID, true)
|
||||
// _ = _prepareSeveralCouponsInDB(consumerID, base.RandString(4), base.RandString(4), anotherCouponTypeID, false)
|
||||
// Convey("Get the coupon with defaultCouponTypeID and only with defaultCouponTypeID", func() {
|
||||
// cs2, _ := getCoupons(consumerID, defaultCouponTypeID)
|
||||
// So(len(cs), ShouldEqual, len(cs2))
|
||||
// c := cs[r.Intn(len(cs))]
|
||||
// for _, c2 := range cs2 {
|
||||
// if c.ID == c2.ID {
|
||||
// So(c.ConsumerID, ShouldEqual, c2.ConsumerID)
|
||||
// So(c.ConsumerRefID, ShouldEqual, c2.ConsumerRefID)
|
||||
// So(c.ChannelID, ShouldEqual, c2.ChannelID)
|
||||
// So(c.CouponTypeID, ShouldEqual, c2.CouponTypeID)
|
||||
// So(c.State, ShouldEqual, c2.State)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
// Convey("Given a coupon in an empty table and will query with wrong input", t, func() {
|
||||
// c := _prepareARandomCouponInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, true)
|
||||
// Convey("Get the coupon with wrong type and should contain nothing", func() {
|
||||
// cs, _ := getCoupons(c.ConsumerID, anotherCouponTypeID)
|
||||
// So(len(cs), ShouldEqual, 0)
|
||||
// })
|
||||
|
||||
// Convey("Get the coupon with wrong consumerID and should contain nothing", func() {
|
||||
// wrongConsumerID := "this is not a real type"
|
||||
// cs, _ := getCoupons(wrongConsumerID, defaultCouponTypeID)
|
||||
// So(len(cs), ShouldEqual, 0)
|
||||
// })
|
||||
// })
|
||||
} |
||||
|
||||
func Test_GetCoupon(t *testing.T) { |
||||
Convey("Given a coupon in an empty table", t, func() { |
||||
c := _prepareARandomCouponInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, true) |
||||
Convey("get this coupon and should be same with the coupon just created", func() { |
||||
c2, _ := getCoupon(c.ID) |
||||
So(c.ID, ShouldEqual, c2.ID) |
||||
So(c.ConsumerID, ShouldEqual, c2.ConsumerID) |
||||
So(c.ConsumerRefID, ShouldEqual, c2.ConsumerRefID) |
||||
So(c.ChannelID, ShouldEqual, c2.ChannelID) |
||||
So(c.CouponTypeID, ShouldEqual, c2.CouponTypeID) |
||||
So(c.State, ShouldEqual, c2.State) |
||||
So(c.Properties, ShouldContainKey, "the_key") |
||||
So(c.Properties["the_key"], ShouldEqual, "the value") |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given several coupons in an empty table", t, func() { |
||||
cs := _prepareSeveralCouponsInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, true) |
||||
Convey("get the random coupon and should be same with the first coupon just created", func() { |
||||
index := r.Intn(len(cs)) |
||||
c2, _ := getCoupon(cs[index].ID) |
||||
So(cs[index].ID, ShouldEqual, c2.ID) |
||||
So(cs[index].ConsumerID, ShouldEqual, c2.ConsumerID) |
||||
So(cs[index].ConsumerRefID, ShouldEqual, c2.ConsumerRefID) |
||||
So(cs[index].ChannelID, ShouldEqual, c2.ChannelID) |
||||
So(cs[index].CouponTypeID, ShouldEqual, c2.CouponTypeID) |
||||
So(cs[index].State, ShouldEqual, c2.State) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a truncated table", t, func() { |
||||
_, _ = dbConnection.Exec("DELETE FROM coupons") |
||||
Convey("get with a non existed coupon id and the coupon should be null", func() { |
||||
c, _ := getCoupon("some_coupon_id") |
||||
So(c, ShouldBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_CreateCoupon(t *testing.T) { |
||||
Convey("Given a coupon", t, func() { |
||||
state := r.Intn(int(SUnknown)) |
||||
var p map[string]interface{} |
||||
p = make(map[string]interface{}, 1) |
||||
p["the_key"] = "the value" |
||||
cc := _aCoupon(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, State(state), p) |
||||
Convey("Save the coupon to database", func() { |
||||
createCoupon(cc) |
||||
Convey("The coupon just created can be queried", func() { |
||||
s := fmt.Sprintf(`SELECT id, consumerID, consumerRefID, channelID, couponTypeID, state, properties, createdTime FROM coupons WHERE id = '%s'`, cc.ID) |
||||
row := dbConnection.QueryRow(s) |
||||
var c Coupon |
||||
var pstr string |
||||
_ = row.Scan(&c.ID, &c.ConsumerID, &c.ConsumerRefID, &c.ChannelID, &c.CouponTypeID, &c.State, &pstr, &c.CreatedTime) |
||||
c.SetPropertiesFromString(pstr) |
||||
c.CreatedTimeToLocal() |
||||
Convey("The coupon queried should be same with original", func() { |
||||
So(cc.ID, ShouldEqual, c.ID) |
||||
So(cc.ConsumerID, ShouldEqual, c.ConsumerID) |
||||
So(cc.ConsumerRefID, ShouldEqual, c.ConsumerRefID) |
||||
So(cc.ChannelID, ShouldEqual, c.ChannelID) |
||||
So(cc.CouponTypeID, ShouldEqual, c.CouponTypeID) |
||||
So(cc.State, ShouldEqual, c.State) |
||||
So(cc.Properties, ShouldContainKey, "the_key") |
||||
So(cc.Properties["the_key"], ShouldEqual, "the value") |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given nil coupon", t, func() { |
||||
err := createCoupon(nil) |
||||
Convey("Nothing happened", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_CreateCoupons(t *testing.T) { |
||||
Convey("Given several coupons", t, func() { |
||||
cs := _someCoupons(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID) |
||||
Convey("createCoupons save them to database", func() { |
||||
_, _ = dbConnection.Exec("DELETE FROM coupons") |
||||
createCoupons(cs) |
||||
Convey("Saved coupons should same with prepared", func() { |
||||
for _, c := range cs { |
||||
s := fmt.Sprintf(`SELECT id, consumerID, consumerRefID, channelID, couponTypeID, state, properties, createdTime FROM coupons WHERE id = '%s'`, c.ID) |
||||
row := dbConnection.QueryRow(s) |
||||
var c2 Coupon |
||||
var pstr string |
||||
_ = row.Scan(&c2.ID, &c2.ConsumerID, &c2.ConsumerRefID, &c2.ChannelID, &c2.CouponTypeID, &c2.State, &pstr, &c2.CreatedTime) |
||||
c2.SetPropertiesFromString(pstr) |
||||
So(c.ID, ShouldEqual, c2.ID) |
||||
So(c.ConsumerID, ShouldEqual, c2.ConsumerID) |
||||
So(c.ConsumerRefID, ShouldEqual, c2.ConsumerRefID) |
||||
So(c.ChannelID, ShouldEqual, c2.ChannelID) |
||||
So(c.CouponTypeID, ShouldEqual, c2.CouponTypeID) |
||||
So(c.State, ShouldEqual, c2.State) |
||||
So(c2.Properties, ShouldContainKey, "the_key") |
||||
So(c2.Properties["the_key"], ShouldEqual, "the value") |
||||
} |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given empty coupon list", t, func() { |
||||
err := createCoupons(make([]*Coupon, 0)) |
||||
Convey("Nothing happened", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_UpdateCouponState(t *testing.T) { |
||||
Convey("Given a coupon in an empty table", t, func() { |
||||
c := _prepareARandomCouponInDB(base.RandString(4), base.RandString(4), base.RandString(4), defaultCouponTypeID, true) |
||||
Convey("Reset the coupon state", func() { |
||||
oldState := int(c.State) |
||||
c.State = State((oldState + 1) % int(SUnknown)) |
||||
newState := int(c.State) |
||||
So(oldState, ShouldNotEqual, int(c.State)) |
||||
updateCouponState(c) |
||||
Convey("The latest coupon should have diff state with original", func() { |
||||
c2, _ := getCoupon(c.ID) |
||||
So(c2.State, ShouldEqual, newState) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given nil coupon", t, func() { |
||||
err := updateCouponState(nil) |
||||
Convey("Nothing happened", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_CreateCouponTransaction(t *testing.T) { |
||||
Convey("Given a coupon transaction", t, func() { |
||||
tt := r.Intn(int(TTUnknownTransaction) + 1) |
||||
t := _aTransaction(base.RandString(4), uuid.New().String(), TransType(tt), base.RandString(4)) |
||||
Convey("Save the transaction to database", func() { |
||||
createCouponTransaction(t) |
||||
Convey("The transaction just created can be queried", func() { |
||||
s := fmt.Sprintf(`SELECT id, couponID, actorID, transType, extraInfo, createdTime FROM couponTransactions WHERE id = '%s'`, t.ID) |
||||
row := dbConnection.QueryRow(s) |
||||
var t2 Transaction |
||||
var ei string |
||||
_ = row.Scan(&t2.ID, &t2.CouponID, &t2.ActorID, &t2.TransType, &ei, &t2.CreatedTime) |
||||
t2.DecryptExtraInfo(ei) |
||||
Convey("The transaction queried should be same with original", func() { |
||||
So(t.ID, ShouldEqual, t2.ID) |
||||
So(t.CouponID, ShouldEqual, t2.CouponID) |
||||
So(t.ActorID, ShouldEqual, t2.ActorID) |
||||
So(t.TransType, ShouldEqual, t2.TransType) |
||||
So(t.ExtraInfo, ShouldEqual, t2.ExtraInfo) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given nil coupon transaction", t, func() { |
||||
err := createCouponTransaction(nil) |
||||
Convey("Nothing happened", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_CreateCouponTransactions(t *testing.T) { |
||||
Convey("Given several coupon transactions", t, func() { |
||||
ts := _someTransaction(base.RandString(4), uuid.New().String(), base.RandString(4)) |
||||
Convey("createCouponTransactions save them to database", func() { |
||||
_, _ = dbConnection.Exec("DELETE FROM coupons") |
||||
createCouponTransactions(ts) |
||||
Convey("Saved coupon transactions should same with prepared", func() { |
||||
for _, t := range ts { |
||||
s := fmt.Sprintf(`SELECT id, couponID, actorID, transType, extraInfo, createdTime FROM couponTransactions WHERE id = '%s'`, t.ID) |
||||
row := dbConnection.QueryRow(s) |
||||
var t2 Transaction |
||||
var ei string |
||||
_ = row.Scan(&t2.ID, &t2.CouponID, &t2.ActorID, &t2.TransType, &ei, &t2.CreatedTime) |
||||
t2.DecryptExtraInfo(ei) |
||||
So(t.ID, ShouldEqual, t2.ID) |
||||
So(t.CouponID, ShouldEqual, t2.CouponID) |
||||
So(t.ActorID, ShouldEqual, t2.ActorID) |
||||
So(t.TransType, ShouldEqual, t2.TransType) |
||||
So(t.ExtraInfo, ShouldEqual, t2.ExtraInfo) |
||||
} |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given empty coupon transaction list", t, func() { |
||||
err := createCouponTransactions(make([]*Transaction, 0)) |
||||
Convey("Nothing happened", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_GetCouponTransactionsWithType(t *testing.T) { |
||||
Convey("Given a coupon transaction in an empty table", t, func() { |
||||
tt := r.Intn(int(TTUnknownTransaction)) |
||||
t := _prepareARandomCouponTransactionInDB(base.RandString(4), uuid.New().String(), TransType(tt), true) |
||||
Convey("getCouponTransactionsWithType can get the coupon transaction", func() { |
||||
ts, _ := getCouponTransactionsWithType(t.CouponID, TransType(tt)) |
||||
Convey("And should contain the coupon transaction just created", func() { |
||||
So(len(ts), ShouldEqual, 1) |
||||
t2 := ts[0] |
||||
So(base.IsValidUUID(t2.ID), ShouldBeTrue) |
||||
So(t.CouponID, ShouldEqual, t2.CouponID) |
||||
So(t.ActorID, ShouldEqual, t2.ActorID) |
||||
So(t.TransType, ShouldEqual, t2.TransType) |
||||
}) |
||||
}) |
||||
|
||||
Convey("getCouponTransactionsWithType try to get the coupon transaction with diff trans type", func() { |
||||
tt2 := tt + 1%int(TTUnknownTransaction) |
||||
ts, _ := getCouponTransactionsWithType(t.CouponID, TransType(tt2)) |
||||
Convey("And should not contain the coupon just created", func() { |
||||
So(len(ts), ShouldEqual, 0) |
||||
}) |
||||
}) |
||||
|
||||
Convey("getCouponTransactionsWithType try to get the coupon transaction with wrong coupon id", func() { |
||||
ts, _ := getCouponTransactionsWithType(base.RandString(4), TransType(tt)) |
||||
Convey("And should not contain the coupon just created", func() { |
||||
So(len(ts), ShouldEqual, 0) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given several coupon transactions with different coupon in an empty table", t, func() { |
||||
couponID := uuid.New().String() |
||||
tt3 := r.Intn(int(TTUnknownTransaction)) |
||||
ts := _prepareSeveralCouponTransactionsInDB(base.RandString(4), couponID, TransType(tt3), true) |
||||
_ = _prepareSeveralCouponTransactionsInDB(base.RandString(4), uuid.New().String(), TransType(tt3), false) |
||||
Convey("getCouponTransactionsWithType only get the coupon transactions with given couponID", func() { |
||||
ts2, _ := getCouponTransactionsWithType(couponID, TransType(tt3)) |
||||
Convey("And should contain the coupon ransactions just created with given couponID", func() { |
||||
So(len(ts), ShouldEqual, len(ts2)) |
||||
t := ts[0] |
||||
for _, t2 := range ts2 { |
||||
if t.ID == t2.ID { |
||||
So(t.ID, ShouldEqual, t2.ID) |
||||
So(t.CouponID, ShouldEqual, t2.CouponID) |
||||
So(t.ActorID, ShouldEqual, t2.ActorID) |
||||
So(t.TransType, ShouldEqual, t2.TransType) |
||||
break |
||||
} |
||||
} |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_GetCouponTransactionCountWithType(t *testing.T) { |
||||
Convey("Given a clean db", t, func() { |
||||
dbConnection.Exec("DELETE FROM couponTransactions") |
||||
Convey("There is no coupon transactio", func() { |
||||
tt := r.Intn(int(TTUnknownTransaction)) |
||||
count, err := getCouponTransactionCountWithType(base.RandString(4), TransType(tt)) |
||||
So(0, ShouldEqual, count) |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given several coupon transactions with different trans type in an empty table", t, func() { |
||||
couponID := uuid.New().String() |
||||
tt := r.Intn(int(TTUnknownTransaction)) |
||||
ts := _prepareSeveralCouponTransactionsInDB(base.RandString(4), couponID, TransType(tt), true) |
||||
tt2 := (tt + 1) % int(TTUnknownTransaction) |
||||
_ = _prepareSeveralCouponTransactionsInDB(base.RandString(4), couponID, TransType(tt2), false) |
||||
Convey("getCouponTransactionsWithType only get the coupon transactions with given type", func() { |
||||
count, err := getCouponTransactionCountWithType(couponID, TransType(tt)) |
||||
Convey("And should contain the coupon ransactions just created with given couponID", func() { |
||||
So(len(ts), ShouldEqual, count) |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func _prepareSeveralCouponTransactionsInDB(actorID string, couponID string, tt TransType, bTrancate bool) []*Transaction { |
||||
count := r.Intn(10) + 1 |
||||
if bTrancate { |
||||
_, _ = dbConnection.Exec("DELETE FROM couponTransactions") |
||||
} |
||||
ts := make([]*Transaction, 0, count) |
||||
for i := 0; i < count; i++ { |
||||
ts = append(ts, _prepareARandomCouponTransactionInDB(actorID, couponID, tt, false)) |
||||
} |
||||
return ts |
||||
} |
||||
|
||||
func _prepareARandomCouponTransactionInDB(actorID string, couponID string, tt TransType, bTrancate bool) *Transaction { |
||||
t := _aTransaction(actorID, couponID, TransType(tt), base.RandString(4)) |
||||
if bTrancate { |
||||
_, _ = dbConnection.Exec("DELETE FROM couponTransactions") |
||||
} |
||||
s := fmt.Sprintf(`INSERT INTO couponTransactions VALUES ("%s","%s","%s",%d,"%s","%d")`, t.ID, t.CouponID, t.ActorID, t.TransType, t.EncryptExtraInfo(), t.CreatedTime.Unix()) |
||||
_, e := dbConnection.Exec(s) |
||||
if nil != e { |
||||
fmt.Println("dbConnection.Exec(s) ==== ", e.Error()) |
||||
} |
||||
return t |
||||
} |
||||
|
||||
func _prepareSeveralCouponsInDB(consumerID string, consumerRefID string, channelID string, couponType string, bTrancate bool) []*Coupon { |
||||
count := r.Intn(10) + 1 |
||||
if bTrancate { |
||||
_, _ = dbConnection.Exec("DELETE FROM coupons") |
||||
} |
||||
cs := make([]*Coupon, 0, count) |
||||
for i := 0; i < count; i++ { |
||||
cs = append(cs, _prepareARandomCouponInDB(consumerID, consumerRefID, channelID, couponType, false)) |
||||
} |
||||
return cs |
||||
} |
||||
|
||||
func _prepareARandomCouponInDB(consumerID string, consumerRefID string, channelID string, couponType string, bTrancate bool) *Coupon { |
||||
state := r.Intn(int(SUnknown)) |
||||
var p map[string]interface{} |
||||
p = make(map[string]interface{}, 1) |
||||
p["the_key"] = "the value" |
||||
c := _aCoupon(consumerID, consumerRefID, channelID, couponType, State(state), p) |
||||
if bTrancate { |
||||
_, _ = dbConnection.Exec("DELETE FROM coupons") |
||||
} |
||||
s := fmt.Sprintf(`INSERT INTO coupons VALUES ("%s","%s","%s","%s","%s",%d,"%s",%d)`, c.ID, c.CouponTypeID, c.ConsumerID, c.ConsumerRefID, c.ChannelID, c.State, "some string", c.CreatedTime.Unix()) |
||||
_, _ = dbConnection.Exec(s) |
||||
return c |
||||
} |
||||
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
) |
||||
|
||||
//ErrRuleJudgeNotFound - 找不到一个规则的校验。
|
||||
var ErrRuleJudgeNotFound = base.ErrorWithCode{ |
||||
Code: 1000, |
||||
Message: "rule judge not found", |
||||
} |
||||
|
||||
//ErrRuleNotFound - 找不到一个规则的校验。
|
||||
var ErrRuleNotFound = base.ErrorWithCode{ |
||||
Code: 1001, |
||||
Message: "rule not found", |
||||
} |
||||
|
||||
// ErrCouponRulesApplyTimesExceeded - 已经达到最大领用次数。
|
||||
var ErrCouponRulesApplyTimesExceeded = base.ErrorWithCode{ |
||||
Code: 1002, |
||||
Message: "coupon apply times exceeded", |
||||
} |
||||
|
||||
// ErrCouponRulesApplyTimeExpired - 卡券已经过了申领期限。
|
||||
var ErrCouponRulesApplyTimeExpired = base.ErrorWithCode{ |
||||
Code: 1003, |
||||
Message: "the coupon applied has been expired", |
||||
} |
||||
|
||||
// ErrCouponRulesBadFormat - 卡券附加的验证规则有错误。
|
||||
var ErrCouponRulesBadFormat = base.ErrorWithCode{ |
||||
Code: 1004, |
||||
Message: "the coupon has a bad formated rules", |
||||
} |
||||
|
||||
// ErrCouponRulesRedemptionNotStart - 卡券还没开始核销。
|
||||
var ErrCouponRulesRedemptionNotStart = base.ErrorWithCode{ |
||||
Code: 1005, |
||||
Message: "the coupon has not start the redemption", |
||||
} |
||||
|
||||
// ErrCouponRulesRedeemTimesExceeded - 已经达到最大领用次数。
|
||||
var ErrCouponRulesRedeemTimesExceeded = base.ErrorWithCode{ |
||||
Code: 1006, |
||||
Message: "coupon redeem times exceeded", |
||||
} |
||||
|
||||
// ErrCouponRulesNoRedeemTimes - 已经达到最大领用次数。
|
||||
var ErrCouponRulesNoRedeemTimes = base.ErrorWithCode{ |
||||
Code: 1007, |
||||
Message: "coupon has no redeem times rule", |
||||
} |
||||
|
||||
// ErrCouponRulesRedemptionExpired - 卡券核销已经过期。
|
||||
var ErrCouponRulesRedemptionExpired = base.ErrorWithCode{ |
||||
Code: 1008, |
||||
Message: "the coupon is expired", |
||||
} |
||||
|
||||
// ErrCouponRulesUnsuportTimeUnit - 不支持的时间单位。
|
||||
var ErrCouponRulesUnsuportTimeUnit = base.ErrorWithCode{ |
||||
Code: 1009, |
||||
Message: "the coupon redeem time unit unsupport", |
||||
} |
||||
|
||||
//ErrCouponTemplateNotFound - 签发新的Coupon时,找不到Coupon的模板的类型
|
||||
var ErrCouponTemplateNotFound = base.ErrorWithCode{ |
||||
Code: 1100, |
||||
Message: "coupon template not found", |
||||
} |
||||
|
||||
//ErrCouponIDInvalid - 签发新的Coupon时,用户id或者卡券类型不合法
|
||||
var ErrCouponIDInvalid = base.ErrorWithCode{ |
||||
Code: 1200, |
||||
Message: "coupon id is invalid", |
||||
} |
||||
|
||||
//ErrCouponNotFound - 没找到Coupon时
|
||||
var ErrCouponNotFound = base.ErrorWithCode{ |
||||
Code: 1201, |
||||
Message: "coupon not found", |
||||
} |
||||
|
||||
//ErrCouponIsNotActive - 没找到Coupon时
|
||||
var ErrCouponIsNotActive = base.ErrorWithCode{ |
||||
Code: 1202, |
||||
Message: "coupon is not active", |
||||
} |
||||
|
||||
//ErrCouponWasRedeemed - 没找到Coupon时
|
||||
var ErrCouponWasRedeemed = base.ErrorWithCode{ |
||||
Code: 1203, |
||||
Message: "coupon was redeemed", |
||||
} |
||||
|
||||
//ErrCouponTooMuchToRedeem - 没找到Coupon时
|
||||
var ErrCouponTooMuchToRedeem = base.ErrorWithCode{ |
||||
Code: 1204, |
||||
Message: "too much coupons to redeem", |
||||
} |
||||
|
||||
//ErrCouponWrongConsumer - 没找到Coupon时
|
||||
var ErrCouponWrongConsumer = base.ErrorWithCode{ |
||||
Code: 1205, |
||||
Message: "the coupon's owner is not the provided consumer", |
||||
} |
||||
|
||||
//ErrConsumerIDAndCouponTypeIDInvalid - 签发新的Coupon时,用户id或者卡券类型不合法
|
||||
var ErrConsumerIDAndCouponTypeIDInvalid = base.ErrorWithCode{ |
||||
Code: 1300, |
||||
Message: "consumer id or coupon type is invalid", |
||||
} |
||||
|
||||
//ErrConsumerIDInvalid - 签发新的Coupon时,用户id或者卡券类型不合法
|
||||
var ErrConsumerIDInvalid = base.ErrorWithCode{ |
||||
Code: 1301, |
||||
Message: "consumer id is invalid", |
||||
} |
||||
|
||||
//ErrConsumerIDsAndRefIDsMismatch - 消费者的RefID和ID数量不匹配
|
||||
var ErrConsumerIDsAndRefIDsMismatch = base.ErrorWithCode{ |
||||
Code: 1302, |
||||
Message: "consumer ids and the ref ids mismatch", |
||||
} |
||||
|
||||
//ErrRequesterForbidden - 没找到Coupon时
|
||||
var ErrRequesterForbidden = base.ErrorWithCode{ |
||||
Code: 1400, |
||||
Message: "requester was forbidden to do this action", |
||||
} |
||||
|
||||
//ErrRequesterHasNoBrand - 没找到Coupon时
|
||||
var ErrRequesterHasNoBrand = base.ErrorWithCode{ |
||||
Code: 1401, |
||||
Message: "requester has no brand information", |
||||
} |
||||
|
||||
//ErrRedeemWithDiffBrand - 没找到Coupon时
|
||||
var ErrRedeemWithDiffBrand = base.ErrorWithCode{ |
||||
Code: 1402, |
||||
Message: "redeem coupon with different brand", |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "database/sql"
|
||||
"encoding/json" |
||||
// "fmt"
|
||||
"log" |
||||
) |
||||
|
||||
// GetPropertiesString 获取属性(map[string]string)的字符串格式。
|
||||
func (c *Coupon) GetPropertiesString() (*string, error) { |
||||
jsonBytes, err := json.Marshal(c.Properties) |
||||
if nil != err { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
var str = string(jsonBytes) |
||||
return &str, nil |
||||
} |
||||
|
||||
// SetPropertiesFromString 根据string来设置Coupon属性(map[string]string)
|
||||
func (c *Coupon) SetPropertiesFromString(properties string) error{ |
||||
err := json.Unmarshal([]byte(properties), &c.Properties) |
||||
if nil != err { |
||||
log.Println(err) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// GetRules 获取卡券的规则
|
||||
func (c *Coupon) GetRules() map[string]interface{} { |
||||
var rules map[string]interface{} |
||||
var ok bool |
||||
if rules, ok = c.Properties[KeyBindingRuleProperties].(map[string]interface{}); ok { |
||||
return rules |
||||
} |
||||
// log.Println("============if rules, ok============" )
|
||||
// log.Println(ok )
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,828 @@
@@ -0,0 +1,828 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"errors" |
||||
"strconv" |
||||
|
||||
// "fmt"
|
||||
"log" |
||||
// "net/http"
|
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
"loreal.com/dit/utils" |
||||
|
||||
"github.com/google/uuid" |
||||
) |
||||
|
||||
var bufferSize int = 5 |
||||
var templates []*Template |
||||
var publishedCouponTypes []*PublishedCouponType |
||||
var encryptKey []byte |
||||
|
||||
// 做一下访问控制
|
||||
var couponMessageChannels map[chan Message]bool |
||||
var serviceMutex *sync.RWMutex |
||||
|
||||
// Init 初始化一些数据
|
||||
// TODO: 目前是hard code的数据,后期要重构
|
||||
func Init(temps []*Template, |
||||
couponTypes []*PublishedCouponType, |
||||
rules []*Rule, |
||||
databaseConnection *sql.DB, |
||||
key string) { |
||||
templates = temps |
||||
publishedCouponTypes = couponTypes |
||||
couponMessageChannels = make(map[chan Message]bool) |
||||
encryptKey = []byte(key) |
||||
serviceMutex = new(sync.RWMutex) |
||||
staticsInit(databaseConnection) |
||||
ruleInit(rules) |
||||
dbInit() |
||||
} |
||||
|
||||
// ActivateTestedCoupontypes 激活测试用的卡券
|
||||
// TODO: 实现卡券类型的相关API后,将会移除
|
||||
func ActivateTestedCoupontypes() { |
||||
var cts []*PublishedCouponType |
||||
utils.LoadOrCreateJSON("coupon/test/coupon_types.json", &cts) |
||||
for _, t := range cts { |
||||
t.InitRules() |
||||
} |
||||
publishedCouponTypes = cts |
||||
log.Printf("[GetCoupons] 新的publishedCouponTypes: %#v\n", publishedCouponTypes) |
||||
} |
||||
|
||||
func _checkCouponType(couponTypeID string) *PublishedCouponType { |
||||
var pct *PublishedCouponType |
||||
for _, value := range publishedCouponTypes { |
||||
if value.ID == couponTypeID { |
||||
pct = value |
||||
return pct |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// GetCouponTypes 获取卡券类型列表
|
||||
// TODO: 加上各种过滤条件
|
||||
func GetCouponTypes(requester *base.Requester) ([]*PublishedCouponType, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[GetCoupons] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_ISSUER) && !requester.HasRole(base.ROLE_COUPON_REDEEMER) { |
||||
return nil, &ErrRequesterForbidden |
||||
} |
||||
// if base.IsBlankString(consumerID) {
|
||||
// return nil, &ErrConsumerIDInvalid
|
||||
// }
|
||||
|
||||
return publishedCouponTypes, nil |
||||
} |
||||
|
||||
// IssueCoupons 批量签发卡券
|
||||
// TODO: 测试下单次调用多少用户量合适
|
||||
func IssueCoupons(requester *base.Requester, consumerIDs string, consumerRefIDs string, channelID string, couponTypeID string) (*[]*Coupon, map[string][]error, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[IssueCoupons] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_ISSUER) { |
||||
return nil, nil, &ErrRequesterForbidden |
||||
} |
||||
|
||||
// 1.分拆 consumers
|
||||
consumerIDArray := strings.Split(consumerIDs, ",") |
||||
consumerIDArray = removeDuplicatedConsumers(consumerIDArray) |
||||
var consumerRefIDArray []string = nil |
||||
if !base.IsBlankString(consumerRefIDs) { |
||||
consumerRefIDArray = strings.Split(consumerRefIDs, ",") |
||||
consumerRefIDArray = removeDuplicatedConsumers(consumerRefIDArray) |
||||
} |
||||
|
||||
if consumerRefIDArray != nil && len(consumerIDArray) != len(consumerRefIDArray) { |
||||
return nil, nil, &ErrConsumerIDsAndRefIDsMismatch |
||||
} |
||||
|
||||
// 2. 查询coupontype
|
||||
// TODO: 未来改成数据库模式,此处要重构
|
||||
pct := _checkCouponType(couponTypeID) |
||||
if nil == pct { |
||||
return nil, nil, &ErrCouponTemplateNotFound |
||||
} |
||||
|
||||
// 3. 校验申请条件
|
||||
// TODO: 判断是否有重复的rule
|
||||
|
||||
// 用来记录所有人的规则错误
|
||||
// TODO: 此处尝试用 gorouting
|
||||
// TODO: 记得测试有很多错误的情况下,前端得到什么。
|
||||
var allConsumersRuleCheckErrors map[string][]error = make(map[string][]error, 0) |
||||
for _, consumerID := range consumerIDArray { |
||||
rerrs, rerr := validateTemplateRules(consumerID, couponTypeID, pct) |
||||
if rerr != nil { |
||||
switch rerr { |
||||
case &ErrRuleNotFound: |
||||
{ |
||||
log.Println(ErrRuleNotFound) |
||||
return nil, nil, &ErrRuleNotFound |
||||
} |
||||
case &ErrRuleJudgeNotFound: |
||||
{ |
||||
log.Println(ErrRuleJudgeNotFound) |
||||
return nil, nil, &ErrRuleJudgeNotFound |
||||
} |
||||
default: |
||||
{ |
||||
log.Println("未知校验规则错误") |
||||
return nil, nil, errors.New("内部错误") |
||||
} |
||||
} |
||||
} |
||||
if len(rerrs) > 0 { |
||||
allConsumersRuleCheckErrors[consumerID] = rerrs |
||||
} |
||||
} |
||||
if len(allConsumersRuleCheckErrors) > 0 { |
||||
return nil, allConsumersRuleCheckErrors, nil |
||||
} |
||||
|
||||
// 4. issue
|
||||
lt := time.Now().Local() |
||||
composedRules, err := marshalCouponRules(requester, couponTypeID, pct.Rules) |
||||
if nil != err { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
var newCs []*Coupon = make([]*Coupon, 0, len(consumerIDArray)) |
||||
for idx, consumerID := range consumerIDArray { |
||||
refID := "" |
||||
if consumerRefIDArray != nil { |
||||
refID = consumerRefIDArray[idx] |
||||
} |
||||
|
||||
var newC = Coupon{ |
||||
ID: uuid.New().String(), |
||||
CouponTypeID: couponTypeID, |
||||
ConsumerID: consumerID, |
||||
ConsumerRefID: refID, |
||||
ChannelID: channelID, |
||||
State: SActive, |
||||
Properties: make(map[string]interface{}), |
||||
CreatedTime: <, |
||||
Transactions: make([]*Transaction, 0), |
||||
} |
||||
newC.Properties[KeyBindingRuleProperties] = composedRules |
||||
newCs = append(newCs, &newC) |
||||
} |
||||
|
||||
tx, err := dbConnection.Begin() |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, nil, errors.New("内部错误") |
||||
} |
||||
|
||||
err = createCoupons(newCs) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return nil, nil, err |
||||
} |
||||
|
||||
// 5. 记录log
|
||||
var ts []*Transaction = make([]*Transaction, 0, len(newCs)) |
||||
for _, c := range newCs { |
||||
var t = Transaction{ |
||||
ID: uuid.New().String(), |
||||
CouponID: c.ID, |
||||
ActorID: requester.UserID, |
||||
TransType: TTIssueCoupon, |
||||
ExtraInfo: "", |
||||
CreatedTime: time.Now().Local(), |
||||
} |
||||
ts = append(ts, &t) |
||||
} |
||||
|
||||
err = createCouponTransactions(ts) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return nil, nil, err |
||||
} |
||||
|
||||
tx.Commit() |
||||
|
||||
return &newCs, nil, nil |
||||
} |
||||
|
||||
// IssueCoupon 签发一个卡券
|
||||
// func IssueCoupon(requester *base.Requester, consumerID string, couponTypeID string) (*Coupon, []error, error) {
|
||||
// if !requester.HasRole(base.ROLE_COUPON_ISSUER) {
|
||||
// return nil, nil, &ErrRequesterForbidden
|
||||
// }
|
||||
|
||||
// //1. 查询coupontype
|
||||
// // TODO: 未来改成数据库模式,此处要重构
|
||||
// pct := _checkCouponType(couponTypeID)
|
||||
// if nil == pct {
|
||||
// return nil, nil, &ErrCouponTemplateNotFound
|
||||
// }
|
||||
|
||||
// // 2. 校验申请条件
|
||||
// // TODO: 判断是否有重复的rule
|
||||
// rerrs, rerr := validateTemplateRules(consumerID, couponTypeID, pct)
|
||||
// if rerr != nil {
|
||||
// switch rerr {
|
||||
// case &ErrRuleNotFound:
|
||||
// {
|
||||
// log.Println(ErrRuleNotFound)
|
||||
// return nil, nil, &ErrRuleNotFound
|
||||
// }
|
||||
// case &ErrRuleJudgeNotFound:
|
||||
// {
|
||||
// log.Println(ErrRuleJudgeNotFound)
|
||||
// return nil, nil, &ErrRuleJudgeNotFound
|
||||
// }
|
||||
// default:
|
||||
// {
|
||||
// log.Println("未知校验规则错误")
|
||||
// return nil, nil, errors.New("内部错误")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if len(rerrs) > 0 {
|
||||
// return nil, rerrs, nil
|
||||
// }
|
||||
|
||||
// // 3. issue
|
||||
// lt := time.Now().Local()
|
||||
// var newC = Coupon{
|
||||
// ID: uuid.New().String(),
|
||||
// CouponTypeID: couponTypeID,
|
||||
// ConsumerID: consumerID,
|
||||
// State: SActive,
|
||||
// Properties: make(map[string]string),
|
||||
// CreatedTime: <,
|
||||
// }
|
||||
|
||||
// composedRules, err := marshalCouponRules(requester, couponTypeID, pct.Rules)
|
||||
// if nil != err {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
// newC.Properties[KeyBindingRuleProperties] = composedRules
|
||||
|
||||
// tx, err := dbConnection.Begin()
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return nil, nil, errors.New("内部错误")
|
||||
// }
|
||||
|
||||
// err = createCoupon(&newC)
|
||||
// if nil != err {
|
||||
// tx.Rollback()
|
||||
// return nil, nil, err
|
||||
// }
|
||||
|
||||
// // 4. 记录log
|
||||
// var t = Transaction{
|
||||
// ID: uuid.New().String(),
|
||||
// CouponID: newC.ID,
|
||||
// ActorID: requester.UserID,
|
||||
// TransType: TTIssueCoupon,
|
||||
// CreatedTime: time.Now().Local(),
|
||||
// }
|
||||
// err = createCouponTransaction(&t)
|
||||
// if nil != err {
|
||||
// tx.Rollback()
|
||||
// return nil, nil, err
|
||||
// }
|
||||
// tx.Commit()
|
||||
// return &newC, nil, nil
|
||||
// }
|
||||
|
||||
// ValidateCouponExpired - Valiete whether request coupon is expired.
|
||||
func ValidateCouponExpired(requester *base.Requester, c *Coupon) (bool, error) { |
||||
return validateCouponExpired(requester, c.ConsumerID, c) |
||||
} |
||||
|
||||
// GetCoupon 获取一个卡券的信息
|
||||
func GetCoupon(requester *base.Requester, id string) (*Coupon, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[GetCoupon] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_ISSUER) && !requester.HasRole(base.ROLE_COUPON_REDEEMER) { |
||||
return nil, &ErrRequesterForbidden |
||||
} |
||||
if base.IsBlankString(id) { |
||||
return nil, &ErrCouponIDInvalid |
||||
} |
||||
|
||||
c, err := getCoupon(id) |
||||
if nil != err { |
||||
return nil, err |
||||
} |
||||
|
||||
ts := make([]*Transaction, 0) |
||||
c.Transactions = ts |
||||
|
||||
return c, nil |
||||
} |
||||
|
||||
// GetCouponWithTransactions - Get coupon by specified couponID and return associated transactions with specified transType in the same time.
|
||||
func GetCouponWithTransactions(requester *base.Requester, id string, transType string) (*Coupon, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[GetCoupon] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
c, err := GetCoupon(requester, id) |
||||
if nil != err { |
||||
return nil, err |
||||
} |
||||
|
||||
transTypeID, err := strconv.Atoi(transType) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ts := make([]*Transaction, 0) |
||||
ts, err = getCouponTransactionsWithType(id, TransType(transTypeID)) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
c.Transactions = ts |
||||
|
||||
return c, nil |
||||
} |
||||
|
||||
// GetCoupons 搜索用户所有的卡券
|
||||
// TODO: 增加各种过滤条件
|
||||
func GetCoupons(requester *base.Requester, consumerID string, couponTypeID string) ([]*Coupon, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[GetCoupons] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_ISSUER) && !requester.HasRole(base.ROLE_COUPON_REDEEMER) { |
||||
return nil, &ErrRequesterForbidden |
||||
} |
||||
if base.IsBlankString(consumerID) { |
||||
return nil, &ErrConsumerIDInvalid |
||||
} |
||||
|
||||
cs, err := getCoupons(consumerID, couponTypeID) |
||||
if nil != err { |
||||
return nil, err |
||||
} |
||||
|
||||
return cs, nil |
||||
} |
||||
|
||||
// GetCouponsWithTransactions - Get conpons and specified type of transactions
|
||||
func GetCouponsWithTransactions(requester *base.Requester, consumerID string, couponTypeID string, transTypeID string) ([]*Coupon, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[GetCoupons] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
cs, err := GetCoupons(requester, consumerID, couponTypeID) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for _, c := range cs { |
||||
tt, err := strconv.Atoi(transTypeID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ts, err := getCouponTransactionsWithType(c.ID, TransType(tt)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(ts) == 0 { |
||||
c.Transactions = make([]*Transaction, 0) |
||||
} |
||||
c.Transactions = ts |
||||
} |
||||
|
||||
return cs, nil |
||||
} |
||||
|
||||
// RedeemCoupon 兑换卡券
|
||||
func RedeemCoupon(requester *base.Requester, consumerID string, couponID string, extraInfo string) ([]error, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[RedeemCoupon] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_REDEEMER) { |
||||
return nil, &ErrRequesterForbidden |
||||
} |
||||
|
||||
//1. 查询coupon
|
||||
c, err := GetCoupon(requester, couponID) |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
|
||||
err = validateCouponBasicForRedeem(c, consumerID) |
||||
if nil != err { |
||||
return nil, err |
||||
} |
||||
|
||||
// 2. validation rules
|
||||
rerrs, rerr := validateCouponRules(requester, consumerID, c) |
||||
if rerr != nil { |
||||
return nil, &ErrRuleNotFound |
||||
} |
||||
|
||||
if len(rerrs) > 0 { |
||||
return rerrs, nil |
||||
} |
||||
|
||||
// 3. redeem
|
||||
var newT = Transaction{ |
||||
ID: uuid.New().String(), |
||||
CouponID: couponID, |
||||
ActorID: requester.UserID, |
||||
TransType: TTRedeemCoupon, |
||||
ExtraInfo: extraInfo, |
||||
CreatedTime: time.Now().Local(), |
||||
} |
||||
|
||||
tx, err := dbConnection.Begin() |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, errors.New("内部错误") |
||||
} |
||||
err = createCouponTransaction(&newT) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return nil, err |
||||
} |
||||
|
||||
// 4. update coupon state
|
||||
var count uint |
||||
count, err = restRedeemTimes(c) |
||||
if nil != err { |
||||
if &ErrCouponRulesNoRedeemTimes == err { |
||||
count = 0 |
||||
} else { |
||||
tx.Rollback() |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
if 0 == count { |
||||
// 已经兑换完
|
||||
c.State = SRedeemed |
||||
err = updateCouponState(c) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
tx.Commit() |
||||
|
||||
// 5. 发通知
|
||||
// TODO: 缓存数据,避免失败后丢失数据,当然也可以采用从数据库捞数据的方式,使用cursor
|
||||
allCoupons := make([]*Coupon, 0) |
||||
allCoupons = append(allCoupons, c) |
||||
_sendRedeemMessage(allCoupons, extraInfo) |
||||
|
||||
return nil, nil |
||||
} |
||||
|
||||
func _addErrForConsumer(mps map[string][]error, consumerID string, err error) { |
||||
if mps[consumerID] == nil { |
||||
mps[consumerID] = make([]error, 0, 1) |
||||
} |
||||
mps[consumerID] = append(mps[consumerID], err) |
||||
} |
||||
|
||||
// RedeemCouponByType 根据卡券类型兑换卡券,
|
||||
// 要求消费者针对该类型卡券类型只有一张卡券
|
||||
func RedeemCouponByType(requester *base.Requester, consumerIDs string, couponTypeID string, extraInfo string) (map[string][]error, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[RedeemCouponByType] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_REDEEMER) { |
||||
return nil, &ErrRequesterForbidden |
||||
} |
||||
|
||||
var allConsumersRuleCheckErrors map[string][]error = make(map[string][]error, 0) |
||||
|
||||
//1. 查询coupon
|
||||
var consumerIDArray []string = make([]string, 0) |
||||
if !base.IsBlankString(consumerIDs) { |
||||
consumerIDArray = strings.Split(consumerIDs, ",") |
||||
} |
||||
|
||||
allCoupons := make([]*Coupon, 0, len(consumerIDArray)) |
||||
for _, consumerID := range consumerIDArray { |
||||
cs, err := GetCoupons(requester, consumerID, couponTypeID) |
||||
if err != nil { |
||||
log.Println(err) |
||||
_addErrForConsumer(allConsumersRuleCheckErrors, consumerID, err) |
||||
continue |
||||
} |
||||
if nil == cs || len(cs) == 0 { |
||||
_addErrForConsumer(allConsumersRuleCheckErrors, consumerID, &ErrCouponNotFound) |
||||
continue |
||||
} |
||||
|
||||
// 目前限制只有一份卡券时才可以兑换
|
||||
if len(cs) > 1 { |
||||
_addErrForConsumer(allConsumersRuleCheckErrors, consumerID, &ErrCouponTooMuchToRedeem) |
||||
continue |
||||
} |
||||
|
||||
c := cs[0] |
||||
allCoupons = append(allCoupons, c) |
||||
|
||||
err = validateCouponBasicForRedeem(c, consumerID) |
||||
if nil != err { |
||||
_addErrForConsumer(allConsumersRuleCheckErrors, consumerID, err) |
||||
continue |
||||
} |
||||
|
||||
// 2. validation rules
|
||||
rerrs, rerr := validateCouponRules(requester, consumerID, c) |
||||
if rerr != nil { |
||||
_addErrForConsumer(allConsumersRuleCheckErrors, consumerID, &ErrRuleNotFound) |
||||
continue |
||||
} |
||||
|
||||
if len(rerrs) > 0 { |
||||
allConsumersRuleCheckErrors[consumerID] = append(allConsumersRuleCheckErrors[consumerID], rerrs...) |
||||
continue |
||||
} |
||||
} |
||||
|
||||
if len(allConsumersRuleCheckErrors) > 0 { |
||||
return allConsumersRuleCheckErrors, nil |
||||
} |
||||
|
||||
// 3. redeem
|
||||
ts := make([]*Transaction, 0, len(allCoupons)) |
||||
for _, c := range allCoupons { |
||||
var newT = Transaction{ |
||||
ID: uuid.New().String(), |
||||
CouponID: c.ID, |
||||
ActorID: requester.UserID, |
||||
TransType: TTRedeemCoupon, |
||||
ExtraInfo: extraInfo, |
||||
CreatedTime: time.Now().Local(), |
||||
} |
||||
ts = append(ts, &newT) |
||||
} |
||||
|
||||
tx, err := dbConnection.Begin() |
||||
if err != nil { |
||||
log.Println(err) |
||||
return nil, errors.New("内部错误") |
||||
} |
||||
err = createCouponTransactions(ts) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return nil, err |
||||
} |
||||
|
||||
// 4. update coupon state
|
||||
for _, c := range allCoupons { |
||||
var count uint |
||||
count, err = restRedeemTimes(c) |
||||
if nil != err { |
||||
if &ErrCouponRulesNoRedeemTimes == err { |
||||
count = 0 |
||||
} else { |
||||
tx.Rollback() |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
if 0 == count { |
||||
// 已经兑换完
|
||||
c.State = SRedeemed |
||||
err = updateCouponState(c) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return nil, err |
||||
} |
||||
} |
||||
} |
||||
|
||||
tx.Commit() |
||||
|
||||
// 5. 发通知
|
||||
// TODO: 缓存数据,避免失败后丢失数据,当然也可以采用从数据库捞数据的方式,使用cursor
|
||||
_sendRedeemMessage(allCoupons, extraInfo) |
||||
|
||||
return nil, nil |
||||
} |
||||
|
||||
// RedeemCouponsInMaintenance - Redeem coupon in maintenance situation
|
||||
func RedeemCouponsInMaintenance(requester *base.Requester, couponIDs string, extraInfo string) error { |
||||
if base.IsBlankString(couponIDs) { |
||||
return errors.New("empty couponIDs not allowed") |
||||
} |
||||
|
||||
couponIDArray := strings.Split(couponIDs, ",") |
||||
|
||||
var limit int = 50 |
||||
var batches int = len(couponIDArray) / limit |
||||
|
||||
if len(couponIDArray)%limit > 0 { |
||||
batches++ |
||||
} |
||||
|
||||
for i := 0; i < batches; i++ { |
||||
min := i * limit |
||||
max := (i + 1) * limit |
||||
if max > len(couponIDArray) { |
||||
max = len(couponIDArray) |
||||
} |
||||
cslice := couponIDArray[min:max] |
||||
coupons := make([]*Coupon, 0) |
||||
for _, cid := range cslice { |
||||
c, err := GetCoupon(requester, cid) |
||||
|
||||
if err != nil { |
||||
log.Println(err) |
||||
continue |
||||
} |
||||
|
||||
coupons = append(coupons, c) |
||||
} |
||||
redeemCoupons(requester, coupons, extraInfo) |
||||
|
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func redeemCoupons(requester *base.Requester, allCoupons []*Coupon, extraInfo string) error { |
||||
ts := make([]*Transaction, 0, len(allCoupons)) |
||||
for _, c := range allCoupons { |
||||
var newT = Transaction{ |
||||
ID: uuid.New().String(), |
||||
CouponID: c.ID, |
||||
ActorID: requester.UserID, |
||||
TransType: TTRedeemCoupon, |
||||
ExtraInfo: extraInfo, |
||||
CreatedTime: time.Now().Local(), |
||||
} |
||||
ts = append(ts, &newT) |
||||
} |
||||
|
||||
tx, err := dbConnection.Begin() |
||||
if err != nil { |
||||
log.Println(err) |
||||
return errors.New("内部错误") |
||||
} |
||||
err = createCouponTransactions(ts) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
|
||||
for _, c := range allCoupons { |
||||
var count uint |
||||
count, err = restRedeemTimes(c) |
||||
if nil != err { |
||||
if &ErrCouponRulesNoRedeemTimes == err { |
||||
count = 0 |
||||
} else { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
} |
||||
|
||||
if 0 == count { |
||||
// 已经兑换完
|
||||
c.State = SRedeemed |
||||
err = updateCouponState(c) |
||||
if nil != err { |
||||
tx.Rollback() |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
tx.Commit() |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func validateCouponBasicForRedeem(c *Coupon, consumerID string) error { |
||||
if nil == c { |
||||
return &ErrCouponNotFound |
||||
} |
||||
|
||||
if SRedeemed == c.State { |
||||
return &ErrCouponWasRedeemed |
||||
} |
||||
|
||||
if SActive != c.State { |
||||
return &ErrCouponIsNotActive |
||||
} |
||||
|
||||
if c.ConsumerID != consumerID { |
||||
return &ErrCouponWrongConsumer |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
//TODO: 关于消息还没有单元测试 []*Coupon
|
||||
func _sendRedeemMessage(cs []*Coupon, extraInfo string) { |
||||
if nil == cs || len(cs) < 0 { |
||||
return |
||||
} |
||||
m := Message{ |
||||
Type: MTRedeemed, |
||||
Payload: RedeemedCoupons{ |
||||
ExtraInfo: extraInfo, |
||||
Coupons: cs, |
||||
}, |
||||
} |
||||
serviceMutex.RLock() |
||||
defer serviceMutex.RUnlock() |
||||
for chn := range couponMessageChannels { |
||||
chn <- m |
||||
} |
||||
} |
||||
|
||||
// DeleteCoupon 删除一个卡券。
|
||||
// 注意,这里不是用户自己删除, 自己删除的需要考虑后续的业务场景,比如是否可以重新领取
|
||||
func DeleteCoupon(id string) (*Coupon, error) { |
||||
|
||||
return nil, nil |
||||
} |
||||
|
||||
// GetLatestCouponMessage 获取最新的卡券信息
|
||||
// TODO: 可能有多个服务器
|
||||
// TODO: 使用cursor
|
||||
func GetLatestCouponMessage(requester *base.Requester) (interface{}, error) { |
||||
defer func() { |
||||
if v := recover(); nil != v { |
||||
log.Printf("[GetLatestCouponMessage] 未知错误: %#v\n", v) |
||||
} |
||||
}() |
||||
|
||||
if !requester.HasRole(base.ROLE_COUPON_LISTENER) { |
||||
return nil, &ErrRequesterForbidden |
||||
} |
||||
|
||||
chn := make(chan Message) |
||||
|
||||
serviceMutex.Lock() |
||||
couponMessageChannels[chn] = true |
||||
serviceMutex.Unlock() |
||||
|
||||
defer func() { |
||||
close(chn) |
||||
serviceMutex.Lock() |
||||
delete(couponMessageChannels, chn) |
||||
serviceMutex.Unlock() |
||||
}() |
||||
|
||||
msg := <-chn |
||||
|
||||
return msg, nil |
||||
} |
||||
|
||||
// removeDuplicatedConsumers 批量生成卡券时,移除多余的consumerID和consumerRefID
|
||||
func removeDuplicatedConsumers(consumers []string) []string { |
||||
result := make([]string, 0, len(consumers)) |
||||
temp := map[string]struct{}{} |
||||
for _, item := range consumers { |
||||
if _, ok := temp[item]; !ok { |
||||
temp[item] = struct{}{} |
||||
result = append(result, item) |
||||
} |
||||
} |
||||
return result |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,304 @@
@@ -0,0 +1,304 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "encoding/json"
|
||||
"log" |
||||
"time" |
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
|
||||
"github.com/jinzhu/now" |
||||
"github.com/mitchellh/mapstructure" |
||||
) |
||||
|
||||
// var daysMap = map[string]int {
|
||||
// "YEAR" : 366, //理想的是根据是否闰年来计算
|
||||
|
||||
// }
|
||||
|
||||
// TemplateJudge 发卡券时用来验证是否符合rules
|
||||
type TemplateJudge interface { |
||||
// JudgeTemplate 验证模板
|
||||
JudgeTemplate(consumerID string, couponTypeID string, ruleBody map[string]interface{}, pct *PublishedCouponType) error |
||||
} |
||||
|
||||
// Judge 兑换卡券时用来验证是否符合rules
|
||||
type Judge interface { |
||||
// JudgeCoupon 验证模板
|
||||
JudgeCoupon(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) error |
||||
} |
||||
|
||||
// RedeemPeriodJudge 验证有效期
|
||||
type RedeemPeriodJudge struct { |
||||
} |
||||
|
||||
// RedeemInCurrentNatureTimeUnitJudge 验证自然月,季度,年度有效期
|
||||
type RedeemInCurrentNatureTimeUnitJudge struct { |
||||
} |
||||
|
||||
//ApplyTimesJudge 验证领用次数
|
||||
type ApplyTimesJudge struct { |
||||
} |
||||
|
||||
//RedeemTimesJudge 验证兑换次数
|
||||
type RedeemTimesJudge struct { |
||||
} |
||||
|
||||
//RedeemBySameBrandJudge 验证是否同品牌兑换
|
||||
type RedeemBySameBrandJudge struct { |
||||
} |
||||
|
||||
// TODO: 重构rule结构实现这些接口,统一处理。
|
||||
|
||||
// JudgeCoupon 验证有效期
|
||||
// TODO: 未来加上DAY, WEEK, SEARON, YEAR 等
|
||||
func (*RedeemInCurrentNatureTimeUnitJudge) JudgeCoupon(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) error { |
||||
|
||||
var ntu natureTimeUnit |
||||
// var err error
|
||||
var start time.Time |
||||
var end time.Time |
||||
|
||||
if err := mapstructure.Decode(ruleBody, &ntu); err != nil { |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
// TODO: 支持季度,年啥的。
|
||||
if base.IsBlankString(ntu.Unit) || ntu.Unit != "MONTH" { |
||||
return &ErrCouponRulesUnsuportTimeUnit |
||||
} |
||||
|
||||
switch ntu.Unit { |
||||
case "MONTH": |
||||
{ |
||||
ctMonth := now.With(*c.CreatedTime) |
||||
start = ctMonth.BeginningOfMonth() |
||||
end = ctMonth.EndOfMonth().AddDate(0, 0, ntu.EndInAdvance*-1) |
||||
} |
||||
} |
||||
n := time.Now() |
||||
if n.Before(start) { |
||||
return &ErrCouponRulesRedemptionNotStart |
||||
} |
||||
|
||||
if n.After(end) { |
||||
return &ErrCouponRulesRedemptionExpired |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// JudgeCoupon 验证有效期
|
||||
func (*RedeemPeriodJudge) JudgeCoupon(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) error { |
||||
|
||||
var ts timeSpan |
||||
var err error |
||||
var start time.Time |
||||
var end time.Time |
||||
// err := json.Unmarshal([]byte(jsonString), &ts)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
// var startTime, endTime string
|
||||
// var ok bool
|
||||
// if startTime, ok = ruleBody["startTime"].(string); !ok {
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
if err := mapstructure.Decode(ruleBody, &ts); err != nil { |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
if !base.IsBlankString(ts.StartTime) { |
||||
start, err = time.Parse(timeLayout, ts.StartTime) |
||||
if nil != err { |
||||
log.Println(err) |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
} |
||||
|
||||
// if endTime, ok = ruleBody["endTime"].(string); !ok {
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
if !base.IsBlankString(ts.EndTime) { |
||||
end, err = time.Parse(timeLayout, ts.EndTime) |
||||
if nil != err { |
||||
log.Println(err) |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
} |
||||
|
||||
if time.Now().Before(start) { |
||||
return &ErrCouponRulesRedemptionNotStart |
||||
} |
||||
|
||||
if time.Now().After(end) { |
||||
return &ErrCouponRulesRedemptionExpired |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// JudgeCoupon 验证兑换次数
|
||||
func (*RedeemTimesJudge) JudgeCoupon(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) error { |
||||
|
||||
var rt redeemTimes |
||||
// err := json.Unmarshal([]byte(jsonString), &rt)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return err
|
||||
// }
|
||||
|
||||
tas, err := getCouponTransactionsWithType(c.ID, TTRedeemCoupon) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if nil == tas { |
||||
return nil |
||||
} |
||||
|
||||
// var i int
|
||||
// var ok bool
|
||||
// if i, ok = ruleBody["times"].(int); !ok {
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
if err := mapstructure.Decode(ruleBody, &rt); err != nil { |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
if len(tas) >= int(rt.Times) { |
||||
return &ErrCouponRulesRedeemTimesExceeded |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// JudgeCoupon 验证只能同品牌兑换
|
||||
func (*RedeemBySameBrandJudge) JudgeCoupon(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) error { |
||||
if base.IsEmptyString(requester.Brand) { |
||||
return &ErrRequesterHasNoBrand |
||||
} |
||||
|
||||
var brand sameBrand |
||||
// err := json.Unmarshal([]byte(jsonString), &brand)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return err
|
||||
// }
|
||||
|
||||
// var brand string
|
||||
// var ok bool
|
||||
if err := mapstructure.Decode(ruleBody, &brand); err != nil { |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
// if brand, ok = ruleBody["brand"].(string); !ok {
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
if requester.Brand != brand.Brand { |
||||
return &ErrRedeemWithDiffBrand |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// JudgeTemplate 验证领用次数
|
||||
func (*ApplyTimesJudge) JudgeTemplate(consumerID string, couponTypeID string, ruleBody map[string]interface{}, pct *PublishedCouponType) error { |
||||
coupons, err := getCoupons(consumerID, couponTypeID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var at applyTimes |
||||
// err2 := json.Unmarshal([]byte(jsonString), &at)
|
||||
// if err2 != nil {
|
||||
// log.Println(err2)
|
||||
// return err2
|
||||
// }
|
||||
|
||||
// var i int
|
||||
// var ok bool
|
||||
|
||||
if err := mapstructure.Decode(ruleBody, &at); err != nil { |
||||
return &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
// if i, ok = ruleBody["inDays"].(int); !ok {
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
if at.InDays > 0 { |
||||
var expiredTime = pct.CreatedTime.AddDate(0, 0, int(at.InDays)) |
||||
if !time.Now().Before(expiredTime) { |
||||
return &ErrCouponRulesApplyTimeExpired |
||||
} |
||||
} |
||||
|
||||
// if i, ok = ruleBody["times"].(int); !ok {
|
||||
// return &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
if at.Times <= uint(len(coupons)) { |
||||
return &ErrCouponRulesApplyTimesExceeded |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// JudgeNTUExpired - To validate experiation status of coupon for NTU type.
|
||||
func JudgeNTUExpired(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) (bool, error) { |
||||
var ntu natureTimeUnit |
||||
var end time.Time |
||||
|
||||
if err := mapstructure.Decode(ruleBody, &ntu); err != nil { |
||||
return false, &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
if base.IsBlankString(ntu.Unit) || ntu.Unit != "MONTH" { |
||||
return false, &ErrCouponRulesUnsuportTimeUnit |
||||
} |
||||
|
||||
switch ntu.Unit { |
||||
case "MONTH": |
||||
{ |
||||
ctMonth := now.With(*c.CreatedTime) |
||||
end = ctMonth.EndOfMonth().AddDate(0, 0, ntu.EndInAdvance*-1) |
||||
} |
||||
} |
||||
n := time.Now() |
||||
|
||||
if n.After(end) { |
||||
return true, nil |
||||
} |
||||
|
||||
return false, nil |
||||
} |
||||
|
||||
// JudgePeriodExpired - To validate experiation status of coupon for normal period type.
|
||||
func JudgePeriodExpired(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) (bool, error) { |
||||
var ts timeSpan |
||||
var err error |
||||
var end time.Time |
||||
|
||||
if err := mapstructure.Decode(ruleBody, &ts); err != nil { |
||||
return false, &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
if !base.IsBlankString(ts.EndTime) { |
||||
end, err = time.Parse(timeLayout, ts.EndTime) |
||||
if nil != err { |
||||
log.Println(err) |
||||
return false, &ErrCouponRulesBadFormat |
||||
} |
||||
} |
||||
|
||||
if time.Now().After(end) { |
||||
return true, nil |
||||
} |
||||
|
||||
return false, nil |
||||
} |
||||
@ -0,0 +1,432 @@
@@ -0,0 +1,432 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "encoding/json"
|
||||
"fmt" |
||||
// "reflect"
|
||||
"testing" |
||||
"time" |
||||
|
||||
// "loreal.com/dit/cmd/coupon-service/base"
|
||||
|
||||
"bou.ke/monkey" |
||||
"github.com/jinzhu/now" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
func Test_ValidPeriodJudge_judgeCoupon(t *testing.T) { |
||||
var judge RedeemPeriodJudge |
||||
Convey("Given a regular time span and include today", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"startTime": "2020-01-01 00:00:00 +08", |
||||
"endTime": "3020-02-14 00:00:00 +08", |
||||
} |
||||
// var ruleBody = `{"startTime": "2020-01-01 00:00:00 +08", "endTime": "3020-02-14 00:00:00 +08" }`
|
||||
//base.RandString(4)
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, nil) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a past time span", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"startTime": "1989-06-04 01:23:45 +08", |
||||
"endTime": "2008-02-08 20:08:00 +08", |
||||
} |
||||
// var ruleBody = `{"startTime": "1989-06-04 01:23:45 +08", "endTime": "2008-02-08 20:08:00 +08" }`
|
||||
//base.RandString(4)
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, nil) |
||||
Convey("Should ErrCouponRulesRedemptionExpired", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedemptionExpired) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a future time span", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"startTime": "3456-07-08 09:10:12 +08", |
||||
"endTime": "3456-07-08 09:20:12 +08", |
||||
} |
||||
// var ruleBody = `{"startTime": "3456-07-08 09:10:12 +08", "endTime": "3456-07-08 09:20:12 +08" }`
|
||||
//base.RandString(4)
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, nil) |
||||
Convey("Should ErrCouponRulesRedemptionNotStart", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedemptionNotStart) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given some time spans with bad format", t, func() { |
||||
Convey("bad start format", func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"startTime": 1234567890, |
||||
"endTime": "3456-07-08 09:20:12 +08", |
||||
} |
||||
// var ruleBody = `{"startTime": 1234567890, "endTime": "3456-07-08 09:20:12 +08" }`
|
||||
err := judge.JudgeCoupon(nil, "", ruleBody, nil) |
||||
Convey("Should ErrCouponRulesBadFormat", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesBadFormat) |
||||
}) |
||||
}) |
||||
|
||||
Convey("bad end format", func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"startTime": "3456-07-08 09:10:12 +08", |
||||
"endTime": 1234567890, |
||||
} |
||||
// var ruleBody = `{"startTime": "3456-07-08 09:10:12 +08", "endTime": "3456=07-08 09:20:12 +08" }`
|
||||
err := judge.JudgeCoupon(nil, "", ruleBody, nil) |
||||
Convey("Should ErrCouponRulesBadFormat", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesBadFormat) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_RedeemInCurrentNatureMonthSeasonYear_judgeCoupon(t *testing.T) { |
||||
var judge RedeemInCurrentNatureTimeUnitJudge |
||||
ruleBody := map[string]interface{}{ |
||||
"unit": "MONTH", |
||||
"endInAdvance": 0, |
||||
} |
||||
Convey("Given a regular sample", t, func() { |
||||
// var ruleBody = `{"startTime": "2020-01-01 00:00:00 +08", "endTime": "3020-02-14 00:00:00 +08" }`
|
||||
//base.RandString(4)
|
||||
c := _aCoupon("abc", "xxx", "yyy", "def", 0, nil) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, c) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a coupon applied last month", t, func() { |
||||
c := _aCoupon("abc", "xxx", "yyy", "def", 0, nil) |
||||
ct := c.CreatedTime.AddDate(0, 0, -31) |
||||
c.CreatedTime = &ct |
||||
fmt.Print(c.CreatedTime) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, c) |
||||
Convey("Should ErrCouponRulesRedemptionExpired", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedemptionExpired) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a coupon applied before the month, maybe caused by daylight saving time", t, func() { |
||||
c := _aCoupon("abc", "xxx", "yyy", "def", 0, nil) |
||||
ct := c.CreatedTime.AddDate(0, 0, 31) |
||||
c.CreatedTime = &ct |
||||
// var ruleBody = `{"startTime": "3456-07-08 09:10:12 +08", "endTime": "3456-07-08 09:20:12 +08" }`
|
||||
//base.RandString(4)
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, c) |
||||
Convey("Should ErrCouponRulesRedemptionNotStart", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedemptionNotStart) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a time unit with unsupport unit", t, func() { |
||||
Convey("season unit...", func() { |
||||
rb := map[string]interface{}{ |
||||
"unit": "SEASON", |
||||
"endInAdvance": 0, |
||||
} |
||||
err := judge.JudgeCoupon(nil, "", rb, nil) |
||||
Convey("Should ErrCouponRulesUnsuportTimeUnit", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesUnsuportTimeUnit) |
||||
}) |
||||
}) |
||||
|
||||
Convey("year unit...", func() { |
||||
rb := map[string]interface{}{ |
||||
"unit": "YEAR", |
||||
"endInAdvance": 0, |
||||
} |
||||
err := judge.JudgeCoupon(nil, "", rb, nil) |
||||
Convey("Should ErrCouponRulesUnsuportTimeUnit", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesUnsuportTimeUnit) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a sample with not default endInAdvance", t, func() { |
||||
rb := map[string]interface{}{ |
||||
"unit": "MONTH", |
||||
"endInAdvance": 10, |
||||
} |
||||
c := _aCoupon("abc", "xxx", "yyy", "def", 0, nil) |
||||
nov := now.With(time.Date(2020, time.November, 2, 0, 0, 0, 0, time.UTC)) |
||||
pg1 := monkey.Patch(now.With, func(time.Time) *now.Now { |
||||
return nov |
||||
}) |
||||
|
||||
Convey("assume in valid period", func() { |
||||
ct := time.Date(2020, time.November, 15, 0, 0, 0, 0, time.UTC) |
||||
pg2 := monkey.Patch(time.Now, func() time.Time { |
||||
return ct |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", rb, c) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
pg2.Unpatch() |
||||
}) |
||||
|
||||
Convey("assume out of period", func() { |
||||
ct := time.Date(2020, time.November, 30, 0, 0, 0, 0, time.UTC) |
||||
pg2 := monkey.Patch(time.Now, func() time.Time { |
||||
return ct |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", rb, c) |
||||
Convey("Should ErrCouponRulesRedemptionExpired", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedemptionExpired) |
||||
}) |
||||
}) |
||||
pg2.Unpatch() |
||||
}) |
||||
|
||||
pg1.Unpatch() |
||||
}) |
||||
|
||||
Convey("Given a sample with not default endInAdvance", t, func() { |
||||
rb := map[string]interface{}{ |
||||
"unit": "MONTH", |
||||
"endInAdvance": -10, |
||||
} |
||||
c := _aCoupon("abc", "xxx", "yyy", "def", 0, nil) |
||||
nov := now.With(time.Date(2020, time.November, 2, 0, 0, 0, 0, time.UTC)) |
||||
pg1 := monkey.Patch(now.With, func(time.Time) *now.Now { |
||||
return nov |
||||
}) |
||||
|
||||
Convey("assume in valid period", func() { |
||||
ct := time.Date(2020, time.December, 5, 0, 0, 0, 0, time.UTC) |
||||
pg2 := monkey.Patch(time.Now, func() time.Time { |
||||
return ct |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", rb, c) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
pg2.Unpatch() |
||||
}) |
||||
|
||||
Convey("assume out of period", func() { |
||||
ct := time.Date(2020, time.December, 15, 0, 0, 0, 0, time.UTC) |
||||
pg2 := monkey.Patch(time.Now, func() time.Time { |
||||
return ct |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", rb, c) |
||||
Convey("Should ErrCouponRulesRedemptionExpired", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedemptionExpired) |
||||
}) |
||||
}) |
||||
pg2.Unpatch() |
||||
}) |
||||
|
||||
pg1.Unpatch() |
||||
}) |
||||
} |
||||
|
||||
func Test_RedeemTimesJudge_judgeCoupon(t *testing.T) { |
||||
var judge RedeemTimesJudge |
||||
var c Coupon |
||||
Convey("Given a valid redeem times", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"times": 2, |
||||
} |
||||
// var ruleBody = `{"times": 2}`
|
||||
Convey("Fisrt give some redeem logs which less than the coupon max redeem times", func() { |
||||
monkey.Patch(getCouponTransactionsWithType, func(_ string, _ TransType) ([]*Transaction, error) { |
||||
return make([]*Transaction, 1, 1), nil |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, &c) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Second give no redeem logs", func() { |
||||
monkey.Patch(getCouponTransactionsWithType, func(_ string, _ TransType) ([]*Transaction, error) { |
||||
return nil, nil |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, &c) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Third give some redeem logs which greater than the coupon max redeem times", func() { |
||||
monkey.Patch(getCouponTransactionsWithType, func(_ string, _ TransType) ([]*Transaction, error) { |
||||
return make([]*Transaction, 3, 3), nil |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, &c) |
||||
Convey("Should has ErrCouponRulesRedeemTimesExceeded", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesRedeemTimesExceeded) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("If has some db err....", func() { |
||||
monkey.Patch(getCouponTransactionsWithType, func(_ string, _ TransType) ([]*Transaction, error) { |
||||
return nil, fmt.Errorf("hehehe") |
||||
}) |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(nil, "", ruleBody, &c) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldNotBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_RedeemBySameBrandJudge_judgeCoupon(t *testing.T) { |
||||
var judge RedeemBySameBrandJudge |
||||
var brand = "Lancome" |
||||
Convey("Given a reqeust with no brand", t, func() { |
||||
var requester = _aRequester("", nil, "") |
||||
ruleBody := map[string]interface{}{} |
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(requester, "", ruleBody, nil) |
||||
Convey("Should has ErrRequesterHasNoBrand", func() { |
||||
So(err, ShouldEqual, &ErrRequesterHasNoBrand) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a bad fromat rule body", t, func() { |
||||
var requester = _aRequester("", nil, brand) |
||||
ruleBody := map[string]interface{}{ |
||||
"bra--nd": "Lancome", |
||||
} |
||||
// var ruleBody = `{"brand"="Lancome"}`
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(requester, "", ruleBody, nil) |
||||
Convey("Should has error", func() { |
||||
So(err, ShouldNotBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a request with wrong brand", t, func() { |
||||
var requester = _aRequester("", nil, brand) |
||||
ruleBody := map[string]interface{}{ |
||||
"brand": "Lancome+bad+brand", |
||||
} |
||||
// var ruleBody = `{"brand":"Lancome+bad+brand"}`
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(requester, "", ruleBody, nil) |
||||
Convey("Should has ErrRedeemWithDiffBrand", func() { |
||||
So(err, ShouldEqual, &ErrRedeemWithDiffBrand) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a request with correct brand", t, func() { |
||||
var requester = _aRequester("", nil, brand) |
||||
ruleBody := map[string]interface{}{ |
||||
"brand": "Lancome", |
||||
} |
||||
|
||||
// var ruleBody = `{"brand":"Lancome"}`
|
||||
Convey("Call JudgeCoupon", func() { |
||||
err := judge.JudgeCoupon(requester, "", ruleBody, nil) |
||||
Convey("Should has no error", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_ApplyTimesJudge_judgeCoupon(t *testing.T) { |
||||
var judge ApplyTimesJudge |
||||
Convey("The data base has something wrong... ", t, func() { |
||||
monkey.Patch(getCoupons, func(_ string, _ string) ([]*Coupon, error) { |
||||
return nil, fmt.Errorf("hehehe") |
||||
}) |
||||
Convey("Call JudgeTemplate", func() { |
||||
ruleBody := map[string]interface{}{} |
||||
err := judge.JudgeTemplate("", "", ruleBody, nil) |
||||
Convey("Should has error", func() { |
||||
So(err, ShouldNotBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a coupon template which is expired", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"inDays": 365, |
||||
"times": 2, |
||||
} |
||||
// var ruleBody = `{"inDays": 365, "times": 2 }`
|
||||
var pct PublishedCouponType |
||||
pct.CreatedTime = time.Now().Local().AddDate(0, 0, -366) |
||||
monkey.Patch(getCoupons, func(_ string, _ string) ([]*Coupon, error) { |
||||
return nil, nil |
||||
}) |
||||
Convey("Call JudgeTemplate", func() { |
||||
err := judge.JudgeTemplate("", "", ruleBody, &pct) |
||||
Convey("Should has ErrCouponRulesApplyTimeExpired", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesApplyTimeExpired) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Set the user had applyed coupons and reach the max times", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"inDays": 365, |
||||
"times": 2, |
||||
} |
||||
// var ruleBody = `{"inDays": 365, "times": 2 }`
|
||||
var pct PublishedCouponType |
||||
pct.CreatedTime = time.Now().Local() |
||||
monkey.Patch(getCoupons, func(_ string, _ string) ([]*Coupon, error) { |
||||
return make([]*Coupon, 2, 2), nil |
||||
}) |
||||
Convey("Call JudgeTemplate", func() { |
||||
err := judge.JudgeTemplate("", "", ruleBody, &pct) |
||||
Convey("Should has ErrCouponRulesApplyTimesExceeded", func() { |
||||
So(err, ShouldEqual, &ErrCouponRulesApplyTimesExceeded) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Set the user has not reach the max appling times", t, func() { |
||||
ruleBody := map[string]interface{}{ |
||||
"inDays": 365, |
||||
"times": 2, |
||||
} |
||||
// var ruleBody = `{"inDays": 365, "times": 2 }`
|
||||
var pct PublishedCouponType |
||||
pct.CreatedTime = time.Now().Local() |
||||
monkey.Patch(getCoupons, func(_ string, _ string) ([]*Coupon, error) { |
||||
return make([]*Coupon, 1, 2), nil |
||||
}) |
||||
Convey("Call JudgeTemplate", func() { |
||||
err := judge.JudgeTemplate("", "", ruleBody, &pct) |
||||
Convey("Should has ErrCouponRulesApplyTimesExceeded", func() { |
||||
So(err, ShouldBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "encoding/json"
|
||||
// "log"
|
||||
"time" |
||||
|
||||
// "loreal.com/dit/cmd/coupon-service/coupon"
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
|
||||
"github.com/mitchellh/mapstructure" |
||||
) |
||||
|
||||
const timeLayout string = "2006-01-02 15:04:05 -07" |
||||
|
||||
// BodyComposer 发卡时生成rule的body,用来存在coupon中
|
||||
type BodyComposer interface { |
||||
Compose(requester *base.Requester, couponTypeID string, ruleBody map[string]interface{}) (map[string]interface{}, error) |
||||
} |
||||
|
||||
// RedeemTimesBodyComposer 生成兑换次数的内容
|
||||
type RedeemTimesBodyComposer struct { |
||||
} |
||||
|
||||
// RedeemBySameBrandBodyComposer 生成兑换次数的内容
|
||||
type RedeemBySameBrandBodyComposer struct { |
||||
} |
||||
|
||||
// RedeemPeriodWithOffsetBodyComposer 生成有效期的内容
|
||||
type RedeemPeriodWithOffsetBodyComposer struct { |
||||
} |
||||
|
||||
// RedeemInCurrentNatureTimeUnitBodyComposer 生成有效期的内容
|
||||
type RedeemInCurrentNatureTimeUnitBodyComposer struct { |
||||
} |
||||
|
||||
// Compose 生成兑换次数的内容
|
||||
// 规则体像 {"times": 1024}
|
||||
func (*RedeemTimesBodyComposer) Compose(requester *base.Requester, couponTypeID string, ruleBody map[string]interface{}) (map[string]interface{}, error) { |
||||
return ruleBody, nil |
||||
} |
||||
|
||||
// Compose 生成由同品牌才能兑换的内容
|
||||
// 规则体像 {"brand":"Lancome"}
|
||||
func (*RedeemBySameBrandBodyComposer) Compose(requester *base.Requester, couponTypeID string, ruleBody map[string]interface{}) (map[string]interface{}, error) { |
||||
if base.IsEmptyString(requester.Brand) { |
||||
return nil, &ErrRequesterHasNoBrand |
||||
} |
||||
var brand = make(map[string]interface{}) |
||||
brand["brand"] = requester.Brand |
||||
// var brand = sameBrand{
|
||||
// Brand: requester.Brand,
|
||||
// }
|
||||
return brand, nil |
||||
// return (&brand).(map[string]interface{}), nil
|
||||
} |
||||
|
||||
// Compose 生成卡券有效期的规则体
|
||||
// 模板内的规则类似这样的格式: {"offSetFromAppliedDay": 14,"timeSpan": 365}, 表示从领用日期延期14天后生效可以兑换,截止日期是14+365天内。
|
||||
// 如果offSetFromAppliedDay=0,则当天生效。如果 timeSpan=0,则无过期时间。
|
||||
// 时间单位都是天。
|
||||
// 生成卡券后的规则:{ "startTime": "2020-02-14T00:00:00+08:00", "endTime": null }, 表示从2020-02-14日 0点开始,无过期时间。
|
||||
func (*RedeemPeriodWithOffsetBodyComposer) Compose(requester *base.Requester, couponTypeID string, ruleBody map[string]interface{}) (map[string]interface{}, error) { |
||||
var offset offsetSpan |
||||
|
||||
if err := mapstructure.Decode(ruleBody, &offset); err != nil { |
||||
return nil, &ErrCouponRulesBadFormat |
||||
} |
||||
var span = make(map[string]interface{}) |
||||
|
||||
// var i int
|
||||
// var ok bool
|
||||
// if i, ok = ruleBody["offSetFromAppliedDay"].(int); !ok {
|
||||
// return nil, &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
var st = time.Now().Local().AddDate(0, 0, int(offset.OffSetFromAppliedDay)) |
||||
// 时分秒清零
|
||||
// st = Time.Date(st.Year())
|
||||
span["startTime"] = st.Format(timeLayout) |
||||
|
||||
// if i, ok = ruleBody["timeSpan"].(int); !ok {
|
||||
// return nil, &ErrCouponRulesBadFormat
|
||||
// }
|
||||
|
||||
ts := offset.TimeSpan |
||||
if 0 != ts { |
||||
var et = st.AddDate(0, 0, int(ts)) |
||||
span["endTime"] = et.Format(timeLayout) |
||||
} |
||||
|
||||
return span, nil |
||||
} |
||||
|
||||
// Compose 生成卡券有效期的规则体,基于自然月,季度,年等。
|
||||
// 模板内的规则类似这样的格式: {"unit": "MONTH", "endInAdvance": 5}, 表示领用当月生效,但在当月结束前5天过期。
|
||||
// endInAdvance 的单位是 天, 默认为 0。
|
||||
func (*RedeemInCurrentNatureTimeUnitBodyComposer) Compose(requester *base.Requester, couponTypeID string, ruleBody map[string]interface{}) (map[string]interface{}, error) { |
||||
return ruleBody, nil |
||||
} |
||||
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "reflect"
|
||||
"testing" |
||||
"time" |
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
|
||||
. "github.com/chenhg5/collection" |
||||
"github.com/mitchellh/mapstructure" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
// "bou.ke/monkey"
|
||||
) |
||||
|
||||
func Test_RedeemTimesBodyComposer_compose(t *testing.T) { |
||||
Convey("Given a RedeemTimesBodyComposer instance and some input", t, func() { |
||||
var composer RedeemTimesBodyComposer |
||||
// var ruleInternalID = base.RandString(4)
|
||||
ruleBody := map[string]interface{}{ |
||||
"times": 123, |
||||
} |
||||
Convey("Call Compose", func() { |
||||
rrf, _ := composer.Compose(nil, "", ruleBody) |
||||
Convey("The composed value should contain correct value", func() { |
||||
|
||||
So(Collect(rrf).Has("times"), ShouldBeTrue) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
// var st = time.Now().Local().AddDate(0, 0, 0)
|
||||
// fmt.Print(st.String())
|
||||
|
||||
// t.Errorf(st.Format("2006-01-02 15:04:05 -07"))
|
||||
|
||||
} |
||||
|
||||
func Test_RedeemInCurrentNatureTimeUnitBodyComposer_compose(t *testing.T) { |
||||
Convey("Given a RedeemInCurrentNatureTimeUnitBodyComposer instance and a rule body with positive value", t, func() { |
||||
var composer RedeemInCurrentNatureTimeUnitBodyComposer |
||||
// var ruleInternalID = base.RandString(4)
|
||||
ruleBody := map[string]interface{}{ |
||||
"unit": "MONTH", |
||||
"endInAdvance": 10, |
||||
} |
||||
Convey("Call Compose", func() { |
||||
rrf, _ := composer.Compose(nil, "", ruleBody) |
||||
Convey("The composed value should contain correct value", func() { |
||||
So(Collect(rrf).Has("endInAdvance"), ShouldBeTrue) |
||||
So(Collect(rrf).Has("unit"), ShouldBeTrue) |
||||
var ntu natureTimeUnit |
||||
mapstructure.Decode(rrf, &ntu) |
||||
So(ntu.Unit, ShouldEqual, "MONTH") |
||||
So(ntu.EndInAdvance, ShouldEqual, 10) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a RedeemInCurrentNatureTimeUnitBodyComposer instance and a rule body with negative value", t, func() { |
||||
var composer RedeemInCurrentNatureTimeUnitBodyComposer |
||||
// var ruleInternalID = base.RandString(4)
|
||||
ruleBody := map[string]interface{}{ |
||||
"unit": "MONTH", |
||||
"endInAdvance": -10, |
||||
} |
||||
Convey("Call Compose", func() { |
||||
rrf, _ := composer.Compose(nil, "", ruleBody) |
||||
Convey("The composed value should contain correct value", func() { |
||||
So(Collect(rrf).Has("endInAdvance"), ShouldBeTrue) |
||||
So(Collect(rrf).Has("unit"), ShouldBeTrue) |
||||
var ntu natureTimeUnit |
||||
mapstructure.Decode(rrf, &ntu) |
||||
So(ntu.Unit, ShouldEqual, "MONTH") |
||||
So(ntu.EndInAdvance, ShouldEqual, -10) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
} |
||||
|
||||
func Test_RedeemBySameBrandBodyComposer_compose(t *testing.T) { |
||||
var composer RedeemBySameBrandBodyComposer |
||||
// var ruleInternalID = base.RandString(4)
|
||||
ruleBody := map[string]interface{}{} |
||||
Convey("Given a RedeemBySameBrandBodyComposer instance and some input", t, func() { |
||||
var brand = base.RandString(4) |
||||
var requester = _aRequester("", nil, brand) |
||||
// monkey.PatchInstanceMethod(reflect.TypeOf(requester), "HasRole", func(_ *base.Requester, _ string) bool {
|
||||
// return true
|
||||
// })
|
||||
Convey("Call Compose", func() { |
||||
|
||||
rrf, _ := composer.Compose(requester, "", ruleBody) |
||||
Convey("The composed value should contain brand info", func() { |
||||
// So(ruleInternalID, ShouldEqual, rrf.RuleInternalID)
|
||||
// var sb sameBrand
|
||||
// _ = json.Unmarshal([]byte(rrf.RuleBody), &sb)
|
||||
So(Collect(rrf).Has("brand"), ShouldBeTrue) |
||||
So(rrf["brand"], ShouldEqual, brand) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given a requester with no brand", t, func() { |
||||
var requester = _aRequester("", nil, "") |
||||
Convey("Call Compose", func() { |
||||
_, err := composer.Compose(requester, "", ruleBody) |
||||
Convey("The call should failed with ErrRequesterHasNoBrand", func() { |
||||
So(err, ShouldEqual, &ErrRequesterHasNoBrand) |
||||
}) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func Test_ValidPeriodWithOffsetBodyComposer_compose(t *testing.T) { |
||||
var composer RedeemPeriodWithOffsetBodyComposer |
||||
// var ruleInternalID = base.RandString(4)
|
||||
Convey("Given a RedeemPeriodWithOffsetBodyComposer instance and some input", t, func() { |
||||
var offSetFromAppliedDay = r.Intn(1000) |
||||
var span = r.Intn(1000) |
||||
ruleBody := map[string]interface{}{ |
||||
"offSetFromAppliedDay": offSetFromAppliedDay, |
||||
"timeSpan": span, |
||||
} |
||||
// var bodyString = fmt.Sprintf(`{"offSetFromAppliedDay": %d,"timeSpan": %d}`, offSetFromAppliedDay, span)
|
||||
// monkey.PatchInstanceMethod(reflect.TypeOf(requester), "HasRole", func(_ *base.Requester, _ string) bool {
|
||||
// return true
|
||||
// })
|
||||
Convey("Call Compose", func() { |
||||
rrf, _ := composer.Compose(nil, "", ruleBody) |
||||
Convey("The composed value should contain an offset value", func() { |
||||
// So(ruleInternalID, ShouldEqual, rrf.RuleInternalID)
|
||||
|
||||
var st = time.Now().Local().AddDate(0, 0, offSetFromAppliedDay) |
||||
var end = st.AddDate(0, 0, span) |
||||
// var ts timeSpan
|
||||
// _ = json.Unmarshal([]byte(rrf.RuleBody), &ts)
|
||||
So(Collect(rrf).Has("startTime"), ShouldBeTrue) |
||||
So(Collect(rrf).Has("endTime"), ShouldBeTrue) |
||||
So(rrf["startTime"], ShouldEqual, st.Format("2006-01-02 15:04:05 -07")) |
||||
So(rrf["endTime"], ShouldEqual, end.Format("2006-01-02 15:04:05 -07")) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
} |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
package coupon |
||||
|
||||
// MessageType 消息类型
|
||||
type MessageType int32 |
||||
|
||||
const ( |
||||
//MTIssue - Issue
|
||||
MTIssue MessageType = iota |
||||
//MTRevoked - Revoked
|
||||
MTRevoked |
||||
//MTRedeemed - Redeemed
|
||||
MTRedeemed |
||||
//MTUnknown - Unknown
|
||||
MTUnknown |
||||
) |
||||
|
||||
// Message 消息结构
|
||||
type Message struct { |
||||
Type MessageType `json:"type,omitempty"` |
||||
Payload interface{} `json:"payload,omitempty"` |
||||
} |
||||
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"log" |
||||
"time" |
||||
|
||||
"loreal.com/dit/utils" |
||||
// "loreal.com/dit/cmd/coupon-service/rule"
|
||||
) |
||||
|
||||
// KeyBindingRuleProperties 生成的Coupon包含类型为map的Properties字段,用来保存多样化数据。KeyBindingRuleProperties对应的值是该券在兑换时需要满足的条件。
|
||||
const KeyBindingRuleProperties string = "binding_rule_properties" |
||||
|
||||
// Template 卡券的模板
|
||||
type Template struct { |
||||
ID string `json:"id"` |
||||
Name string `json:"name"` |
||||
Description string `json:"description"` |
||||
Creator string `json:"creator"` |
||||
Rules map[string]interface{} `json:"rules"` |
||||
CreatedTime time.Time `json:"created_time,omitempty" type:"DATETIME" default:"datetime('now','localtime')"` |
||||
UpdatedTime time.Time `json:"updated_time,omitempty" type:"DATETIME"` |
||||
DeletedTime time.Time `json:"deleted_time,omitempty" type:"DATETIME"` |
||||
} |
||||
|
||||
// CTState 卡券类型的状态定义
|
||||
type CTState int32 |
||||
|
||||
// 卡券的状态
|
||||
const ( |
||||
CTSActive State = iota |
||||
CTSRevoked |
||||
CTSUnknown |
||||
) |
||||
|
||||
// PublishedCouponType 已经发布的卡券类型
|
||||
type PublishedCouponType struct { |
||||
ID string `json:"id"` |
||||
Name string `json:"name"` |
||||
TemplateID string `json:"template_id"` |
||||
Description string `json:"description"` |
||||
InternalDescription string `json:"internal_description"` |
||||
State CTState `json:"state"` |
||||
Publisher string `json:"publisher"` |
||||
VisibleStartTime time.Time `json:"visible_start_time" type:"DATETIME"` |
||||
VisibleEndTime time.Time `json:"visible_end_time"` |
||||
StrRules map[string]string `json:"rules"` |
||||
Rules map[string]map[string]interface{} |
||||
CreatedTime time.Time `json:"created_time" type:"DATETIME" default:"datetime('now','localtime')"` |
||||
DeletedTime time.Time `json:"deleted_time" type:"DATETIME"` |
||||
} |
||||
|
||||
// InitRules //TODO 未来会重构掉
|
||||
func (t *PublishedCouponType) InitRules() { |
||||
t.Rules = map[string]map[string]interface{}{} |
||||
for k, v := range t.StrRules { |
||||
var obj map[string]interface{} |
||||
err := json.Unmarshal([]byte(v), &obj) |
||||
if nil == err { |
||||
t.Rules[k] = obj |
||||
} else { |
||||
log.Panic(err) |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
// State 卡券的状态类型定义
|
||||
type State int32 |
||||
|
||||
// 卡券的状态
|
||||
// TODO [HUBIN]: 增加 SExpired 状态
|
||||
const ( |
||||
SActive State = iota //如果非一次兑换类型,那么在有效兑换次数内,仍然是active
|
||||
SRevoked |
||||
SDeleteCoupon |
||||
SRedeemed //无论多次还是一次,全部用完后置为该状态
|
||||
SExpired |
||||
SUnknown |
||||
) |
||||
|
||||
// Coupon 用来封装一个Coupon实体的结构
|
||||
type Coupon struct { |
||||
ID string |
||||
CouponTypeID string |
||||
ConsumerID string |
||||
ConsumerRefID string |
||||
ChannelID string |
||||
State State |
||||
Properties map[string]interface{} |
||||
CreatedTime *time.Time |
||||
Transactions []*Transaction |
||||
} |
||||
|
||||
// CreatedTimeToLocal 使用localtime
|
||||
func (c *Coupon) CreatedTimeToLocal() { |
||||
l := c.CreatedTime.Local() |
||||
c.CreatedTime = &l |
||||
} |
||||
|
||||
// RedeemedCoupons 传递被核销卡券信息的结构,
|
||||
type RedeemedCoupons struct { |
||||
ExtraInfo string `json:"extrainfo,omitempty"` |
||||
Coupons []*Coupon `json:"coupons,omitempty"` |
||||
} |
||||
|
||||
// TransType 卡券被操作的状态类型
|
||||
type TransType int32 |
||||
|
||||
// 卡券被操作的种类
|
||||
const ( |
||||
TTIssueCoupon TransType = iota |
||||
TTDeactiveCoupon |
||||
TTDeleteCoupon |
||||
TTRedeemCoupon //可多次存在
|
||||
TTExpired |
||||
TTUnknownTransaction |
||||
) |
||||
|
||||
// Transaction 用来封装一次Coupon的状态变动
|
||||
type Transaction struct { |
||||
ID string |
||||
CouponID string |
||||
ActorID string |
||||
TransType TransType |
||||
ExtraInfo string |
||||
CreatedTime time.Time |
||||
} |
||||
|
||||
// EncryptExtraInfo 给ExtraInfo 使用AES256加密
|
||||
func (t *Transaction) EncryptExtraInfo() string { |
||||
return utils.AES256URLEncrypt(t.ExtraInfo, encryptKey) |
||||
} |
||||
|
||||
// DecryptExtraInfo 给ExtraInfo 使用AES256解密
|
||||
func (t *Transaction) DecryptExtraInfo(emsg string) error { |
||||
p, e := utils.AES256URLDecrypt(emsg, encryptKey) |
||||
if nil != e { |
||||
return e |
||||
} |
||||
t.ExtraInfo = p |
||||
return nil |
||||
} |
||||
|
||||
// CreatedTimeToLocal 给ExtraInfo 使用AES256解密
|
||||
func (t *Transaction) CreatedTimeToLocal() { |
||||
l := t.CreatedTime.Local() |
||||
t.CreatedTime = l |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// Rule 卡券的规则
|
||||
type Rule struct { |
||||
ID string `json:"id"` |
||||
Name string `json:"name"` |
||||
InternalID string `json:"internal_id"` |
||||
Description string `json:"description,omitempty"` |
||||
RuleBody string `json:"rule_body"` |
||||
Creator string `json:"creator"` |
||||
CreatedTime time.Time `json:"created_time,omitempty" type:"DATETIME" default:"datetime('now','localtime')"` |
||||
UpdatedTime time.Time `json:"updated_time,omitempty" type:"DATETIME"` |
||||
DeletedTime time.Time `json:"deleted_time,omitempty" type:"DATETIME"` |
||||
} |
||||
|
||||
type offsetSpan struct { |
||||
OffSetFromAppliedDay uint `json:"offSetFromAppliedDay"` |
||||
TimeSpan uint `json:"timeSpan"` |
||||
} |
||||
|
||||
type timeSpan struct { |
||||
StartTime string `json:"startTime"` |
||||
EndTime string `json:"endTime"` |
||||
} |
||||
|
||||
type natureTimeUnit struct { |
||||
Unit string `json:"unit"` |
||||
EndInAdvance int `json:"endInAdvance"` |
||||
} |
||||
|
||||
type applyTimes struct { |
||||
InDays uint `json:"inDays"` |
||||
Times uint `json:"times"` |
||||
} |
||||
|
||||
type redeemTimes struct { |
||||
Times uint `json:"times"` |
||||
} |
||||
|
||||
type sameBrand struct { |
||||
Brand string `json:"brand"` |
||||
} |
||||
@ -0,0 +1,237 @@
@@ -0,0 +1,237 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
"log" |
||||
|
||||
"github.com/mitchellh/mapstructure" |
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
) |
||||
|
||||
var supportedRules []*Rule |
||||
|
||||
var issueJudges map[string]TemplateJudge |
||||
var ruleBodyComposers map[string]BodyComposer |
||||
var redeemJudges map[string]Judge |
||||
|
||||
// Init 初始化规则的一些基础数据
|
||||
// TODO: 这些可以通过配置文件进行,避免未来修改程序后才能部署
|
||||
func ruleInit(rules []*Rule) { |
||||
supportedRules = rules |
||||
issueJudges = make(map[string]TemplateJudge) |
||||
issueJudges["APPLY_TIMES"] = new(ApplyTimesJudge) |
||||
|
||||
redeemJudges = make(map[string]Judge) |
||||
redeemJudges["REDEEM_PERIOD_WITH_OFFSET"] = new(RedeemPeriodJudge) |
||||
redeemJudges["REDEEM_TIMES"] = new(RedeemTimesJudge) |
||||
redeemJudges["REDEEM_BY_SAME_BRAND"] = new(RedeemBySameBrandJudge) |
||||
redeemJudges["REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR"] = new(RedeemInCurrentNatureTimeUnitJudge) |
||||
|
||||
ruleBodyComposers = make(map[string]BodyComposer) |
||||
ruleBodyComposers["REDEEM_PERIOD_WITH_OFFSET"] = new(RedeemPeriodWithOffsetBodyComposer) |
||||
ruleBodyComposers["REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR"] = new(RedeemInCurrentNatureTimeUnitBodyComposer) |
||||
ruleBodyComposers["REDEEM_TIMES"] = new(RedeemTimesBodyComposer) |
||||
ruleBodyComposers["REDEEM_BY_SAME_BRAND"] = new(RedeemBySameBrandBodyComposer) |
||||
} |
||||
|
||||
// ValidateTemplateRules 发券时验证是否可以领用
|
||||
// 目前要求至少配置一个领用规则
|
||||
func validateTemplateRules(consumerID string, couponTypeID string, pct *PublishedCouponType) ([]error, error) { |
||||
var errs []error = make([]error, 0) |
||||
|
||||
var judgeTimes int = 0 |
||||
for ruleInternalID, tempRuleBody := range pct.Rules { |
||||
var rule = findRule(ruleInternalID) |
||||
if nil == rule { |
||||
return nil, &ErrRuleNotFound |
||||
} |
||||
if judge, ok := issueJudges[ruleInternalID]; ok { |
||||
judgeTimes++ |
||||
var err = judge.JudgeTemplate(consumerID, couponTypeID, tempRuleBody, pct) |
||||
if nil != err { |
||||
errs = append(errs, err) |
||||
} |
||||
continue |
||||
} |
||||
|
||||
} |
||||
|
||||
if 0 == judgeTimes { |
||||
return nil, &ErrRuleJudgeNotFound |
||||
} |
||||
|
||||
return errs, nil |
||||
} |
||||
|
||||
// ValidateCouponRules 在redeem时需要验证卡券的规则
|
||||
// 目前要求至少配置一个兑换规则
|
||||
func validateCouponRules(requester *base.Requester, consumerID string, c *Coupon) ([]error, error) { |
||||
var errs []error = make([]error, 0) |
||||
|
||||
var judgeTimes int = 0 |
||||
// var rulesString = c.Properties[KeyBindingRuleProperties]
|
||||
// ruleBodyRefs, err := unmarshalCouponRules(rulesString)
|
||||
// if nil != err {
|
||||
// return nil, err
|
||||
// }
|
||||
// log.Println(c.GetRules())
|
||||
// for ruleInternalID, ruleBody := range c.Properties[KeyBindingRuleProperties].(map[string]interface {}) {
|
||||
for ruleInternalID, ruleBody := range c.GetRules() { |
||||
// TODO: 未来性能优化,考虑这个findRule去掉
|
||||
var rule = findRule(ruleInternalID) |
||||
if nil == rule { |
||||
return nil, &ErrRuleNotFound |
||||
} |
||||
if judge, ok := redeemJudges[ruleInternalID]; ok { |
||||
judgeTimes++ |
||||
var err = judge.JudgeCoupon(requester, consumerID, ruleBody.(map[string]interface{}), c) |
||||
if nil != err { |
||||
errs = append(errs, err) |
||||
} |
||||
continue |
||||
} |
||||
} |
||||
|
||||
// for _, ruleRef := range ruleBodyRefs {
|
||||
// var ruleInternalID = ruleRef.RuleInternalID
|
||||
// var ruleBody = ruleRef.RuleBody
|
||||
// var rule = findRule(ruleInternalID)
|
||||
// if nil == rule {
|
||||
// return nil, &ErrRuleNotFound
|
||||
// }
|
||||
// if judge, ok := redeemJudges[rule.InternalID]; ok {
|
||||
// judgeTimes++
|
||||
// var err = judge.JudgeCoupon(requester, consumerID, ruleBody, c)
|
||||
// if nil != err {
|
||||
// errs = append(errs, err)
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
if 0 == judgeTimes { |
||||
return nil, &ErrRuleJudgeNotFound |
||||
} |
||||
|
||||
return errs, nil |
||||
} |
||||
|
||||
// validateCouponExpired
|
||||
func validateCouponExpired(requester *base.Requester, consumerID string, c *Coupon) (bool, error) { |
||||
for ruleInternalID, ruleBody := range c.GetRules() { |
||||
var rule = findRule(ruleInternalID) |
||||
if nil == rule { |
||||
return false, &ErrRuleNotFound |
||||
} |
||||
if _, ok := redeemJudges[ruleInternalID]; ok { |
||||
if ruleInternalID == "REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR" { |
||||
expired, err := JudgeNTUExpired(requester, consumerID, ruleBody.(map[string]interface{}), c) |
||||
return expired, err |
||||
} |
||||
|
||||
if ruleInternalID == "REDEEM_PERIOD_WITH_OFFSET" { |
||||
expired, err := JudgePeriodExpired(requester, consumerID, ruleBody.(map[string]interface{}), c) |
||||
return expired, err |
||||
} |
||||
} |
||||
} |
||||
return false, &ErrRuleNotFound |
||||
} |
||||
|
||||
func findRule(ruleInternalID string) *Rule { |
||||
for _, rule := range supportedRules { |
||||
if rule.InternalID == ruleInternalID { |
||||
return rule |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// marshalCouponRules 发券时生成最终的规则字符串,用来在核销时验证
|
||||
func marshalCouponRules(requester *base.Requester, couponTypeID string, rules map[string]map[string]interface{}) (map[string]interface{}, error) { |
||||
var ruleBodyMap map[string]interface{} = make(map[string]interface{}, 0) |
||||
for ruleInternalID, tempRuleBody := range rules { |
||||
var rule = findRule(ruleInternalID) |
||||
if nil == rule { |
||||
return nil, &ErrRuleNotFound |
||||
} |
||||
|
||||
if composer, ok := ruleBodyComposers[ruleInternalID]; ok { |
||||
ruleBody, err := composer.Compose(requester, couponTypeID, tempRuleBody) |
||||
if nil != err { |
||||
log.Println(err) |
||||
return nil, err |
||||
} |
||||
ruleBodyMap[ruleInternalID] = ruleBody |
||||
} |
||||
} |
||||
return ruleBodyMap, nil |
||||
// jsonBytes, err := json.Marshal(ruleBodyRefs)
|
||||
// if nil != err {
|
||||
// log.Println(err)
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
// return string(jsonBytes), nil
|
||||
} |
||||
|
||||
// unmarshalCouponRules 兑换时将卡券附带的规则解析成[]*RuleRef
|
||||
// func unmarshalCouponRules(rulesString string) ([]*RuleRef, error) {
|
||||
// var ruleBodyRefs []*RuleRef = make([]*RuleRef, 0)
|
||||
// err := json.Unmarshal([]byte(rulesString), &ruleBodyRefs)
|
||||
// if nil != err {
|
||||
// log.Println(err)
|
||||
// return nil, &ErrCouponRulesBadFormat
|
||||
// }
|
||||
// return ruleBodyRefs, nil
|
||||
// }
|
||||
|
||||
// restRedeemTimes 查询剩下redeem的次数
|
||||
func restRedeemTimes(c *Coupon) (uint, error) { |
||||
// for ruleInternalID, ruleBody := range c.Properties[KeyBindingRuleProperties].(map[string]interface {}) {
|
||||
for ruleInternalID, ruleBody := range c.GetRules() { |
||||
if "REDEEM_TIMES" == ruleInternalID { |
||||
// var count uint
|
||||
count, err := getCouponTransactionCountWithType(c.ID, TTRedeemCoupon) |
||||
if nil != err { |
||||
return 0, err |
||||
} |
||||
var rt redeemTimes |
||||
if err = mapstructure.Decode(ruleBody, &rt); err != nil { |
||||
return 0, &ErrCouponRulesBadFormat |
||||
} |
||||
|
||||
// comparing first, to avoid negative result causes uint type out of bound
|
||||
if uint(rt.Times) >= count { |
||||
return uint(rt.Times) - count, nil |
||||
} |
||||
|
||||
return 0, nil |
||||
} |
||||
} |
||||
|
||||
// TODO: 未来考虑此处查找优化,提升效率
|
||||
// for _, ruleRef := range ruleBodyRefs {
|
||||
// if ruleRef.RuleInternalID == "REDEEM_TIMES" {
|
||||
// var ruleBody = ruleRef.RuleBody
|
||||
// var rt redeemTimes
|
||||
// err := json.Unmarshal([]byte(ruleBody), &rt)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return 0, err
|
||||
// }
|
||||
|
||||
// // 查询已经兑换次数
|
||||
// var count uint
|
||||
// count, err = getCouponTransactionCountWithType(c.ID, TTRedeemCoupon)
|
||||
// if nil != err {
|
||||
// return 0, err
|
||||
// }
|
||||
|
||||
// return rt.Times - count, nil
|
||||
// }
|
||||
// }
|
||||
|
||||
// 卡券没有设置兑换次数限制
|
||||
return 0, &ErrCouponRulesNoRedeemTimes |
||||
} |
||||
@ -0,0 +1,364 @@
@@ -0,0 +1,364 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "log"
|
||||
"reflect" |
||||
"testing" |
||||
"time" |
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
|
||||
"bou.ke/monkey" |
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
func _aREDEEM_TIMES_RuleRef(times int) (string, map[string]interface{}) { |
||||
ruleBody := map[string]interface{}{ |
||||
"times": times, |
||||
} |
||||
return "REDEEM_TIMES", ruleBody |
||||
} |
||||
|
||||
func _aVAILD_PERIOD_WITH_OFFSET_RuleRef(offset int, span int) (string, map[string]interface{}) { |
||||
ruleBody := map[string]interface{}{ |
||||
"offSetFromAppliedDay": offset, |
||||
"timeSpan": span, |
||||
} |
||||
return "REDEEM_PERIOD_WITH_OFFSET", ruleBody |
||||
} |
||||
|
||||
func _aAPPLY_TIMES_RuleRef(indays int, times int) (string, map[string]interface{}) { |
||||
ruleBody := map[string]interface{}{ |
||||
"inDays": indays, |
||||
"times": times, |
||||
} |
||||
return "APPLY_TIMES", ruleBody |
||||
} |
||||
|
||||
func _aREDEEM_BY_SAME_BRAND_RuleRef() (string, map[string]interface{}) { |
||||
ruleBody := map[string]interface{}{} |
||||
return "REDEEM_BY_SAME_BRAND", ruleBody |
||||
} |
||||
func _someRules() map[string]map[string]interface{} { |
||||
var rrs = make(map[string]map[string]interface{}, 4) |
||||
rid, rbd := _aREDEEM_TIMES_RuleRef(1) |
||||
rrs[rid] = rbd |
||||
rid, rbd = _aVAILD_PERIOD_WITH_OFFSET_RuleRef(0, 100) |
||||
rrs[rid] = rbd |
||||
rid, rbd = _aAPPLY_TIMES_RuleRef(0, 1) |
||||
rrs[rid] = rbd |
||||
rid, rbd = _aREDEEM_BY_SAME_BRAND_RuleRef() |
||||
rrs[rid] = rbd |
||||
return rrs |
||||
} |
||||
|
||||
func _aPublishedCouponType(rules map[string]map[string]interface{}) *PublishedCouponType { |
||||
var pct = PublishedCouponType{ |
||||
ID: base.RandString(4), |
||||
Name: base.RandString(4), |
||||
TemplateID: base.RandString(4), |
||||
Description: base.RandString(4), |
||||
InternalDescription: base.RandString(4), |
||||
State: 0, |
||||
Publisher: base.RandString(4), |
||||
VisibleStartTime: time.Now().Local().AddDate(0, 0, -100), |
||||
VisibleEndTime: time.Now().Local().AddDate(0, 0, 100), |
||||
Rules: rules, |
||||
CreatedTime: time.Now().Local().AddDate(0, 0, -1), |
||||
} |
||||
return &pct |
||||
} |
||||
|
||||
func Test_ruleInit(t *testing.T) { |
||||
Convey("validate rule init correctly", t, func() { |
||||
So(len(issueJudges), ShouldEqual, 1) |
||||
So(len(redeemJudges), ShouldEqual, 4) |
||||
So(len(ruleBodyComposers), ShouldEqual, 4) |
||||
}) |
||||
} |
||||
|
||||
func Test_validateTemplateRules(t *testing.T) { |
||||
Convey("Given a coupon will no rule errors", t, func() { |
||||
var pct = _aPublishedCouponType(_someRules()) |
||||
var atJudge *ApplyTimesJudge |
||||
// atJudge = new(ApplyTimesJudge)
|
||||
atJudge = issueJudges["APPLY_TIMES"].(*ApplyTimesJudge) |
||||
patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(atJudge), "JudgeTemplate", func(_ *ApplyTimesJudge, _ string, _ string, _ map[string]interface{}, _ *PublishedCouponType) error { |
||||
return nil |
||||
}) |
||||
|
||||
Convey("Start validate", func() { |
||||
errs, err := validateTemplateRules("", "", pct) |
||||
Convey("Should no errors", func() { |
||||
So(err, ShouldBeNil) |
||||
So(len(errs), ShouldEqual, 0) |
||||
}) |
||||
}) |
||||
|
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
Convey("Given such env with no match rules", t, func() { |
||||
var pct = _aPublishedCouponType(_someRules()) |
||||
patchGuard := monkey.Patch(findRule, func(ruleInternalID string) *Rule { |
||||
return nil |
||||
}) |
||||
|
||||
Convey("Start validate", func() { |
||||
_, err := validateTemplateRules("", "", pct) |
||||
Convey("Should has ErrRuleNotFound", func() { |
||||
So(err, ShouldEqual, &ErrRuleNotFound) |
||||
}) |
||||
}) |
||||
|
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
Convey("Given a template with no rules", t, func() { |
||||
var pct = _aPublishedCouponType(nil) |
||||
|
||||
Convey("Start validate", func() { |
||||
_, err := validateTemplateRules("", "", pct) |
||||
Convey("Should has ErrRuleJudgeNotFound", func() { |
||||
So(err, ShouldEqual, &ErrRuleJudgeNotFound) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
Convey("Given an env that some judge will fail", t, func() { |
||||
var pct = _aPublishedCouponType(_someRules()) |
||||
var atJudge *ApplyTimesJudge |
||||
// atJudge = new(ApplyTimesJudge)
|
||||
atJudge = issueJudges["APPLY_TIMES"].(*ApplyTimesJudge) |
||||
|
||||
Convey("Assume ErrCouponRulesApplyTimeExpired", func() { |
||||
|
||||
patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(atJudge), "JudgeTemplate", func(_ *ApplyTimesJudge, _ string, _ string, _ map[string]interface{}, _ *PublishedCouponType) error { |
||||
return &ErrCouponRulesApplyTimeExpired |
||||
}) |
||||
errs, _ := validateTemplateRules("", "", pct) |
||||
Convey("Should have ErrCouponRulesApplyTimeExpired", func() { |
||||
So(len(errs), ShouldEqual, 1) |
||||
So(errs[0], ShouldEqual, &ErrCouponRulesApplyTimeExpired) |
||||
}) |
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
Convey("Assume ErrCouponRulesApplyTimesExceeded", func() { |
||||
|
||||
patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(atJudge), "JudgeTemplate", func(_ *ApplyTimesJudge, _ string, _ string, _ map[string]interface{}, _ *PublishedCouponType) error { |
||||
return &ErrCouponRulesApplyTimesExceeded |
||||
}) |
||||
errs, _ := validateTemplateRules("", "", pct) |
||||
Convey("Should have ErrCouponRulesApplyTimesExceeded", func() { |
||||
So(len(errs), ShouldEqual, 1) |
||||
So(errs[0], ShouldEqual, &ErrCouponRulesApplyTimesExceeded) |
||||
}) |
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
}) |
||||
} |
||||
|
||||
func Test_validateCouponRules(t *testing.T) { |
||||
|
||||
Convey("Given a coupon with no rules", t, func() { |
||||
state := r.Intn(int(SUnknown)) |
||||
var p map[string]interface{} |
||||
p = make(map[string]interface{}, 1) |
||||
p[KeyBindingRuleProperties] = map[string]interface{}{} |
||||
c := _aCoupon(base.RandString(4), "xxx", "yyy", defaultCouponTypeID, State(state), p) |
||||
// pg1 := monkey.Patch(unmarshalCouponRules, func(_ string)([]*RuleRef, error) {
|
||||
// return make([]*RuleRef, 0), nil
|
||||
// })
|
||||
|
||||
Convey("Start validate", func() { |
||||
_, err := validateCouponRules(nil, "", c) |
||||
Convey("Should has ErrRuleJudgeNotFound", func() { |
||||
So(err, ShouldEqual, &ErrRuleJudgeNotFound) |
||||
}) |
||||
}) |
||||
// pg1.Unpatch()
|
||||
}) |
||||
|
||||
Convey("Given an env that some judge will fail", t, func() { |
||||
state := r.Intn(int(SUnknown)) |
||||
var p map[string]interface{} |
||||
p = make(map[string]interface{}, 1) |
||||
var rrs = make(map[string]interface{}, 1) |
||||
rid, rbd := _aREDEEM_TIMES_RuleRef(999) |
||||
rrs[rid] = rbd |
||||
p[KeyBindingRuleProperties] = rrs |
||||
c := _aCoupon(base.RandString(4), "xxx", "yyy", defaultCouponTypeID, State(state), p) |
||||
// c.Properties = p
|
||||
|
||||
var rtJudge *RedeemTimesJudge |
||||
rtJudge = redeemJudges["REDEEM_TIMES"].(*RedeemTimesJudge) |
||||
|
||||
Convey("Assume ErrCouponRulesRedeemTimesExceeded", func() { |
||||
|
||||
patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(rtJudge), "JudgeCoupon", func(_ *RedeemTimesJudge, _ *base.Requester, _ string, _ map[string]interface{}, _ *Coupon) error { |
||||
// log.Println("=======monkey.JudgeCoupon=====")
|
||||
return &ErrCouponRulesRedeemTimesExceeded |
||||
}) |
||||
// pg5 := monkey.PatchInstanceMethod(reflect.TypeOf(c), "GetRules", func(_ *Coupon) map[string]interface{} {
|
||||
// log.Println("=======monkey.GetRules=====")
|
||||
// return rrs
|
||||
// })
|
||||
errs, _ := validateCouponRules(nil, "", c) |
||||
// if nil != err {
|
||||
// log.Println("=======err is not nil =====")
|
||||
// log.Println(c.GetRules())
|
||||
// log.Println(err)
|
||||
// }
|
||||
// pg5.Unpatch()
|
||||
Convey("Should have ErrCouponRulesRedeemTimesExceeded", func() { |
||||
So(len(errs), ShouldEqual, 1) |
||||
So(errs[0], ShouldEqual, &ErrCouponRulesRedeemTimesExceeded) |
||||
}) |
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
}) |
||||
|
||||
Convey("Given an env that everything is Okay", t, func() { |
||||
state := r.Intn(int(SUnknown)) |
||||
c := _aCoupon(base.RandString(4), "xxx", "yyy", defaultCouponTypeID, State(state), nil) |
||||
// pg1 := monkey.Patch(unmarshalCouponRules, func(_ string)([]*RuleRef, error) {
|
||||
// rrs := make([]*RuleRef, 0, 1)
|
||||
// rrs = append(rrs, _aREDEEM_TIMES_RuleRef(1))
|
||||
// return rrs, nil
|
||||
// })
|
||||
|
||||
Convey("Start validate", func() { |
||||
var rtJudge *RedeemTimesJudge |
||||
rtJudge = redeemJudges["REDEEM_TIMES"].(*RedeemTimesJudge) |
||||
|
||||
patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(rtJudge), "JudgeCoupon", func(_ *RedeemTimesJudge, _ *base.Requester, _ string, _ map[string]interface{}, _ *Coupon) error { |
||||
return nil |
||||
}) |
||||
errs, _ := validateCouponRules(nil, "", c) |
||||
Convey("Should no error", func() { |
||||
So(len(errs), ShouldEqual, 0) |
||||
|
||||
}) |
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
// pg1.Unpatch()
|
||||
}) |
||||
} |
||||
|
||||
func Test_marshalCouponRules(t *testing.T) { |
||||
Convey("Given a rule will not be found", t, func() { |
||||
patchGuard := monkey.Patch(findRule, func(_ string) *Rule { |
||||
return nil |
||||
}) |
||||
|
||||
var rrs = make(map[string]map[string]interface{}, 4) |
||||
rid, rbd := _aREDEEM_TIMES_RuleRef(1) |
||||
rrs[rid] = rbd |
||||
|
||||
Convey("Start marshal", func() { |
||||
_, err := marshalCouponRules(nil, "", rrs) |
||||
Convey("Should has ErrRuleNotFound", func() { |
||||
So(err, ShouldEqual, &ErrRuleNotFound) |
||||
}) |
||||
}) |
||||
|
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
Convey("Given a composer should return error", t, func() { |
||||
var rrs = make(map[string]map[string]interface{}, 4) |
||||
rid, rbd := _aREDEEM_TIMES_RuleRef(1) |
||||
rrs[rid] = rbd |
||||
|
||||
var rdbComposer *RedeemTimesBodyComposer |
||||
rdbComposer = ruleBodyComposers["REDEEM_TIMES"].(*RedeemTimesBodyComposer) |
||||
|
||||
patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(rdbComposer), "Compose", func(_ *RedeemTimesBodyComposer, _ *base.Requester, _ string, _ map[string]interface{}) (map[string]interface{}, error) { |
||||
// just pick an error for test, don't care if it is logical
|
||||
return nil, &ErrRequesterHasNoBrand |
||||
}) |
||||
|
||||
Convey("Start marshal", func() { |
||||
_, err := marshalCouponRules(nil, "", rrs) |
||||
Convey("Should has ErrRequesterHasNoBrand", func() { |
||||
So(err, ShouldEqual, &ErrRequesterHasNoBrand) |
||||
}) |
||||
}) |
||||
|
||||
patchGuard.Unpatch() |
||||
}) |
||||
|
||||
Convey("Given an env that everything is Okay", t, func() { |
||||
var rrs = make(map[string]map[string]interface{}, 4) |
||||
rid, rbd := _aREDEEM_TIMES_RuleRef(1) |
||||
rrs[rid] = rbd |
||||
|
||||
Convey("Start marshal", func() { |
||||
m, err := marshalCouponRules(nil, "", rrs) |
||||
Convey("Should has correct result", func() { |
||||
So(err, ShouldBeNil) |
||||
So(m["REDEEM_TIMES"], ShouldNotBeNil) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
} |
||||
|
||||
// func Test_unmarshalCouponRules(t *testing.T) {
|
||||
// Convey("Given a rule string with bad format (non json format)", t, func() {
|
||||
// _, err := unmarshalCouponRules("bad_format")
|
||||
// So(err, ShouldEqual, &ErrCouponRulesBadFormat)
|
||||
// })
|
||||
|
||||
// Convey("Given a rule string with not RuleRef format", t, func() {
|
||||
// _, err := unmarshalCouponRules(`{"hello" : "world"}`)
|
||||
// So(err, ShouldEqual, &ErrCouponRulesBadFormat)
|
||||
// })
|
||||
|
||||
// Convey("Given a rule string with RuleRef format", t, func() {
|
||||
// rrfs, err := unmarshalCouponRules(` [ {"rule_id":"REDEEM_TIMES","rule_body":"abc"}]`)
|
||||
// So(err, ShouldBeNil)
|
||||
// So(len(rrfs), ShouldEqual,1)
|
||||
// rrf := rrfs[0]
|
||||
// So(rrf.RuleInternalID, ShouldEqual, "REDEEM_TIMES")
|
||||
// So(rrf.RuleBody, ShouldEqual, "abc")
|
||||
// })
|
||||
// }
|
||||
|
||||
func Test_restRedeemTimes(t *testing.T) { |
||||
state := r.Intn(int(SUnknown)) |
||||
var p map[string]interface{} |
||||
p = make(map[string]interface{}, 1) |
||||
var rrs = make(map[string]interface{}, 4) |
||||
rid, rbd := _aREDEEM_TIMES_RuleRef(10000000) |
||||
rrs[rid] = rbd |
||||
p[KeyBindingRuleProperties] = rrs |
||||
c := _aCoupon(base.RandString(4), "xxx", "yyy", defaultCouponTypeID, State(state), p) |
||||
|
||||
Convey("Given a env assume the db is down", t, func() { |
||||
pg1 := monkey.Patch(getCouponTransactionCountWithType, func(_ string, _ TransType) (uint, error) { |
||||
return 0, &ErrCouponRulesBadFormat |
||||
}) |
||||
_, err := restRedeemTimes(c) |
||||
So(err, ShouldEqual, &ErrCouponRulesBadFormat) |
||||
pg1.Unpatch() |
||||
}) |
||||
|
||||
Convey("Given a env assume the coupon had been redeemed n times", t, func() { |
||||
ts := r.Intn(10000) |
||||
pg1 := monkey.Patch(getCouponTransactionCountWithType, func(_ string, _ TransType) (uint, error) { |
||||
return uint(ts), nil |
||||
}) |
||||
restts, err := restRedeemTimes(c) |
||||
So(err, ShouldBeNil) |
||||
So(restts, ShouldEqual, 10000000-ts) |
||||
pg1.Unpatch() |
||||
}) |
||||
|
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"database/sql" |
||||
) |
||||
|
||||
var dbConnection *sql.DB |
||||
|
||||
func staticsInit(databaseConnection *sql.DB) { |
||||
dbConnection = databaseConnection |
||||
} |
||||
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
[ |
||||
{ |
||||
"name": "A", |
||||
"id": "63f9f1ce-2ad0-462a-b798-4ead5e5ab3a5", |
||||
"template_id": "dba25cb3-4ad9-44c2-8815-6d8b12c10f5a", |
||||
"description": "普通的case", |
||||
"internal_description":"这是发布模板的内部描述", |
||||
"state": 0, |
||||
"publisher":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"visible_start_time":"2019-01-01T00:00:00+08:00" , |
||||
"visible_end_time":"2030-01-31T23:59:59+08:00", |
||||
"created_time":"2019-12-12T15:12:12+08:00" , |
||||
"deleted_time":null, |
||||
"rules": |
||||
{ |
||||
"REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR":"{ \"unit\": \"MONTH\", \"endInAdvance\": 0 }", |
||||
"APPLY_TIMES":"{ \"inDays\": 0, \"times\": 1 }", |
||||
"REDEEM_TIMES":"{\"times\": 1}" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "B", |
||||
"id": "58b388ff-689e-445a-8bbd-8d707dbe70ef", |
||||
"template_id": "dba25cb3-4ad9-44c2-8815-6d8b12c10f5a", |
||||
"description": "一些稍微特别的case, 可以申请2次,核销2次,下个月还可以兑换", |
||||
"internal_description":"这是发布模板的内部描述", |
||||
"state": 0, |
||||
"publisher":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"visible_start_time":"2019-01-01T00:00:00+08:00" , |
||||
"visible_end_time":"2030-01-31T23:59:59+08:00", |
||||
"created_time":"2019-12-12T15:12:12+08:00" , |
||||
"deleted_time":null, |
||||
"rules": |
||||
{ |
||||
"APPLY_TIMES":"{ \"inDays\": 0, \"times\": 2 }", |
||||
"REDEEM_TIMES":"{\"times\": 2}", |
||||
"REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR":"{ \"unit\": \"MONTH\", \"endInAdvance\": -31 }", |
||||
"REDEEM_BY_SAME_BRAND":"{\"brand\": \"\"}" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "C", |
||||
"id": "abd73dbe-cc91-4b61-b10c-c6532d7a7770", |
||||
"template_id": "dba25cb3-4ad9-44c2-8815-6d8b12c10f5a", |
||||
"description": "永远也不能被兑换", |
||||
"internal_description":"这是发布模板的内部描述", |
||||
"state": 0, |
||||
"publisher":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"visible_start_time":"2019-01-01T00:00:00+08:00" , |
||||
"visible_end_time":"2030-01-31T23:59:59+08:00", |
||||
"created_time":"2019-12-12T15:12:12+08:00" , |
||||
"deleted_time":null, |
||||
"rules": |
||||
{ |
||||
"APPLY_TIMES":"{ \"inDays\": 0, \"times\": 300 }", |
||||
"REDEEM_TIMES":"{\"times\": 3}", |
||||
"REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR":"{ \"unit\": \"MONTH\", \"endInAdvance\": 31 }" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "D", |
||||
"id": "ca0ff68f-dc05-488d-b185-660b101a1068", |
||||
"template_id": "dba25cb3-4ad9-44c2-8815-6d8b12c10f5a", |
||||
"description": "延迟几天天兑换", |
||||
"internal_description":"这是发布模板的内部描述", |
||||
"state": 0, |
||||
"publisher":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"visible_start_time":"2019-01-01T00:00:00+08:00" , |
||||
"visible_end_time":"2030-01-31T23:59:59+08:00", |
||||
"created_time":"2019-12-12T15:12:12+08:00" , |
||||
"deleted_time":null, |
||||
"rules": |
||||
{ |
||||
"REDEEM_IN_CURRENT_NATURE_MONTH_SEASON_YEAR":"{ \"unit\": \"MONTH\", \"endInAdvance\": -10 }", |
||||
"APPLY_TIMES":"{ \"inDays\": 0, \"times\": 20000 }", |
||||
"REDEEM_TIMES":"{\"times\": 200000}" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "E", |
||||
"id": "7c0fb6b2-7362-4933-ad15-cd4ad9fccfec", |
||||
"template_id": "dba25cb3-4ad9-44c2-8815-6d8b12c10f5a", |
||||
"description": "延迟几天天兑换", |
||||
"internal_description":"这是发布模板的内部描述", |
||||
"state": 0, |
||||
"publisher":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"visible_start_time":"2019-01-01T00:00:00+08:00" , |
||||
"visible_end_time":"2030-01-31T23:59:59+08:00", |
||||
"created_time":"2019-12-12T15:12:12+08:00" , |
||||
"deleted_time":null, |
||||
"rules": |
||||
{ |
||||
"REDEEM_PERIOD_WITH_OFFSET": "{\"offSetFromAppliedDay\": 0,\"timeSpan\": 365}", |
||||
"APPLY_TIMES":"{ \"inDays\": 0, \"times\": 20000 }", |
||||
"REDEEM_TIMES":"{\"times\": 200000}" |
||||
} |
||||
} |
||||
] |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
[ |
||||
{ |
||||
"id": "f5d58d56-49ca-4df3-83f9-df9d4ea59974", |
||||
"name": "使用次数限制", |
||||
"internal_id":"REDEEM_TIMES", |
||||
"description": "卡券使用次数限制,比如领用后,只能使用一次,默认一次。", |
||||
"rule_body":"{\"times\": 1}", |
||||
"creator":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"created_time":"2019-12-12T15:12:12+08:00" , |
||||
"updated_time":null, |
||||
"deleted_time": null |
||||
|
||||
}, |
||||
{ |
||||
"id": "b3df26d8-39c8-496c-8299-5da6febb1c60", |
||||
"name": "开始日期可偏移的生效时间", |
||||
"internal_id":"REDEEM_PERIOD_WITH_OFFSET", |
||||
"description": "卡券申领后的生效日期。offSetFromAppliedDay是相对申领日期的延后日期,单位为天。timeSpan是生效时间跨度。-1为没有到期就失效的时间。举例:offSetFromAppliedDay = 14,timeSpan, 用户在2020年1月10日领取,那么在2020年1月23日生效可兑换,2021年23日过期", |
||||
"rule_body": "{\"offSetFromAppliedDay\": 0,\"timeSpan\": 365}", |
||||
"creator":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"created_time":"2019-12-12T15:12:12+08:00", |
||||
"updated_time":null, |
||||
"deleted_time":null |
||||
|
||||
}, |
||||
{ |
||||
"id": "1aabf6e1-7b7f-4c72-8526-1180eeaf0ef4", |
||||
"name": "在一定期限内的领用次数限制", |
||||
"internal_id":"APPLY_TIMES", |
||||
"description": "卡券申领次数限制,可以设置在若干天内的申请次数,也意味这卡券开放申请的时间限制。inDays为0时,则不限领用过期时间。当inDays为99时,则卡券发布后的99天内可以申领。", |
||||
"rule_body":"{ \"inDays\": 365, \"times\": 1 }", |
||||
"creator":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"created_time": "2019-12-12T15:12:12+08:00", |
||||
"updated_time": null, |
||||
"deleted_time": null |
||||
}, |
||||
{ |
||||
"id": "f2b2025f-2e78-483c-b414-2935146e6d05", |
||||
"name": "限制在同品牌兑换", |
||||
"internal_id":"REDEEM_BY_SAME_BRAND", |
||||
"description": "核销时校验核销者所属的品牌是否和签发者所属的品牌一致,如果否,则不允许核销", |
||||
"rule_body":"{\"brand\": \"\"}", |
||||
"creator":"ff27204e-6ef2-48e2-a437-7e48cc49d659", |
||||
"created_time": "2020-01-06T15:12:12+08:00", |
||||
"updated_time": null, |
||||
"deleted_time": null |
||||
} |
||||
] |
||||
@ -0,0 +1,116 @@
@@ -0,0 +1,116 @@
|
||||
package coupon |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"os" |
||||
|
||||
// "fmt"
|
||||
"math/rand" |
||||
|
||||
// "reflect"
|
||||
"testing" |
||||
"time" |
||||
|
||||
"loreal.com/dit/cmd/coupon-service/base" |
||||
"loreal.com/dit/utils" |
||||
|
||||
"github.com/google/uuid" |
||||
migrate "github.com/rubenv/sql-migrate" |
||||
) |
||||
|
||||
var r *rand.Rand = rand.New(rand.NewSource(time.Now().Unix())) |
||||
|
||||
const defaultCouponTypeID string = "678719f5-44a8-4ac8-afd0-288d2f14daf8" |
||||
const anotherCouponTypeID string = "dff0710e-f5af-4ecf-a4b5-cc5599d98030" |
||||
|
||||
func TestMain(m *testing.M) { |
||||
_setUp() |
||||
m.Run() |
||||
_tearDown() |
||||
} |
||||
|
||||
func _setUp() { |
||||
encryptKey = []byte("a9ad231b0f2a4f448b8846fd1f57813a") |
||||
err := os.Remove("../data/testdata.sqlite") |
||||
dbConnection, err = sql.Open("sqlite3", "../data/testdata.sqlite?cache=shared&mode=rwc") |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
migrations := &migrate.FileMigrationSource{ |
||||
Dir: "../sql-migrations", |
||||
} |
||||
_, err = migrate.Exec(dbConnection, "sqlite3", migrations, migrate.Up) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
dbInit() |
||||
|
||||
utils.LoadOrCreateJSON("./test/rules.json", &supportedRules) |
||||
ruleInit(supportedRules) |
||||
} |
||||
|
||||
func _tearDown() { |
||||
os.Remove("./data/testdata.sqlite") |
||||
dbConnection.Close() |
||||
} |
||||
|
||||
func _aCoupon(consumerID string, consumerRefID string, channelID string, couponTypeID string, state State, p map[string]interface{}) *Coupon { |
||||
lt := time.Now().Local() |
||||
|
||||
var c = Coupon{ |
||||
ID: uuid.New().String(), |
||||
CouponTypeID: couponTypeID, |
||||
ConsumerID: consumerID, |
||||
ConsumerRefID: consumerRefID, |
||||
ChannelID: channelID, |
||||
State: state, |
||||
Properties: p, |
||||
CreatedTime: <, |
||||
} |
||||
return &c |
||||
} |
||||
|
||||
func _someCoupons(consumerID string, consumerRefID string, channelID string, couponTypeID string) []*Coupon { |
||||
count := r.Intn(10) + 1 |
||||
cs := make([]*Coupon, 0, count) |
||||
for i := 0; i < count; i++ { |
||||
state := r.Intn(int(SUnknown)) |
||||
var p map[string]interface{} |
||||
p = make(map[string]interface{}, 1) |
||||
p["the_key"] = "the value" |
||||
cs = append(cs, _aCoupon(consumerID, consumerRefID, channelID, couponTypeID, State(state), p)) |
||||
} |
||||
return cs |
||||
} |
||||
|
||||
func _aTransaction(actorID string, couponID string, tt TransType, extraInfo string) *Transaction { |
||||
var t = Transaction{ |
||||
ID: uuid.New().String(), |
||||
CouponID: couponID, |
||||
ActorID: actorID, |
||||
TransType: tt, |
||||
ExtraInfo: extraInfo, |
||||
CreatedTime: time.Now().Local(), |
||||
} |
||||
return &t |
||||
} |
||||
|
||||
func _someTransaction(actorID string, couponID string, extraInfo string) []*Transaction { |
||||
count := r.Intn(10) + 1 |
||||
ts := make([]*Transaction, 0, count) |
||||
for i := 0; i < count; i++ { |
||||
tt := r.Intn(int(TTUnknownTransaction)) |
||||
ts = append(ts, _aTransaction(actorID, couponID, TransType(tt), extraInfo)) |
||||
} |
||||
return ts |
||||
} |
||||
|
||||
func _aRequester(userID string, roles []string, brand string) *base.Requester { |
||||
var requester base.Requester |
||||
requester.UserID = userID |
||||
requester.Roles = map[string]([]string){ |
||||
"roles": roles, |
||||
} |
||||
requester.Brand = brand |
||||
return &requester |
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"fmt" |
||||
"log" |
||||
|
||||
"github.com/gobuffalo/packr/v2" |
||||
_ "github.com/mattn/go-sqlite3" |
||||
migrate "github.com/rubenv/sql-migrate" |
||||
) |
||||
|
||||
//InitDB - initialized database
|
||||
func (a *App) InitDB() { |
||||
//init database tables
|
||||
|
||||
var err error |
||||
for _, env := range a.Runtime { |
||||
env.db, err = sql.Open("sqlite3", fmt.Sprintf("%s%s?cache=shared&mode=rwc", env.Config.DataFolder, env.Config.SqliteDB)) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
log.Printf("[INFO] - applying database [%s] migrations...\n", env.Config.Name) |
||||
|
||||
// migrations := &migrate.FileMigrationSource{
|
||||
// Dir: "sql-migrations",
|
||||
// }
|
||||
migrations := &migrate.PackrMigrationSource{ |
||||
Box: packr.New("sql-migrations", "./sql-migrations"), |
||||
} |
||||
n, err := migrate.Exec(env.db, "sqlite3", migrations, migrate.Up) |
||||
|
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
log.Printf("[INFO] - [%d] migration file applied\n", n) |
||||
|
||||
log.Printf("[INFO] - DB for [%s] ready!\n", env.Config.Name) |
||||
} |
||||
} |
||||
@ -0,0 +1,485 @@
@@ -0,0 +1,485 @@
|
||||
# Larry的工作内容交接 |
||||
|
||||
## 卡券服务 |
||||
|
||||
注:可以结合intergartion guide for third-parties一起阅读关于卡券的内容。 |
||||
|
||||
### 源代码库 |
||||
|
||||
https://github.com/iamhubin/loreal.com/tree/master/dit/cmd/coupon-service 目前已经做了转移给 **@iamhubin** 等待approve |
||||
|
||||
### 源代码结构 |
||||
|
||||
├── Dockerfile 制作编译卡券服务的docker 镜像 |
||||
├── app.go app对象,包含卡券服务的一些初始化和各种配置 |
||||
├── base |
||||
│ ├── baseerror.go 卡券服务错误的结构定义 |
||||
│ ├── baseerror_test.go |
||||
│ ├── config.default.go 服务的默认配置 |
||||
│ ├── config.go 服务配置的数据结构 |
||||
│ ├── lightutils.go 轻量的工具函数 |
||||
│ ├── lightutils_test.go |
||||
│ └── requester.go 封装了请求者的信息,比如角色,品牌等 |
||||
├── config |
||||
│ ├── accounts.json 暂未用到,hubin之前的代码遗留 |
||||
│ └── config.json 程序的运行时配置,将会覆盖默认配置 |
||||
├── coupon |
||||
│ ├── db.coupon.go 卡券服务的dao层 |
||||
│ ├── db.coupon_test.go |
||||
│ ├── errors.go 卡券服务的各种错误定义 |
||||
│ ├── logic.coupon.go 卡券结构的一些方法 |
||||
│ ├── logic.couponservice.go 卡券服务的一些方法,比如创建卡券,查询卡券,核销卡券 |
||||
│ ├── logic.couponservice_test.go |
||||
│ ├── logic.judge.go 规则校验者的定义,如果返回错误,则校验失败。 |
||||
│ ├── logic.judge_test.go |
||||
│ ├── logic.rulecomposer.go 签发卡券时,生成卡券的规则体,比如核销有效时间 |
||||
│ ├── logic.rulecomposer_test.go |
||||
│ ├── message.go 核销卡券时,可以通知一些相关方核销的信息,这是消息结构。 |
||||
│ ├── module.coupon.go 卡券以及卡券类型的数据结构 |
||||
│ ├── module.rule.go 规则以及各个规则细节的结构 |
||||
│ ├── ruleengine.go 规则引擎,统一调用各个规则体生产规则字符串,以及调用规则校验者校验卡券 |
||||
│ ├── ruleengine_test.go |
||||
│ ├── statics.go 一些常量 |
||||
│ ├── test |
||||
│ │ ├── coupon_types.json 定义一些卡券类型,用来api测试的。关于api测试,参考后面章节 |
||||
│ │ └── rules.json 用于单元测试的一些规则 |
||||
│ └── testbase_test.go |
||||
├── data |
||||
│ ├── data.db 运行时的sqlite数据库(api测试也会用这个数据库) |
||||
│ └── testdata.sqlite 单元测试时的数据库 |
||||
├── db.go 初始化数据库以及每次启动时执行升级脚本 |
||||
├── docs |
||||
│ ├── authorization\ server\ handbook.md 认证服务器手册 |
||||
│ ├── context\ of\ coupon\ service.md 部署目标服务器的上下文环境 |
||||
│ ├── go-live\ handbook.md 上线手册 |
||||
│ ├── intergartion\ guide\ for\ third-parties.md 第三方开发手册 |
||||
│ └── technical\ and\ functional\ specification.md 卡券服务功能/技术规格 |
||||
├── endpoints.debug.go |
||||
├── endpoints.gateway.go 暂未涉及,hubin之前的代码遗留 |
||||
├── endpoints.go 服务的http入口定义,以及做为api成对一些参数进行校验 |
||||
├── logic.db.go 暂未涉及,hubin之前的代码遗留 |
||||
├── logic.gateway.go 暂未涉及,hubin之前的代码遗留 |
||||
├── logic.gateway.upstream.token.go 暂未涉及,hubin之前的代码遗留 |
||||
├── logic.gateway_test.go |
||||
├── logic.go 暂未涉及,hubin之前的代码遗留 |
||||
├── logic.task.maintenance.go 暂未涉及,hubin之前的代码遗留 |
||||
├── main.go 主函数入口,载入资源,程序初始化。 |
||||
├── makefile |
||||
├── message.go 暂未涉及,hubin之前的代码遗留 |
||||
├── model.brand.go 暂未涉及,hubin之前的代码遗留 |
||||
├── model.const.go 暂未涉及,hubin之前的代码遗留 |
||||
├── model.go 暂未涉及,hubin之前的代码遗留 |
||||
├── module.predefineddata.go 因为卡券类型尚未开发,这里hardcode一些初始化的卡券类型数据。 |
||||
├── net.config.go 暂未涉及,hubin之前的代码遗留 |
||||
├── oauth |
||||
│ └── oauthcheck.go 校验requester的token |
||||
├── pre-defined |
||||
│ └── predefined-data.json hardcode的卡券类型数据,以及规则数据。 |
||||
├── restful 该文件夹下的文件暂未涉及,hubin之前的代码遗留 |
||||
├── sql-migrations |
||||
│ └── init-20191213144434.sql 数据库升级脚本 |
||||
└── task.register.go 暂未涉及,hubin之前的代码遗留 |
||||
|
||||
### 重要源代码文件列表 |
||||
|
||||
#### endpoints.go |
||||
|
||||
定义了api入口,部分采用了restful风格,方法内会对输入参数进行接口层的校验。 |
||||
|
||||
```go |
||||
func (a *App) initEndpoints() { |
||||
rt := a.getRuntime("prod") |
||||
a.Endpoints = map[string]EndpointEntry{ |
||||
"api/kvstore": {Handler: a.kvstoreHandler, Middlewares: a.noAuthMiddlewares("api/kvstore")}, |
||||
"api/visit": {Handler: a.pvHandler}, |
||||
"error": {Handler: a.errorHandler, Middlewares: a.noAuthMiddlewares("error")}, |
||||
"debug": {Handler: a.debugHandler}, |
||||
"maintenance/fe/upgrade": {Handler: a.feUpgradeHandler}, |
||||
"api/gw": {Handler: a.gatewayHandler}, |
||||
"api/events/": {Handler: longPollingHandler}, |
||||
"api/coupontypes": {Handler: couponTypeHandler}, |
||||
"api/coupons/": {Handler: couponHandler}, |
||||
"api/redemptions": {Handler: redemptionHandler}, |
||||
"api/apitester": {Handler: apitesterHandler}, |
||||
} |
||||
|
||||
postPrepareDB(rt) |
||||
} |
||||
``` |
||||
|
||||
#### db.go |
||||
|
||||
下面的代码段是打包数据库升级脚本文件以及执行升级脚本的代码。 |
||||
|
||||
注意:在mac和windows成功执行了从打包文件中读取脚本,但CentOS没有成功,所以目前是手动拷贝的。 |
||||
|
||||
```go |
||||
migrations := &migrate.PackrMigrationSource{ |
||||
Box: packr.New("sql-migrations", "./sql-migrations"), |
||||
} |
||||
n, err := migrate.Exec(env.db, "sqlite3", migrations, migrate.Up) |
||||
|
||||
``` |
||||
|
||||
#### sql-migrations/init-20191213144434.sql |
||||
|
||||
这是初始数据库升级脚本。 |
||||
|
||||
注意:升级脚本一旦发布,只可增加,不可修改。 |
||||
|
||||
#### pre-defined/predefined-data.json |
||||
|
||||
因为卡券类型模块(用户可以通过api创建卡券类型)尚未开发,所以目前是根据业务的需要hard code卡券类型到这里。代码中的第一个卡券类型是测试用。其他6个是正式的卡券。 |
||||
|
||||
#### coupon/ruleengine.go |
||||
|
||||
创建卡券时,规则引擎将会检查用户是否可以创建,如果可以,这里会生成各种规则体,附加到卡券上。 |
||||
|
||||
核销卡券是,规则引擎检查是否可以核销。 |
||||
|
||||
#### coupon/module.rule.go |
||||
|
||||
规则结构,用来描述一个规则,比如核销几次。 |
||||
|
||||
#### coupon/logic.rulecomposer.go |
||||
|
||||
rule composer将会根据卡券类型中配置的规则来生成某个规则的规则体,比如 |
||||
|
||||
```json |
||||
"REDEEM_TIMES": { |
||||
"times": 3 |
||||
} |
||||
``` |
||||
|
||||
表示可以核销3次。 |
||||
|
||||
注意,卡券结构中的规则体是json格式字符串。 |
||||
|
||||
#### coupon/logic.judge.go |
||||
|
||||
judge是每个规则的校验者,如果有问题就返回错误。 |
||||
|
||||
比如卡券超兑,会返回 ErrCouponRulesRedeemTimesExceeded |
||||
|
||||
```json |
||||
{ |
||||
"error-code": 1006, |
||||
"error-message": "coupon redeem times exceeded" |
||||
} |
||||
``` |
||||
|
||||
#### coupon/logic.couponservice.go |
||||
|
||||
相当于传统3层架构的业务层,主要处理卡券相关的业务,签发,查询,核销等。 |
||||
|
||||
#### coupon/db.coupon.go |
||||
|
||||
相当于传统3层架构的数据层 |
||||
|
||||
#### base/requester.go |
||||
|
||||
表示api请求者身份的。 |
||||
|
||||
### 重要的数据结构 |
||||
|
||||
#### Rule |
||||
|
||||
rule是描述一个规则,因为尚未开发卡券类型模块,没有对应的数据库表。 |
||||
|
||||
这里的结构可以映射为一个数据库表。 |
||||
|
||||
其中InternalID是uniqu human readable字符串,比如 REDEEM_TIMES, 表示核销次数规则。 |
||||
|
||||
RuleBody是一个json格式的字符串。未来在数据库中应该是一个字符串。 |
||||
|
||||
```go |
||||
type Rule struct { |
||||
ID string `json:"id"` |
||||
Name string `json:"name"` |
||||
InternalID string `json:"internal_id"` |
||||
Description string `json:"description,omitempty"` |
||||
RuleBody string `json:"rule_body"` |
||||
Creator string `json:"creator"` |
||||
CreatedTime time.Time `json:"created_time,omitempty" type:"DATETIME" default:"datetime('now','localtime')"` |
||||
UpdatedTime time.Time `json:"updated_time,omitempty" type:"DATETIME"` |
||||
DeletedTime time.Time `json:"deleted_time,omitempty" type:"DATETIME"` |
||||
} |
||||
``` |
||||
|
||||
#### Template |
||||
|
||||
这是一个卡券的原始模板,品牌可以根据末班创建自己的卡券类型。 |
||||
|
||||
Creator是创建者,未来可以用于访问控制。 |
||||
|
||||
Rules是一个map,保存若干规则,参见 pre-defined/predefined-data.json |
||||
|
||||
```go |
||||
type Template struct { |
||||
ID string `json:"id"` |
||||
Name string `json:"name"` |
||||
Description string `json:"description"` |
||||
Creator string `json:"creator"` |
||||
Rules map[string]interface{} `json:"rules"` |
||||
CreatedTime time.Time `json:"created_time,omitempty" type:"DATETIME" default:"datetime('now','localtime')"` |
||||
UpdatedTime time.Time `json:"updated_time,omitempty" type:"DATETIME"` |
||||
DeletedTime time.Time `json:"deleted_time,omitempty" type:"DATETIME"` |
||||
} |
||||
``` |
||||
|
||||
#### PublishedCouponType |
||||
|
||||
这是根据Template创建的卡券类型。前台系统可以根据卡券类型签发卡券。 |
||||
|
||||
TemplateID是基于卡券模板。 |
||||
|
||||
Publisher 发布者,未来可以据此进行访问控制。 |
||||
|
||||
StrRules 字符串类型的规则。 |
||||
|
||||
Rules是 struct类型的规则,用于系统内部处理业务。 |
||||
|
||||
```go |
||||
type PublishedCouponType struct { |
||||
ID string `json:"id"` |
||||
Name string `json:"name"` |
||||
TemplateID string `json:"template_id"` |
||||
Description string `json:"description"` |
||||
InternalDescription string `json:"internal_description"` |
||||
State CTState `json:"state"` |
||||
Publisher string `json:"publisher"` |
||||
VisibleStartTime time.Time `json:"visible_start_time" type:"DATETIME"` |
||||
VisibleEndTime time.Time `json:"visible_end_time"` |
||||
StrRules map[string]string `json:"rules"` |
||||
Rules map[string]map[string]interface{} |
||||
CreatedTime time.Time `json:"created_time" type:"DATETIME" default:"datetime('now','localtime')"` |
||||
DeletedTime time.Time `json:"deleted_time" type:"DATETIME"` |
||||
} |
||||
``` |
||||
|
||||
#### Coupon |
||||
|
||||
描述一个卡券。 |
||||
|
||||
CouponTypeID是PublishedCouponType的ID. |
||||
|
||||
请参考intergartion guide for third-parties 了解更多 |
||||
|
||||
```go |
||||
type Coupon struct { |
||||
ID string |
||||
CouponTypeID string |
||||
ConsumerID string |
||||
ConsumerRefID string |
||||
ChannelID string |
||||
State State |
||||
Properties map[string]interface{} |
||||
CreatedTime *time.Time |
||||
} |
||||
``` |
||||
|
||||
#### Transaction |
||||
|
||||
用户针对卡券做一个操作后,Transaction将描述这一行为。 |
||||
|
||||
ActorID是操作者的id。 |
||||
|
||||
TransType是操作类型。 |
||||
|
||||
ExtraInfo 是操作者附加的信息,用于后期获取后处理前台业务。 |
||||
|
||||
```go |
||||
type Transaction struct { |
||||
ID string |
||||
CouponID string |
||||
ActorID string |
||||
TransType TransType |
||||
ExtraInfo string |
||||
CreatedTime time.Time |
||||
} |
||||
``` |
||||
|
||||
### 重要的接口 |
||||
|
||||
#### TemplateJudge |
||||
|
||||
签发卡券时,验证卡券模板。 |
||||
|
||||
```go |
||||
// TemplateJudge 发卡券时用来验证是否符合rules |
||||
type TemplateJudge interface { |
||||
// JudgeTemplate 验证模板 |
||||
JudgeTemplate(consumerID string, couponTypeID string, ruleBody map[string]interface{}, pct *PublishedCouponType) error |
||||
} |
||||
``` |
||||
|
||||
#### Judge |
||||
|
||||
核销卡券时,验证卡券。 |
||||
|
||||
```go |
||||
// Judge 兑换卡券时用来验证是否符合rules |
||||
type Judge interface { |
||||
// JudgeCoupon 验证模板 |
||||
JudgeCoupon(requester *base.Requester, consumerID string, ruleBody map[string]interface{}, c *Coupon) error |
||||
} |
||||
``` |
||||
|
||||
#### BodyComposer |
||||
|
||||
签发卡券时,生成规则的规则体,用于附加在卡券上。 |
||||
|
||||
```go |
||||
// BodyComposer 发卡时生成rule的body,用来存在coupon中 |
||||
type BodyComposer interface { |
||||
Compose(requester *base.Requester, couponTypeID string, ruleBody map[string]interface{}) (map[string]interface{}, error) |
||||
} |
||||
``` |
||||
|
||||
### 单元测试 |
||||
|
||||
目前代码中针对coupon文件夹下增加了若干单元测试。 |
||||
|
||||
#### 知识准备 |
||||
|
||||
阅读单元测试,请先了解: |
||||
|
||||
github.com/smartystreets/goconvey/convey 这是一个可以使用易于描述的方式组织单元测试结构。 |
||||
|
||||
bou.ke/monkey 这是一个mock方法的第三方库。 |
||||
|
||||
#### 运行单元测试 |
||||
|
||||
命令行进入 /coupon, 执行 |
||||
|
||||
```sh |
||||
go test -gcflags=-l |
||||
``` |
||||
|
||||
因为内联函数的缘故,需要加上 -gcflags=-l |
||||
|
||||
### API测试 |
||||
|
||||
目前代码中针对coupon相关的api增加了若干测试。 |
||||
|
||||
代码路径: https://github.com/iamhubin/loreal.com/tree/master/dit/cmd/api-tests-for-coupon-service |
||||
|
||||
#### 知识准备 |
||||
|
||||
阅读API测试,请先了解: |
||||
|
||||
github.com/smartystreets/goconvey/convey 这是一个可以使用易于描述的方式组织单元测试结构。 |
||||
|
||||
github.com/gavv/httpexpect 这是一个api调用并且可以验证结果的第三方库。 |
||||
|
||||
#### 执行测试 |
||||
|
||||
命令行进入 /api-tests-for-coupon-service, 执行 |
||||
|
||||
```sh |
||||
go test |
||||
``` |
||||
|
||||
### 相关文档 |
||||
|
||||
请参阅 https://github.com/iamhubin/loreal.com/tree/master/dit/cmd/coupon-service/docs |
||||
|
||||
### 其他文档 |
||||
|
||||
文档压缩包中包含如下几个文件: |
||||
|
||||
| 文件名 | 备注 | | |
||||
| ----------------------------------------------------------- | --------------------------------------- | ---- | |
||||
| 02_卡券类型申请表单_入会礼_0224.xlsx | dennis提交的入会礼卡券 | | |
||||
| 副本03_客户端申请表单_线上渠道_0219.xlsx | dennis提交的客户端 | | |
||||
| 副本05_用户申请表单_Yiyun_0219.xlsx | dennis提交的用户 | | |
||||
| 卡券类型申请表单_生日礼_0224.xlsx | dennis提交的生日礼卡券 | | |
||||
| 02_Web Application Criticality Determination Form_0219.xlsx | pt测试申请表单 | | |
||||
| 03_Penetration Test Request Form_0219.xlsx | pt测试申请表单 | | |
||||
| 卡券服务组件架构图.vsdx | 早期文档,用处不大。 | | |
||||
| 卡券中心最简MVP实施计划.xlsx | larry个人做了一点记录,用处不大 | | |
||||
| CardServiceDBSchema.graphml | 数据库设计,用处不大,建议看代码中的DDL | | |
||||
| Loreal卡券服务思维导图.pdf | 早期构思卡券服务功能时的思维导图 | | |
||||
| data flow diagram.pdf | pt测试需要的数据流图 | | |
||||
| network architecture.pdf | pt测试需要的网络架构图 | | |
||||
|
||||
## oAuth2认证服务 |
||||
|
||||
认证服务采用了https://hub.docker.com/r/jboss/keycloak。 |
||||
|
||||
数据库是https://hub.docker.com/_/mysql。 |
||||
|
||||
启动服务的关键命令如下: |
||||
|
||||
```sh |
||||
#创建docker的虚拟网络 |
||||
sudo docker network create keycloak-network |
||||
|
||||
#启动mysql,注意参数,这不是产线环境参数。 |
||||
docker run --name mysql -d --net keycloak-network -e MYSQL_DATABASE=keycloak -e MYSQL_USER=keycloak -e MYSQL_PASSWORD=password -e MYSQL_ROOT_PASSWORD=root_password mysql |
||||
|
||||
#启动keycloak,注意参数,这不是产线环境参数。 |
||||
docker run --name keycloak --net keycloak-network -p 8080:8080 -e KEYCLOAK_USER=yhl10000 -e KEYCLOAK_PASSWORD=Passw0rd jboss/keycloak |
||||
``` |
||||
|
||||
|
||||
如何使用认证服务请参阅:卡券服务-相关文档章节。 |
||||
|
||||
## 开发测试环境 |
||||
|
||||
开发测试环境的服务器从兰伯特那边接过来的。 |
||||
|
||||
服务地址:https://gua.e-loreal.cn/#/ |
||||
|
||||
服务登录方式请询问hubin。 |
||||
|
||||
认证服务请docker ps 相关容器。 |
||||
|
||||
卡券服务目录: /home/larryyu/coupon-service。 |
||||
|
||||
关于目录结构以及相关功能请咨询hubin。 |
||||
|
||||
卡券服务测试服务器是否启动请访问:http://52.130.73.180/ceh/cvc/health |
||||
|
||||
认证服务管理入口:http://52.130.73.180/auth/ |
||||
|
||||
## SIT集成测试环境 |
||||
|
||||
SIT集成环境用来给供应商开发测试用。 |
||||
|
||||
服务登录方式请询问hubin。 |
||||
|
||||
服务器有两台,10.162.66.29 和 10.162.66.30 。 |
||||
|
||||
卡券服务器目前只用了一台10.162.66.29,类似开发测试环境,包含了认证和卡券两个服务。服务器登录账号目前用的是arvato的账号 **arvatoadmin** 密码是:【请询问hubin】 |
||||
|
||||
认证服务请docker ps 相关容器。 |
||||
|
||||
卡券服务目录: /home/arvatoadmin/coupon-service。 |
||||
|
||||
卡券服务测试服务器是否启动请访问:https://dl-api-uat.lorealchina.com/ceh/cvc/health |
||||
|
||||
认证服务管理入口:跳板机内配置bitvise后,浏览器访问 http://10.162.66.29/auth/ |
||||
|
||||
## PRD产线环境 |
||||
|
||||
服务登录方式请询问hubin。 |
||||
|
||||
服务器有两台: |
||||
|
||||
10.162.65.217 :认证服务器 |
||||
|
||||
10.162.65.218 :卡券服务器 |
||||
|
||||
服务器登录账号目前用的是**appdmin** 密码是:【请询问hubin】 |
||||
|
||||
认证服务请docker ps 相关容器。 |
||||
|
||||
注意:认证服务数据库在/data1t |
||||
|
||||
卡券服务目录: /home/appadmin/coupon-service。 |
||||
|
||||
注意:卡券数据库在/data1t |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,210 @@
@@ -0,0 +1,210 @@
|
||||
# 用户认证服务上线/运营手册 |
||||
|
||||
v 0.0.1 |
||||
|
||||
by 欧莱雅IT |
||||
|
||||
## 修订历史 |
||||
|
||||
| 版本 | 修订说明 | 提交人 | 生效日期 | |
||||
| ----- | -------------- | -------- | -------- | |
||||
| 0.0.1 | 初始化创建文档 | Larry Yu | | |
||||
| | | | | |
||||
|
||||
[TOC] |
||||
|
||||
|
||||
|
||||
## 引言 |
||||
|
||||
### 目前的部署情况 |
||||
|
||||
SIT 环境:https://dl-api-uat.lorealchina.com/auth/realms/Lorealcn/protocol/openid-connect/token |
||||
PRD 环境:https://dl-api.lorealchina.com/auth/realms/Lorealcn/protocol/openid-connect/token |
||||
|
||||
### 目的 |
||||
|
||||
用户认证服务将会提供欧莱雅内部用户的账号维护,以及为依赖用户认证服务的应用签发令牌和核验令牌。 |
||||
|
||||
为方便相关人员理解系统以及如何操作,本文档介绍如何上线部署用户认证服务,以及后期运营。 |
||||
|
||||
### 准备工作 |
||||
|
||||
如果阅读者是部署人员,需要了解docker,mysql,linux环境。 |
||||
|
||||
如果是配置人员,需要了解oAuth2。 |
||||
|
||||
## 部署 |
||||
|
||||
请参考 go-live handbook |
||||
|
||||
【注意】部署时,注意初始化管理员账号。 |
||||
|
||||
部署成功后,可以打开{host}/auth/ 来测试是否部署成功。 |
||||
|
||||
## 导入realm[可选] |
||||
|
||||
因为在测试环境已经创建了realm,为了简化操作,可以直接导入已经存在的realm。 |
||||
|
||||
## 配置realm |
||||
|
||||
如果没有导入一个现有的realm,那么需要创建一个新的。 |
||||
|
||||
### Login 标签页, |
||||
|
||||
- 配置用email登录 |
||||
- 外部请求使用SSL |
||||
- 其他可以关闭 |
||||
|
||||
### Keys标签页 |
||||
|
||||
查看RS256的公约,用来给卡券服务作为认证之用。 |
||||
|
||||
### Tokens标签页 |
||||
|
||||
一般默认就可,除非特别配置。 |
||||
|
||||
### 导出配置 |
||||
|
||||
为了方便管理以及迁移数据,管理员可以有限导出realm的数据。包括: |
||||
|
||||
| 项目 | 描述 | | |
||||
| -------- | ------------------------------------------------------------ | ---- | |
||||
| 组和角色 | 根据业务的需要,可以创建一些组,比如具有相同角色的人可以放在一个组里面。<BR>角色用来描述一个用户可以做什么事情。 | | |
||||
| 客户端 | 客户端是用来描述一个接入oAuth2服务的程序或者服务。比如欧莱雅内部campaign tool。<BR>客户端功能可以有效区分不同的应用,分别配置访问资源的权限,更大可能保护用户的资源等。 | | |
||||
| | | | |
||||
|
||||
注意:**管理员无法导出用户信息。** |
||||
|
||||
## 管理Clients |
||||
|
||||
这里的客户端,也就是应用,目前我们有campaign tool和云积分,也就是说,至少有两个客户端需要配置。 |
||||
|
||||
原则上,为了安全,一个单独的服务,就是一个client。 |
||||
|
||||
### 配置Tab |
||||
|
||||
- 配置Client ID,将会交付给应用开发商。 |
||||
- Enabled标志应用是否被启用。 |
||||
- Consent required 选择False. 【注意】因为用户是欧莱雅内部员工,所以此处不需要Consent,这不是常用的选择。 |
||||
- Client Protocol 选择 openid-connect。 |
||||
- Access Type 选择 confidential。 |
||||
- Authorization Enable 选择false。 |
||||
|
||||
### Crendentias Tab |
||||
|
||||
Client Authenticator 选择 client id and secret, 然后生成一个secret。【**注意**】**<u>这个secret是保密内容,请使用安全的方式交付给应用开发商</u>**。 |
||||
|
||||
### Mappers Tab |
||||
|
||||
这里为一个client配置一个mapper,将会在用户的token里增加一些项,比如用户所属的品牌信息。 |
||||
|
||||
新建一个mapper,取一个有意义的名字,比如**用户所属品牌**, |
||||
|
||||
打开mapper,编辑各个属性: |
||||
|
||||
- Protocol :选择openid-connect |
||||
- Mapper Type :选择 User Attribute |
||||
- User Attribute :**brand**,【 注意】这是预先定义好的,不要修改成其他的。 |
||||
- Token Claim Name :**brand**,【 注意】这是预先定义好的,不要修改成其他的。 |
||||
- Claim JSON Type :String |
||||
- Add to ID token :选择ON |
||||
- Add to access token : 选择ON |
||||
- Add to userinfo : 选择ON |
||||
|
||||
## 角色 |
||||
|
||||
新建三个角色,如下表: |
||||
|
||||
| 角色名 | 用途 | 备注 | |
||||
| --------------- | -------------------------------------- | ------------------------------------------------------------ | |
||||
| coupon_issuer | 可以签发卡券 | 比如campaign tool需要发券,那么内置的用户需要这个角色 | |
||||
| coupon_listener | 可以监听卡券服务的事件,比如核销事件。 | 比如campaign tool想得知哪个消费者核销了哪个券,可以配置这个角色,然后长轮询核销卡券的事件。 | |
||||
| coupon_redeemer | 可以核销卡券 | 比如云积分需要发券,那么内置的用户需要这个角色 | |
||||
|
||||
## 组(Groups) |
||||
|
||||
因为有一些业务需求是不允许夸品牌兑换,通过配置组可以实现用户品牌的区分。 |
||||
|
||||
用户认证服务里面的组相当于欧莱雅的品牌。 |
||||
|
||||
比如新建组:**兰蔻** 。然后在**属性Tab**里面增加一个属性: |
||||
|
||||
| Key | Value | 备注 | |
||||
| :---- | :------ | :----------------------------------------------------------- | |
||||
| brand | LANCOME | key 必须是brand,卡券中心将依赖这个配置。Value配置成有意义的值,一旦配置好后,因为业务依赖,将很难被更改掉。 | |
||||
|
||||
## 用户 |
||||
|
||||
### 新增用户 |
||||
|
||||
目前除了Username是必选项,其他默认也行,但为了管理,最好丰富下其他信息。 |
||||
|
||||
### 配置用户 |
||||
|
||||
#### Role Mappings |
||||
|
||||
这里配置用户的角色,根据业务可选前面提到的**角色**。将角色添加到**Assigned Roles**. |
||||
|
||||
#### Groups |
||||
|
||||
给用户配置组别,前面提到有些服务需要组别来判断用户的品牌属性。 |
||||
|
||||
在右侧可选的组别中根据业务选择一个组,【注意】只选择一个组。 |
||||
|
||||
## 日志管理 |
||||
|
||||
在keycloak的管理-事件模块,可以管理日志。 |
||||
|
||||
### 开启日志 |
||||
|
||||
在**Config** tab,可以分别开启登录和管理两类事件日志。 |
||||
|
||||
### 查看日志 |
||||
|
||||
在**登录事件**和**管理时间**两个tab,可以看到两类事件的详情。 |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue