4264 changed files with 1833330 additions and 0 deletions
@ -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 @@ |
|||||||
|
cd ..\src\loreal.com\dit\cmd\coupon-service |
||||||
|
make windows |
||||||
|
cd ..\..\..\..\..\bin |
||||||
|
|
||||||
@ -0,0 +1,5 @@ |
|||||||
|
#!/bin/bash |
||||||
|
cd ../src/loreal.com/dit/cmd/coupon-service |
||||||
|
make linux |
||||||
|
cd ../../../../../bin |
||||||
|
|
||||||
@ -0,0 +1,4 @@ |
|||||||
|
cd ..\src\loreal.com\dit\cmd\coupon-service |
||||||
|
make test |
||||||
|
cd ..\..\..\..\..\bin |
||||||
|
|
||||||
@ -0,0 +1,5 @@ |
|||||||
|
#!/bin/bash |
||||||
|
cd ../src/loreal.com/dit/cmd/coupon-service |
||||||
|
make test |
||||||
|
cd ../../../../../bin |
||||||
|
|
||||||
@ -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 @@ |
|||||||
|
{ |
||||||
|
// 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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
################################################ |
||||||
|
############### .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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
//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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
{ |
||||||
|
// 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
fmt.Println("this project only for api test") |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
vendor |
||||||
|
web/**/node_modules |
||||||
|
dump.rdb |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
{ |
||||||
|
"javascript.format.insertSpaceBeforeFunctionParenthesis": true, |
||||||
|
"vetur.format.defaultFormatter.js": "vscode-typescript", |
||||||
|
"vetur.format.defaultFormatter.ts": "vscode-typescript" |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
//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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
module restful |
||||||
|
|
||||||
|
go 1.13 |
||||||
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
{ |
||||||
|
// 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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
package coupon |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
) |
||||||
|
|
||||||
|
var dbConnection *sql.DB |
||||||
|
|
||||||
|
func staticsInit(databaseConnection *sql.DB) { |
||||||
|
dbConnection = databaseConnection |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
[ |
||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
# 用户认证服务上线/运营手册 |
||||||
|
|
||||||
|
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