import from internal gitlab
This commit is contained in:
parent
c6c9fcaf41
commit
4cbf048e0e
|
@ -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="yhl10000@gmail.com"
|
||||
|
||||
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