Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/service/live/wallet/cmd:all-srcs",
"//app/service/live/wallet/conf:all-srcs",
"//app/service/live/wallet/dao:all-srcs",
"//app/service/live/wallet/http:all-srcs",
"//app/service/live/wallet/model:all-srcs",
"//app/service/live/wallet/service:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,60 @@
# v2.0.5
1. 避免当开启事务失败返回的指针没有初始化panic
# v2.0.4
1. 兑换硬币如果硬币接口失败还是认为兑换已经成功且设置返回避免panic
# v2.0.3
1. 兑换硬币如果硬币接口失败还是认为兑换已经成功
# v2.0.2
1. 取数据没取到不写db
2. 新建一个goroutine发消息队列
# v2.0.1
1. cache出错不回源db
2. 记流水时修改cnt字段
# v2.0.0
1. 事务化
2. 双余额
# v1.0.11
1. 修改retry为0
# v1.0.10
1. 修改retry为1
# v1.0.9
1. add test
# v1.0.8
1. add version support
# v1.0.7
1. add cache version support
# v1.0.6
1. add bi support
2. add cost_base support
# v1.0.5
1. build bazel
# v1.0.4
1. add pub wallet change info
# v1.0.3
1. add reason for pay metal
2. fix err bug
# v1.0.2
1. fix coin stream index can not fix
# v1.0.1
1. 增加delCache接口
2. recordCoinStream 增加写入数据库的字段
3. move dir
# v1.0.0
1. 新增

View File

@@ -0,0 +1,10 @@
# Owner
zhaohailin
zhouzhichao
# Author
zhouzhichao
# Reviewer
zhaohailin
liuzhen

View File

@@ -0,0 +1,15 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- zhaohailin
- zhouzhichao
labels:
- live
- service
- service/live/wallet
options:
no_parent_owners: true
reviewers:
- liuzhen
- zhaohailin
- zhouzhichao

View File

@@ -0,0 +1,14 @@
# /live-wallet
# 项目简介
1.
# 编译环境
# 依赖包
# 编译执行

View File

@@ -0,0 +1,42 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
tags = ["automanaged"],
)
go_library(
name = "go_default_library",
srcs = ["main.go"],
data = ["live-wallet-test.toml"],
importpath = "go-common/app/service/live/wallet/cmd",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/http:go_default_library",
"//app/service/live/wallet/service:go_default_library",
"//library/log:go_default_library",
"//library/net/trace:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,132 @@
# This is a TOML document. Boom
version = "1.0.0"
user = "root"
dir = "/root/golang/go-project/src/go-common/business/service/live-wallet"
family = "live-wallet"
address = ""
trace = false
debug = false
perf = "0.0.0.0:20201"
[log]
dir = "/data/log/live-wallet-service"
[db]
[db.wallet]
addr = "172.22.34.101:3312"
dsn = "livetestuat:livetestuat20180711@tcp(172.22.34.101:3312)/live_wallet?timeout=5s&readTimeout=30s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8,utf8mb4"
active = 5
idle = 5
queryTimeout = "1s"
execTimeout = "1s"
tranTimeout = "2s"
[db.wallet.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[bm]
[bm.inner]
addr = "0.0.0.0:9901"
maxListen = 10
timeout = "1s"
[bm.local]
addr = "0.0.0.0:9902"
maxListen = 10
timeout = "1s"
[httpClient]
key = "fb06a25c6338edbc"
secret = "fd10bd177559780c2e4a44f1fa47fa83"
dial = "1s"
timeout = "4s"
keepAlive = "60s"
timer = 1000
[httpClient.breaker]
window = "3s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify]
whiteAccessKey = ""
whiteMid = 0
csrfOn = false
[identify.app]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
[identify.memcache]
name = "go-business/identify"
proto = "tcp"
addr = "172.16.33.251:11211"
active = 15
idle = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "80s"
[identify.host]
auth = "http://passport.bilibili.co"
secret = "http://open.bilibili.co"
[identify.httpClient]
key = "7c7ac0db1aa05587"
secret = "9a6d62d93290c5f771ad381e9ca23f26"
dial = "30ms"
timeout = "100ms"
keepAlive = "60s"
[identify.httpClient.breaker]
window = "10s"
sleep = "100ms"
bucket = 10
ratio = 0.5
request = 100
[identify.httpClient.url]
"http://passport.bilibili.co/intranet/auth/tokenInfo" = {timeout = "100ms"}
"http://passport.bilibili.co/intranet/auth/cookieInfo" = {timeout = "100ms"}
"http://open.bilibili.co/api/getsecret" = {timeout = "500ms"}
[memcache]
walletExpire = "3s"
[memcache.wallet]
name = "live-wallet-service/wallet"
proto = "tcp"
addr = "172.18.33.67:11211"
idle = 5
active = 10
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "80s"
[redis]
[redis.wallet]
name = "wallet/wallet"
proto = "tcp"
addr = "172.18.33.67:6379"
idle = 100
active = 100
dialTimeout = "500ms"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "80s"
expire = "10m"
[databus]
[databus.Change]
key = "ec4c0820d525d67b"
secret= "2bdf3bd4ecab041b5d5640a1da4f7f81"
group= "LiveWalletChange-LiveLive-P"
topic= "LiveWalletChange-T"
action="pub"
name = "live-wallet"
proto = "tcp"
addr = "172.16.38.154:6205"
idle = 100
active = 100
dialTimeout = "1s"
readTimeout = "1s"
writeTimeout = "1s"
idleTimeout = "10s"

View File

@@ -0,0 +1,48 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/http"
"go-common/app/service/live/wallet/service"
"go-common/library/log"
"go-common/library/net/trace"
)
func main() {
flag.Parse()
if err := conf.Init(); err != nil {
log.Error("conf.Init() error(%v)", err)
panic(err)
}
// init log
log.Init(conf.Conf.Log)
defer log.Close()
log.Info("/live-wallet start")
// init trace
trace.Init(conf.Conf.Tracer)
defer trace.Close()
// service init
svr := service.New(conf.Conf)
http.Init(conf.Conf, svr)
// init pprof conf.Conf.Perf
// init signal
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
log.Info("/live-wallet get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
log.Info("/live-wallet exit")
return
case syscall.SIGHUP:
default:
return
}
}
}

View File

@@ -0,0 +1,39 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["conf.go"],
importpath = "go-common/app/service/live/wallet/conf",
tags = ["automanaged"],
deps = [
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/conf:go_default_library",
"//library/database/sql:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/trace:go_default_library",
"//library/queue/databus:go_default_library",
"//library/time:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,122 @@
package conf
import (
"errors"
"flag"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
"go-common/library/conf"
"go-common/library/database/sql"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/trace"
"go-common/library/queue/databus"
"go-common/library/time"
"github.com/BurntSushi/toml"
)
// global var
var (
ConfPath string
client *conf.Client
// Conf config
Conf = &Config{}
)
// Config config set
type Config struct {
// base
// elk
Log *log.Config
BM *HTTPServers
// tracer
Tracer *trace.Config
// memcache
Memcache *Memcache
WalletExpire int32
// redis
Redis *Redis
// db
DB *DB
// http client
HTTPClient *bm.ClientConfig
//DataBus
DataBus *DataBus
}
type DataBus struct {
Change *databus.Config
}
type HTTPServers struct {
Inner *bm.ServerConfig
Local *bm.ServerConfig
}
// DB db config.
type DB struct {
Wallet *sql.Config
}
type Memcache struct {
Wallet *memcache.Config
WalletExpire time.Duration
}
type Redis struct {
Wallet *redis.Config
}
func init() {
flag.StringVar(&ConfPath, "conf", "", "default config path")
}
// Init init conf
func Init() error {
if ConfPath != "" {
return local()
}
return remote()
}
func local() (err error) {
_, err = toml.DecodeFile(ConfPath, &Conf)
return
}
func remote() (err error) {
if client, err = conf.New(); err != nil {
return
}
if err = load(); err != nil {
return
}
go func() {
for range client.Event() {
log.Info("config reload")
if load() != nil {
log.Error("config reload error (%v)", err)
}
}
}()
return
}
func load() (err error) {
var (
s string
ok bool
tmpConf *Config
)
if s, ok = client.Toml(); !ok {
return errors.New("load config center error")
}
if _, err = toml.Decode(s, &tmpConf); err != nil {
return errors.New("could not decode config")
}
*Conf = *tmpConf
return
}

View File

@@ -0,0 +1,76 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"coinexchange_test.go",
"coinstream_test.go",
"mc_wallet_test.go",
"metal_test.go",
"pub_test.go",
"redis_test.go",
"tx_test.go",
"wallet_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"coinexchange.go",
"coinstream.go",
"dao.go",
"mc_wallet.go",
"metal.go",
"pub.go",
"redis.go",
"tx.go",
"wallet.go",
],
importpath = "go-common/app/service/live/wallet/dao",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/model:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/cache/redis:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/queue/databus:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,25 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/live/wallet/model"
)
const (
_newCoinExchange = "insert into %s (uid, transaction_id, src_type,src_num,dest_type,dest_num,status,exchange_time) values(?,?,?,?,?,?,?,?)"
)
func getCoinExchangeTableIndex(uid int64) string {
return fmt.Sprintf("%02d", uid%10)
}
func getCoinExchangeTable(uid int64) string {
return fmt.Sprintf("t_coin_exchange_%s", getCoinExchangeTableIndex(uid))
}
func (d *Dao) NewCoinExchangeRecord(c context.Context, record *model.CoinExchangeRecord) (int64, error) {
s := fmt.Sprintf(_newCoinExchange, getCoinExchangeTable(record.Uid))
date := model.GetWalletFormatTime(record.ExchangeTime)
return execSqlWithBindParams(d, c, &s, record.Uid, record.TransactionId, record.SrcType, record.SrcNum, record.DestType, record.DestNum, record.Status, date)
}

View File

@@ -0,0 +1,27 @@
package dao
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"testing"
"time"
)
func TestDao_NewCoinExchangeRecord(t *testing.T) {
Convey("New CoinExchange", t, func() {
once.Do(startService)
record := new(model.CoinExchangeRecord)
record.ExchangeTime = time.Now().Unix()
record.Status = 1
record.DestNum = 1
record.DestType = 1
record.SrcType = 2
record.SrcNum = 1
record.TransactionId = "abcdef"
record.Uid = 1
affect, err := d.NewCoinExchangeRecord(ctx, record)
So(affect, ShouldEqual, 1)
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,91 @@
package dao
import (
"context"
"crypto/md5"
"fmt"
"go-common/app/service/live/wallet/model"
"go-common/library/log"
"time"
)
const (
_newCoinStremaRecord = "INSERT INTO %s (uid, transaction_id, extend_tid, coin_type, delta_coin_num, org_coin_num, op_result, op_reason, op_type, op_time, reserved2, area, source, reserved3, reserved4,platform,reserved1, reserved5) values(?, ?, ?,?, ?, ?,?, ?, ?,?, ?, ?,?, ?, ?, ?, ?, ?)"
_getCoinStream = "SELECT uid,transaction_id,extend_tid, coin_type,delta_coin_num,org_coin_num,op_result,op_reason,op_type, reserved3,area,reserved2,source,reserved4,platform,reserved1,reserved5 FROM %s WHERE transaction_id = ? order by id desc limit %d,1"
_getCoinStreamByUid = "SELECT uid,transaction_id,extend_tid, coin_type,delta_coin_num,org_coin_num,op_result,op_reason,op_type, reserved3,area,reserved2,source,reserved4, platform , reserved1,reserved5 FROM %s WHERE transaction_id = ? and uid = ? order by id desc limit %d,1"
)
func hexdec(s string) uint64 {
d := uint64(0)
for i := 0; i < len(s); i++ {
x := uint64(s[i])
if x >= 'a' {
x -= 'a' - 'A'
}
d1 := x - '0'
if d1 > 9 {
d1 = 10 + d1 - ('A' - '0')
}
if 0 > d1 || d1 > 15 {
return 0
}
d = (16 * d) + d1
}
return d
}
func getCoinStreamTable(transactionId string) string {
tlen := len(transactionId)
year := hexdec(transactionId[tlen-3 : tlen-1])
if year == 0 {
log.Error("illegal tid : %s", transactionId)
}
year = year + 2000
t := fmt.Sprintf("t_coin_stream_%d%02d", year, hexdec(transactionId[tlen-1:tlen]))
return t
}
func GetTid(serviceType model.ServiceType, v interface{}) string {
s := fmt.Sprintf("%v%s", v, randomString(5))
bizTid := fmt.Sprintf("%x", md5.Sum([]byte(s)))
now := time.Now()
year := now.Year() - 2000
month := int(now.Month())
return fmt.Sprintf("%s%x%02x%x", bizTid, serviceType, year, month)
}
func (d *Dao) NewCoinStreamRecord(c context.Context, record *model.CoinStreamRecord) (int64, error) {
s := fmt.Sprintf(_newCoinStremaRecord, getCoinStreamTable(record.TransactionId))
date := model.GetWalletFormatTime(record.OpTime)
return execSqlWithBindParams(d, c, &s, record.Uid, record.TransactionId, record.ExtendTid, record.CoinType,
record.DeltaCoinNum, record.OrgCoinNum, record.OpResult, record.OpReason, record.OpType, date,
record.BizCode, record.Area, record.Source, record.MetaData, record.BizSource,
record.Platform, record.Reserved1, record.Version,
)
}
func (d *Dao) GetCoinStreamByTid(c context.Context, tid string) (record *model.CoinStreamRecord, err error) {
return d.GetCoinStreamByTidAndOffset(c, tid, 0)
}
func (d *Dao) GetCoinStreamByTidAndOffset(c context.Context, tid string, offset int) (record *model.CoinStreamRecord, err error) {
s := fmt.Sprintf(_getCoinStream, getCoinStreamTable(tid), offset)
row := d.db.QueryRow(c, s, tid)
record = &model.CoinStreamRecord{}
err = row.Scan(&record.Uid, &record.TransactionId, &record.ExtendTid, &record.CoinType, &record.DeltaCoinNum,
&record.OrgCoinNum, &record.OpResult, &record.OpReason, &record.OpType, &record.MetaData, &record.Area,
&record.BizCode, &record.Source, &record.BizSource, &record.Platform, &record.Reserved1, &record.Version)
return
}
func (d *Dao) GetCoinStreamByUidTid(c context.Context, uid int64, tid string) (record *model.CoinStreamRecord, err error) {
s := fmt.Sprintf(_getCoinStreamByUid, getCoinStreamTable(tid), 0)
row := d.db.QueryRow(c, s, tid, uid)
record = &model.CoinStreamRecord{}
err = row.Scan(&record.Uid, &record.TransactionId, &record.ExtendTid, &record.CoinType, &record.DeltaCoinNum,
&record.OrgCoinNum, &record.OpResult, &record.OpReason, &record.OpType, &record.MetaData, &record.Area,
&record.BizCode, &record.Source, &record.BizSource, &record.Platform, &record.Reserved1, &record.Version)
return
}

View File

@@ -0,0 +1,61 @@
package dao
import (
"testing"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"time"
)
func TestGetTable(t *testing.T) {
Convey("Get Table", t, func() {
once.Do(startService)
t := getCoinStreamTable("89689d8c2290d93183b96c7ec83ea3c61114")
So(t, ShouldEqual, "t_coin_stream_201704")
t = getCoinStreamTable("31ea4d87162d3ed9c1dfb804eb960c640119")
So(t, ShouldEqual, "t_coin_stream_201709")
t = getCoinStreamTable("9937727a8c7fed5bbecd93642b4982561121")
So(t, ShouldEqual, "t_coin_stream_201801")
tid := GetTid(1, map[string]string{"a": "a", "b": "b"})
t = getCoinStreamTable(tid)
now := time.Now()
So(t, ShouldEqual, fmt.Sprintf("t_coin_stream_%d%02d", now.Year(), int(now.Month())))
})
}
func TestDao_NewCoinStreamRecord(t *testing.T) {
Convey("new coin stream", t, func() {
once.Do(startService)
r := model.CoinStreamRecord{
Uid: 1,
TransactionId: GetTid(1, map[string]string{"a": "a", "b": "b"}),
ExtendTid: "abcdef",
DeltaCoinNum: 1,
CoinType: 0,
OrgCoinNum: 100,
OpResult: 2,
OpReason: 0,
OpType: 1,
OpTime: time.Now().Unix(),
BizCode: "wtf",
Area: 0,
Source: "",
MetaData: "",
BizSource: "",
}
affect, err := d.NewCoinStreamRecord(ctx, &r)
So(err, ShouldBeNil)
So(affect, ShouldEqual, 1)
})
}

View File

@@ -0,0 +1,235 @@
package dao
import (
"context"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/cache/memcache"
"go-common/library/cache/redis"
xsql "go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
httpx "go-common/library/net/http/blademaster"
"go-common/library/queue/databus"
"math/rand"
"time"
)
type Dao struct {
c *conf.Config
mc *memcache.Pool
db *xsql.DB
redis *redis.Pool
cacheExpire int32
httpClient *httpx.Client
changeDataBus *databus.Databus
}
// New init mysql db
func New(c *conf.Config) (dao *Dao) {
dao = &Dao{
c: c,
mc: memcache.NewPool(c.Memcache.Wallet),
db: xsql.NewMySQL(c.DB.Wallet),
redis: redis.NewPool(c.Redis.Wallet),
cacheExpire: c.WalletExpire,
httpClient: httpx.NewClient(c.HTTPClient),
changeDataBus: databus.New(c.DataBus.Change),
}
return
}
// Close close the resource.
func (d *Dao) Close() {
d.mc.Close()
d.db.Close()
d.redis.Close()
d.changeDataBus.Close()
}
// Ping dao ping
func (d *Dao) Ping(c context.Context) (err error) {
if err = d.db.Ping(c); err != nil {
log.Error("PingDb error(%v)", err)
return
}
if err = d.pingMC(c); err != nil {
return err
}
return d.PingRedis(c)
}
// pingMc ping
func (d *Dao) pingMC(c context.Context) (err error) {
item := &memcache.Item{
Key: "ping",
Value: []byte{1},
Expiration: d.cacheExpire,
}
conn := d.mc.Get(c)
err = conn.Set(item)
conn.Close()
if err != nil {
log.Error("PingMemcache conn.Set(%v) error(%v)", item, err)
}
return
}
func (d *Dao) PingRedis(c context.Context) (err error) {
var conn = d.redis.Get(c)
_, err = conn.Do("SET", "PING", "PONG")
conn.Close()
return
}
// 通过提供的sql和bind来更新没有实际业务意义只是为了少写重复代码
func execSqlWithBindParams(d *Dao, c context.Context, sql *string, bindParams ...interface{}) (affect int64, err error) {
res, err := d.db.Exec(c, *sql, bindParams...)
if err != nil {
log.Error("db.Exec(%s) error(%v)", *sql, err)
return
}
return res.RowsAffected()
}
func randomString(l int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyz"
bytes := []byte(str)
result := []byte{}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < l; i++ {
result = append(result, bytes[r.Intn(len(bytes))])
}
return string(result)
}
func (d *Dao) GetDetailByCache(c context.Context, uid int64) (wallet *model.Detail, err error) {
mcDetail, err := d.WalletCache(c, uid)
if err == ecode.ServerErr {
return
}
if err == nil {
if d.IsNewVersion(c, mcDetail) {
wallet = mcDetail.Detail
return
} else {
log.Info("user wallet hit but version old uid: %d", uid)
}
}
if wallet, err = d.Detail(c, uid); err == nil {
d.SetWalletCache(c, &model.McDetail{Detail: wallet, Exist: true, Version: d.CacheVersion(c)}, 86400)
}
return
}
// Melonseed 获取瓜子数
func (d *Dao) GetMelonseedByCache(c context.Context, uid int64) (wallet *model.Melonseed, err error) {
detail, err := d.GetDetailByCache(c, uid)
wallet = new(model.Melonseed)
if err == nil {
wallet = &model.Melonseed{
Uid: detail.Uid,
Gold: detail.Gold,
IapGold: detail.IapGold,
Silver: detail.Silver,
}
}
return
}
func (d *Dao) ModifyCoin(c context.Context, coinNum int, uid int64, coinTypeNo int32, delCache bool) (bool, error) {
var (
affect int64
err error
res bool
)
switch coinTypeNo {
case model.SysCoinTypeIosGold:
affect, err = d.AddIapGold(c, uid, coinNum)
case model.SysCoinTypeSilver:
affect, err = d.AddSilver(c, uid, coinNum)
case model.SysCoinTypeGold:
affect, err = d.AddGold(c, uid, coinNum)
default:
// do nothing
}
if affect > 0 {
res = true
if delCache {
d.DelWalletCache(c, uid)
}
}
return res, err
}
func (d *Dao) GetCoin(c context.Context, coinTypeNo int32, uid int64) (interface{}, error) {
userCoin, err := d.Melonseed(c, uid)
switch coinTypeNo {
case model.SysCoinTypeIosGold:
return userCoin.IapGold, err
case model.SysCoinTypeGold:
return userCoin.Gold, err
case model.SysCoinTypeSilver:
return userCoin.Silver, err
case model.SysCoinTypeMetal:
metal, err := d.GetMetal(c, uid)
return metal, err
default:
return nil, nil
}
}
func (d *Dao) RechargeCoin(c context.Context, coinNum int, uid int64, coinTypeNo int32, delCache bool) (bool, error) {
var (
affect int64
err error
res bool
)
switch coinTypeNo {
case model.SysCoinTypeIosGold:
affect, err = d.RechargeIapGold(c, uid, coinNum)
case model.SysCoinTypeSilver:
affect, err = d.AddSilver(c, uid, coinNum)
case model.SysCoinTypeGold:
affect, err = d.RechargeGold(c, uid, coinNum)
default:
// do nothing
}
if affect > 0 {
res = true
if delCache {
d.DelWalletCache(c, uid)
}
}
return res, err
}
func (d *Dao) ConsumeCoin(c context.Context, coinNum int, uid int64, coinTypeNo int32, seeds int64, delCache bool, reason interface{}) (success bool, err error) {
var affect int64
switch coinTypeNo {
case model.SysCoinTypeGold:
affect, err = d.ConsumeGold(c, uid, coinNum)
case model.SysCoinTypeIosGold:
affect, err = d.ConsumeIapGold(c, uid, coinNum)
case model.SysCoinTypeSilver:
affect, err = d.ConsumeSilver(c, uid, coinNum)
case model.SysCoinTypeMetal:
success, _, err = d.ModifyMetal(c, uid, int64(-1*coinNum), seeds, reason)
default:
}
if model.IsLocalCoin(coinTypeNo) {
if affect > 0 {
success = true
if delCache {
d.DelWalletCache(c, uid)
}
}
}
return
}

View File

@@ -0,0 +1,80 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/live/wallet/model"
mc "go-common/library/cache/memcache"
"go-common/library/ecode"
"go-common/library/log"
)
const (
_walletMcKey = "wu:%d" // 钱包数据的mc缓存
)
func mcKey(uid int64) string {
return fmt.Sprintf(_walletMcKey, uid)
}
func (d *Dao) CacheVersion(c context.Context) int32 {
return 1
}
func (d *Dao) IsNewVersion(c context.Context, detail *model.McDetail) bool {
return detail.Version == d.CacheVersion(c)
}
// WalletCache 获取钱包缓存
func (d *Dao) WalletCache(c context.Context, uid int64) (detail *model.McDetail, err error) {
key := mcKey(uid)
conn := d.mc.Get(c)
defer conn.Close()
r, err := conn.Get(key)
if err != nil {
if err == mc.ErrNotFound {
return
}
log.Error("[dao.mc_wallet|WalletCache] conn.Get(%s) error(%v)", key, err)
err = ecode.ServerErr
return
}
detail = &model.McDetail{}
if err = conn.Scan(r, detail); err != nil {
log.Error("[dao.mc_wallet|WalletCache] conn.Scan(%s) error(%v)", string(r.Value), err)
}
return
}
// SetWalletCache 设置钱包缓存
func (d *Dao) SetWalletCache(c context.Context, detail *model.McDetail, expire int32) (err error) {
key := mcKey(detail.Detail.Uid)
conn := d.mc.Get(c)
defer conn.Close()
if err = conn.Set(&mc.Item{
Key: key,
Object: detail,
Flags: mc.FlagProtobuf,
Expiration: expire,
}); err != nil {
log.Error("[dao.mc_wallet|SetWalletCache] conn.Set(%s, %v) error(%v)", key, detail, err)
}
return
}
// DelWalletCache 删除等级缓存
func (d *Dao) DelWalletCache(c context.Context, uid int64) (err error) {
key := mcKey(uid)
conn := d.mc.Get(c)
defer conn.Close()
if err = conn.Delete(key); err == mc.ErrNotFound {
return
}
if err != nil {
log.Error("[dao.mc_wallet|DelWalletCache] conn.Delete(%s) error(%v)", key, err)
}
return
}

View File

@@ -0,0 +1,103 @@
package dao
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
"go-common/app/service/live/wallet/model"
mc "go-common/library/cache/memcache"
"math/rand"
"time"
)
func TestDao_WalletCache(t *testing.T) {
once.Do(startService)
Convey("Get Nil Cache", t, func() {
r = rand.New(rand.NewSource(time.Now().UnixNano()))
uid := r.Int63n(1000000)
_, err := d.WalletCache(ctx, uid)
So(err, ShouldEqual, mc.ErrNotFound)
})
Convey("Set And Get And Del", t, func() {
r = rand.New(rand.NewSource(time.Now().UnixNano()))
uid := r.Int63n(1000000)
detail := &model.Detail{Uid: uid, Gold: 1, IapGold: 1, Silver: 1, GoldPayCnt: 1, GoldRechargeCnt: 1, SilverPayCnt: 1}
mcDetail := &model.McDetail{Detail: detail, Exist: true, Version: d.CacheVersion(ctx)}
err := d.SetWalletCache(ctx, mcDetail, d.cacheExpire)
So(err, ShouldBeNil)
nd, err := d.WalletCache(ctx, uid)
So(err, ShouldBeNil)
So(nd.Exist, ShouldEqual, true)
So(nd.Detail.Gold, ShouldEqual, 1)
So(nd.Detail.IapGold, ShouldEqual, 1)
So(nd.Detail.Silver, ShouldEqual, 1)
So(nd.Detail.GoldPayCnt, ShouldEqual, 1)
So(nd.Detail.GoldRechargeCnt, ShouldEqual, 1)
So(nd.Detail.SilverPayCnt, ShouldEqual, 1)
So(nd.Version, ShouldEqual, d.CacheVersion(ctx))
err = d.DelWalletCache(ctx, uid)
So(err, ShouldBeNil)
nnd, err := d.WalletCache(ctx, uid)
So(err, ShouldEqual, mc.ErrNotFound)
So(nnd, ShouldBeNil)
})
Convey("version", t, func() {
Convey("just set", func() {
uid := int64(1)
d.DelWalletCache(ctx, uid)
d.GetDetailByCache(ctx, uid)
detail, err := d.WalletCache(ctx, uid)
So(err, ShouldBeNil)
So(detail.Version, ShouldEqual, 1)
})
Convey("old to new", func() {
uid := int64(1)
od, err := d.Detail(ctx, uid)
So(err, ShouldBeNil)
d.DelWalletCache(ctx, uid)
detail := &model.Detail{Uid: uid, Gold: od.Gold + 1, IapGold: 1, Silver: 1, GoldPayCnt: 1, GoldRechargeCnt: 1, SilverPayCnt: 1} // fake data
mcDetail := &model.McDetail{Detail: detail, Exist: true} // old version
err = d.SetWalletCache(ctx, mcDetail, d.cacheExpire)
So(err, ShouldBeNil)
So(mcDetail.Version, ShouldEqual, 0)
So(mcDetail.Detail.Gold, ShouldEqual, od.Gold+1)
So(mcDetail.Detail.IapGold, ShouldEqual, 1)
So(mcDetail.Detail.Silver, ShouldEqual, 1)
So(mcDetail.Detail.GoldPayCnt, ShouldEqual, 1)
mcDetail1, err := d.WalletCache(ctx, uid) //
So(err, ShouldBeNil)
So(mcDetail1.Detail.Gold, ShouldEqual, od.Gold+1)
fd, err := d.GetDetailByCache(ctx, uid) // check version if it is old update cache
So(err, ShouldBeNil)
So(fd.Gold, ShouldEqual, od.Gold)
So(fd.Silver, ShouldEqual, od.Silver)
So(fd.GoldPayCnt, ShouldEqual, od.GoldPayCnt)
mcDetail2, err := d.WalletCache(ctx, uid)
So(err, ShouldBeNil)
So(mcDetail2.Version, ShouldEqual, d.CacheVersion(ctx))
So(mcDetail2.Detail.Gold, ShouldEqual, od.Gold)
})
})
}

View File

@@ -0,0 +1,132 @@
package dao
import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"go-common/library/log"
"net/http"
"net/url"
"time"
)
const (
ModifyUrl = "http://api.bilibili.co/x/internal/v1/coin/user/modify"
InfoUrl = "http://api.bilibili.co/x/internal/v1/coin/user/count"
ExchangeSilverReason = "兑换直播银瓜子 %d"
ExchangeMetalReason = "银瓜子兑换硬币"
)
var (
respCodeError = errors.New("query response code err")
paramError = errors.New("param error")
)
func (d *Dao) GetMetal(c context.Context, uid int64) (metal float64, err error) {
params := url.Values{}
params.Set("mid", fmt.Sprintf("%d", uid))
now := time.Now().Unix()
params.Set("ts", fmt.Sprintf("%d", now))
var req *http.Request
if req, err = d.httpClient.NewRequest("GET", InfoUrl, "", params); err != nil {
log.Error("wallet metal newRequest err:%s", err.Error())
return
}
queryUrl := req.URL.String()
var res struct {
Code int `json:"code"`
Data model.MetalData `json:"data"`
}
err = d.httpClient.Do(c, req, &res)
if err != nil {
log.Error("wallet query metal err url: %s,err:%s", queryUrl, err.Error())
return
}
resStr, _ := json.Marshal(res)
log.Info("wallet query metal success url:%s,res:%s", queryUrl, resStr)
if res.Code != 0 {
err = respCodeError
} else {
metal = res.Data.Count
}
return
}
func (d *Dao) ModifyMetal(c context.Context, uid int64, coins int64, seeds int64, reason interface{}) (success bool, code int, err error) {
if coins == 0 {
err = paramError
return
}
if coins < 0 {
// 检查是否足够
metal, _ := d.GetMetal(c, uid)
if metal < float64(0-coins) {
err = ecode.CoinNotEnough
return
}
}
params := url.Values{}
params.Set("mid", fmt.Sprintf("%d", uid))
params.Set("count", fmt.Sprintf("%d", coins))
now := time.Now().Unix()
params.Set("ts", fmt.Sprintf("%d", now))
var realReason string
switch reason.(type) {
case string:
realReason = reason.(string)
default:
if coins < 0 {
realReason = fmt.Sprintf(ExchangeSilverReason, seeds)
} else {
realReason = ExchangeMetalReason
}
}
log.Info("user %d consume or recharge metal %d by reason %s", uid, coins, realReason)
params.Set("reason", realReason)
var req *http.Request
req, err = d.httpClient.NewRequest("POST", ModifyUrl, "", params)
if err != nil {
log.Error("wallet metal newRequest err:%s", err.Error())
return
}
queryUrl := req.URL.String()
var res struct {
Code int `json:"code"`
}
err = d.httpClient.Do(c, req, &res)
if err != nil {
log.Error("Metal#wallet query metal err url: %s,err:%s uid:%d, count:%d", queryUrl, err.Error(), uid, coins)
// 认为成功
err = nil
success = true
code = 0
return
}
resStr, _ := json.Marshal(res)
log.Info("wallet query metal success url:%s, uid:%d, count:%d res:%s", queryUrl, uid, coins, resStr)
code = res.Code
if res.Code == 0 {
success = true
} else {
if res.Code == ecode.LackOfCoins.Code() {
err = ecode.CoinNotEnough
} else {
log.Error("Metal#wallet query metal code failed : uid:%d, count:%d,code:%d", uid, coins, res.Code)
err = respCodeError
}
}
return
}

View File

@@ -0,0 +1,62 @@
package dao
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/library/ecode"
"testing"
)
func TestDao_GetMetal(t *testing.T) {
Convey("GetMetal", t, func() {
once.Do(startService)
var uid int64 = 1
metal, err := d.GetMetal(ctx, uid)
So(err, ShouldBeNil)
So(metal, ShouldBeGreaterThanOrEqualTo, 0)
})
}
func TestDao_ModifyMetal(t *testing.T) {
Convey("ModifyMetal", t, func() {
once.Do(startService)
var uid int64 = 1
metal, err := d.GetMetal(ctx, uid)
So(err, ShouldBeNil)
if metal < 10 {
d.ModifyMetal(ctx, uid, 10, 0, nil)
}
var coins int64 = -5
success, code, err := d.ModifyMetal(ctx, uid, coins, 500, "ut")
So(code, ShouldEqual, 0)
So(success, ShouldEqual, true)
So(err, ShouldBeNil)
coins = 0 - coins
success, code, err = d.ModifyMetal(ctx, uid, coins, 0, nil)
So(code, ShouldEqual, 0)
So(success, ShouldEqual, true)
So(err, ShouldBeNil)
nmetal, err := d.GetMetal(ctx, uid)
So(err, ShouldBeNil)
So(metal, ShouldEqual, nmetal)
})
Convey("ModifyMetal not enough", t, func() {
once.Do(startService)
var uid int64 = 1
metal, err := d.GetMetal(ctx, uid)
So(err, ShouldBeNil)
coins := 0 - int64(metal+1)
_, _, err = d.ModifyMetal(ctx, uid, coins, 400, nil)
So(err, ShouldEqual, ecode.CoinNotEnough)
})
}

View File

@@ -0,0 +1,16 @@
package dao
import (
"context"
"go-common/library/log"
"strconv"
)
func (d *Dao) Pub(c context.Context, uid int64, msg interface{}) error {
key := strconv.FormatInt(uid, 10)
err := d.changeDataBus.Send(c, key, msg)
if err != nil {
log.Error("pub wallet change failed uid:%d, msg:%+v", uid, msg)
}
return err
}

View File

@@ -0,0 +1,29 @@
package dao
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
type testMsg struct {
Name string
Age int64
}
func TestDao_Pub(t *testing.T) {
Convey("pub", t, func() {
once.Do(startService)
//startSubDataBus()
//msgs := subDataBus.Messages()
var uid int64 = 1
u := testMsg{
Name: "test",
Age: 23,
}
err := d.Pub(ctx, uid, &u)
So(err, ShouldBeNil)
t.Logf("pub")
})
}

View File

@@ -0,0 +1,115 @@
package dao
import (
"context"
"fmt"
"github.com/pkg/errors"
"go-common/library/cache/redis"
"go-common/library/log"
"time"
)
var (
UnLockGetWrong = "UnLockGetWrong"
LockFailed = "LockFailed"
UserWalletPrefix = "user_wallet_lock_uid_"
ErrUnLockGet = errors.New(UnLockGetWrong)
ErrLockFailed = errors.New(LockFailed)
)
func (d *Dao) IsLockFailedError(err error) bool {
return err == ErrLockFailed
}
func lockKey(k string) string {
return "wallet_lock_key:" + k
}
/*
ttl ms
retry 重试次数
retryDelay us
*/
func (d *Dao) Lock(ctx context.Context, key string, ttl int, retry int, retryDelay int) (err error, gotLock bool, lockValue string) {
if retry <= 0 {
retry = 1
}
lockValue = "locked:" + randomString(5)
retryTimes := 0
conn := d.redis.Get(ctx)
defer conn.Close()
realKey := lockKey(key)
for ; retryTimes < retry; retryTimes++ {
var res interface{}
res, err = conn.Do("SET", realKey, lockValue, "PX", ttl, "NX")
if err != nil {
log.Error("redis_lock failed:%s:%s", realKey, err.Error())
break
}
if res != nil {
gotLock = true
break
}
time.Sleep(time.Duration(retryDelay * 1000))
}
return
}
func (d *Dao) UnLock(ctx context.Context, key string, lockValue string) (err error) {
conn := d.redis.Get(ctx)
defer conn.Close()
realKey := lockKey(key)
res, err := redis.String(conn.Do("GET", realKey))
if err != nil {
return
}
if res != lockValue {
err = ErrUnLockGet
return
}
_, err = conn.Do("DEL", realKey)
return
}
func (d *Dao) ForceUnLock(ctx context.Context, key string) (err error) {
realKey := lockKey(key)
conn := d.redis.Get(ctx)
defer conn.Close()
_, err = conn.Do("DEL", realKey)
return
}
func (d *Dao) LockTransactionId(ctx context.Context, tid string) (err error) {
err, gotLock, _ := d.Lock(ctx, tid, 300*1000, 0, 200000)
if err != nil {
return
}
if !gotLock {
err = ErrLockFailed
}
return
}
func (d *Dao) LockUser(ctx context.Context, uid int64) (err error, gotLock bool, lockValue string) {
lockTime := 600
retry := 1
retryDelay := 10
return d.Lock(ctx, getUserLockKey(uid), lockTime*1000, retry, retryDelay*1000)
}
func (d *Dao) UnLockUser(ctx context.Context, uid int64, lockValue string) error {
return d.UnLock(ctx, getUserLockKey(uid), lockValue)
}
func getUserLockKey(uid int64) string {
return fmt.Sprintf("%s%v", UserWalletPrefix, uid)
}

View File

@@ -0,0 +1,153 @@
package dao
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"go-common/library/cache/redis"
"testing"
)
func TestDao_Lock(t *testing.T) {
Convey("Lock", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
ttl := 2000
retryDelay := 10000
err, gotLock, lockValue := d.Lock(ctx, key, ttl, 0, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
})
Convey("Lock Twice", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
ttl := 2000
retryDelay := 10000
err, gotLock, lockValue := d.Lock(ctx, key, ttl, 0, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
err, gotLock, lockValue = d.Lock(ctx, key, ttl, 3, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, false)
})
}
func TestDao_UnLock(t *testing.T) {
Convey("UnLock", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
value := "test"
err := d.UnLock(ctx, key, value)
So(err, ShouldEqual, redis.ErrNil)
})
Convey("UnLock wrong value", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
ttl := 2000
retryDelay := 10000
err, gotLock, lockValue := d.Lock(ctx, key, ttl, 3, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
value := "test"
err = d.UnLock(ctx, key, value)
So(err, ShouldEqual, ErrUnLockGet)
})
Convey("LockAndUnLock", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
ttl := 2000
retryDelay := 10000
err, gotLock, lockValue := d.Lock(ctx, key, ttl, 0, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
err = d.UnLock(ctx, key, lockValue)
So(err, ShouldBeNil)
err, gotLock, lockValue = d.Lock(ctx, key, ttl, 0, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
})
}
func TestDao_ForceUnLock(t *testing.T) {
Convey("ForceUnLock", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
err := d.ForceUnLock(ctx, key)
So(err, ShouldBeNil)
})
Convey("LockAndForceUnLock", t, func() {
once.Do(startService)
key := "test_lock_key" + randomString(5)
ttl := 2000
retryDelay := 10000
err, gotLock, lockValue := d.Lock(ctx, key, ttl, 0, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
err = d.ForceUnLock(ctx, key)
So(err, ShouldBeNil)
err, gotLock, lockValue = d.Lock(ctx, key, ttl, 0, retryDelay)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
So(lockValue, ShouldNotBeNil)
})
}
func TestDao_LockTransactionId(t *testing.T) {
Convey("LockTid Twice", t, func() {
tid := GetTid(model.PAYTYPE, "test")
err := d.LockTransactionId(ctx, tid)
So(err, ShouldBeNil)
err = d.LockTransactionId(ctx, tid)
So(err, ShouldEqual, ErrLockFailed)
})
}
func TestDao_LockUser(t *testing.T) {
Convey("LockUser", t, func() {
uid := r.Int63n(100000000)
err, gotLock, localValue := d.LockUser(ctx, uid)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
err = d.UnLockUser(ctx, uid, localValue)
So(err, ShouldBeNil)
err, gotLock, localValue = d.LockUser(ctx, uid)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
})
Convey("LockUser Twice", t, func() {
uid := r.Int63n(100000000)
err, gotLock, _ := d.LockUser(ctx, uid)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, true)
err, gotLock, _ = d.LockUser(ctx, uid)
So(err, ShouldBeNil)
So(gotLock, ShouldEqual, false)
})
}

View File

@@ -0,0 +1,190 @@
package dao
import (
"context"
"fmt"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
"time"
)
const (
_selWallet = "SELECT uid,gold,iap_gold,silver,cost_base,gold_recharge_cnt,gold_pay_cnt,silver_pay_cnt,snapshot_time,snapshot_gold,snapshot_iap_gold,snapshot_silver,reserved1,reserved2 FROM user_wallet_%d WHERE uid=? FOR UPDATE"
_recharge = "UPDATE user_wallet_%d set %s = %s + %d,%s=%s+%d where uid = ?"
_rechargeWihoutCnt = "UPDATE user_wallet_%d set %s=%s + %d where uid = ?"
_rechargeWithSnap = "UPDATE user_wallet_%d set %s=%s + %d,%s = %s+ %d, snapshot_time = ? , snapshot_gold = ? , snapshot_iap_gold = ? , snapshot_silver = ? where uid = ?"
_rechargeWithSnapWithoutCnt = "UPDATE user_wallet_%d set %s = %s + %d, snapshot_time = ? , snapshot_gold = ? , snapshot_iap_gold = ? , snapshot_silver = ? where uid = ?"
_exchange = "UPDATE user_wallet_%d set %s = %s - %d , %s = %s + %d, %s = %s + %d, %s = %s + %d where uid = ?"
_exhcangeWithSnap = "UPDATE user_wallet_%d set %s = %s - %d , %s = %s + %d, %s = %s + %d, %s = %s + %d, snapshot_time = ? , snapshot_gold = ? , snapshot_iap_gold = ? , snapshot_silver = ? where uid = ? "
_modifyCnt = "UPDATE user_wallet_%d set gold_pay_cnt = gold_pay_cnt + %d, gold_recharge_cnt = gold_recharge_cnt + %d, silver_pay_cnt = silver_pay_cnt + %d where uid = ?"
)
// 开启事务
func (d *Dao) BeginTx(c context.Context) (conn *sql.Tx, err error) {
return d.db.Begin(c)
}
func (d *Dao) DoTx(c context.Context, doFunc func(conn *sql.Tx) (v interface{}, err error)) (v interface{}, err error) {
conn, err := d.BeginTx(c)
if err != nil {
err = ecode.ServerErr
return
}
v, err = doFunc(conn)
var txErr error
if err != nil {
conn.Rollback()
err = ecode.ServerErr
} else {
txErr = conn.Commit()
if txErr != nil {
err = ecode.ServerErr
v = nil
}
}
return v, err
}
// 为了后续的更新获取数据
func (d *Dao) WalletForUpdate(conn *sql.Tx, uid int64) (wallet *model.DetailWithSnapShot, err error) {
row := conn.QueryRow(fmt.Sprintf(_selWallet, tableIndex(uid)), uid)
wallet = &model.DetailWithSnapShot{}
var snapShotTime time.Time
if err = row.Scan(&wallet.Uid, &wallet.Gold, &wallet.IapGold, &wallet.Silver, &wallet.CostBase, &wallet.GoldRechargeCnt,
&wallet.GoldPayCnt, &wallet.SilverPayCnt, &snapShotTime, &wallet.SnapShotGold, &wallet.SnapShotIapGold,
&wallet.SnapShotSilver, &wallet.Reserved1, &wallet.Reserved2); err == sql.ErrNoRows {
// 查询结果为空时,初始化数据
_, err = d.InitWalletInTx(conn, uid, 0, 0, 0)
wallet.SnapShotTime = snapShotTime.Format("2006-01-02 15:04:05")
return
}
if err != nil {
log.Error("[tx.wallet|Melonseed] row.Scan err: %s", err.Error())
return
}
wallet.SnapShotTime = snapShotTime.Format("2006-01-02 15:04:05")
return
}
// InitExp 初始化用户钱包,用于首次查询
func (d *Dao) InitWalletInTx(conn *sql.Tx, uid int64, gold int64, iap_gold int64, silver int64) (row int64, err error) {
res, err := conn.Exec(fmt.Sprintf(_insWallet, tableIndex(uid)), uid, gold, iap_gold, silver)
if err != nil {
log.Error("[tx.wallet|InitWallet] Exec err: %v", err)
return
}
return res.RowsAffected()
}
func (d *Dao) execSqlInTx(conn *sql.Tx, sql *string, params ...interface{}) (affect int64, err error) {
res, err := conn.Exec(*sql, params...)
if err != nil {
log.Error("[tx.wallet|execSqlInTx] Exec err: %v sql:%s", err, *sql)
return
}
return res.RowsAffected()
}
func (d *Dao) changeCoinInTx(conn *sql.Tx, uid int64, sysCoinTypeNo int32, num int64, originWallet *model.DetailWithSnapShot, cntField string) (affect int64, err error) {
// 判断
coinType := model.GetSysCoinTypeByNo(sysCoinTypeNo)
var s string
absNum := num
if absNum < 0 {
absNum = -absNum
}
if model.TodayNeedSnapShot(originWallet) {
if cntField == "" {
s = getRechargeWithoutCntWithSnapShotSQL(uid, coinType, num)
} else {
s = getRechargeWithSnapShotSQL(uid, coinType, num, cntField, absNum)
}
date := time.Now().Format("2006-01-02 15:04:05")
return d.execSqlInTx(conn, &s, date, originWallet.Gold, originWallet.IapGold, originWallet.Silver, uid)
} else {
if cntField == "" {
s = getRechargeWithoutCntSQL(uid, coinType, num)
} else {
s = getRechargeSQL(uid, coinType, num, cntField, absNum)
}
return d.execSqlInTx(conn, &s, uid)
}
}
// RechargeGold 充值IOS金瓜子 记入充值总值
func (d *Dao) RechargeCoinInTx(conn *sql.Tx, uid int64, sysCoinTypeNo int32, num int64, originWallet *model.DetailWithSnapShot) (affect int64, err error) {
rechargerCntField := model.GetRechargeCnt(sysCoinTypeNo)
return d.changeCoinInTx(conn, uid, sysCoinTypeNo, num, originWallet, rechargerCntField)
}
func (d *Dao) PayCoinInTx(conn *sql.Tx, uid int64, sysCoinTypeNo int32, num int64, originWallet *model.DetailWithSnapShot) (affect int64, err error) {
cntField := model.GetPayCnt(sysCoinTypeNo)
return d.changeCoinInTx(conn, uid, sysCoinTypeNo, -num, originWallet, cntField)
}
func (d *Dao) ModifyCoinInTx(conn *sql.Tx, uid int64, sysCoinTypeNo int32, num int64, originWallet *model.DetailWithSnapShot) (affect int64, err error) {
return d.changeCoinInTx(conn, uid, sysCoinTypeNo, num, originWallet, "")
}
func (d *Dao) ExchangeCoinInTx(conn *sql.Tx, uid int64, srcSysCoinTypeNo int32, srcNum int64, destSysCoinTypeNo int32, destNum int64, originWallet *model.DetailWithSnapShot) (affect int64, err error) {
rechargeCntNum := destNum
var rechargerCntField string
if destSysCoinTypeNo == model.SysCoinTypeSilver { // 如果为银瓜子则不存在充值统计总数字段 使用 消费统计字段代替 适配sql
rechargerCntField = "silver_pay_cnt"
rechargeCntNum = 0
} else {
rechargerCntField = model.GetRechargeCnt(destSysCoinTypeNo)
}
payCntField := model.GetPayCnt(srcSysCoinTypeNo)
srcCoinType := model.GetSysCoinTypeByNo(srcSysCoinTypeNo)
destCoinType := model.GetSysCoinTypeByNo(destSysCoinTypeNo)
var s string
if model.TodayNeedSnapShot(originWallet) {
s = fmt.Sprintf(_exhcangeWithSnap, tableIndex(uid), srcCoinType, srcCoinType, srcNum, destCoinType, destCoinType, destNum, rechargerCntField, rechargerCntField, rechargeCntNum, payCntField, payCntField, srcNum)
date := time.Now().Format("2006-01-02 15:04:05")
return d.execSqlInTx(conn, &s, date, originWallet.Gold, originWallet.IapGold, originWallet.Silver, uid)
} else {
s = fmt.Sprintf(_exchange, tableIndex(uid), srcCoinType, srcCoinType, srcNum, destCoinType, destCoinType, destNum, rechargerCntField, rechargerCntField, rechargeCntNum, payCntField, payCntField, srcNum)
return d.execSqlInTx(conn, &s, uid)
}
}
func getRechargeWithSnapShotSQL(uid int64, coinType string, num int64, cntField string, cntNum int64) string {
return fmt.Sprintf(_rechargeWithSnap, tableIndex(uid), coinType, coinType, num, cntField, cntField, cntNum)
}
func getRechargeWithoutCntWithSnapShotSQL(uid int64, coinType string, num int64) string {
return fmt.Sprintf(_rechargeWithSnapWithoutCnt, tableIndex(uid), coinType, coinType, num)
}
func getRechargeSQL(uid int64, coinType string, num int64, cntField string, cntNum int64) string {
return fmt.Sprintf(_recharge, tableIndex(uid), coinType, coinType, num, cntField, cntField, cntNum)
}
func getRechargeWithoutCntSQL(uid int64, coinType string, num int64) string {
return fmt.Sprintf(_rechargeWihoutCnt, tableIndex(uid), coinType, coinType, num)
}
func (d *Dao) NewCoinStreamRecordInTx(conn *sql.Tx, record *model.CoinStreamRecord) (int64, error) {
s := fmt.Sprintf(_newCoinStremaRecord, getCoinStreamTable(record.TransactionId))
date := model.GetWalletFormatTime(record.OpTime)
return d.execSqlInTx(conn, &s, record.Uid, record.TransactionId, record.ExtendTid, record.CoinType,
record.DeltaCoinNum, record.OrgCoinNum, record.OpResult, record.OpReason, record.OpType, date,
record.BizCode, record.Area, record.Source, record.MetaData, record.BizSource,
record.Platform, record.Reserved1, record.Version)
}
func (d *Dao) NewCoinExchangeRecordInTx(conn *sql.Tx, record *model.CoinExchangeRecord) (int64, error) {
s := fmt.Sprintf(_newCoinExchange, getCoinExchangeTable(record.Uid))
date := model.GetWalletFormatTime(record.ExchangeTime)
return d.execSqlInTx(conn, &s, record.Uid, record.TransactionId, record.SrcType, record.SrcNum, record.DestType, record.DestNum, record.Status, date)
}
func (d *Dao) ModifyCntInTx(conn *sql.Tx, uid int64, goldPay int, goldRecharge int, silverPay int) (int64, error) {
s := fmt.Sprintf(_modifyCnt, tableIndex(uid), goldPay, goldRecharge, silverPay)
return d.execSqlInTx(conn, &s, uid)
}

View File

@@ -0,0 +1,138 @@
package dao
import (
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"testing"
"time"
)
func TestDao_Tx1(t *testing.T) {
Convey("commit wrong sample", t, func() {
once.Do(startService)
uid := int64(1)
var wallet *model.Melonseed
wallet, err := d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
og := wallet.Gold
So(err, ShouldBeNil)
So(1, ShouldEqual, 1)
tx, err := d.db.Begin(ctx)
So(err, ShouldBeNil)
affect, err := d.AddGold(ctx, uid, 1)
So(affect, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
err = tx.Commit()
So(err, ShouldBeNil)
wallet, err = d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
So(err, ShouldBeNil)
So(wallet.Gold-og, ShouldEqual, 1)
})
Convey("rollback wrong sample use db ins instead tx", t, func() {
once.Do(startService)
uid := int64(1)
var wallet *model.Melonseed
wallet, err := d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
og := wallet.Gold
So(err, ShouldBeNil)
So(1, ShouldEqual, 1)
tx, err := d.db.Begin(ctx)
So(err, ShouldBeNil)
affect, err := d.AddGold(ctx, uid, 1)
So(affect, ShouldBeGreaterThan, 0)
So(err, ShouldBeNil)
err = tx.Rollback()
So(err, ShouldBeNil)
wallet, err = d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
So(err, ShouldBeNil)
So(wallet.Gold-og, ShouldEqual, 1)
})
Convey("commit", t, func() {
once.Do(startService)
uid := int64(1)
var wallet *model.Melonseed
wallet, err := d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
og := wallet.Gold
So(err, ShouldBeNil)
So(1, ShouldEqual, 1)
tx, err := d.db.Begin(ctx)
So(err, ShouldBeNil)
res, err := tx.Exec(fmt.Sprintf("update user_wallet_%d set gold = gold + ? where uid = ?", uid%10), 1, uid)
So(err, ShouldBeNil)
affect, err := res.RowsAffected()
So(affect, ShouldEqual, 1)
So(err, ShouldBeNil)
err = tx.Commit()
So(err, ShouldBeNil)
wallet, err = d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
So(err, ShouldBeNil)
So(wallet.Gold-og, ShouldEqual, 1)
})
Convey("rollback", t, func() {
once.Do(startService)
uid := int64(1)
var wallet *model.Melonseed
wallet, err := d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
og := wallet.Gold
So(err, ShouldBeNil)
So(1, ShouldEqual, 1)
tx, err := d.db.Begin(ctx)
So(err, ShouldBeNil)
res, err := tx.Exec(fmt.Sprintf("update user_wallet_%d set gold = gold + ? where uid = ?", uid%10), 1, uid)
So(err, ShouldBeNil)
affect, err := res.RowsAffected()
So(affect, ShouldEqual, 1)
So(err, ShouldBeNil)
err = tx.Rollback()
So(err, ShouldBeNil)
wallet, err = d.Melonseed(ctx, uid)
t.Logf("uid gold : %d", wallet.Gold)
So(err, ShouldBeNil)
So(wallet.Gold-og, ShouldEqual, 0)
})
}
func TestDao_DoubleCoin(t *testing.T) {
Convey("check1", t, func() {
d := &model.DetailWithSnapShot{}
So(model.NeedSnapshot(d, time.Now()), ShouldBeTrue)
d.SnapShotTime = "2018-09-22 16:39:05"
So(model.NeedSnapshot(d, time.Now()), ShouldBeTrue)
now, _ := time.Parse("2006-01-02 15:04:05", "2018-09-22 16:39:04")
t.Logf("today:%+v", now)
So(model.NeedSnapshot(d, now), ShouldBeFalse)
now = model.GetTodayTime(now)
t.Logf("today:%+v", now)
d.SnapShotTime = "2018-09-22 04:39:04"
So(model.NeedSnapshot(d, now), ShouldBeFalse)
d.SnapShotTime = "2018-09-21 04:39:04"
So(model.NeedSnapshot(d, now), ShouldBeTrue)
d.SnapShotTime = time.Now().Format("2006-01-02 15:04:05")
t.Logf("today:%+v", d.SnapShotTime)
So(model.TodayNeedSnapShot(d), ShouldBeFalse)
d.SnapShotTime = time.Now().Add(-time.Second * 86400).Format("2006-01-02 15:04:05")
t.Logf("today:%+v", d.SnapShotTime)
So(model.TodayNeedSnapShot(d), ShouldBeTrue)
})
}

View File

@@ -0,0 +1,146 @@
package dao
import (
"context"
"database/sql"
"fmt"
"go-common/app/service/live/wallet/model"
"go-common/library/log"
)
const (
_shard = 10
_insWallet = "INSERT IGNORE INTO user_wallet_%d (uid,gold,iap_gold,silver) VALUES(?,?,?,?)"
_selMelonseed = "SELECT uid,gold,iap_gold,silver FROM user_wallet_%d WHERE uid=?"
_selDetail = "SELECT uid,gold,iap_gold,silver,gold_recharge_cnt,gold_pay_cnt,silver_pay_cnt, cost_base FROM user_wallet_%d WHERE uid=?"
_addGold = "INSERT INTO user_wallet_%d(uid,gold) VALUES(?,?) ON DUPLICATE KEY UPDATE gold=gold+%d"
_rechargeGold = "INSERT INTO user_wallet_%d(uid,gold,gold_recharge_cnt) VALUES(?,?,?) ON DUPLICATE KEY UPDATE gold=gold+%d,gold_recharge_cnt=gold_recharge_cnt+%d"
_addIapGold = "INSERT INTO user_wallet_%d(uid,iap_gold) VALUES(?,?) ON DUPLICATE KEY UPDATE iap_gold=iap_gold+%d"
_rechargeIapGold = "INSERT INTO user_wallet_%d(uid,iap_gold,gold_recharge_cnt) VALUES(?,?,?) ON DUPLICATE KEY UPDATE iap_gold=iap_gold+%d,gold_recharge_cnt=gold_recharge_cnt+%d"
_addSilver = "INSERT INTO user_wallet_%d(uid,silver) VALUES(?,?) ON DUPLICATE KEY UPDATE silver=silver+%d"
_consumeGold = "UPDATE user_wallet_%d SET gold=gold-%d,gold_pay_cnt=gold_pay_cnt+%d WHERE uid=?"
_consumeIapGold = "UPDATE user_wallet_%d SET iap_gold=iap_gold-%d,gold_pay_cnt=gold_pay_cnt+%d WHERE uid=?"
_consumeSilver = "UPDATE user_wallet_%d SET silver=silver-%d,silver_pay_cnt=silver_pay_cnt+%d WHERE uid=?"
_changeCosetBase = "UPDATE user_wallet_%d SET cost_base=? WHERE uid=?"
)
func tableIndex(uid int64) int64 {
return uid % _shard
}
// InitExp 初始化用户钱包,用于首次查询
func (d *Dao) InitWallet(c context.Context, uid int64, gold int64, iap_gold int64, silver int64) (row int64, err error) {
res, err := d.db.Exec(c, fmt.Sprintf(_insWallet, tableIndex(uid)), uid, gold, iap_gold, silver)
if err != nil {
log.Error("[dao.wallet|InitWallet] d.db.Exec err: %v", err)
return
}
return res.RowsAffected()
}
// Melonseed 获取瓜子数
func (d *Dao) Melonseed(c context.Context, uid int64) (wallet *model.Melonseed, err error) {
row := d.db.QueryRow(c, fmt.Sprintf(_selMelonseed, tableIndex(uid)), uid)
wallet = &model.Melonseed{}
if err = row.Scan(&wallet.Uid, &wallet.Gold, &wallet.IapGold, &wallet.Silver); err == sql.ErrNoRows {
// 查询结果为空时,初始化数据
_, err = d.InitWallet(c, uid, 0, 0, 0)
}
if err != nil {
log.Error("[dao.wallet|Melonseed] row.Scan err: %v", err)
return
}
return
}
// Detail 详细数据
func (d *Dao) Detail(c context.Context, uid int64) (detail *model.Detail, err error) {
row := d.db.QueryRow(c, fmt.Sprintf(_selDetail, tableIndex(uid)), uid)
detail = &model.Detail{}
err = row.Scan(&detail.Uid, &detail.Gold, &detail.IapGold, &detail.Silver, &detail.GoldRechargeCnt,
&detail.GoldPayCnt, &detail.SilverPayCnt, &detail.CostBase)
if err == sql.ErrNoRows {
detail.Gold = 0
detail.IapGold = 0
detail.Silver = 0
detail.GoldPayCnt = 0
detail.GoldRechargeCnt = 0
detail.SilverPayCnt = 0
detail.CostBase = 0
detail.Uid = uid
err = nil
return
}
if err != nil {
log.Error("[dao.wallet|Detail] row.Scan err: %v", err)
return
}
return
}
func (d *Dao) DetailWithoutDefault(c context.Context, uid int64) (detail *model.Detail, err error) {
row := d.db.QueryRow(c, fmt.Sprintf(_selDetail, tableIndex(uid)), uid)
detail = &model.Detail{}
err = row.Scan(&detail.Uid, &detail.Gold, &detail.IapGold, &detail.Silver, &detail.GoldRechargeCnt,
&detail.GoldPayCnt, &detail.SilverPayCnt, &detail.CostBase)
return
}
// AddGold 添加金瓜子
func (d *Dao) AddGold(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_addGold, tableIndex(uid), num)
return execSqlWithBindParams(d, c, &s, uid, num)
}
// RechargeGold 充值金瓜子 记入充值总值
func (d *Dao) RechargeGold(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_rechargeGold, tableIndex(uid), num, num)
return execSqlWithBindParams(d, c, &s, uid, num, num)
}
// RechargeGold 添加IOS金瓜子
func (d *Dao) AddIapGold(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_addIapGold, tableIndex(uid), num)
return execSqlWithBindParams(d, c, &s, uid, num)
}
// RechargeGold 充值IOS金瓜子 记入充值总值
func (d *Dao) RechargeIapGold(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_rechargeIapGold, tableIndex(uid), num, num)
return execSqlWithBindParams(d, c, &s, uid, num, num)
}
// AddSilver 添加银瓜子
func (d *Dao) AddSilver(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_addSilver, tableIndex(uid), num)
return execSqlWithBindParams(d, c, &s, uid, num)
}
// AddSilver 消费金瓜子 记入消费总值
func (d *Dao) ConsumeGold(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_consumeGold, tableIndex(uid), num, num)
return execSqlWithBindParams(d, c, &s, uid)
}
// AddSilver 消费IOS金瓜子 记入消费总值
func (d *Dao) ConsumeIapGold(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_consumeIapGold, tableIndex(uid), num, num)
return execSqlWithBindParams(d, c, &s, uid)
}
// ConsumeSilver 消费银瓜子 记入消费总值
func (d *Dao) ConsumeSilver(c context.Context, uid int64, num int) (affect int64, err error) {
s := fmt.Sprintf(_consumeSilver, tableIndex(uid), num, num)
return execSqlWithBindParams(d, c, &s, uid)
}
func (d *Dao) ChangeCostBase(c context.Context, uid int64, num int64) (affect int64, err error) {
s := fmt.Sprintf(_changeCosetBase, tableIndex(uid))
return execSqlWithBindParams(d, c, &s, num, uid)
}
// 更新镜像时间 方便测试
func (d *Dao) UpdateSnapShotTime(context context.Context, uid int64, time string) (int64, error) {
s := fmt.Sprintf("update user_wallet_%d set snapshot_time = ?", tableIndex(uid))
return execSqlWithBindParams(d, context, &s, time)
}

View File

@@ -0,0 +1,283 @@
package dao
import (
"context"
"os"
"sync"
"testing"
"time"
"go-common/app/service/live/wallet/conf"
"go-common/library/log"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"math/rand"
)
var (
once sync.Once
d *Dao
ctx = context.Background()
testUID int64
r *rand.Rand
)
func initConf() {
if err := conf.Init(); err != nil {
panic(err)
}
log.Init(conf.Conf.Log)
defer log.Close()
}
func TestMain(m *testing.M) {
conf.ConfPath = "../cmd/live-wallet-test.toml"
once.Do(startService)
os.Exit(m.Run())
}
func startService() {
initConf()
d = New(conf.Conf)
r = rand.New(rand.NewSource(time.Now().UnixNano()))
testUID = r.Int63n(100000000)
log.Info("test uid:%ld", testUID)
time.Sleep(time.Second * 1)
}
func TestInitWallet(t *testing.T) {
Convey("Init Wallet", t, func() {
once.Do(startService)
_, err := d.InitWallet(ctx, testUID, 0, 0, 0)
So(err, ShouldBeNil)
})
}
func TestMelonseed(t *testing.T) {
Convey("Melon seed", t, func() {
once.Do(startService)
var wallet *model.Melonseed
wallet, err := d.Melonseed(ctx, testUID)
So(err, ShouldBeNil)
So(wallet.Gold, ShouldEqual, 0)
})
}
func TestDetail(t *testing.T) {
Convey("Detail", t, func() {
once.Do(startService)
var detail *model.Detail
detail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(detail.Gold, ShouldEqual, 0)
So(detail.Silver, ShouldEqual, 0)
So(detail.SilverPayCnt, ShouldEqual, 0)
So(detail.IapGold, ShouldEqual, 0)
So(detail.GoldRechargeCnt, ShouldEqual, 0)
So(detail.GoldPayCnt, ShouldEqual, 0)
})
}
func TestDao_AddGold(t *testing.T) {
Convey("Add Gold", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.AddGold(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.Gold-odetail.Gold, ShouldEqual, num)
So(ndetail.GoldRechargeCnt, ShouldEqual, odetail.GoldRechargeCnt)
})
}
func TestDao_RechargeGold(t *testing.T) {
Convey("recharge gold", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.RechargeGold(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.Gold-odetail.Gold, ShouldEqual, num)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, 0)
So(ndetail.GoldRechargeCnt-odetail.GoldRechargeCnt, ShouldEqual, num)
})
}
func TestDao_RechargeIapGold(t *testing.T) {
Convey("recharge iap gold", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.RechargeIapGold(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.Gold-odetail.Gold, ShouldEqual, 0)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, num)
So(ndetail.GoldRechargeCnt-odetail.GoldRechargeCnt, ShouldEqual, num)
})
}
func TestDao_AddIapGold(t *testing.T) {
Convey("Add iap Gold", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.AddIapGold(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.Gold-odetail.Gold, ShouldEqual, 0)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, num)
So(ndetail.GoldRechargeCnt, ShouldEqual, odetail.GoldRechargeCnt)
})
}
func TestDao_AddSilver(t *testing.T) {
Convey("Add silver", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.AddSilver(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.Gold-odetail.Gold, ShouldEqual, 0)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, 0)
So(ndetail.Silver-odetail.Silver, ShouldEqual, num)
})
}
func TestDao_ConsumeGold(t *testing.T) {
Convey("Consume Gold", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.RechargeGold(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
pay := 500
affect, err = d.ConsumeGold(ctx, testUID, pay)
So(err, ShouldBeNil)
So(affect, ShouldEqual, 1)
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.Gold-odetail.Gold, ShouldEqual, num-pay)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, 0)
So(ndetail.GoldRechargeCnt-odetail.GoldRechargeCnt, ShouldEqual, num)
So(ndetail.GoldPayCnt-odetail.GoldPayCnt, ShouldEqual, pay)
})
}
func TestDao_ConsumeIapGold(t *testing.T) {
Convey("Consume Iap Gold", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.RechargeIapGold(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
pay := 500
affect, err = d.ConsumeIapGold(ctx, testUID, pay)
So(err, ShouldBeNil)
So(affect, ShouldEqual, 1)
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, num-pay)
So(ndetail.Gold-odetail.Gold, ShouldEqual, 0)
So(ndetail.GoldRechargeCnt-odetail.GoldRechargeCnt, ShouldEqual, num)
So(ndetail.GoldPayCnt-odetail.GoldPayCnt, ShouldEqual, pay)
})
}
func TestDao_ConsumeSilver(t *testing.T) {
Convey("Consume Silver", t, func() {
once.Do(startService)
var testUID = r.Int63n(100000000)
var odetail *model.Detail
odetail, err := d.Detail(ctx, testUID)
So(err, ShouldBeNil)
num := 1000
affect, err := d.AddSilver(ctx, testUID, num)
So(err, ShouldBeNil)
So(affect, ShouldBeGreaterThanOrEqualTo, 1) // 插入affect=1 更新affect=2
pay := 500
affect, err = d.ConsumeSilver(ctx, testUID, pay)
So(err, ShouldBeNil)
So(affect, ShouldEqual, 1)
var ndetail *model.Detail
ndetail, err = d.Detail(ctx, testUID)
So(err, ShouldBeNil)
So(ndetail.IapGold-odetail.IapGold, ShouldEqual, 0)
So(ndetail.Gold-odetail.Gold, ShouldEqual, 0)
So(ndetail.Silver-odetail.Silver, ShouldEqual, num-pay)
So(ndetail.GoldRechargeCnt-odetail.GoldRechargeCnt, ShouldEqual, 0)
So(ndetail.GoldPayCnt-odetail.GoldPayCnt, ShouldEqual, 0)
So(ndetail.SilverPayCnt-odetail.SilverPayCnt, ShouldEqual, pay)
})
}

View File

@@ -0,0 +1,64 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"delCache_test.go",
"exchange_test.go",
"getTid_test.go",
"get_test.go",
"pay_test.go",
"recharge_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/dao:go_default_library",
"//app/service/live/wallet/model:go_default_library",
"//app/service/live/wallet/service:go_default_library",
"//library/ecode:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"http.go",
"wallet.go",
],
importpath = "go-common/app/service/live/wallet/http",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/model:go_default_library",
"//app/service/live/wallet/service:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/http/blademaster:go_default_library",
"//library/net/http/blademaster/middleware/verify:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,43 @@
package http
import (
"context"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/library/ecode"
"net/url"
"testing"
)
type DelCacheRes struct {
Code int `json:"code"`
}
func queryDelCache(t *testing.T, uid int64) *DelCacheRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", uid))
req, _ := client.NewRequest("GET", _delCacheURL, "127.0.0.1", params)
var res DelCacheRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func TestDelCache(t *testing.T) {
once.Do(startHTTP)
Convey("Del Cache", t, func() {
var uid int64 = 1
queryGet(t, uid, "pc")
r := queryDelCache(t, uid)
So(r.Code, ShouldEqual, 0)
r = queryDelCache(t, uid)
So(r.Code, ShouldEqual, ecode.NothingFound)
})
}

View File

@@ -0,0 +1,95 @@
package http
import (
"context"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"net/url"
"testing"
"time"
)
func queryExchange(t *testing.T, form *model.ExchangeForm, platform string) *RechargeRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", form.Uid))
params.Set("src_coin_type", form.SrcCoinType)
params.Set("src_coin_num", fmt.Sprintf("%d", form.SrcCoinNum))
params.Set("dest_coin_type", form.DestCoinType)
params.Set("dest_coin_num", fmt.Sprintf("%d", form.DestCoinNum))
params.Set("extend_tid", form.ExtendTid)
params.Set("timestamp", fmt.Sprintf("%d", form.Timestamp))
params.Set("transaction_id", form.TransactionId)
req, _ := client.NewRequest("POST", _exchangeURL, "127.0.0.1", params)
req.Header.Set("platform", platform)
var res RechargeRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func getTestExchangeForm(t *testing.T, uid int64, srcCoinType string, srcCoinNum int64, destCoinType string, destCoinNum int64, tid interface{}) *model.ExchangeForm {
if tid == nil {
res := queryGetTid(t, int32(model.EXCHANGETYPE), getTestParamsJson())
if res.Code != 0 {
t.Errorf("get tid failed code : %d", res.Code)
t.FailNow()
}
tid = res.Resp.TransactionId
}
return &model.ExchangeForm{
Uid: uid,
SrcCoinType: srcCoinType,
SrcCoinNum: srcCoinNum,
ExtendTid: getTestExtendTid(),
Timestamp: time.Now().Unix(),
TransactionId: tid.(string),
DestCoinNum: destCoinNum,
DestCoinType: destCoinType,
}
}
func TestExchange(t *testing.T) {
once.Do(startHTTP)
Convey("exchange normal 先调用get接口 再调用exchange 再调用get接口 比较用户钱包数据", t, func() {
platforms := []string{"pc", "android", "ios"}
var num int64 = 1000
var exchangeNum int64 = 100
uid := getTestRandUid()
for _, platform := range platforms {
beforeWallet := getTestWallet(t, uid, platform)
res := queryRecharge(t, getTestRechargeOrPayForm(t, int32(model.RECHARGETYPE), uid, "gold", num, nil), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Gold)-getIntCoinForTest(beforeWallet.Gold), ShouldEqual, num)
res = queryRecharge(t, getTestRechargeOrPayForm(t, int32(model.RECHARGETYPE), uid, "silver", num, nil), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Silver)-getIntCoinForTest(beforeWallet.Silver), ShouldEqual, num)
afterWallet := getTestWallet(t, uid, platform)
So(getIntCoinForTest(afterWallet.Gold)-getIntCoinForTest(beforeWallet.Gold), ShouldEqual, num)
So(getIntCoinForTest(afterWallet.Silver)-getIntCoinForTest(beforeWallet.Silver), ShouldEqual, num)
res = queryExchange(t, getTestExchangeForm(t, uid, "gold", exchangeNum, "silver", exchangeNum, nil), platform)
So(res.Code, ShouldEqual, 0)
afterExchangeWallet := getTestWallet(t, uid, platform)
So(getIntCoinForTest(afterExchangeWallet.Gold)-getIntCoinForTest(afterWallet.Gold), ShouldEqual, exchangeNum*-1)
So(getIntCoinForTest(afterExchangeWallet.Silver)-getIntCoinForTest(afterWallet.Silver), ShouldEqual, exchangeNum)
}
})
}

View File

@@ -0,0 +1,139 @@
package http
import (
"context"
"encoding/json"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"net/url"
"sync"
"testing"
"time"
)
type TidRes struct {
Code int `json:"code"`
Resp *model.TidResp `json:"data"`
}
type TestTidParams struct {
Biz string
Time int64
}
func getTestTidParams(biz string) *TestTidParams {
return &TestTidParams{
Biz: biz,
Time: time.Now().Unix(),
}
}
func getTestRandServiceType() int32 {
return r.Int31n(4)
}
func testWith(f func()) func() {
once.Do(startHTTP)
return f
}
func getTestParamsJson() string {
params := getTestTidParams("gift")
paramsBytes, _ := json.Marshal(params)
paramsJson := string(paramsBytes[:])
return paramsJson
}
func queryGetTid(t *testing.T, serviceType int32, tidParams string) *TidRes {
params := url.Values{}
params.Set("type", fmt.Sprintf("%d", serviceType))
params.Set("params", tidParams)
req, _ := client.NewRequest("POST", _getTidURL, "127.0.0.1", params)
var res TidRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func TestGetTid(t *testing.T) {
Convey("normal", t, testWith(func() {
serviceType := getTestRandServiceType()
paramsJson := getTestParamsJson()
res := queryGetTid(t, serviceType, paramsJson)
So(res.Code, ShouldEqual, 0)
So(res.Resp.TransactionId, ShouldNotEqual, "")
}))
Convey("Twice Same params 同样的参数调用getTid 得到的tid应该不一样", t, testWith(func() {
serviceType := getTestRandServiceType()
paramsJson := getTestParamsJson()
res := queryGetTid(t, serviceType, paramsJson)
So(res.Code, ShouldEqual, 0)
So(res.Resp.TransactionId, ShouldNotEqual, "")
res1 := queryGetTid(t, serviceType, paramsJson)
So(res1.Code, ShouldEqual, 0)
So(res1.Resp.TransactionId, ShouldNotEqual, res.Resp.TransactionId)
}))
Convey("Test multi Same params", t, testWith(func() {
serviceType := getTestRandServiceType()
paramsJson := getTestParamsJson()
resMap := make(map[int]*TidRes)
wg := sync.WaitGroup{}
var mutex sync.Mutex
times := 10
for i := 0; i < times; i++ {
wg.Add(1)
go func(i int) {
mutex.Lock()
resMap[i] = queryGetTid(t, serviceType, paramsJson)
mutex.Unlock()
wg.Done()
}(i)
}
wg.Wait()
tidMap := map[string]bool{}
for _, res := range resMap {
So(res.Code, ShouldEqual, 0)
So(res.Resp.TransactionId, ShouldNotEqual, "")
_, ok := tidMap[res.Resp.TransactionId]
So(ok, ShouldEqual, false) // 应该找不到 因为同样的参数应该生成不同的tid
tidMap[res.Resp.TransactionId] = true
}
}))
Convey("params error", t, func() {
Convey("serviceType 应该为 0 1 2 3 ", testWith(func() {
wrongServiceTypes := []int32{-1, 5}
for _, v := range wrongServiceTypes {
paramsJson := getTestParamsJson()
res := queryGetTid(t, v, paramsJson)
So(res.Code, ShouldEqual, ecode.RequestErr.Code())
}
}))
Convey("param不为空", testWith(func() {
st := getTestRandServiceType()
params := ""
res := queryGetTid(t, st, params)
So(res.Code, ShouldEqual, ecode.RequestErr.Code())
}))
})
}

View File

@@ -0,0 +1,155 @@
package http
import (
"context"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"net/url"
"strconv"
"testing"
)
type GetRes struct {
Code int `json:"code"`
Resp *model.MelonseedWithMetalResp `json:"data"`
}
type StatusRes struct {
Code int `json:"code"`
Resp *model.QueryResp `json:"data"`
}
type GetAllRes struct {
Code int `json:"code"`
Resp *model.DetailWithMetalResp `json:"data"`
}
func queryGet(t *testing.T, uid int64, platform string) *GetRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", uid))
req, _ := client.NewRequest("GET", _getURL, "127.0.0.1", params)
req.Header.Set("platform", platform)
var res GetRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func queryStatus(t *testing.T, uid int64, tid string) *StatusRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", uid))
params.Set("transaction_id", tid)
req, _ := client.NewRequest("GET", _queryURL, "127.0.0.1", params)
req.Header.Set("platform", "pc")
var res StatusRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func queryGetAll(t *testing.T, uid int64, platform string) *GetAllRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", uid))
req, _ := client.NewRequest("GET", _getAllURL, "127.0.0.1", params)
req.Header.Set("platform", platform)
var res GetAllRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func getTestWallet(t *testing.T, uid int64, platform string) *model.MelonseedWithMetalResp {
res := queryGet(t, uid, platform)
if res.Code != 0 {
t.Errorf("get wallet failed uid : %d, code :%d", uid, res.Code)
t.FailNow()
}
return res.Resp
}
/*
useless now
func getTestWalletDetail(t *testing.T, uid int64, platform string) *model.DetailWithMetalResp {
res := queryGetAll(t, uid, platform)
if res.Code != 0 {
t.Errorf("get wallet failed uid : %d, code :%d", uid, res.Code)
t.FailNow()
}
return res.Resp
}*/
func TestGet(t *testing.T) {
once.Do(startHTTP)
Convey("get normal", t, func() {
res := queryGet(t, 1, "pc")
So(res.Code, ShouldEqual, 0)
melon := res.Resp
coin, err := strconv.Atoi(melon.Gold)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
coin, err = strconv.Atoi(melon.Silver)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
})
Convey("uid params error", t, func() {
res := queryGet(t, -1, "pc")
So(res.Code, ShouldEqual, ecode.RequestErr)
})
Convey("platform params error", t, func() {
res := queryGet(t, 1, "pc1")
So(res.Code, ShouldEqual, ecode.RequestErr)
})
}
func TestGetAll(t *testing.T) {
once.Do(startHTTP)
Convey("normal", t, func() {
res := queryGetAll(t, 1, "pc")
t.Logf("all:%v", res)
So(res.Code, ShouldEqual, 0)
melon := res.Resp
coin, err := strconv.Atoi(melon.Gold)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
coin, err = strconv.Atoi(melon.Silver)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
coin, err = strconv.Atoi(melon.SilverPayCnt)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
coin, err = strconv.Atoi(melon.GoldPayCnt)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
coin, err = strconv.Atoi(melon.GoldRechargeCnt)
So(err, ShouldBeNil)
So(coin, ShouldBeGreaterThan, -1)
So(melon.CostBase, ShouldBeGreaterThanOrEqualTo, 0)
})
}

View File

@@ -0,0 +1,69 @@
package http
import (
"net/http"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/service"
"go-common/library/log"
bm "go-common/library/net/http/blademaster"
"go-common/library/net/http/blademaster/middleware/verify"
)
var (
// depend service
idfSvc *verify.Verify
walletSvr *service.Service
)
func Init(c *conf.Config, s *service.Service) {
// init service
walletSvr = s
// init external router
idfSvc = verify.New(nil)
engineOut := bm.DefaultServer(c.BM.Inner)
innerRouter(engineOut)
// init Inner serve
if err := engineOut.Start(); err != nil {
log.Error("bm.DefaultServer error(%v)", err)
panic(err)
}
engineLocal := bm.DefaultServer(c.BM.Local)
localRouter(engineLocal)
// init Local serve
if err := engineLocal.Start(); err != nil {
log.Error("bm.DefaultServer error(%v)", err)
panic(err)
}
}
// outerRouter init outer router api path.
func innerRouter(e *bm.Engine) {
// init api
e.Ping(ping)
group := e.Group("/x/internal/livewallet", idfSvc.Verify)
{
group.GET("/wallet/get", get)
group.GET("/wallet/delCache", delCache)
group.GET("/wallet/getAll", getAll)
group.POST("/wallet/getTid", getTid)
group.POST("/wallet/recharge", recharge)
group.GET("/wallet/query", query)
group.POST("/wallet/pay", pay)
group.POST("/wallet/exchange", exchange)
group.POST("/wallet/modify", modify)
group.POST("/flowwater/recordCoinStream", recordCoinStream)
}
}
// localRouter init local router.
func localRouter(e *bm.Engine) {
}
// ping check server ok.
func ping(c *bm.Context) {
if err := walletSvr.Ping(c); err != nil {
log.Error("live-userwallet http service ping error(%v)", err)
c.AbortWithStatus(http.StatusServiceUnavailable)
}
}

View File

@@ -0,0 +1,116 @@
package http
import (
"context"
"fmt"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"net/url"
"testing"
)
func queryPay(t *testing.T, form *model.RechargeOrPayForm, platform string) *RechargeRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", form.Uid))
params.Set("coin_type", form.CoinType)
params.Set("coin_num", fmt.Sprintf("%d", form.CoinNum))
params.Set("extend_tid", form.ExtendTid)
params.Set("timestamp", fmt.Sprintf("%d", form.Timestamp))
params.Set("transaction_id", form.TransactionId)
req, _ := client.NewRequest("POST", _payURL, "127.0.0.1", params)
req.Header.Set("platform", platform)
var res RechargeRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func queryPayWithReason(t *testing.T, form *model.RechargeOrPayForm, platform string, reason string) *RechargeRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", form.Uid))
params.Set("coin_type", form.CoinType)
params.Set("coin_num", fmt.Sprintf("%d", form.CoinNum))
params.Set("extend_tid", form.ExtendTid)
params.Set("timestamp", fmt.Sprintf("%d", form.Timestamp))
params.Set("transaction_id", form.TransactionId)
params.Set("reason", reason)
req, _ := client.NewRequest("POST", _payURL, "127.0.0.1", params)
req.Header.Set("platform", platform)
var res RechargeRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func TestPay(t *testing.T) {
once.Do(startHTTP)
Convey("pay normal 先调用get接口 再调用pay 再调用get接口 比较用户钱包数据", t, func() {
platforms := []string{"pc", "android", "ios"}
var num int64 = 1000
var payNum int64 = 100
uid := getTestRandUid()
for _, platform := range platforms {
beforeWallet := getTestWallet(t, uid, platform)
res := queryRecharge(t, getTestRechargeOrPayForm(t, int32(model.RECHARGETYPE), uid, "gold", num, nil), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Gold)-getIntCoinForTest(beforeWallet.Gold), ShouldEqual, num)
res = queryRecharge(t, getTestRechargeOrPayForm(t, int32(model.RECHARGETYPE), uid, "silver", num, nil), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Silver)-getIntCoinForTest(beforeWallet.Silver), ShouldEqual, num)
afterWallet := getTestWallet(t, uid, platform)
So(getIntCoinForTest(afterWallet.Gold)-getIntCoinForTest(beforeWallet.Gold), ShouldEqual, num)
So(getIntCoinForTest(afterWallet.Silver)-getIntCoinForTest(beforeWallet.Silver), ShouldEqual, num)
f1 := getTestRechargeOrPayForm(t, int32(model.PAYTYPE), uid, "gold", payNum, nil)
res = queryPay(t, f1, platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Gold)-getIntCoinForTest(afterWallet.Gold), ShouldEqual, -1*payNum)
sr := queryStatus(t, uid, f1.TransactionId)
So(sr.Code, ShouldEqual, 0)
So(sr.Resp.Status, ShouldEqual, 0)
res = queryPay(t, getTestRechargeOrPayForm(t, int32(model.PAYTYPE), uid, "silver", payNum, nil), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Silver)-getIntCoinForTest(afterWallet.Silver), ShouldEqual, -1*payNum)
payWallet := getTestWallet(t, uid, platform)
So(getIntCoinForTest(payWallet.Gold)-getIntCoinForTest(afterWallet.Gold), ShouldEqual, -1*payNum)
So(getIntCoinForTest(payWallet.Silver)-getIntCoinForTest(afterWallet.Silver), ShouldEqual, -1*payNum)
}
})
}
func TestPayMetal(t *testing.T) {
once.Do(startHTTP)
Convey("pay metal", t, func() {
var uid int64 = 1
platform := "pc"
f1 := getTestRechargeOrPayForm(t, int32(model.PAYTYPE), uid, "metal", 1, nil)
res := queryPayWithReason(t, f1, platform, "ut")
So(res.Code == 0 || res.Code == 1000000, ShouldBeTrue)
})
}

View File

@@ -0,0 +1,151 @@
package http
import (
"fmt"
"sync"
"testing"
"context"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/dao"
"go-common/app/service/live/wallet/model"
"go-common/app/service/live/wallet/service"
httpx "go-common/library/net/http/blademaster"
"math/rand"
"net/url"
"strconv"
"time"
)
const (
_getURL = "http://localhost:9901/x/internal/livewallet/wallet/get"
_delCacheURL = "http://localhost:9901/x/internal/livewallet/wallet/delCache"
_getAllURL = "http://localhost:9901/x/internal/livewallet/wallet/getAll"
_getTidURL = "http://localhost:9901/x/internal/livewallet/wallet/getTid"
_rechargeURL = "http://localhost:9901/x/internal/livewallet/wallet/recharge"
_payURL = "http://localhost:9901/x/internal/livewallet/wallet/pay"
_exchangeURL = "http://localhost:9901/x/internal/livewallet/wallet/exchange"
_queryURL = "http://localhost:9901/x/internal/livewallet/wallet/query"
)
var (
once sync.Once
client *httpx.Client
r *rand.Rand
)
type RechargeRes struct {
Code int `json:"code"`
Resp *model.MelonseedResp `json:"data"`
}
func getTestRandUid() int64 {
return r.Int63n(10000000)
}
func getTestExtendTid() string {
return fmt.Sprintf("test:ex:%d", r.Int31n(1000000))
}
func getTestRechargeOrPayForm(t *testing.T, serviceType int32, uid int64, coinType string, coinNum int64, tid interface{}) *model.RechargeOrPayForm {
if tid == nil {
res := queryGetTid(t, serviceType, getTestParamsJson())
if res.Code != 0 {
t.Errorf("get tid failed code : %d", res.Code)
t.FailNow()
}
tid = res.Resp.TransactionId
}
return &model.RechargeOrPayForm{
Uid: uid,
CoinType: coinType,
CoinNum: coinNum,
ExtendTid: getTestExtendTid(),
Timestamp: time.Now().Unix(),
TransactionId: tid.(string),
}
}
func queryRecharge(t *testing.T, form *model.RechargeOrPayForm, platform string) *RechargeRes {
params := url.Values{}
params.Set("uid", fmt.Sprintf("%d", form.Uid))
params.Set("coin_type", form.CoinType)
params.Set("coin_num", fmt.Sprintf("%d", form.CoinNum))
params.Set("extend_tid", form.ExtendTid)
params.Set("timestamp", fmt.Sprintf("%d", form.Timestamp))
params.Set("transaction_id", form.TransactionId)
params.Set("biz_reason", "2")
params.Set("version", "1")
req, _ := client.NewRequest("POST", _rechargeURL, "127.0.0.1", params)
req.Header.Set("platform", platform)
var res RechargeRes
err := client.Do(context.TODO(), req, &res)
if err != nil {
t.Errorf("client.Do() error(%v)", err)
t.FailNow()
}
return &res
}
func startHTTP() {
if err := conf.Init(); err != nil {
panic(fmt.Errorf("conf.Init() error(%v)", err))
}
svr := service.New(conf.Conf)
client = httpx.NewClient(conf.Conf.HTTPClient)
Init(conf.Conf, svr)
r = rand.New(rand.NewSource(time.Now().UnixNano()))
}
func getIntCoinForTest(coinStr string) int64 {
coin, _ := strconv.Atoi(coinStr)
return int64(coin)
}
func TestRecharge(t *testing.T) {
Convey("recharge normal 先调用get接口 再调用recharge 再调用get接口 比较用户钱包数据", t, testWith(func() {
platforms := []string{"pc", "android", "ios"}
var num int64 = 1000
uid := getTestRandUid()
d := dao.New(conf.Conf)
for _, platform := range platforms {
beforeWallet := getTestWallet(t, uid, platform)
resTid := queryGetTid(t, int32(model.RECHARGETYPE), getTestParamsJson())
if resTid.Code != 0 {
t.Errorf("get tid failed code : %d", resTid.Code)
t.FailNow()
}
tid := resTid.Resp.TransactionId
res := queryRecharge(t, getTestRechargeOrPayForm(t, int32(model.RECHARGETYPE), uid, "gold", num, tid), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Gold)-getIntCoinForTest(beforeWallet.Gold), ShouldEqual, num)
record, err := d.GetCoinStreamByTidAndOffset(context.TODO(), tid, 0)
So(err, ShouldBeNil)
So(record.Reserved1, ShouldEqual, 2)
So(record.Version, ShouldEqual, 1)
res = queryRecharge(t, getTestRechargeOrPayForm(t, int32(model.RECHARGETYPE), uid, "silver", num, nil), platform)
So(res.Code, ShouldEqual, 0)
So(getIntCoinForTest(res.Resp.Silver)-getIntCoinForTest(beforeWallet.Silver), ShouldEqual, num)
afterWallet := getTestWallet(t, uid, platform)
So(getIntCoinForTest(afterWallet.Gold)-getIntCoinForTest(beforeWallet.Gold), ShouldEqual, num)
So(getIntCoinForTest(afterWallet.Silver)-getIntCoinForTest(beforeWallet.Silver), ShouldEqual, num)
}
}))
}

View File

@@ -0,0 +1,207 @@
package http
import (
"strconv"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
bm "go-common/library/net/http/blademaster"
)
func getBasicParam(c *bm.Context) *model.BasicParam {
bp := new(model.BasicParam)
var err error
bp.TransactionId = c.Request.Form.Get("transaction_id")
bp.BizCode = c.Request.Form.Get("biz_code")
bp.Area, err = strconv.ParseInt(c.Request.Form.Get("area_id"), 10, 64)
if err != nil {
bp.Area = 0
}
bp.Source = c.Request.Form.Get("source")
bp.BizSource = c.Request.Form.Get("biz_source")
bp.MetaData = c.Request.Form.Get("metadata")
bp.Reason, err = strconv.ParseInt(c.Request.Form.Get("biz_reason"), 10, 64)
if err != nil {
bp.Reason = 0
}
bp.Version, err = strconv.ParseInt(c.Request.Form.Get("version"), 10, 64)
if err != nil {
bp.Version = 0
}
return bp
}
func getWithMetal(c *bm.Context) (withMetal int, err error) {
withMetalStr := c.Request.Form.Get("with_metal")
if withMetalStr == "" {
withMetal = 0
return
}
// check params
withMetal, err = strconv.Atoi(withMetalStr)
if err != nil || (withMetal != 0 && withMetal != 1) {
err = ecode.RequestErr
return
}
return
}
func get(c *bm.Context) {
uidStr := c.Request.Form.Get("uid")
// check params
uid, err := strconv.ParseInt(uidStr, 10, 64)
if err != nil || uid <= 0 {
c.JSON(nil, ecode.RequestErr)
return
}
withMetal, err := getWithMetal(c)
if err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
c.JSON(walletSvr.Get(c, bp, uid, platform, withMetal))
}
func delCache(c *bm.Context) {
uidStr := c.Request.Form.Get("uid")
// check params
uid, err := strconv.ParseInt(uidStr, 10, 64)
if err != nil || uid <= 0 {
c.JSON(nil, ecode.RequestErr)
return
}
bp := getBasicParam(c)
c.JSON(walletSvr.DelCache(c, bp, uid))
}
func getAll(c *bm.Context) {
uidStr := c.Request.Form.Get("uid")
// check params
uid, err := strconv.ParseInt(uidStr, 10, 64)
if err != nil || uid <= 0 {
c.JSON(nil, ecode.RequestErr)
return
}
withMetal, err := getWithMetal(c)
if err != nil {
c.JSON(nil, ecode.RequestErr)
return
}
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
c.JSON(walletSvr.GetAll(c, bp, uid, platform, withMetal))
}
func getTid(c *bm.Context) {
typeStr := c.Request.Form.Get("type")
// check params
serviceType64, err := strconv.ParseInt(typeStr, 10, 64)
serviceType := int32(serviceType64)
if err != nil || serviceType < 0 {
c.JSON(nil, ecode.RequestErr)
return
}
params := c.Request.Form.Get("params")
if params == "" {
c.JSON(nil, ecode.RequestErr)
return
}
bp := getBasicParam(c)
c.JSON(walletSvr.GetTid(c, bp, 0, serviceType, params))
}
func recharge(c *bm.Context) {
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
arg := &model.RechargeOrPayForm{}
if err := c.Bind(arg); err != nil {
return
}
c.JSON(walletSvr.Recharge(c, bp, arg.Uid, platform, arg))
}
func modify(c *bm.Context) {
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
arg := &model.RechargeOrPayForm{}
if err := c.Bind(arg); err != nil {
return
}
c.JSON(walletSvr.Modify(c, bp, arg.Uid, platform, arg))
}
func pay(c *bm.Context) {
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
arg := &model.RechargeOrPayForm{}
if err := c.Bind(arg); err != nil {
return
}
var reason interface{}
reasonFromHttp := c.Request.Form.Get("reason")
if reasonFromHttp == "" {
reason = nil
} else {
reason = reasonFromHttp
}
c.JSON(walletSvr.Pay(c, bp, arg.Uid, platform, arg, reason))
}
func exchange(c *bm.Context) {
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
arg := &model.ExchangeForm{}
if err := c.Bind(arg); err != nil {
return
}
c.JSON(walletSvr.Exchange(c, bp, arg.Uid, platform, arg))
}
func query(c *bm.Context) {
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
if platform == "" {
platform = "pc"
}
tid := c.Request.Form.Get("transaction_id")
if tid == "" {
c.JSON(nil, ecode.RequestErr)
return
}
uidStr := c.Request.Form.Get("uid")
uid, err := strconv.ParseInt(uidStr, 10, 64)
if err != nil || uid <= 0 {
uid = 0
return
}
c.JSON(walletSvr.Query(c, bp, uid, platform, tid))
}
func recordCoinStream(c *bm.Context) {
bp := getBasicParam(c)
platform := c.Request.Header.Get("platform")
arg := &model.RecordCoinStreamForm{}
if err := c.Bind(arg); err != nil {
return
}
c.JSON(walletSvr.RecordCoinStream(c, bp, arg.Uid, platform, arg))
}

View File

@@ -0,0 +1,55 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
load(
"@io_bazel_rules_go//proto:def.bzl",
"go_proto_library",
)
proto_library(
name = "model_proto",
srcs = ["wallet.proto"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["@gogo_special_proto//github.com/gogo/protobuf/gogoproto"],
)
go_proto_library(
name = "model_go_proto",
compilers = ["@io_bazel_rules_go//proto:gogofast_proto"],
importpath = "go-common/app/service/live/wallet/model",
proto = ":model_proto",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = ["@com_github_gogo_protobuf//gogoproto:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["helper.go"],
embed = [":model_go_proto"],
importpath = "go-common/app/service/live/wallet/model",
tags = ["automanaged"],
deps = [
"//library/ecode:go_default_library",
"@com_github_gogo_protobuf//gogoproto:go_default_library",
"@com_github_golang_protobuf//proto:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,482 @@
package model
import (
"fmt"
"go-common/library/ecode"
"time"
)
/*
const STREAM_OP_RESULT_SUCCESS = 0 //交易成功
const STREAM_OP_RESULT_IN_PROGRESS = 1 //交易进行中
const STREAM_OP_RESULT_FAILED = 2 //交易失败
const STREAM_OP_RESULT_ROLLBACK_SUCC = 2 //回滚成功
const STREAM_OP_RESULT_ROLLBACK_IN_PROGRESS = 2 //交易回滚中
const STREAM_OP_RESULT_ROLLBACK_FAILED = 2 //回滚成功
*/
const STREAM_OP_RESULT_SUB_SUCC = 1 //扣款成功
const STREAM_OP_RESULT_ADD_SUCC = 2 //加款成功
const STREAM_OP_RESULT_SUB_FAILED = -1 //扣款失败
const STREAM_OP_RESULT_ADD_FAILED = -2 //加款失败
const STREAM_OP_REASON_EXECUTE_UNKNOWN = -6 //系统内部逻辑错误: 结果未知,当作失败
const STREAM_OP_REASON_POST_QUERY_FAILED = -5 //系统内部错误,后置查询失败
const STREAM_OP_REASON_QUERY_FAILED = -4 //系统内部错误,查询失败
const STREAM_OP_REASON_LOCK_ERROR = -3 //系统内部错误,获取锁异常
const STREAM_OP_REASON_EXECUTE_FAILED = -2 //系统内部逻辑错误: 执行失败,连接异常 OR SQL错误 OR 修改要求的条件不满足(有别的进程越过用户锁,对货币数进行了变更)
const STREAM_OP_REASON_PRE_QUERY_FAILED = -1 //前置查询失败
const STREAM_OP_REASON_NOT_ENOUGH_COIN = 1
const STREAM_OP_REASON_LOCK_FAILED = 2
const (
LIVE_PLATFORM_IOS = "ios"
LIVE_PLATFORM_PC = "pc"
LIVE_PLATFORM_ANDROID = "android"
LIVE_PLATFORM_H5 = "h5"
COIN_TYPE_IOS_GOLD = "iap_gold"
COIN_TYPE_GOLD = "gold"
COIN_TYPE_SILVER = "silver"
COIN_TYPE_METAL = "metal" //主站硬币(主站提供硬币数查询、硬币扣除接口供调用)
)
type RechargeOrPayForm struct {
Uid int64 `form:"uid" validate:"required"`
CoinType string `form:"coin_type" validate:"required"`
CoinNum int64 `form:"coin_num" validate:"required"`
ExtendTid string `form:"extend_tid" validate:"required"`
Timestamp int64 `form:"timestamp" validate:"required"`
TransactionId string `form:"transaction_id" validate:"required"`
}
type ExchangeForm struct {
Uid int64 `form:"uid" validate:"required"`
ExtendTid string `form:"extend_tid" validate:"required"`
Timestamp int64 `form:"timestamp" validate:"required"`
TransactionId string `form:"transaction_id" validate:"required"`
SrcCoinType string `form:"src_coin_type" validate:"required"`
SrcCoinNum int64 `form:"src_coin_num" validate:"required"`
DestCoinType string `form:"dest_coin_type" validate:"required"`
DestCoinNum int64 `form:"dest_coin_num" validate:"required"`
}
type RecordCoinStreamForm struct {
Uid int64 `form:"uid" validate:"required"`
Data string `form:"data" validate:"required"`
}
type ServiceType int32
const (
PAYTYPE ServiceType = 0
RECHARGETYPE ServiceType = 1
EXCHANGETYPE ServiceType = 2
ROLLBACKTYPE ServiceType = 3
SysCoinTypeIosGold int32 = 2
SysCoinTypeGold int32 = 1
SysCoinTypeSilver int32 = 0
SysCoinTypeMetal int32 = 3
)
func IsValidServiceType(serviceType int32) bool {
st := ServiceType(serviceType)
return st == PAYTYPE ||
st == RECHARGETYPE ||
st == EXCHANGETYPE ||
st == ROLLBACKTYPE
}
var (
validPlatformMap = map[string]string{LIVE_PLATFORM_ANDROID: LIVE_PLATFORM_ANDROID, LIVE_PLATFORM_H5: LIVE_PLATFORM_H5, LIVE_PLATFORM_PC: LIVE_PLATFORM_PC, LIVE_PLATFORM_IOS: LIVE_PLATFORM_IOS}
validCoinTypeMap = map[string]int32{COIN_TYPE_IOS_GOLD: SysCoinTypeIosGold, COIN_TYPE_GOLD: SysCoinTypeGold, COIN_TYPE_SILVER: SysCoinTypeSilver, COIN_TYPE_METAL: SysCoinTypeMetal}
validPlatformNoMap = map[string]int32{LIVE_PLATFORM_PC: 1, LIVE_PLATFORM_ANDROID: 2, LIVE_PLATFORM_IOS: 3, LIVE_PLATFORM_H5: 4}
)
func IsValidCoinType(coinType string) bool {
_, ok := validCoinTypeMap[coinType]
return ok
}
func GetCoinTypeNumber(coinType string) int32 {
n := validCoinTypeMap[coinType]
return n
}
func IsValidPlatform(platform string) bool {
_, ok := validPlatformMap[platform]
return ok
}
func IsPlatformIOS(platform string) bool {
return platform == LIVE_PLATFORM_IOS
}
func IsLocalCoin(coinTypeNo int32) bool {
return coinTypeNo != SysCoinTypeMetal
}
func GetSysCoinType(coinType string, platform string) string {
if IsPlatformIOS(platform) && coinType == COIN_TYPE_GOLD {
coinType = COIN_TYPE_IOS_GOLD
}
return coinType
}
func GetSysCoinTypeByNo(coinTypeNo int32) string {
switch coinTypeNo {
case SysCoinTypeGold:
return COIN_TYPE_GOLD
case SysCoinTypeIosGold:
return COIN_TYPE_IOS_GOLD
case SysCoinTypeSilver:
return COIN_TYPE_SILVER
case SysCoinTypeMetal:
return COIN_TYPE_METAL
default:
return "not_define"
}
}
func GetRechargeCnt(coinTypeNo int32) string {
var rechargeCntField string
if coinTypeNo == SysCoinTypeSilver {
rechargeCntField = ""
} else if coinTypeNo == SysCoinTypeIosGold {
rechargeCntField = "gold_recharge_cnt"
} else if coinTypeNo == SysCoinTypeGold {
rechargeCntField = "gold_recharge_cnt"
}
return rechargeCntField
}
func GetPayCnt(coinTypeNo int32) string {
var cntField string
if coinTypeNo == SysCoinTypeSilver {
cntField = "silver_pay_cnt"
} else if coinTypeNo == SysCoinTypeIosGold {
cntField = "gold_pay_cnt"
} else if coinTypeNo == SysCoinTypeGold {
cntField = "gold_pay_cnt"
}
return cntField
}
func GetWalletFormatTime(opTime int64) string {
tm := time.Unix(opTime, 0)
date := tm.Format("2006-01-02 15:04:05")
return date
}
func NewCoinStream(uid int64, tid string, extendTid string, coinType int32, coinNum int64, opType int32, opTime int64, bizCode string, area int64, source string, bizSource string, metadata string) *CoinStreamRecord {
return &CoinStreamRecord{
Uid: uid,
TransactionId: tid,
ExtendTid: extendTid,
CoinType: coinType,
DeltaCoinNum: coinNum,
OpType: opType,
OpTime: opTime,
BizCode: bizCode,
Area: area,
Source: source,
BizSource: bizSource,
MetaData: metadata,
}
}
func NewExchangeSteam(uid int64, tid string, srcCoinType int32, srcCoinNum int32, destCoinType int32, destCoinNum int32, opTime int64, status int32) *CoinExchangeRecord {
return &CoinExchangeRecord{
Uid: uid,
TransactionId: tid,
SrcType: srcCoinType,
SrcNum: srcCoinNum,
DestType: destCoinType,
DestNum: destCoinNum,
ExchangeTime: opTime,
Status: status,
}
}
func (m *CoinStreamRecord) SetOpReason(r int32) {
m.OpReason = r
}
func GetMelonseedResp(platform string, melonseed *Melonseed) *MelonseedResp {
gold := getPlatformGold(melonseed.Gold, melonseed.IapGold, platform)
return &MelonseedResp{
Silver: fmt.Sprintf("%d", melonseed.Silver),
Gold: fmt.Sprintf("%d", gold),
}
}
func GetMelonseedWithMetalResp(platform string, melonseed *Melonseed, metal float64) *MelonseedWithMetalResp {
gold := getPlatformGold(melonseed.Gold, melonseed.IapGold, platform)
return &MelonseedWithMetalResp{
Silver: fmt.Sprintf("%d", melonseed.Silver),
Gold: fmt.Sprintf("%d", gold),
Metal: fmt.Sprintf("%.2f", metal),
}
}
func GetDetailResp(platform string, detail *Detail) *DetailResp {
gold := getPlatformGold(detail.Gold, detail.IapGold, platform)
return &DetailResp{
Silver: fmt.Sprintf("%d", detail.Silver),
Gold: fmt.Sprintf("%d", gold),
GoldPayCnt: fmt.Sprintf("%d", detail.GoldPayCnt),
GoldRechargeCnt: fmt.Sprintf("%d", detail.GoldRechargeCnt),
SilverPayCnt: fmt.Sprintf("%d", detail.SilverPayCnt),
CostBase: detail.CostBase,
}
}
func GetDetailWithMetalResp(platform string, detail *Detail, metal float64) *DetailWithMetalResp {
gold := getPlatformGold(detail.Gold, detail.IapGold, platform)
return &DetailWithMetalResp{
Silver: fmt.Sprintf("%d", detail.Silver),
Gold: fmt.Sprintf("%d", gold),
GoldPayCnt: fmt.Sprintf("%d", detail.GoldPayCnt),
GoldRechargeCnt: fmt.Sprintf("%d", detail.GoldRechargeCnt),
SilverPayCnt: fmt.Sprintf("%d", detail.SilverPayCnt),
Metal: fmt.Sprintf("%.2f", metal),
CostBase: detail.CostBase,
}
}
func GetTidResp(tid string) *TidResp {
return &TidResp{TransactionId: tid}
}
func getPlatformGold(normalGold int64, iapGold int64, platform string) int64 {
gold := normalGold
if IsPlatformIOS(platform) {
gold = iapGold
}
return gold
}
func IncrMelonseedCoin(userCoins *Melonseed, num int64, coinTypeNo int32) {
switch coinTypeNo {
case SysCoinTypeIosGold:
userCoins.IapGold += num
case SysCoinTypeGold:
userCoins.Gold += num
case SysCoinTypeSilver:
userCoins.Silver += num
default:
}
}
func GetCoinByMelonseed(coinTypeNo int32, userCoin *Melonseed) int64 {
switch coinTypeNo {
case SysCoinTypeIosGold:
return userCoin.IapGold
case SysCoinTypeGold:
return userCoin.Gold
case SysCoinTypeSilver:
return userCoin.Silver
default:
return 0
}
}
func GetCoinByDetailWithSnapShot(coinTypeNo int32, userCoin *DetailWithSnapShot) int64 {
switch coinTypeNo {
case SysCoinTypeIosGold:
return userCoin.IapGold
case SysCoinTypeGold:
return userCoin.Gold
case SysCoinTypeSilver:
return userCoin.Silver
default:
return 0
}
}
func CompareCoin(origin interface{}, num int64) bool {
switch origin.(type) {
case int64:
return origin.(int64) >= num
case float64:
return int64(origin.(float64)) >= num
default:
return false
}
}
// 得到数据库适配的货币数据由于数据库的org_coin_num delta_coin_num都是整型但是硬币的类型是浮点数所以做一下适配
func GetDbFitCoin(v interface{}) int64 {
switch v.(type) {
case int64:
return v.(int64)
case float64:
return int64(v.(float64))
default:
return 0
}
}
func SubCoin(v1 interface{}, v2 interface{}) int64 {
switch v1.(type) {
case int64:
return v1.(int64) - v2.(int64)
case float64:
return int64(v1.(float64) - v2.(float64))
default:
return 0
}
}
func AddMoreParam2CoinStream(stream *CoinStreamRecord, bp *BasicParam, platform string) {
platformNo := GetPlatformNo(platform)
stream.Platform = platformNo
stream.Reserved1 = bp.Reason
stream.Version = bp.Version
}
type CoinStreamFieldInject interface {
GetExtendTid() string
GetTimestamp() int64
GetTransactionId() string
GetBizCode() string
GetArea() int64
GetBizSource() string
GetSource() string
GetReason() int64
GetVersion() int64
GetMetaData() string
GetPlatform() string
GetUid() int64
}
func InjectFieldToCoinStream(stream *CoinStreamRecord, inject CoinStreamFieldInject) {
stream.ExtendTid = inject.GetExtendTid()
stream.TransactionId = inject.GetTransactionId()
stream.OpTime = inject.GetTimestamp()
stream.BizCode = inject.GetBizCode()
stream.Area = inject.GetArea()
stream.BizSource = inject.GetBizSource()
stream.MetaData = inject.GetMetaData()
stream.Source = inject.GetSource()
stream.Reserved1 = inject.GetReason()
stream.Version = inject.GetVersion()
platformNo := GetPlatformNo(inject.GetPlatform())
stream.Platform = platformNo
stream.Uid = inject.GetUid()
}
func GetPlatformNo(platform string) int32 {
platformNo, ok := validPlatformNoMap[platform]
if !ok {
platformNo = 0
}
return platformNo
}
var (
validRecordCoinStreamItemType = map[string]bool{"recharge": true, "pay": true}
)
func (m *RecordCoinStreamItem) IsValidType() bool {
_, ok := validRecordCoinStreamItemType[m.Type]
return ok
}
func (m *RecordCoinStreamItem) IsPayType() bool {
return m.Type == "pay"
}
func (m *RecordCoinStreamItem) IsRechargeType() bool {
return m.Type == "recharge"
}
func (m *RecordCoinStreamItem) GetOpType() int32 {
if m.IsPayType() {
return int32(PAYTYPE)
} else {
return int32(RECHARGETYPE)
}
}
func (m *RecordCoinStreamItem) GetOpResult() int32 {
if m.IsPayType() {
return STREAM_OP_RESULT_SUB_SUCC
} else {
return STREAM_OP_RESULT_ADD_SUCC
}
}
func (m *RecordCoinStreamItem) IsValid() (valid bool) {
valid = false
if m.OrgCoinNum < 0 {
return
}
if !m.IsValidType() {
return
}
if !IsValidCoinType(m.CoinType) {
return
}
if m.IsPayType() && m.CoinNum >= 0 {
return
}
if m.IsRechargeType() && m.CoinNum <= 0 {
return
}
valid = true
return
}
func GetMelonByDetailWithSnapShot(wallet *DetailWithSnapShot, platform string) (melon *MelonseedResp) {
gold := wallet.Gold
if platform == LIVE_PLATFORM_IOS {
gold = wallet.IapGold
}
return &MelonseedResp{
Silver: fmt.Sprintf("%d", wallet.Silver),
Gold: fmt.Sprintf("%d", gold),
}
}
func ModifyCoinInDetailWithSnapShot(wallet *DetailWithSnapShot, sysCoinTypeNo int32, coinNum int64) {
switch sysCoinTypeNo {
case SysCoinTypeGold:
wallet.Gold += coinNum
case SysCoinTypeIosGold:
wallet.IapGold += coinNum
case SysCoinTypeSilver:
wallet.Silver += coinNum
}
}
// 根据锁的错误设置数据库的reason
func SetReasonByLockErr(lockErr error, coinStream *CoinStreamRecord) {
if lockErr == ecode.TargetBlocked {
coinStream.OpReason = STREAM_OP_REASON_LOCK_FAILED
} else {
coinStream.OpReason = STREAM_OP_REASON_LOCK_ERROR
}
}
func NeedSnapshot(wallet *DetailWithSnapShot, now time.Time) bool {
lastTime, _ := time.Parse("2006-01-02 15:04:05", wallet.SnapShotTime)
return now.After(lastTime)
}
func GetTodayTime(now time.Time) time.Time {
timeStr := now.Format("2006-01-02") + " 00:00:00"
today, _ := time.Parse("2006-01-02 15:04:05", timeStr)
return today
}
func TodayNeedSnapShot(wallet *DetailWithSnapShot) bool {
now := GetTodayTime(time.Now())
return NeedSnapshot(wallet, now)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
syntax = "proto3";
package model;
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
message RechargeOrPayParam {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
string CoinType = 2 [(gogoproto.jsontag) = "coin_type"];
int64 CoinNum = 3 [(gogoproto.jsontag) = "coin_num"];
string ExtendTid = 4 [(gogoproto.jsontag) = "extend_tid"];
int64 Timestamp = 5 [(gogoproto.jsontag) = "timestamp"];
string TransactionId = 6 [(gogoproto.jsontag) = "transaction_id"];
string BizCode = 7 [(gogoproto.jsontag) = "biz_code"];
int64 Area = 8 [(gogoproto.jsontag) = "area"];
string Source = 9 [(gogoproto.jsontag) = "source"];
string MetaData = 10 [(gogoproto.jsontag) = "metadata"];
string BizSource = 11 [(gogoproto.jsontag) = "biz_source"];
int64 Reason = 12 [(gogoproto.jsontag) = "biz_reason"];
int64 Version = 13 [(gogoproto.jsontag) = "version"];
string Platform = 14 [(gogoproto.jsontag) = "platform"];
}
message ExchangeParam {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
string SrcCoinType = 2 [(gogoproto.jsontag) = "src_coin_type"];
int64 SrcCoinNum = 3 [(gogoproto.jsontag) = "src_coin_num"];
string DestCoinType = 4 [(gogoproto.jsontag) = "dest_coin_type"];
int64 DestCoinNum = 5 [(gogoproto.jsontag) = "dest_coin_num"];
string ExtendTid = 6 [(gogoproto.jsontag) = "extend_tid"];
int64 Timestamp = 7 [(gogoproto.jsontag) = "timestamp"];
string TransactionId = 8 [(gogoproto.jsontag) = "transaction_id"];
string BizCode = 9 [(gogoproto.jsontag) = "biz_code"];
int64 Area = 10 [(gogoproto.jsontag) = "area"];
string Source = 11 [(gogoproto.jsontag) = "source"];
string MetaData = 12 [(gogoproto.jsontag) = "metadata"];
string BizSource = 13 [(gogoproto.jsontag) = "biz_source"];
int64 Reason = 14 [(gogoproto.jsontag) = "biz_reason"];
int64 Version = 15 [(gogoproto.jsontag) = "version"];
string Platform = 16 [(gogoproto.jsontag) = "platform"];
}
message Melonseed {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
int64 Gold = 2 [(gogoproto.jsontag) = "gold"];
int64 IapGold = 3 [(gogoproto.jsontag) = "iap_gold"];
int64 Silver = 4 [(gogoproto.jsontag) = "silver"];
}
message Detail {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
int64 Gold = 2 [(gogoproto.jsontag) = "gold"];
int64 IapGold = 3 [(gogoproto.jsontag) = "iap_gold"];
int64 Silver = 4 [(gogoproto.jsontag) = "silver"];
int64 GoldRechargeCnt = 5 [(gogoproto.jsontag) = "gold_recharge_cnt"];
int64 GoldPayCnt = 6 [(gogoproto.jsontag) = "gold_pay_cnt"];
int64 SilverPayCnt = 7 [(gogoproto.jsontag) = "silver_pay_cnt"];
int64 CostBase = 8 [(gogoproto.jsontag) = "cost_base"];
}
message DetailWithSnapShot {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
int64 Gold = 2 [(gogoproto.jsontag) = "gold"];
int64 IapGold = 3 [(gogoproto.jsontag) = "iap_gold"];
int64 Silver = 4 [(gogoproto.jsontag) = "silver"];
int64 GoldRechargeCnt = 5 [(gogoproto.jsontag) = "gold_recharge_cnt"];
int64 GoldPayCnt = 6 [(gogoproto.jsontag) = "gold_pay_cnt"];
int64 SilverPayCnt = 7 [(gogoproto.jsontag) = "silver_pay_cnt"];
int64 CostBase = 8 [(gogoproto.jsontag) = "cost_base"];
string SnapShotTime = 9 [(gogoproto.jsontag) = "snapshot_time"];
int64 SnapShotGold = 10 [(gogoproto.jsontag) = "snapshot_gold"];
int64 SnapShotIapGold = 11 [(gogoproto.jsontag) = "snapshot_iap_gold"];
int64 SnapShotSilver = 12 [(gogoproto.jsontag) = "snapshot_silver"];
int64 Reserved1 = 13 [(gogoproto.jsontag) = "reserved1"];
string Reserved2 = 14 [(gogoproto.jsontag) = "reserved2"];
}
message McDetail {
Detail Detail = 1 [(gogoproto.jsontag) = "detail"];
bool Exist = 2 [(gogoproto.jsontag) = "exist"];
int32 Version = 3 [(gogoproto.jsontag) = "version"];
}
message CoinStreamRecord {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
string TransactionId = 2 [(gogoproto.jsontag) = "transaction_id"];
string ExtendTid = 3 [(gogoproto.jsontag) = "extend_tid"];
int32 CoinType = 4 [(gogoproto.jsontag) = "coin_type"];
int64 DeltaCoinNum = 5 [(gogoproto.jsontag) = "delta_coin_num"];
int64 OrgCoinNum = 6 [(gogoproto.jsontag) = "org_coin_num"];
int32 OpResult = 7 [(gogoproto.jsontag) = "op_result"];
int32 OpReason = 8 [(gogoproto.jsontag) = "op_reason"];
int32 OpType = 9 [(gogoproto.jsontag) = "op_type"];
int64 OpTime = 10 [(gogoproto.jsontag) = "op_time"];
string BizCode = 11 [(gogoproto.jsontag) = "biz_code"];
int64 area = 12 [(gogoproto.jsontag) = "area"];
string Source = 13 [(gogoproto.jsontag) = "source"];
string MetaData = 14 [(gogoproto.jsontag) = "metadata"];
string BizSource = 15 [(gogoproto.jsontag) = "biz_source"];
int32 Platform = 16 [(gogoproto.jsontag) = "platform"];
int64 Reserved1 = 17 [(gogoproto.jsontag) = "reserved1"];
int64 Version = 18 [(gogoproto.jsontag) = "reserved5"];
}
message CoinExchangeRecord {
int64 Uid = 1 [(gogoproto.jsontag) = "uid"];
string TransactionId = 2 [(gogoproto.jsontag) = "transaction_id"];
int32 SrcType = 3 [(gogoproto.jsontag) = "src_type"];
int32 SrcNum = 4 [(gogoproto.jsontag) = "src_num"];
int32 DestType = 5 [(gogoproto.jsontag) = "dest_type"];
int32 DestNum = 6 [(gogoproto.jsontag) = "dest_num"];
int32 Status = 7 [(gogoproto.jsontag) = "status"];
int64 ExchangeTime = 8 [(gogoproto.jsontag) = "exchange_time"];
}
message RecordCoinStreamItem {
string TransactionId = 1 [(gogoproto.jsontag) = "transaction_id"];
string ExtendTid = 2 [(gogoproto.jsontag) = "extend_tid"];
string CoinType = 3 [(gogoproto.jsontag) = "coin_type"];
int64 CoinNum = 4 [(gogoproto.jsontag) = "coin_num"];
int64 OrgCoinNum = 5 [(gogoproto.jsontag) = "left_coins"];
string Type = 6 [(gogoproto.jsontag) = "type"];
int64 Timestamp = 7 [(gogoproto.jsontag) = "timestamp"];
int64 Reserved1 = 8 [(gogoproto.jsontag) = "biz_reason"];
}
message BasicParam {
string TransactionId = 1 [(gogoproto.jsontag) = "transaction_id"];
string BizCode = 2 [(gogoproto.jsontag) = "biz_code"];
int64 area = 3 [(gogoproto.jsontag) = "area"];
string Source = 4 [(gogoproto.jsontag) = "source"];
string MetaData = 5 [(gogoproto.jsontag) = "metadata"];
string BizSource = 6 [(gogoproto.jsontag) = "biz_source"];
int64 Reason = 7 [(gogoproto.jsontag) = "biz_reason"];
int64 Version = 8 [(gogoproto.jsontag) = "version"];
}
message MelonseedResp {
string Gold = 1 [(gogoproto.jsontag) = "gold"];
string Silver = 2 [(gogoproto.jsontag) = "silver"];
}
message MelonseedWithMetalResp {
string Gold = 1 [(gogoproto.jsontag) = "gold"];
string Silver = 2 [(gogoproto.jsontag) = "silver"];
string Metal = 3 [(gogoproto.jsontag) = "metal"];
}
message DetailResp {
string Gold = 1 [(gogoproto.jsontag) = "gold"];
string Silver = 2 [(gogoproto.jsontag) = "silver"];
string GoldRechargeCnt = 5 [(gogoproto.jsontag) = "gold_recharge_cnt"];
string GoldPayCnt = 6 [(gogoproto.jsontag) = "gold_pay_cnt"];
string SilverPayCnt = 7 [(gogoproto.jsontag) = "silver_pay_cnt"];
int64 CostBase = 8 [(gogoproto.jsontag) = "cost_base"];
}
message DetailWithMetalResp {
string Gold = 1 [(gogoproto.jsontag) = "gold"];
string Silver = 2 [(gogoproto.jsontag) = "silver"];
string GoldRechargeCnt = 5 [(gogoproto.jsontag) = "gold_recharge_cnt"];
string GoldPayCnt = 6 [(gogoproto.jsontag) = "gold_pay_cnt"];
string SilverPayCnt = 7 [(gogoproto.jsontag) = "silver_pay_cnt"];
string Metal = 8 [(gogoproto.jsontag) = "metal"];
int64 CostBase = 9 [(gogoproto.jsontag) = "cost_base"];
}
message TidResp {
string TransactionId = 1 [(gogoproto.jsontag) = "transaction_id"];
}
message QueryResp {
int32 Status = 1 [(gogoproto.jsontag) = "status"];
}
message MetalData {
double Count = 1 [(gogoproto.jsontag) = "count"];
}
message WalletChangeMsg {
string Action = 1 [(gogoproto.jsontag) = "action"];
int64 Uid = 2 [(gogoproto.jsontag) = "uid"];
string CoinType = 3 [(gogoproto.jsontag) = "coin_type"];
string Platfrom = 4 [(gogoproto.jsontag) = "platform"];
int64 Number = 5 [(gogoproto.jsontag) = "number"];
int64 Gold = 6 [(gogoproto.jsontag) = "gold"];
int64 Silver = 7 [(gogoproto.jsontag) = "silver"];
int64 GoldRechargeCnt =8 [(gogoproto.jsontag) = "gold_recharge_cnt"];
int64 GoldPayCnt = 9 [(gogoproto.jsontag) = "gold_pay_cnt"];
int64 SilverPayCnt = 10 [(gogoproto.jsontag) = "silver_pay_cnt"];
string DestCoinType = 11 [(gogoproto.jsontag) = "dest_coin_type"];
int64 DestNumber = 12 [(gogoproto.jsontag) = "dest_number"];
int64 CostBase = 13 [(gogoproto.jsontag) = "cost_base"];
}

View File

@@ -0,0 +1,85 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_test",
"go_library",
)
go_test(
name = "go_default_test",
srcs = [
"delCache_test.go",
"exchange_test.go",
"getTid_test.go",
"get_test.go",
"local_exchange_test.go",
"modify_test.go",
"out_exchange_test.go",
"pay_test.go",
"query_test.go",
"recharge_test.go",
"recordCoinStream_test.go",
"service_test.go",
"wallet_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/model:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode:go_default_library",
"//library/sync/errgroup:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"delCache.go",
"exchange.go",
"get.go",
"getAll.go",
"getTid.go",
"local_exchange.go",
"lock.go",
"modify.go",
"out_exchange.go",
"pay.go",
"query.go",
"recharge.go",
"recordCoinStream.go",
"service.go",
"wallet.go",
],
importpath = "go-common/app/service/live/wallet/service",
tags = ["automanaged"],
deps = [
"//app/service/live/wallet/conf:go_default_library",
"//app/service/live/wallet/dao:go_default_library",
"//app/service/live/wallet/model:go_default_library",
"//library/cache:go_default_library",
"//library/cache/memcache:go_default_library",
"//library/database/sql:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,39 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
mc "go-common/library/cache/memcache"
"go-common/library/ecode"
)
// DelCacheHandler del cache handler
type DelCacheHandler struct {
}
// NeedCheckUid need check uid
func (handler *DelCacheHandler) NeedCheckUid() bool {
return true
}
// NeedTransactionMutex need mutex
func (handler *DelCacheHandler) NeedTransactionMutex() bool {
return false
}
// BizExecute biz execute
func (handler *DelCacheHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
err = ws.s.dao.DelWalletCache(ws.c, uid)
if err == mc.ErrNotFound {
err = ecode.NothingFound
}
return
}
// DelCache del cache
func (s *Service) DelCache(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
handler := DelCacheHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}

View File

@@ -0,0 +1,35 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/library/ecode"
"testing"
)
func queryDelCache(t *testing.T, uid int64) (success bool, err error) {
bp := getTestDefaultBasicParam("")
_, err = s.DelCache(ctx, bp, uid, "")
if err == nil {
success = true
}
return
}
func TestService_DelCache(t *testing.T) {
Convey("not found", t, testWithRandUid(func(uid int64) {
queryDelCache(t, uid)
s, e := queryDelCache(t, uid)
So(s, ShouldEqual, false)
So(e, ShouldEqual, ecode.NothingFound)
}))
Convey("normal", t, testWithTestUser(func(u *TestUser) {
getTestWallet(t, u.uid, "pc")
s, e := queryDelCache(t, u.uid)
So(s, ShouldEqual, true)
So(e, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,163 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"go-common/library/log"
)
// ExchangeHandler exchange handler
type ExchangeHandler struct {
service *WalletService
}
// NeedCheckUid need check uid
func (handler *ExchangeHandler) NeedCheckUid() bool {
return true
}
// NeedTransactionMutex need transaction mutex
func (handler *ExchangeHandler) NeedTransactionMutex() bool {
return true
}
// SetWalletService set wallet service
func (handler *ExchangeHandler) SetWalletService(ws *WalletService) {
handler.service = ws
}
// BizExecute old TODO deprecated
func (handler *ExchangeHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
return
}
func checkByExchangeParam(param *model.ExchangeParam) (r bool) {
if !model.IsValidPlatform(param.Platform) {
return
}
for {
if param.Uid <= 0 || param.ExtendTid == "" || param.TransactionId == "" {
break
}
if param.SrcCoinNum <= 0 || !model.IsValidCoinType(param.SrcCoinType) {
break
}
if param.DestCoinNum <= 0 || !model.IsValidCoinType(param.DestCoinType) {
break
}
r = true
break
}
return r
}
// Exchange exchange
func (handler *ExchangeHandler) Exchange(c context.Context, param *model.ExchangeParam) (resp *model.MelonseedResp, err error) {
if !checkByExchangeParam(param) {
err = ecode.RequestErr
return
}
srcSysCoinType := model.GetSysCoinType(param.SrcCoinType, param.Platform)
srcSysCoinTypeNo := model.GetCoinTypeNumber(srcSysCoinType)
srcIsLocalCoin := model.IsLocalCoin(srcSysCoinTypeNo)
destSysCoinType := model.GetSysCoinType(param.DestCoinType, param.Platform)
destSysCoinTypeNo := model.GetCoinTypeNumber(destSysCoinType)
destIsLocalCoin := model.IsLocalCoin(destSysCoinTypeNo)
// 不允许兑换相同货币
if srcSysCoinTypeNo == destSysCoinTypeNo {
err = ecode.RequestErr
return
}
// 锁住tid
err = handler.service.lockTransactionId(param.TransactionId)
if err != nil {
return
}
log.Info("##TX## Exchange stage:begin| time:%d|tid:%s|uid:%d|platform:%s|etid:%s|srcType:%s|srcNum:%d|destType:%s|destNum:%d",
param.Timestamp, param.TransactionId, param.Uid, param.Platform, param.ExtendTid, srcSysCoinType, param.SrcCoinNum, destSysCoinType, param.DestCoinNum)
srcCoinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&srcCoinStream, param)
srcCoinStream.DeltaCoinNum = -param.SrcCoinNum
srcCoinStream.CoinType = srcSysCoinTypeNo
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
// 初始状态
srcCoinStream.OrgCoinNum = -1
srcCoinStream.OpResult = model.STREAM_OP_RESULT_SUB_FAILED
destCoinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&destCoinStream, param)
destCoinStream.DeltaCoinNum = param.DestCoinNum
destCoinStream.CoinType = destSysCoinTypeNo
destCoinStream.OpType = int32(model.EXCHANGETYPE)
// 初始状态
destCoinStream.OrgCoinNum = -1
destCoinStream.OpResult = model.STREAM_OP_RESULT_ADD_FAILED
exchangeRecord := model.NewExchangeSteam(param.Uid, param.TransactionId, srcSysCoinTypeNo, int32(param.SrcCoinNum), destSysCoinTypeNo, int32(param.DestCoinNum), param.Timestamp, 0)
userLock := handler.service.getUserLock()
// 锁用户
lockErr := userLock.lock(param.Uid)
if lockErr != nil {
model.SetReasonByLockErr(lockErr, &srcCoinStream)
err = lockErr
handler.service.s.dao.NewCoinStreamRecord(c, &srcCoinStream)
return
}
defer userLock.release()
var realHandler RealExchangeHandler
if srcIsLocalCoin && destIsLocalCoin {
realHandler = getLocalPayHandler(handler.service)
} else {
realHandler = getOutExchangeHandler(handler.service)
}
wallet, err := realHandler.exchange(c, param.Uid, param.Platform, srcSysCoinTypeNo, param.SrcCoinNum, destSysCoinTypeNo, param.DestCoinNum, &srcCoinStream, &destCoinStream, exchangeRecord)
if err == nil {
handler.service.s.dao.DelWalletCache(c, param.Uid)
resp = model.GetMelonByDetailWithSnapShot(wallet, param.Platform)
handler.service.s.pubWalletChangeWithDetailSnapShot(handler.service.c, param.Uid, "exchange", param.SrcCoinNum, param.SrcCoinType, param.Platform, param.DestCoinType, param.DestCoinNum, wallet)
}
return
}
func getExchangeHandler(ws *WalletService) *ExchangeHandler {
handler := ExchangeHandler{}
handler.SetWalletService(ws)
return &handler
}
// Exchange exchange
func (s *Service) Exchange(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
arg, _ := params[1].(*model.ExchangeForm)
param := buildExchangeParam(platform, arg, uid, basicParam)
ws := new(WalletService)
ws.c = c
ws.s = s
handler := getExchangeHandler(ws)
handler.SetWalletService(ws)
return handler.Exchange(c, param)
}
// RealExchangeHandler real exchange handler
type RealExchangeHandler interface {
exchange(c context.Context, uid int64, platform string, srcSysCoinTypeNo int32, srcCoinNum int64,
destSysCoinTypeNo int32, destCoinNum int64, srcStream *model.CoinStreamRecord, destStream *model.CoinStreamRecord, exchangeStream *model.CoinExchangeRecord) (*model.DetailWithSnapShot, error)
}

View File

@@ -0,0 +1,405 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"sync"
"testing"
"time"
)
func queryExchange(t *testing.T, uid int64, platform string, srcCoinType string, srcCoinNum int64, destCoinType string, destCoinNum int64) (success bool, tid string, err error) {
tid = getTestTidForCall(t, int32(model.EXCHANGETYPE))
bp := getTestDefaultBasicParam(tid)
_, err = s.Exchange(ctx, bp, uid, platform, getTestExchangeForm(uid, srcCoinType, srcCoinNum, destCoinType, destCoinNum, tid))
if err == nil {
success = true
}
return
}
func assertNormalCoinStream(t *testing.T, platform string, coinType string, tid string, offset int, deltaCoinNum int64, serviceType model.ServiceType, result int, status int) *model.CoinStreamRecord {
record, recordErr := s.dao.GetCoinStreamByTidAndOffset(ctx, tid, offset)
So(recordErr, ShouldBeNil)
So(record.DeltaCoinNum, ShouldEqual, deltaCoinNum)
So(record.OpType, ShouldEqual, int32(serviceType))
So(record.OpResult, ShouldEqual, result)
srcSysCoinType := model.GetSysCoinType(coinType, platform)
srcSysCoinTypeNo := model.GetCoinTypeNumber(srcSysCoinType)
So(record.CoinType, ShouldEqual, srcSysCoinTypeNo)
success, resp, err := queryQueryWithUid(t, tid, record.Uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.Status, ShouldEqual, status)
return record
}
func getCoinNumByMelonRespForTest(wallet *model.MelonseedResp, coinType string) int {
switch coinType {
case "silver":
return atoiForTest(wallet.Silver)
case "gold":
return atoiForTest(wallet.Gold)
default:
return 0
}
}
func getCoinNumByDetailForTest(wallet *model.DetailResp, coinType string) int {
switch coinType {
case "silver":
return atoiForTest(wallet.Silver)
case "gold":
return atoiForTest(wallet.Gold)
default:
return 0
}
}
func TestService_Exchange(t *testing.T) {
Convey("normal", t, testWith(func() {
Convey("gold2silver", testWith(func() {
platforms := []string{"pc", "ios"}
for _, platform := range platforms {
u := initTestUser()
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
beforeDetail := getTestAll(t, u.uid, platform)
success, tid, err := queryExchange(t, u.uid, platform, "gold", 10, "silver", 100)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
wallet := getTestWallet(t, u.uid, platform)
So(atoiForTest(wallet.Silver), ShouldEqual, u.coin+100)
So(atoiForTest(wallet.Gold), ShouldEqual, u.coin-10)
melon, _ := s.dao.Melonseed(ctx, u.uid)
var gold int64
if platform == "ios" {
gold = melon.IapGold
} else {
gold = melon.Gold
}
So(gold, ShouldEqual, u.coin-10)
// 检查CoinStream表
assertNormalCoinStream(t, platform, "gold", tid, 1, -10, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_SUCC, TX_STATUS_SUCC)
assertNormalCoinStream(t, platform, "silver", tid, 0, 100, model.EXCHANGETYPE, model.STREAM_OP_RESULT_ADD_SUCC, TX_STATUS_SUCC)
u.rest()
// check detail
afterDetail := getTestAll(t, u.uid, platform)
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, 10)
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 1000)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1000)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
}
}))
Convey("any2any", testWith(func() {
platforms := []string{"pc", "ios"}
coinTypes := []string{"gold", "silver"}
for _, platform := range platforms {
for _, srcCoinType := range coinTypes {
for _, destCoinType := range coinTypes {
if srcCoinType == destCoinType {
continue
}
u := initTestUser()
var srcCoinNum int64 = 10
var destCoinNum int64 = 100
success, tid, err := queryExchange(t, u.uid, platform, srcCoinType, srcCoinNum, destCoinType, destCoinNum)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
wallet := getTestWallet(t, u.uid, platform)
So(getCoinNumByMelonRespForTest(wallet, destCoinType), ShouldEqual, u.coin+destCoinNum)
So(getCoinNumByMelonRespForTest(wallet, srcCoinType), ShouldEqual, u.coin-srcCoinNum)
// 检查CoinStream表
assertNormalCoinStream(t, platform, srcCoinType, tid, 1, srcCoinNum*-1, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_SUCC, TX_STATUS_SUCC)
assertNormalCoinStream(t, platform, destCoinType, tid, 0, destCoinNum, model.EXCHANGETYPE, model.STREAM_OP_RESULT_ADD_SUCC, TX_STATUS_SUCC)
u.rest()
}
}
}
}))
}))
Convey("params", t, testWith(func() {
Convey("same coinType", testWithRandUid(func(uid int64) {
success, _, err := queryExchange(t, uid, "pc", "gold", 10, "gold", 100)
So(success, ShouldBeFalse)
So(err, ShouldEqual, ecode.RequestErr)
}))
Convey("src or dest coin less than or equal 0", testWithRandUid(func(uid int64) {
invalidCoins := []int64{0, -1, -100}
for _, coin := range invalidCoins {
success, _, err := queryExchange(t, uid, "pc", "gold", coin, "gold", 100)
So(success, ShouldBeFalse)
So(err, ShouldEqual, ecode.RequestErr)
}
for _, coin := range invalidCoins {
success, _, err := queryExchange(t, uid, "pc", "gold", 10, "gold", coin)
So(success, ShouldBeFalse)
So(err, ShouldEqual, ecode.RequestErr)
}
}))
}))
Convey("not enough", t, testWithTestUser(func(u *TestUser) {
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
success, tid, err := queryExchange(t, u.uid, "pc", "silver", 1001, "gold", 100)
So(success, ShouldBeFalse)
So(err, ShouldEqual, ecode.CoinNotEnough)
stream := assertNormalCoinStream(t, "pc", "silver", tid, 0, -1001, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_FAILED, TX_STATUS_FAILED)
So(stream.OpReason, ShouldEqual, 1)
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotTime, ShouldEqual, "0001-01-01 00:00:00")
So(detail2.Silver, ShouldEqual, 1000)
So(detail2.Gold, ShouldEqual, 1000)
So(detail2.IapGold, ShouldEqual, 1000)
tx.Rollback()
t.Logf("uid:%d,tid:%s", u.uid, tid)
}))
}
func TestService_ExchangeMetal(t *testing.T) {
Convey("enough", t, testWithTestUser(func(u *TestUser) {
_, _, err := s.dao.ModifyMetal(ctx, u.uid, 1, 0, nil)
if err != nil {
t.Errorf("add metal failed err : %s", err.Error())
t.FailNow()
}
metal1, _ := s.dao.GetMetal(ctx, u.uid)
success, tid, err := queryExchange(t, u.uid, "pc", "metal", 1, "gold", 100)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
assertNormalCoinStream(t, "pc", "metal", tid, 1, -1, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_SUCC, TX_STATUS_SUCC)
assertNormalCoinStream(t, "pc", "gold", tid, 0, 100, model.EXCHANGETYPE, model.STREAM_OP_RESULT_ADD_SUCC, TX_STATUS_SUCC)
metal2, _ := s.dao.GetMetal(ctx, u.uid)
So(metal2-metal1, ShouldEqual, -1)
t.Logf("uid:%d,tid:%s", u.uid, tid)
}))
Convey("not enough", t, testWithTestUser(func(u *TestUser) {
metal1, _ := s.dao.GetMetal(ctx, u.uid)
success, tid, err := queryExchange(t, u.uid, "pc", "metal", int64(metal1+1), "gold", 100)
So(success, ShouldBeFalse)
So(err, ShouldEqual, ecode.CoinNotEnough)
assertNormalCoinStream(t, "pc", "metal", tid, 0, -int64(metal1+1), model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_FAILED, TX_STATUS_FAILED)
metal2, _ := s.dao.GetMetal(ctx, u.uid)
So(metal2-metal1, ShouldEqual, 0)
t.Logf("uid:%d,tid:%s", u.uid, tid)
}))
}
func TestExchangeHandler_Multi(t *testing.T) {
Convey("multi", t, testWith(func() {
platforms := []string{"pc", "ios"}
coinTypes := []string{"gold", "silver"}
var destCoinNum int64 = 1000
for _, platform := range platforms {
for _, srcCoinType := range coinTypes {
for _, destCoinType := range coinTypes {
if srcCoinType == destCoinType {
continue
}
u := initTestUser()
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
var detail1 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail1, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail1: %+v", detail1)
So(detail1.SnapShotTime, ShouldEqual, "0001-01-01 00:00:00")
tx.Rollback()
step := 10
num := u.coin / int64(step)
times := step * 3
exchangeRespMap := make(map[string]*ServiceResForTest)
var wg sync.WaitGroup
var lock sync.Mutex
for i := 0; i < times; i++ {
wg.Add(1)
go func() {
localService := New(conf.Conf)
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := localService.Exchange(ctx, bp, u.uid, platform, getTestExchangeForm(u.uid, srcCoinType, num, destCoinType, destCoinNum, tid))
lock.Lock()
exchangeRespMap[tid] = &ServiceResForTest{V: v, Err: err}
lock.Unlock()
wg.Done()
}()
time.Sleep(time.Millisecond * 30)
}
wg.Wait()
successNum := 0
validErrs := map[error]bool{ecode.TargetBlocked: true, nil: true, ecode.CoinNotEnough: true}
for tid, res := range exchangeRespMap {
_, ok := validErrs[res.Err]
So(ok, ShouldBeTrue)
if res.Err == nil {
successNum++
}
if res.Err == nil {
assertNormalCoinStream(t, platform, srcCoinType, tid, 1, num*-1, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_SUCC, TX_STATUS_SUCC)
assertNormalCoinStream(t, platform, destCoinType, tid, 0, destCoinNum, model.EXCHANGETYPE, model.STREAM_OP_RESULT_ADD_SUCC, TX_STATUS_SUCC)
} else if res.Err == ecode.CoinNotEnough {
record := assertNormalCoinStream(t, platform, srcCoinType, tid, 0, num*-1, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_FAILED, TX_STATUS_FAILED)
So(record.OpReason, ShouldEqual, model.STREAM_OP_REASON_NOT_ENOUGH_COIN)
} else {
record := assertNormalCoinStream(t, platform, srcCoinType, tid, 0, num*-1, model.EXCHANGETYPE, model.STREAM_OP_RESULT_SUB_FAILED, TX_STATUS_FAILED)
So(record.OpReason, ShouldEqual, model.STREAM_OP_REASON_LOCK_FAILED)
}
}
t.Logf("successNum:%d", successNum)
var detail2 *model.DetailWithSnapShot
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, detail1.Gold)
So(detail2.SnapShotIapGold, ShouldEqual, detail1.IapGold)
So(detail2.SnapShotSilver, ShouldEqual, detail1.Silver)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
So(successNum, ShouldBeLessThanOrEqualTo, step)
So(successNum, ShouldBeGreaterThan, 0)
wallet := getTestWallet(t, u.uid, platform)
So(getCoinNumByMelonRespForTest(wallet, srcCoinType)+successNum*int(num), ShouldEqual, u.coin)
So(getCoinNumByMelonRespForTest(wallet, destCoinType)-successNum*int(destCoinNum), ShouldEqual, u.coin)
u.rest()
}
}
}
}))
}
func TestService_ExchangeBasicParam(t *testing.T) {
Convey("default", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestExchangeForm(u.uid, "gold", 1, "silver", 1, tid)
_, err := s.Exchange(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
record, err := s.dao.GetCoinStreamByTidAndOffset(ctx, tid, 0)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 0)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("silver", platform)))
So(record.OpResult, ShouldEqual, 2)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, 1)
record, err = s.dao.GetCoinStreamByTidAndOffset(ctx, tid, 1)
oP = model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 0)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 1)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, -1)
}
}))
Convey("advanced", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
bp := getTestDefaultBasicParam(tid)
bp.BizCode = "exchange"
bp.Reason = 2
form := getTestExchangeForm(u.uid, "gold", 1, "silver", 1, tid)
_, err := s.Exchange(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
record, err := s.dao.GetCoinStreamByTidAndOffset(ctx, tid, 0)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 2)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("silver", platform)))
So(record.OpResult, ShouldEqual, 2)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, 1)
So(record.BizCode, ShouldEqual, bp.BizCode)
record, err = s.dao.GetCoinStreamByTidAndOffset(ctx, tid, 1)
oP = model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 2)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 1)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, -1)
So(record.BizCode, ShouldEqual, bp.BizCode)
}
}))
}

View File

@@ -0,0 +1,45 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
)
type GetHandler struct {
}
func (handler *GetHandler) NeedCheckUid() bool {
return true
}
func (handler *GetHandler) NeedTransactionMutex() bool {
return false
}
func (handler *GetHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
withMetal, _ := params[1].(int)
if !model.IsValidPlatform(platform) {
err = ecode.RequestErr
return
}
r, err := ws.s.dao.GetMelonseedByCache(ws.c, uid)
if err != nil {
err = ecode.ServerErr
return
}
if withMetal == 1 {
metal, _ := ws.s.dao.GetMetal(ws.c, uid)
v = model.GetMelonseedWithMetalResp(platform, r, metal)
} else {
v = model.GetMelonseedResp(platform, r)
}
return
}
func (s *Service) Get(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
handler := GetHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}

View File

@@ -0,0 +1,45 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
)
type GetAllHandler struct {
}
func (handler *GetAllHandler) NeedCheckUid() bool {
return true
}
func (handler *GetAllHandler) NeedTransactionMutex() bool {
return false
}
func (handler *GetAllHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
withMetal, _ := params[1].(int)
if !model.IsValidPlatform(platform) {
err = ecode.RequestErr
return
}
r, err := ws.s.dao.GetDetailByCache(ws.c, uid)
if err != nil {
err = ecode.ServerErr
return
}
if withMetal == 1 {
metal, _ := ws.s.dao.GetMetal(ws.c, uid)
v = model.GetDetailWithMetalResp(platform, r, metal)
} else {
v = model.GetDetailResp(platform, r)
}
return
}
func (s *Service) GetAll(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
handler := GetAllHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}

View File

@@ -0,0 +1,44 @@
package service
import (
"context"
"go-common/app/service/live/wallet/dao"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"go-common/library/log"
)
type GetTidHandler struct {
}
func (handler *GetTidHandler) NeedCheckUid() bool {
return false
}
func (handler *GetTidHandler) NeedTransactionMutex() bool {
return false
}
func (handler *GetTidHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
serviceType, _ := params[0].(int32)
if !model.IsValidServiceType(serviceType) {
err = ecode.RequestErr
return
}
callParams, _ := params[1].(string)
if callParams == "" {
err = ecode.RequestErr
return
}
log.Info("getTid info : type:%d,callParams:%s", serviceType, callParams)
tid := dao.GetTid(model.ServiceType(serviceType), callParams)
v = model.GetTidResp(tid)
return
}
func (s *Service) GetTid(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
handler := GetTidHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}

View File

@@ -0,0 +1,139 @@
package service
import (
"encoding/json"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"sync"
"testing"
"time"
)
type TestTidParams struct {
Biz string
Time int64
}
func getTestTidParams(biz string) *TestTidParams {
return &TestTidParams{
Biz: biz,
Time: time.Now().Unix(),
}
}
type ServiceResForTest struct {
Err error
V interface{}
}
var tidLen = 36
func getTestParamsJson() string {
params := getTestTidParams("test-service")
paramsBytes, _ := json.Marshal(params)
paramsJson := string(paramsBytes[:])
return paramsJson
}
func getTestRandServiceType() int32 {
return r.Int31n(4)
}
func TestService_GetTid(t *testing.T) {
Convey("normal", t, testWith(func() {
for i := 0; i < 4; i++ {
v, err := s.GetTid(ctx, getTestDefaultBasicParam(""), 0, i, getTestParamsJson())
So(v, ShouldNotBeNil)
So(err, ShouldBeNil)
resp := v.(*model.TidResp)
So(len(resp.TransactionId), ShouldEqual, tidLen)
}
}))
Convey("same params twice", t, testWith(func() {
var uid int64 = 0
st := getTestRandServiceType()
p := getTestDefaultBasicParam("")
qp := getTestParamsJson()
v, err := s.GetTid(ctx, p, uid, st, qp)
So(v, ShouldNotBeNil)
So(err, ShouldBeNil)
resp1 := v.(*model.TidResp)
So(len(resp1.TransactionId), ShouldEqual, tidLen)
v, err = s.GetTid(ctx, p, uid, st, qp)
So(v, ShouldNotBeNil)
So(err, ShouldBeNil)
resp2 := v.(*model.TidResp)
So(len(resp2.TransactionId), ShouldEqual, tidLen)
So(resp1.TransactionId, ShouldNotEqual, resp2.TransactionId)
}))
Convey("params", t, testWith(func() {
Convey("invalid service Type", func() {
invalidService := []int32{-1, -2, 4, 5}
for _, st := range invalidService {
v, err := s.GetTid(ctx, getTestDefaultBasicParam(""), 0, st, getTestParamsJson())
So(err, ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
}
})
Convey("invalid query params", func() {
st := 0
params := ""
v, err := s.GetTid(ctx, getTestDefaultBasicParam(""), 0, st, params)
So(err, ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
})
}))
Convey("multi", t, testWith(func() {
var uid int64 = 0
st := getTestRandServiceType()
p := getTestDefaultBasicParam("")
qp := getTestParamsJson()
var wg sync.WaitGroup
times := 10
tidResMap := make(map[int]*ServiceResForTest)
var lock sync.Mutex
for i := 0; i < times; i++ {
wg.Add(1)
go func(index int) {
localService := New(conf.Conf)
v, err := localService.GetTid(ctx, p, uid, st, qp)
lock.Lock()
tidResMap[index] = &ServiceResForTest{V: v, Err: err}
lock.Unlock()
wg.Done()
}(i)
}
wg.Wait()
tidMap := make(map[string]bool)
for _, item := range tidResMap {
So(item.Err, ShouldBeNil)
resp := item.V.(*model.TidResp)
So(len(resp.TransactionId), ShouldEqual, tidLen)
_, ok := tidMap[resp.TransactionId]
So(ok, ShouldBeFalse)
tidMap[resp.TransactionId] = true
}
So(len(tidMap), ShouldEqual, times)
}))
}

View File

@@ -0,0 +1,171 @@
package service
import (
"context"
"sync"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"math/rand"
"os"
"strconv"
)
var (
once sync.Once
s *Service
ctx = context.Background()
r *rand.Rand
testValidPlatform = []string{"pc", "android", "ios", "h5"}
)
func initConf() {
if err := conf.Init(); err != nil {
panic(err)
}
}
func TestMain(m *testing.M) {
conf.ConfPath = "../cmd/live-wallet-test.toml"
once.Do(startService)
os.Exit(m.Run())
}
func startService() {
initConf()
s = New(conf.Conf)
time.Sleep(time.Millisecond * 100)
r = rand.New(rand.NewSource(time.Now().UnixNano()))
}
func getTestRandUid() int64 {
return r.Int63n(10000000)
}
func testWith(f func()) func() {
once.Do(startService)
return f
}
func getTestBasicParam(tid string, area int64, bizCode string, source string, bizSource string, metaData string) *model.BasicParam {
return &model.BasicParam{
TransactionId: tid,
Area: area,
BizCode: bizCode,
Source: source,
BizSource: bizSource,
MetaData: metaData,
}
}
func getTestDefaultBasicParam(tid string) *model.BasicParam {
return getTestBasicParam(tid, 0, "", "", "", "")
}
func atoiForTest(s string) int {
v, _ := strconv.Atoi(s)
return v
}
func getTestWallet(t *testing.T, uid int64, platform string) *model.MelonseedResp {
v, err := s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
if err != nil {
t.FailNow()
}
return v.(*model.MelonseedResp)
}
func getTestAll(t *testing.T, uid int64, platform string) *model.DetailResp {
v, err := s.GetAll(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
if err != nil {
t.FailNow()
}
return v.(*model.DetailResp)
}
func TestService_Get(t *testing.T) {
Convey("normal get without metal", t, testWith(func() {
Convey("basic", func() {
for _, platform := range testValidPlatform {
uid := getTestRandUid()
v, err := s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
resp := v.(*model.MelonseedResp)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(atoiForTest(resp.Gold), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.Silver), ShouldBeGreaterThanOrEqualTo, 0)
}
})
}))
Convey("params", t, testWith(func() {
Convey("invalid platform", func() {
platform := "unknown"
uid := getTestRandUid()
v, err := s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
So(v, ShouldBeNil)
So(err, ShouldEqual, ecode.RequestErr)
})
Convey("invalid uid", func() {
platform := testValidPlatform[0]
var uid int64 = 0
v, err := s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
So(v, ShouldBeNil)
So(err, ShouldEqual, ecode.RequestErr)
})
}))
Convey("metal", t, testWith(func() {
uid := getTestRandUid()
platform := testValidPlatform[0]
v, err := s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 1)
resp := v.(*model.MelonseedWithMetalResp)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(atoiForTest(resp.Gold), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.Silver), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.Metal), ShouldBeGreaterThanOrEqualTo, 0)
}))
}
func TestService_GetAll(t *testing.T) {
Convey("normal getAll without metal", t, testWith(func() {
Convey("basic", func() {
for _, platform := range testValidPlatform {
uid := getTestRandUid()
resp := getTestAll(t, uid, platform)
So(resp, ShouldNotBeNil)
So(atoiForTest(resp.Gold), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.Silver), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.SilverPayCnt), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.GoldRechargeCnt), ShouldBeGreaterThanOrEqualTo, 0)
So(atoiForTest(resp.GoldPayCnt), ShouldBeGreaterThanOrEqualTo, 0)
}
})
}))
Convey("advanced", t, testWithTestUser(func(u *TestUser) {
resp := getTestAll(t, u.uid, "pc")
So(resp.CostBase, ShouldEqual, 1000)
resp = getTestAll(t, u.uid, "pc")
So(resp.CostBase, ShouldEqual, 1000)
v, err := s.GetAll(ctx, getTestDefaultBasicParam(""), u.uid, "pc", 1)
if err != nil {
t.FailNow()
}
respWithMetal := v.(*model.DetailWithMetalResp)
So(respWithMetal, ShouldNotBeNil)
So(respWithMetal.CostBase, ShouldEqual, 1000)
}))
}

View File

@@ -0,0 +1,88 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
)
type LocalExchangeHandler struct {
service *WalletService
}
func (handler *LocalExchangeHandler) SetWalletService(ws *WalletService) {
handler.service = ws
}
func (handler *LocalExchangeHandler) exchange(c context.Context, uid int64, platform string, srcSysCoinTypeNo int32, srcCoinNum int64,
destSysCoinTypeNo int32, destCoinNum int64, srcStream *model.CoinStreamRecord, destStream *model.CoinStreamRecord, exchangeStream *model.CoinExchangeRecord) (resp *model.DetailWithSnapShot, err error) {
dao := handler.service.s.dao
v, err := dao.DoTx(handler.service.c, func(conn *sql.Tx) (v interface{}, err error) {
return handler.exchangeInTx(conn, uid, platform, srcSysCoinTypeNo, srcCoinNum, destSysCoinTypeNo, destCoinNum, srcStream, destStream, exchangeStream)
})
if err != nil {
return
}
wrapper := v.(*DetailWithSnapShotWrapper)
if wrapper.logicErr == nil && err == nil {
resp = wrapper.resp
}
if wrapper.logicErr != nil {
err = wrapper.logicErr
}
return
}
func (handler *LocalExchangeHandler) exchangeInTx(tx *sql.Tx, uid int64, platform string, srcSysCoinTypeNo int32, srcCoinNum int64,
destSysCoinTypeNo int32, destCoinNum int64, srcStream *model.CoinStreamRecord, destSteam *model.CoinStreamRecord, exchangeStream *model.CoinExchangeRecord) (wrapper *DetailWithSnapShotWrapper, err error) {
wrapper = new(DetailWithSnapShotWrapper)
dao := handler.service.s.dao
// 获取数据 for update
wallet, err := dao.WalletForUpdate(tx, uid)
if err != nil {
return
}
srcCurCoin := model.GetCoinByDetailWithSnapShot(srcSysCoinTypeNo, wallet)
destCurCoin := model.GetCoinByDetailWithSnapShot(destSysCoinTypeNo, wallet)
srcStream.OrgCoinNum = srcCurCoin
destSteam.OrgCoinNum = destCurCoin
needWriteSecondStream := false
defer func() {
if err == nil {
_, err = dao.NewCoinStreamRecordInTx(tx, srcStream)
if err == nil && needWriteSecondStream {
_, err = dao.NewCoinStreamRecordInTx(tx, destSteam)
}
if err == nil && wrapper.logicErr == nil {
_, err = dao.NewCoinExchangeRecordInTx(tx, exchangeStream)
}
}
}()
if srcCurCoin < srcCoinNum {
wrapper.logicErr = ecode.CoinNotEnough
srcStream.OpReason = model.STREAM_OP_REASON_NOT_ENOUGH_COIN
return
}
_, err = dao.ExchangeCoinInTx(tx, uid, srcSysCoinTypeNo, srcCoinNum, destSysCoinTypeNo, destCoinNum, wallet)
if err != nil {
return
}
needWriteSecondStream = true
srcStream.OpResult = model.STREAM_OP_RESULT_SUB_SUCC
destSteam.OpResult = model.STREAM_OP_RESULT_ADD_SUCC
model.ModifyCoinInDetailWithSnapShot(wallet, srcSysCoinTypeNo, -srcCoinNum)
model.ModifyCoinInDetailWithSnapShot(wallet, destSysCoinTypeNo, destCoinNum)
wrapper.resp = wallet
return
}
func getLocalPayHandler(ws *WalletService) *LocalExchangeHandler {
handler := LocalExchangeHandler{service: ws}
return &handler
}

View File

@@ -0,0 +1,138 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"testing"
"time"
)
func getWalletService() *WalletService {
ws := new(WalletService)
ws.c = ctx
ws.s = s
return ws
}
func TestPayHandler_LocalPay(t *testing.T) {
Convey("normal", t, testWithTestUser(func(u *TestUser) {
nd, _ := s.dao.Detail(ctx, u.uid)
h := getLocalPayHandler(getWalletService())
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
ef := model.ExchangeParam{
Uid: u.uid,
SrcCoinType: "gold",
SrcCoinNum: 1,
DestCoinType: "silver",
DestCoinNum: 10,
ExtendTid: getTestExtendTid(),
TransactionId: getTestTidForCall(t, 2),
Timestamp: time.Now().Unix(),
Platform: "pc",
Reason: 1,
}
t.Logf("uid:%d tid:%s", u.uid, ef.TransactionId)
srcCoinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&srcCoinStream, &ef)
srcCoinStream.DeltaCoinNum = -1
srcCoinStream.CoinType = model.SysCoinTypeGold
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
destCoinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&destCoinStream, &ef)
destCoinStream.DeltaCoinNum = 10
destCoinStream.CoinType = model.SysCoinTypeSilver
destCoinStream.OpType = int32(model.EXCHANGETYPE)
exchangeRecord := model.NewExchangeSteam(ef.Uid, ef.TransactionId, 1, int32(ef.SrcCoinNum), 0, int32(ef.DestCoinNum), ef.Timestamp, 0)
resp, err := h.exchange(ctx, u.uid, ef.Platform, model.SysCoinTypeGold, ef.SrcCoinNum, model.SysCoinTypeSilver, 10, &srcCoinStream, &destCoinStream, exchangeRecord)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
t.Logf("%+v", resp)
m1 := getTestWallet(t, u.uid, "pc")
So(m1.Gold, ShouldEqual, "999")
So(m1.Silver, ShouldEqual, "1010")
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 1000)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1000)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
So(detail2.GoldPayCnt-nd.GoldPayCnt, ShouldEqual, 1)
So(detail2.SilverPayCnt-nd.SilverPayCnt, ShouldEqual, 0)
tx.Rollback()
ef.TransactionId = getTestTidForCall(t, 2)
model.InjectFieldToCoinStream(&srcCoinStream, &ef)
srcCoinStream.DeltaCoinNum = -1
srcCoinStream.CoinType = model.SysCoinTypeGold
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
model.InjectFieldToCoinStream(&destCoinStream, &ef)
destCoinStream.DeltaCoinNum = 10
destCoinStream.CoinType = model.SysCoinTypeSilver
destCoinStream.OpType = int32(model.EXCHANGETYPE)
exchangeRecord = model.NewExchangeSteam(ef.Uid, ef.TransactionId, 1, int32(ef.SrcCoinNum), 0, int32(ef.DestCoinNum), ef.Timestamp, 0)
resp, err = h.exchange(ctx, u.uid, ef.Platform, model.SysCoinTypeGold, ef.SrcCoinNum, model.SysCoinTypeSilver, 10, &srcCoinStream, &destCoinStream, exchangeRecord)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
t.Logf("%+v", resp)
d1, _ := s.dao.Detail(ctx, u.uid)
So(d1.Gold, ShouldEqual, 998)
So(d1.Silver, ShouldEqual, 1020)
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 1000)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1000)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
ef.TransactionId = getTestTidForCall(t, 2)
model.InjectFieldToCoinStream(&srcCoinStream, &ef)
srcCoinStream.DeltaCoinNum = -1
srcCoinStream.CoinType = model.SysCoinTypeGold
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
model.InjectFieldToCoinStream(&destCoinStream, &ef)
destCoinStream.DeltaCoinNum = 10
destCoinStream.CoinType = model.SysCoinTypeSilver
destCoinStream.OpType = int32(model.EXCHANGETYPE)
exchangeRecord = model.NewExchangeSteam(ef.Uid, ef.TransactionId, 1, int32(ef.SrcCoinNum), 0, int32(ef.DestCoinNum), ef.Timestamp, 0)
resp, err = h.exchange(ctx, u.uid, ef.Platform, model.SysCoinTypeGold, ef.SrcCoinNum, model.SysCoinTypeSilver, 10, &srcCoinStream, &destCoinStream, exchangeRecord)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
t.Logf("%+v", resp)
d1, _ = s.dao.Detail(ctx, u.uid)
So(d1.Gold, ShouldEqual, 997)
So(d1.Silver, ShouldEqual, 1030)
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 998)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1020)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
}))
}

View File

@@ -0,0 +1,28 @@
package service
type UserLock interface {
lock(uid int64) error
release()
}
type RedisUserLock struct {
ws *WalletService
}
func (r *RedisUserLock) lock(uid int64) error {
return r.ws.lockSpecificUser(uid)
}
func (r *RedisUserLock) release() {
r.ws.unLockUser()
}
type NopUserLock struct {
}
func (r *NopUserLock) lock(uid int64) error {
return nil
}
func (r *NopUserLock) release() {
}

View File

@@ -0,0 +1,173 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
)
type ModifyHandler struct {
service *WalletService
}
func (handler *ModifyHandler) NeedCheckUid() bool {
return true
}
func (handler *ModifyHandler) NeedTransactionMutex() bool {
return true
}
func (handler *ModifyHandler) SetWalletService(ws *WalletService) {
handler.service = ws
}
// old TODO deprecated
func (handler *ModifyHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
return
}
func (handler *ModifyHandler) Modify(c context.Context, param *model.RechargeOrPayParam) (resp *model.MelonseedResp, err error) {
// 校验参数
if param.Uid <= 0 ||
param.CoinNum == 0 ||
param.ExtendTid == "" ||
!model.IsValidCoinType(param.CoinType) ||
!model.IsValidPlatform(param.Platform) {
err = ecode.RequestErr
return
}
sysCoinType := model.GetSysCoinType(param.CoinType, param.Platform)
sysCoinTypeNo := model.GetCoinTypeNumber(sysCoinType)
if !model.IsLocalCoin(sysCoinTypeNo) {
err = ecode.RequestErr
return
}
// 锁住tid
err = handler.service.lockTransactionId(param.TransactionId)
if err != nil {
return
}
log.Info("TAdd# opr: modify, tid:%s,etid:%s,uid,%d,platform:%s,type:%d,num:%d,time:%d",
param.TransactionId, param.GetExtendTid(), param.GetUid(), param.GetPlatform(), sysCoinTypeNo, param.CoinNum, param.Timestamp)
var serviceType model.ServiceType
var failedResult int32
if param.CoinNum < 0 {
serviceType = model.PAYTYPE
failedResult = model.STREAM_OP_RESULT_SUB_FAILED
} else {
serviceType = model.RECHARGETYPE
failedResult = model.STREAM_OP_RESULT_ADD_FAILED
}
coinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&coinStream, param)
coinStream.DeltaCoinNum = param.CoinNum
coinStream.CoinType = sysCoinTypeNo
coinStream.OpType = int32(serviceType)
coinStream.OpResult = failedResult
userLock := handler.service.getUserLock()
// 锁用户
lockErr := userLock.lock(param.Uid)
if lockErr != nil {
model.SetReasonByLockErr(lockErr, &coinStream)
err = lockErr
handler.service.s.dao.NewCoinStreamRecord(c, &coinStream)
return
}
defer userLock.release()
// 实际的db操作
wallet, err := handler.modify(param.Uid, param.Platform, sysCoinTypeNo, param.CoinNum, &coinStream)
if err == nil {
handler.service.s.dao.DelWalletCache(c, param.Uid)
resp = model.GetMelonByDetailWithSnapShot(wallet, param.Platform)
handler.service.s.pubWalletChangeWithDetailSnapShot(c,
param.Uid, "modify", param.CoinNum, param.CoinType, param.Platform, "", 0, wallet)
log.Info("tx#oper modify success uid :%d,CoinTypeNo:%d,coinNum:%d tid:%s", param.Uid, sysCoinTypeNo, param.CoinNum, param.TransactionId)
}
return
}
func (handler *ModifyHandler) modify(uid int64, platform string, sysCoinTypeNo int32, coinNum int64, stream *model.CoinStreamRecord) (resp *model.DetailWithSnapShot, err error) {
dao := handler.service.s.dao
v, err := dao.DoTx(handler.service.c, func(conn *sql.Tx) (v interface{}, err error) {
return handler.modifyInTx(conn, uid, platform, sysCoinTypeNo, coinNum, stream)
})
if err != nil {
return
}
wrapper := v.(*DetailWithSnapShotWrapper)
if wrapper.logicErr == nil && err == nil {
resp = wrapper.resp
}
if wrapper.logicErr != nil {
err = wrapper.logicErr
}
return
}
func (handler *ModifyHandler) modifyInTx(tx *sql.Tx, uid int64, platform string, sysCoinTypeNo int32, coinNum int64, stream *model.CoinStreamRecord) (wrapper *DetailWithSnapShotWrapper, err error) {
wrapper = new(DetailWithSnapShotWrapper)
dao := handler.service.s.dao
// 获取数据 for update
wallet, err := dao.WalletForUpdate(tx, uid)
if err != nil {
return
}
curCoin := model.GetCoinByDetailWithSnapShot(sysCoinTypeNo, wallet)
stream.OrgCoinNum = curCoin
defer func() {
if err == nil {
_, err = dao.NewCoinStreamRecordInTx(tx, stream)
}
}()
if coinNum < 0 {
if curCoin < -coinNum {
wrapper.logicErr = ecode.CoinNotEnough
stream.OpReason = model.STREAM_OP_REASON_NOT_ENOUGH_COIN
return
}
}
_, err = dao.ModifyCoinInTx(tx, uid, sysCoinTypeNo, coinNum, wallet)
if err != nil {
return
}
var succResult int32
if coinNum < 0 {
succResult = model.STREAM_OP_RESULT_SUB_SUCC
} else {
succResult = model.STREAM_OP_RESULT_ADD_SUCC
}
stream.OpResult = succResult
model.ModifyCoinInDetailWithSnapShot(wallet, sysCoinTypeNo, coinNum)
wrapper.resp = wallet
return
}
func (s *Service) Modify(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
arg, _ := params[1].(*model.RechargeOrPayForm)
rechargeParam := buildRechargeOrPayParam(platform, arg, uid, basicParam)
handler := ModifyHandler{}
ws := new(WalletService)
ws.c = c
ws.s = s
ws.SetServiceHandler(&handler)
handler.SetWalletService(ws)
return handler.Modify(c, rechargeParam)
}

View File

@@ -0,0 +1,320 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"sync"
"testing"
"time"
)
func TestService_Modify(t *testing.T) {
Convey("normal add gold", t, testWith(func() {
for _, platform := range testValidPlatform {
u := initTestUser()
beforeDetail := getTestAll(t, u.uid, platform)
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "gold", 100, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
afterDetail := getTestAll(t, u.uid, platform)
So(atoiForTest(afterDetail.GoldRechargeCnt)-atoiForTest(beforeDetail.GoldRechargeCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.Gold)-atoiForTest(beforeDetail.Gold), ShouldEqual, 100)
record, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldBeNil)
So(record.OrgCoinNum, ShouldEqual, u.coin)
So(record.DeltaCoinNum, ShouldEqual, 100)
So(record.OpType, ShouldEqual, int32(model.RECHARGETYPE))
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_ADD_SUCC)
success, resp, err := queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.Status, ShouldEqual, TX_STATUS_SUCC)
_, err = s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldEqual, ecode.TargetBlocked)
}
}))
Convey("normal decrement gold", t, testWith(func() {
for _, platform := range testValidPlatform {
u := initTestUser()
beforeDetail := getTestAll(t, u.uid, platform)
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "gold", -100, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
afterDetail := getTestAll(t, u.uid, platform)
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.Gold)-atoiForTest(beforeDetail.Gold), ShouldEqual, -100)
record, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldBeNil)
So(record.OrgCoinNum, ShouldEqual, u.coin)
So(record.DeltaCoinNum, ShouldEqual, -100)
So(record.OpType, ShouldEqual, int32(model.PAYTYPE))
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_SUB_SUCC)
success, resp, err := queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.Status, ShouldEqual, TX_STATUS_SUCC)
}
}))
Convey("normal add silver", t, testWith(func() {
for _, platform := range testValidPlatform {
u := initTestUser()
beforeDetail := getTestAll(t, u.uid, platform)
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "silver", 100, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
afterDetail := getTestAll(t, u.uid, platform)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.Silver)-atoiForTest(beforeDetail.Silver), ShouldEqual, 100)
record, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldBeNil)
So(record.OrgCoinNum, ShouldEqual, u.coin)
So(record.DeltaCoinNum, ShouldEqual, 100)
So(record.OpType, ShouldEqual, int32(model.RECHARGETYPE))
So(record.CoinType, ShouldEqual, 0)
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_ADD_SUCC)
success, resp, err := queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.Status, ShouldEqual, TX_STATUS_SUCC)
}
}))
Convey("normal decrement silver", t, testWith(func() {
for _, platform := range testValidPlatform {
u := initTestUser()
beforeDetail := getTestAll(t, u.uid, platform)
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "silver", -100, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
afterDetail := getTestAll(t, u.uid, platform)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.Silver)-atoiForTest(beforeDetail.Silver), ShouldEqual, -100)
record, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldBeNil)
So(record.OrgCoinNum, ShouldEqual, u.coin)
So(record.DeltaCoinNum, ShouldEqual, -100)
So(record.OpType, ShouldEqual, int32(model.PAYTYPE))
So(record.CoinType, ShouldEqual, 0)
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_SUB_SUCC)
success, resp, err := queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.Status, ShouldEqual, TX_STATUS_SUCC)
}
}))
Convey("params", t, testWith(func() {
Convey("coinNum equal 0", testWithTestUser(func(u *TestUser) {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
platform := "pc"
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "silver", 0, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldEqual, ecode.RequestErr)
_, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldNotBeNil)
}))
Convey("not local coin", testWithTestUser(func(u *TestUser) {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
platform := "pc"
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "metal", 100, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldEqual, ecode.RequestErr)
_, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldNotBeNil)
}))
}))
Convey("logic", t, testWith(func() {
Convey("not enough", testWith(func() {
for _, platform := range testValidPlatform {
for _, coinType := range testLocalValidCoinType {
u := initTestUser()
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, coinType, -1001, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldEqual, ecode.CoinNotEnough)
coinRecord, recordErr := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(recordErr, ShouldBeNil)
So(coinRecord.OpResult, ShouldEqual, model.STREAM_OP_RESULT_SUB_FAILED)
So(coinRecord.OpReason, ShouldEqual, model.STREAM_OP_REASON_NOT_ENOUGH_COIN)
success, resp, err := queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.Status, ShouldEqual, TX_STATUS_FAILED)
wallet := getTestWallet(t, u.uid, platform)
So(wallet.Gold, ShouldEqual, "1000")
}
}
}))
}))
Convey("multi", t, testWith(func() {
for _, platform := range []string{"pc", "ios"} {
for _, coinType := range []string{"gold", "silver"} {
u := initTestUser()
var detail1 *model.DetailWithSnapShot
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
tx, _ := s.dao.BeginTx(ctx)
detail1, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail1: %+v", detail1)
So(detail1.SnapShotTime, ShouldEqual, "0001-01-01 00:00:00")
tx.Rollback()
beforeDetail := getTestAll(t, u.uid, platform)
var step int64 = 10
num := u.coin / step
times := int(step * 2)
var wg sync.WaitGroup
var lock sync.Mutex
modifyRespMap := make(map[string]*ServiceResForTest)
for i := 0; i < times; i++ {
wg.Add(1)
go func() {
localService := New(conf.Conf)
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, coinType, num*-1, tid)
v, err := localService.Modify(ctx, bp, u.uid, platform, form)
lock.Lock()
modifyRespMap[tid] = &ServiceResForTest{Err: err, V: v}
lock.Unlock()
wg.Done()
}()
time.Sleep(time.Millisecond * 30)
}
wg.Wait()
successNum := 0
validErrs := map[error]bool{ecode.TargetBlocked: true, nil: true, ecode.CoinNotEnough: true}
for tid, res := range modifyRespMap {
_, ok := validErrs[res.Err]
So(ok, ShouldBeTrue)
if res.Err == nil {
assertNormalCoinStream(t, platform, coinType, tid, 0, num*-1, model.PAYTYPE, model.STREAM_OP_RESULT_SUB_SUCC, TX_STATUS_SUCC)
successNum++
} else if res.Err == ecode.TargetBlocked {
record := assertNormalCoinStream(t, platform, coinType, tid, 0, num*-1, model.PAYTYPE, model.STREAM_OP_RESULT_SUB_FAILED, TX_STATUS_FAILED)
So(record.OpReason, ShouldEqual, model.STREAM_OP_REASON_LOCK_FAILED)
} else {
record := assertNormalCoinStream(t, platform, coinType, tid, 0, num*-1, model.PAYTYPE, model.STREAM_OP_RESULT_SUB_FAILED, TX_STATUS_FAILED)
So(record.OpReason, ShouldEqual, model.STREAM_OP_REASON_NOT_ENOUGH_COIN)
}
}
t.Logf("successNum:%d", successNum)
var detail2 *model.DetailWithSnapShot
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, detail1.Gold)
So(detail2.SnapShotIapGold, ShouldEqual, detail1.IapGold)
So(detail2.SnapShotSilver, ShouldEqual, detail1.Silver)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
afterDetail := getTestAll(t, u.uid, platform)
So(getCoinNumByDetailForTest(afterDetail, coinType)-getCoinNumByDetailForTest(beforeDetail, coinType), ShouldEqual, -1*successNum*int(num))
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.GoldRechargeCnt)-atoiForTest(beforeDetail.GoldRechargeCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, 0)
u.rest()
}
}
}))
}
func TestService_ModifyBasicParam(t *testing.T) {
Convey("default", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(u.uid, "gold", 1, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
record, err := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 0)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 2)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, 1)
}
}))
Convey("advanced", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
bp.Reason = 1
bp.BizCode = "send-gift"
form := getTestRechargeOrPayForm(u.uid, "gold", 1, tid)
_, err := s.Modify(ctx, bp, u.uid, platform, form)
So(err, ShouldBeNil)
record, err := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, bp.Reason)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 2)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, 1)
So(record.BizCode, ShouldEqual, bp.BizCode)
}
}))
}

View File

@@ -0,0 +1,121 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
)
type OutExchangeHandler struct {
service *WalletService
}
func (handler *OutExchangeHandler) SetWalletService(ws *WalletService) {
handler.service = ws
}
func (handler *OutExchangeHandler) exchange(c context.Context, uid int64, platform string, srcSysCoinTypeNo int32, srcCoinNum int64,
destSysCoinTypeNo int32, destCoinNum int64, srcStream *model.CoinStreamRecord, destStream *model.CoinStreamRecord, exchangeStream *model.CoinExchangeRecord) (resp *model.DetailWithSnapShot, err error) {
payResp, err := handler.pay(c, uid, platform, srcSysCoinTypeNo, srcCoinNum, destCoinNum, srcStream)
if err != nil {
resp = payResp
return
}
rechargeResp, err := handler.recharge(c, uid, platform, destSysCoinTypeNo, destCoinNum, destStream, exchangeStream)
if err != nil {
resp = rechargeResp
// 走到这里说明pay成功了但是recharge失败且pay的是直播内部货币(非硬币) recharge的是硬币 那么直接认为成功
// 原因: 硬币没有提供幂等性接口,无法知道知道最后是否成功,如果认为失败,可能会影响业务 比如用户会多次去扣银瓜子加硬币
// 如果发生这样的事情 则直播认为成功,用户最后发现硬币没有加上,通过反馈的方式人工接入
// 如果这样的事情发生太多 则需要推动硬币提供幂等接口
if destSysCoinTypeNo == model.SysCoinTypeMetal {
resp = payResp
err = nil
}
return
}
if payResp == nil {
resp = rechargeResp
} else {
resp = payResp
}
return
}
func (handler *OutExchangeHandler) pay(c context.Context, uid int64, platform string, sysCoinTypeNo int32, coinNum int64, destCoinNum int64, stream *model.CoinStreamRecord) (resp *model.DetailWithSnapShot, err error) {
if sysCoinTypeNo == model.SysCoinTypeMetal {
stream.OrgCoinNum = -1
_, err = handler.service.s.dao.ConsumeCoin(handler.service.c, int(coinNum), uid, sysCoinTypeNo, destCoinNum, true, nil)
err = handleMetalResp(err, stream, uid, sysCoinTypeNo, coinNum, handler, model.STREAM_OP_RESULT_SUB_SUCC)
return
}
// 本地货币
payHandler := getPayHandler(handler.service)
resp, err = payHandler.localPay(uid, platform, sysCoinTypeNo, coinNum, stream)
return
}
func (handler *OutExchangeHandler) recharge(c context.Context, uid int64, platform string, sysCoinTypeNo int32, coinNum int64, stream *model.CoinStreamRecord, exchangeStream *model.CoinExchangeRecord) (resp *model.DetailWithSnapShot, err error) {
if sysCoinTypeNo == model.SysCoinTypeMetal {
stream.OrgCoinNum = -1
var success bool
success, _, err = handler.service.s.dao.ModifyMetal(handler.service.c, uid, coinNum, 0, nil)
err = handleMetalResp(err, stream, uid, sysCoinTypeNo, coinNum, handler, model.STREAM_OP_RESULT_ADD_SUCC)
if success {
_, insertErr := handler.service.s.dao.NewCoinExchangeRecord(c, exchangeStream)
if insertErr != nil {
log.Error("tx#exchange#metal handle success but insert exchange stream :%s", insertErr.Error())
}
}
return
}
// 本地货币
rechargeHandler := getRechargeHandler(handler.service)
dao := handler.service.s.dao
v, err := dao.DoTx(handler.service.c, func(conn *sql.Tx) (v interface{}, err error) {
v, err = rechargeHandler.rechargeInTx(conn, uid, platform, sysCoinTypeNo, coinNum, stream)
if err == nil {
_, err = dao.NewCoinExchangeRecordInTx(conn, exchangeStream)
}
return
})
if err == nil {
resp = v.(*model.DetailWithSnapShot)
}
return
}
func handleMetalResp(err error, stream *model.CoinStreamRecord, uid int64, sysCoinTypeNo int32, coinNum int64, handler *OutExchangeHandler, successResult int32) error {
if err != nil {
// 虽然上面检查了余额并加锁但是如硬币这样的主站货币live这边依然无法保证能够锁住依然可能会有余额不够的情况
if err == ecode.CoinNotEnough {
stream.SetOpReason(model.STREAM_OP_REASON_NOT_ENOUGH_COIN)
} else { // 更新失败
stream.SetOpReason(model.STREAM_OP_REASON_EXECUTE_FAILED)
log.Error("tx#oper exchange sub uid :%d,sysCoinTypeNo:%d,coinNum:%d err:%s", uid, sysCoinTypeNo, coinNum, err.Error())
err = ecode.ServerErr
}
} else { // 成功
stream.OpResult = successResult
}
_, insertErr := handler.service.s.dao.NewCoinStreamRecord(handler.service.c, stream)
if insertErr != nil {
log.Error("tx#exchange#metal handle success but insert coin stream :%s", insertErr.Error())
}
return err
}
func getOutExchangeHandler(ws *WalletService) *OutExchangeHandler {
handler := OutExchangeHandler{service: ws}
return &handler
}

View File

@@ -0,0 +1,137 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"testing"
"time"
)
func TestPayHandler_OutPay(t *testing.T) {
Convey("normal", t, testWithTestUser(func(u *TestUser) {
nd, _ := s.dao.Detail(ctx, u.uid)
metal1, _ := s.dao.GetMetal(ctx, u.uid)
t.Logf("origin metal:%f", metal1)
h := getOutExchangeHandler(getWalletService())
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
ef := model.ExchangeParam{
Uid: u.uid,
SrcCoinType: "gold",
SrcCoinNum: 1,
DestCoinType: "metal",
DestCoinNum: 10,
ExtendTid: getTestExtendTid(),
TransactionId: getTestTidForCall(t, 2),
Timestamp: time.Now().Unix(),
Platform: "pc",
Reason: 1,
}
t.Logf("uid:%d tid:%s", u.uid, ef.TransactionId)
srcCoinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&srcCoinStream, &ef)
srcCoinStream.DeltaCoinNum = -1
srcCoinStream.CoinType = model.SysCoinTypeGold
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
destCoinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&destCoinStream, &ef)
destCoinStream.DeltaCoinNum = 10
destCoinStream.CoinType = model.SysCoinTypeMetal
destCoinStream.OpType = int32(model.EXCHANGETYPE)
exchangeRecord := model.NewExchangeSteam(ef.Uid, ef.TransactionId, 1, int32(ef.SrcCoinNum), 0, int32(ef.DestCoinNum), ef.Timestamp, 0)
resp, err := h.exchange(ctx, u.uid, ef.Platform, model.SysCoinTypeGold, ef.SrcCoinNum, model.SysCoinTypeMetal, 10, &srcCoinStream, &destCoinStream, exchangeRecord)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
t.Logf("%+v", resp)
m1 := getTestWallet(t, u.uid, "pc")
So(m1.Gold, ShouldEqual, "999")
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 1000)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1000)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
So(detail2.GoldPayCnt-nd.GoldPayCnt, ShouldEqual, 1)
So(detail2.SilverPayCnt-nd.SilverPayCnt, ShouldEqual, 0)
tx.Rollback()
ef.TransactionId = getTestTidForCall(t, 2)
t.Logf("uid:%d tid:%s", u.uid, ef.TransactionId)
model.InjectFieldToCoinStream(&srcCoinStream, &ef)
srcCoinStream.DeltaCoinNum = -1
srcCoinStream.CoinType = model.SysCoinTypeGold
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
model.InjectFieldToCoinStream(&destCoinStream, &ef)
destCoinStream.DeltaCoinNum = 10
destCoinStream.CoinType = model.SysCoinTypeMetal
destCoinStream.OpType = int32(model.EXCHANGETYPE)
exchangeRecord = model.NewExchangeSteam(ef.Uid, ef.TransactionId, 1, int32(ef.SrcCoinNum), 0, int32(ef.DestCoinNum), ef.Timestamp, 0)
resp, err = h.exchange(ctx, u.uid, ef.Platform, model.SysCoinTypeGold, ef.SrcCoinNum, model.SysCoinTypeMetal, 10, &srcCoinStream, &destCoinStream, exchangeRecord)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
t.Logf("%+v", resp)
d1, _ := s.dao.Detail(ctx, u.uid)
So(d1.Gold, ShouldEqual, 998)
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 1000)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1000)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
ef.TransactionId = getTestTidForCall(t, 2)
t.Logf("uid:%d tid:%s", u.uid, ef.TransactionId)
model.InjectFieldToCoinStream(&srcCoinStream, &ef)
srcCoinStream.DeltaCoinNum = -1
srcCoinStream.CoinType = model.SysCoinTypeGold
srcCoinStream.OpType = int32(model.EXCHANGETYPE)
model.InjectFieldToCoinStream(&destCoinStream, &ef)
destCoinStream.DeltaCoinNum = 10
destCoinStream.CoinType = model.SysCoinTypeMetal
destCoinStream.OpType = int32(model.EXCHANGETYPE)
exchangeRecord = model.NewExchangeSteam(ef.Uid, ef.TransactionId, 1, int32(ef.SrcCoinNum), 0, int32(ef.DestCoinNum), ef.Timestamp, 0)
resp, err = h.exchange(ctx, u.uid, ef.Platform, model.SysCoinTypeGold, ef.SrcCoinNum, model.SysCoinTypeMetal, 10, &srcCoinStream, &destCoinStream, exchangeRecord)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
t.Logf("%+v", resp)
d1, _ = s.dao.Detail(ctx, u.uid)
So(d1.Gold, ShouldEqual, 997)
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, 998)
So(detail2.SnapShotIapGold, ShouldEqual, 1000)
So(detail2.SnapShotSilver, ShouldEqual, 1000)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
metal2, _ := s.dao.GetMetal(ctx, u.uid)
t.Logf("origin metal:%f", metal2)
So(metal2-metal1, ShouldEqual, 30)
}))
}

View File

@@ -0,0 +1,349 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
)
// PayHandler pay handler
type PayHandler struct {
service *WalletService
}
// NeedCheckUid need check uid
func (handler *PayHandler) NeedCheckUid() bool {
return true
}
// NeedTransactionMutex need transaction mutex
func (handler *PayHandler) NeedTransactionMutex() bool {
return true
}
// SetWalletService set wallet service
func (handler *PayHandler) SetWalletService(ws *WalletService) {
handler.service = ws
}
// BizExecute biz execute
func (handler *PayHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
if !model.IsValidPlatform(platform) {
err = ecode.RequestErr
return
}
arg, _ := params[1].(*model.RechargeOrPayForm)
if uid <= 0 || arg.CoinNum <= 0 || arg.ExtendTid == "" || !model.IsValidCoinType(arg.CoinType) {
err = ecode.RequestErr
return
}
var reason interface{}
if len(params) > 2 {
reason = params[2]
} else {
reason = nil
}
sysCoinType := model.GetSysCoinType(arg.CoinType, platform)
sysCoinTypeNo := model.GetCoinTypeNumber(sysCoinType)
log.Info("TAdd# opr: pay, tid:%s,etid:%s,uid,%d,platform:%s,type:%d,num:%d,time:%d",
arg.TransactionId, arg.ExtendTid, uid, platform, sysCoinTypeNo, arg.CoinNum, arg.Timestamp)
coinStreamRecord := model.NewCoinStream(uid, arg.TransactionId, arg.ExtendTid, sysCoinTypeNo, -1*arg.CoinNum, int32(model.PAYTYPE), arg.Timestamp,
basicParam.BizCode, basicParam.Area, basicParam.Source, basicParam.BizSource, basicParam.MetaData)
model.AddMoreParam2CoinStream(coinStreamRecord, basicParam, platform)
// 初始状态
coinStreamRecord.OrgCoinNum = -1
coinStreamRecord.OpResult = model.STREAM_OP_RESULT_SUB_FAILED
var (
executeRes = false
originCoin interface{} = -1
originUserCoin int64 // 用于写入数据库中的org_coin_num字段
)
// 获取交易前的用户数据用来1:校验参数 2: 用于最后返回数据
originMelon, dbErr := ws.s.dao.Melonseed(ws.c, uid)
for {
// 查询出错则结束交易
if dbErr != nil {
err = ecode.ServerErr
log.Error("wallet user get coin failed : uid :%d", uid)
coinStreamRecord.OpReason = model.STREAM_OP_REASON_PRE_QUERY_FAILED
break
}
if model.IsLocalCoin(sysCoinTypeNo) {
originCoin = model.GetCoinByMelonseed(sysCoinTypeNo, originMelon)
} else {
// 如果硬币支付需要单独获取数据,查询失败结束交易
originCoin, err = ws.s.dao.GetMetal(ws.c, uid)
if err != nil {
log.Error("wallet user get metal failed : uid :%d", uid)
coinStreamRecord.OpReason = model.STREAM_OP_REASON_PRE_QUERY_FAILED
originCoin = -1
err = ecode.ServerErr
break
}
}
// 校验参数 货币不足结束交易
if !model.CompareCoin(originCoin, arg.CoinNum) {
coinStreamRecord.OpReason = model.STREAM_OP_REASON_NOT_ENOUGH_COIN
err = ecode.CoinNotEnough
break
}
// 对用户加锁,加锁失败结束交易
lockErr := ws.lockUser()
if lockErr != nil {
if lockErr == ecode.TargetBlocked {
coinStreamRecord.OpReason = model.STREAM_OP_REASON_LOCK_FAILED
} else {
coinStreamRecord.OpReason = model.STREAM_OP_REASON_LOCK_ERROR
}
err = lockErr
// 跳出
break
}
// 锁定成功再次获取数据
var lockAfterCoin interface{}
lockAfterCoin, err = ws.s.dao.GetCoin(ws.c, sysCoinTypeNo, uid)
if err != nil {
log.Error("wallet user get coin failed after lock : uid :%d,coinType:%d", uid, sysCoinTypeNo)
err = ecode.ServerErr
coinStreamRecord.OpReason = model.STREAM_OP_REASON_QUERY_FAILED
break
}
if !model.CompareCoin(lockAfterCoin, arg.CoinNum) {
coinStreamRecord.OpReason = model.STREAM_OP_REASON_NOT_ENOUGH_COIN
err = ecode.CoinNotEnough
break
}
// 赋予锁后的数据
originCoin = lockAfterCoin
// 更新数据
var success bool
success, err = ws.s.dao.ConsumeCoin(ws.c, int(arg.CoinNum), uid, sysCoinTypeNo, 0, true, reason)
if !success {
// 虽然上面检查了余额并加锁但是如硬币这样的主站货币live这边依然无法保证能够锁住依然可能会有余额不够的情况
if err == ecode.CoinNotEnough {
coinStreamRecord.OpReason = model.STREAM_OP_REASON_NOT_ENOUGH_COIN
err = ecode.CoinNotEnough
break
}
if err != nil { // 更新失败
log.Error("tx#oper pay update recharge uid :%d,sysCoinTypeNo:%d,coinNum:%d err:%s", uid, sysCoinTypeNo, arg.CoinNum, err.Error())
err = ecode.PayFailed
break
}
var consumeAfterCoin interface{}
consumeAfterCoin, err = ws.s.dao.GetCoin(ws.c, sysCoinTypeNo, uid)
if err != nil {
err = ecode.PayFailed
coinStreamRecord.OpReason = model.STREAM_OP_REASON_POST_QUERY_FAILED
break
}
if model.SubCoin(originCoin, consumeAfterCoin) == arg.CoinNum {
executeRes = true
break
}
err = ecode.PayFailed
break
}
log.Info("tx#oper pay success uid :%d,sysCoinTypeNo:%d,coinNum:%d ", uid, sysCoinTypeNo, arg.CoinNum)
executeRes = true
goto payEnd
}
payEnd:
originUserCoin = model.GetDbFitCoin(originCoin)
coinStreamRecord.OrgCoinNum = originUserCoin
if executeRes {
coinStreamRecord.OpResult = model.STREAM_OP_RESULT_SUB_SUCC
model.IncrMelonseedCoin(originMelon, 0-arg.CoinNum, sysCoinTypeNo)
v = model.GetMelonseedResp(platform, originMelon)
if model.IsLocalCoin(sysCoinTypeNo) {
ws.s.pubWalletChange(ws.c, uid, "pay", arg.CoinNum, arg.CoinType, platform, "", 0)
}
}
ws.s.dao.NewCoinStreamRecord(ws.c, coinStreamRecord)
return
}
// LocalPay local pay
func (handler *PayHandler) LocalPay(c context.Context, payParam *model.RechargeOrPayParam) (resp *model.MelonseedResp, err error) {
// 校验参数
if payParam.Uid <= 0 ||
payParam.CoinNum <= 0 ||
payParam.ExtendTid == "" ||
!model.IsValidCoinType(payParam.CoinType) ||
!model.IsValidPlatform(payParam.Platform) {
err = ecode.RequestErr
return
}
sysCoinType := model.GetSysCoinType(payParam.CoinType, payParam.Platform)
sysCoinTypeNo := model.GetCoinTypeNumber(sysCoinType)
if !model.IsLocalCoin(sysCoinTypeNo) {
err = ecode.RequestErr
return
}
// 锁住tid
err = handler.service.lockTransactionId(payParam.TransactionId)
if err != nil {
return
}
log.Info("TAdd# opr: pay, tid:%s,etid:%s,uid,%d,platform:%s,type:%d,num:%d,time:%d",
payParam.TransactionId, payParam.GetExtendTid(), payParam.GetUid(), payParam.GetPlatform(), sysCoinTypeNo, payParam.CoinNum, payParam.Timestamp)
coinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&coinStream, payParam)
coinStream.DeltaCoinNum = -payParam.CoinNum
coinStream.CoinType = sysCoinTypeNo
coinStream.OpType = int32(model.PAYTYPE)
// 初始状态
coinStream.OrgCoinNum = -1
coinStream.OpResult = model.STREAM_OP_RESULT_SUB_FAILED
userLock := handler.service.getUserLock()
// 锁用户
lockErr := userLock.lock(payParam.Uid)
if lockErr != nil {
model.SetReasonByLockErr(lockErr, &coinStream)
err = lockErr
handler.service.s.dao.NewCoinStreamRecord(c, &coinStream)
return
}
defer userLock.release()
wallet, err := handler.localPay(payParam.Uid, payParam.Platform, sysCoinTypeNo, payParam.CoinNum, &coinStream)
if err == nil {
handler.service.s.dao.DelWalletCache(c, payParam.Uid)
resp = model.GetMelonByDetailWithSnapShot(wallet, payParam.Platform)
handler.service.s.pubWalletChangeWithDetailSnapShot(c,
payParam.Uid, "pay", payParam.CoinNum, payParam.CoinType, payParam.Platform, "", 0, wallet)
}
return
}
// DetailWithSnapShotWrapper detail with snapshot wrapper
type DetailWithSnapShotWrapper struct {
resp *model.DetailWithSnapShot
logicErr error
}
func (handler *PayHandler) localPay(uid int64, platform string, sysCoinTypeNo int32, coinNum int64, stream *model.CoinStreamRecord) (resp *model.DetailWithSnapShot, err error) {
dao := handler.service.s.dao
v, err := dao.DoTx(handler.service.c, func(conn *sql.Tx) (v interface{}, err error) {
return handler.localPayInTx(conn, uid, platform, sysCoinTypeNo, coinNum, stream)
})
if err != nil {
return
}
wrapper := v.(*DetailWithSnapShotWrapper)
if wrapper.logicErr == nil && err == nil {
resp = wrapper.resp
}
if wrapper.logicErr != nil {
err = wrapper.logicErr
}
return
}
func (handler *PayHandler) localPayInTx(tx *sql.Tx, uid int64, platform string, sysCoinTypeNo int32, coinNum int64, stream *model.CoinStreamRecord) (wrapper *DetailWithSnapShotWrapper, err error) {
wrapper = new(DetailWithSnapShotWrapper)
dao := handler.service.s.dao
// 获取数据 for update
wallet, err := dao.WalletForUpdate(tx, uid)
if err != nil {
return
}
curCoin := model.GetCoinByDetailWithSnapShot(sysCoinTypeNo, wallet)
stream.OrgCoinNum = curCoin
defer func() {
if err == nil {
_, err = dao.NewCoinStreamRecordInTx(tx, stream)
}
}()
if curCoin < coinNum {
wrapper.logicErr = ecode.CoinNotEnough
stream.OpReason = model.STREAM_OP_REASON_NOT_ENOUGH_COIN
return
}
// 减款
_, err = dao.PayCoinInTx(tx, uid, sysCoinTypeNo, coinNum, wallet)
if err != nil {
return
}
stream.OpResult = model.STREAM_OP_RESULT_SUB_SUCC
model.ModifyCoinInDetailWithSnapShot(wallet, sysCoinTypeNo, -coinNum)
wrapper.resp = wallet
return
}
// Pay pay
func (s *Service) Pay(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
if !model.IsValidPlatform(platform) {
err = ecode.RequestErr
return
}
arg, _ := params[1].(*model.RechargeOrPayForm)
if !model.IsValidCoinType(arg.CoinType) {
err = ecode.RequestErr
return
}
sysCoinType := model.GetSysCoinType(arg.CoinType, platform)
sysCoinTypeNo := model.GetCoinTypeNumber(sysCoinType)
if model.IsLocalCoin(sysCoinTypeNo) {
// 走新的
platform, _ := params[0].(string)
arg, _ := params[1].(*model.RechargeOrPayForm)
payParam := buildRechargeOrPayParam(platform, arg, uid, basicParam)
ws := new(WalletService)
ws.c = c
ws.s = s
handler := getPayHandler(ws)
return handler.LocalPay(c, payParam)
} else {
handler := PayHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}
}
func getPayHandler(ws *WalletService) *PayHandler {
handler := PayHandler{}
handler.SetWalletService(ws)
return &handler
}

View File

@@ -0,0 +1,381 @@
package service
import (
"context"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/sync/errgroup"
"sync"
"testing"
"time"
)
func queryPay(t *testing.T, uid int64, platform string, coinType string, coinNum int64) (success bool, tid string, err error) {
tid = getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
_, err = s.Pay(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, coinNum, tid))
if err == nil {
success = true
}
return
}
func queryPayWithReason(t *testing.T, uid int64, platform string, coinType string, coinNum int64, reason string) (success bool, tid string, err error) {
tid = getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
_, err = s.Pay(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, coinNum, tid), reason)
if err == nil {
success = true
}
return
}
func testWithRandUid(f func(uid int64)) func() {
return testWith(func() {
uid := getTestRandUid()
f(uid)
})
}
func TestService_Pay(t *testing.T) {
Convey("normal", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
beforeWallet := getTestWallet(t, u.uid, platform)
beforeDetail := getTestAll(t, u.uid, platform)
var num int64 = 100
// 加金瓜子
success, tid, err := queryPay(t, u.uid, platform, "gold", num)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
success, queryResp, err := queryQueryWithUid(t, tid, u.uid)
t.Logf("tid:%s", tid)
So(success, ShouldBeTrue)
So(queryResp.Status, ShouldEqual, TX_STATUS_SUCC)
So(err, ShouldBeNil)
// 加银瓜子
success, tid, err = queryPay(t, u.uid, platform, "silver", num)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
success, queryResp, err = queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(queryResp.Status, ShouldEqual, TX_STATUS_SUCC)
So(err, ShouldBeNil)
afterWallet := getTestWallet(t, u.uid, platform)
afterDetail := getTestAll(t, u.uid, platform)
So(atoiForTest(afterWallet.Gold)-atoiForTest(beforeWallet.Gold), ShouldEqual, 0-num)
So(atoiForTest(afterWallet.Silver)-atoiForTest(beforeWallet.Silver), ShouldEqual, 0-num)
So(atoiForTest(afterDetail.Gold)-atoiForTest(beforeDetail.Gold), ShouldEqual, 0-num)
So(atoiForTest(afterDetail.Silver)-atoiForTest(beforeDetail.Silver), ShouldEqual, 0-num)
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, num)
So(atoiForTest(afterDetail.GoldRechargeCnt)-atoiForTest(beforeDetail.GoldRechargeCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, num)
}
}))
Convey("logic", t, testWith(func() {
Convey("coin not enough", testWithTestUser(func(u *TestUser) {
var num int64 = 100
for _, platform := range testValidPlatform {
for _, coinType := range testLocalValidCoinType {
_, tid, err := queryPay(t, u.uid, platform, coinType, u.coin+num)
So(err, ShouldEqual, ecode.CoinNotEnough)
record, err := s.dao.GetCoinStreamByTid(ctx, tid)
So(err, ShouldBeNil)
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_SUB_FAILED)
So(record.OpReason, ShouldEqual, model.STREAM_OP_REASON_NOT_ENOUGH_COIN)
success, queryResp, err := queryQueryWithUid(t, tid, u.uid)
So(success, ShouldBeTrue)
So(queryResp.Status, ShouldEqual, TX_STATUS_FAILED)
So(err, ShouldBeNil)
}
}
}))
}))
Convey("params", t, testWith(func() {
Convey("coinNum invalid", testWithRandUid(func(uid int64) {
invalidCoinNums := []int64{-1, 0, -100}
for _, coinNum := range invalidCoinNums {
success, tid, err := queryPay(t, uid, "pc", "gold", coinNum)
So(success, ShouldEqual, false)
So(err, ShouldEqual, ecode.RequestErr)
success, resp, err := queryQueryWithUid(t, tid, uid)
So(success, ShouldEqual, false)
So(resp, ShouldBeNil)
So(err, ShouldEqual, ecode.NothingFound)
}
}))
Convey("uid invalid", testWith(func() {
invalidUIDs := []int64{-1, 0, -2}
for _, uid := range invalidUIDs {
success, _, err := queryPay(t, uid, "pc", "gold", 100)
So(success, ShouldEqual, false)
So(err, ShouldEqual, ecode.RequestErr)
}
}))
}))
}
func TestService_PayMulti(t *testing.T) {
Convey("multi", t, testWith(func() {
rate := 10
times := rate * 2
platforms := []string{"pc", "ios"}
coinTypes := []string{"silver", "gold"}
for _, platform := range platforms {
for _, coinType := range coinTypes {
u := initTestUser()
s.dao.UpdateSnapShotTime(ctx, u.uid, "")
var detail1 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail1, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail1: %+v", detail1)
So(detail1.SnapShotTime, ShouldEqual, "0001-01-01 00:00:00")
tx.Rollback()
payRespMap := make(map[string]*ServiceResForTest)
coinNum := u.coin / int64(rate)
var wg sync.WaitGroup
var lock sync.Mutex
for i := 0; i < times; i++ {
wg.Add(1)
go func() {
localService := New(conf.Conf)
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
v, err := localService.Pay(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, coinType, coinNum, tid))
lock.Lock()
payRespMap[tid] = &ServiceResForTest{V: v, Err: err}
lock.Unlock()
wg.Done()
}()
time.Sleep(time.Millisecond * 30)
}
wg.Wait()
successNum := 0
validErrs := map[error]bool{ecode.TargetBlocked: true, nil: true, ecode.CoinNotEnough: true}
for tid, res := range payRespMap {
_, ok := validErrs[res.Err]
if !ok {
t.Logf("%v", res.Err)
t.FailNow()
}
So(ok, ShouldBeTrue)
querySuccess, queryResp, queryErr := queryQueryWithUid(t, tid, u.uid)
So(querySuccess, ShouldBeTrue)
So(queryErr, ShouldBeNil)
coinRecord, recordErr := s.dao.GetCoinStreamByTid(ctx, tid)
So(recordErr, ShouldBeNil)
if res.Err == nil {
successNum++
So(queryResp.Status, ShouldEqual, TX_STATUS_SUCC)
So(coinRecord.OpResult, ShouldEqual, model.STREAM_OP_RESULT_SUB_SUCC)
So(coinRecord.OpReason, ShouldEqual, 0)
} else {
So(queryResp.Status, ShouldEqual, TX_STATUS_FAILED)
So(coinRecord.OpResult, ShouldEqual, model.STREAM_OP_RESULT_SUB_FAILED)
if res.Err == ecode.TargetBlocked {
So(coinRecord.OpReason, ShouldEqual, model.STREAM_OP_REASON_LOCK_FAILED)
} else {
So(coinRecord.OpReason, ShouldEqual, model.STREAM_OP_REASON_NOT_ENOUGH_COIN)
}
}
}
t.Logf("successNum:%d", successNum)
var detail2 *model.DetailWithSnapShot
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, detail1.Gold)
So(detail2.SnapShotIapGold, ShouldEqual, detail1.IapGold)
So(detail2.SnapShotSilver, ShouldEqual, detail1.Silver)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
subCoin := successNum * int(coinNum)
wallet := getTestWallet(t, u.uid, platform)
var nowCoin int
if coinType == "gold" {
nowCoin = atoiForTest(wallet.Gold)
} else if coinType == "silver" {
nowCoin = atoiForTest(wallet.Silver)
}
So(nowCoin, ShouldBeGreaterThanOrEqualTo, 0)
So(nowCoin, ShouldEqual, int(u.coin)-subCoin)
}
}
}))
}
func TestService_PayForNewUser(t *testing.T) {
Convey("new user", t, testWith(func() {
var uid int64
for {
uid = getTestRandUid()
_, err := s.dao.DetailWithoutDefault(ctx, uid)
if err != nil {
if err == sql.ErrNoRows {
break
} else {
t.Errorf("select wrong :%s", err.Error())
t.FailNow()
}
}
}
platform := "pc"
var wg errgroup.Group
times := 2
for i := 0; i < times; i++ {
wg.Go(func() error {
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
_, err := s.Pay(context.Background(), bp, uid, platform, getTestRechargeOrPayForm(uid, "gold", 1, tid))
So(err, ShouldEqual, ecode.CoinNotEnough)
return nil
})
}
wg.Wait()
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, uid)
t.Logf("detail2:%+v", detail2)
So(detail2.Gold, ShouldEqual, 0)
So(detail2.SnapShotGold, ShouldEqual, 0)
So(detail2.SnapShotIapGold, ShouldEqual, 0)
So(detail2.SnapShotSilver, ShouldEqual, 0)
So(detail2.SnapShotTime, ShouldEqual, "0001-01-01 00:00:00")
tx.Rollback()
}))
}
func TestService_PayMetal(t *testing.T) {
Convey("normal", t, testWith(func() {
var uid int64 = 1
s.dao.ModifyMetal(ctx, uid, 2, 0, nil)
t.Logf("test uid is %d", uid)
platform := "pc"
om, _ := s.dao.GetMetal(ctx, uid)
t.Logf("before pay metal : %f", om)
var num int64 = 1
// 加金瓜子
success, tid, err := queryPayWithReason(t, uid, platform, "metal", num, "ut")
if success {
So(err, ShouldBeNil)
success, queryResp, queryErr := queryQueryWithUid(t, tid, uid)
So(success, ShouldBeTrue)
So(queryResp.Status, ShouldEqual, TX_STATUS_SUCC)
So(queryErr, ShouldBeNil)
} else {
So(err, ShouldEqual, ecode.CoinNotEnough)
success, queryResp, queryErr := queryQueryWithUid(t, tid, uid)
So(success, ShouldBeTrue)
So(queryResp.Status, ShouldEqual, TX_STATUS_FAILED)
So(queryErr, ShouldBeNil)
}
om, _ = s.dao.GetMetal(ctx, uid)
t.Logf("after pay metal : %f", om)
}))
}
func TestService_PayBasicParam(t *testing.T) {
Convey("default", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
var num int64 = 100
// 加金瓜子
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
_, err := s.Pay(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "gold", num, tid))
So(err, ShouldBeNil)
record, err := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(err, ShouldBeNil)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 0)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 1)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, -1*num)
}
}))
Convey("advanced", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
var num int64 = 100
// 加金瓜子
tid := getTestTidForCall(t, int32(model.PAYTYPE))
bp := getTestDefaultBasicParam(tid)
bp.Reason = 1
bp.BizCode = "send-gift-pay"
bp.Version = 2
_, err := s.Pay(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "gold", num, tid))
So(err, ShouldBeNil)
record, err := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(err, ShouldBeNil)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 1)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, -1*num)
So(record.Reserved1, ShouldEqual, bp.Reason)
So(record.BizCode, ShouldEqual, bp.BizCode)
So(record.Version, ShouldEqual, 2)
}
}))
}

View File

@@ -0,0 +1,71 @@
package service
import (
"context"
"database/sql"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
)
const TX_STATUS_SUCC = 0 //事务执行成功
const TX_STATUS_FAILED = 1 //事务执行失败
type QueryHandler struct {
}
func (handler *QueryHandler) NeedCheckUid() bool {
return false
}
func (handler *QueryHandler) NeedTransactionMutex() bool {
return false
}
func (handler *QueryHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
tid, _ := params[1].(string)
var record *model.CoinStreamRecord
if uid == 0 {
record, err = ws.s.dao.GetCoinStreamByTid(ws.c, tid)
} else {
record, err = ws.s.dao.GetCoinStreamByUidTid(ws.c, uid, tid)
}
if err != nil {
if err == sql.ErrNoRows {
err = ecode.NothingFound
} else {
err = ecode.ServerErr
}
return
}
var (
result bool
opType model.ServiceType
)
opType = model.ServiceType(record.OpType)
switch opType {
case model.PAYTYPE:
result = record.OpReason == 0
case model.RECHARGETYPE:
result = record.OpReason == 0 || record.OpResult == model.STREAM_OP_REASON_POST_QUERY_FAILED
case model.EXCHANGETYPE:
result = record.OpReason == 0 || record.OpResult == model.STREAM_OP_REASON_EXECUTE_UNKNOWN
default:
}
res := &model.QueryResp{}
if result {
res.Status = TX_STATUS_SUCC
} else {
res.Status = TX_STATUS_FAILED
}
v = res
return
}
func (s *Service) Query(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
handler := QueryHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}

View File

@@ -0,0 +1,71 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"testing"
)
const RandSource = "abcdefghijklmnopq12345678"
func testGetRandString(size int) string {
res := ""
sourceLen := int64(len(RandSource))
for i := 0; i < size; i++ {
index := r.Int63n(sourceLen)
res = res + RandSource[index:index+1]
}
return res
}
func queryQuery(t *testing.T, tid string) (success bool, resp *model.QueryResp, err error) {
platform := "pc" // 对于query接口platform目前不起作用
bp := getTestDefaultBasicParam(tid)
v, err := s.Query(ctx, bp, 0, platform, tid)
if err == nil {
success = true
resp = v.(*model.QueryResp)
}
return
}
func queryQueryWithUid(t *testing.T, tid string, uid int64) (success bool, resp *model.QueryResp, err error) {
platform := "pc" // 对于query接口platform目前不起作用
bp := getTestDefaultBasicParam(tid)
v, err := s.Query(ctx, bp, uid, platform, tid)
if err == nil {
success = true
resp = v.(*model.QueryResp)
}
return
}
func TestService_Query(t *testing.T) {
Convey("normal", t, testWith(func() {
tid := getTestTidForCall(t, 0)
success, resp, err := queryQueryWithUid(t, tid, 1)
if !success {
So(err, ShouldEqual, ecode.NothingFound)
} else {
So(resp.Status == TX_STATUS_FAILED || resp.Status == TX_STATUS_SUCC, ShouldBeTrue)
}
}))
Convey("tid invalid", t, testWith(func() {
tid := testGetRandString(32)
success, resp, err := queryQuery(t, tid)
So(success, ShouldBeFalse)
So(resp, ShouldBeNil)
So(err, ShouldEqual, ecode.ServerErr)
}))
}

View File

@@ -0,0 +1,147 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
)
type RechargeHandler struct {
service *WalletService
}
func (handler *RechargeHandler) NeedCheckUid() bool {
return true
}
func (handler *RechargeHandler) NeedTransactionMutex() bool {
return true
}
func (handler *RechargeHandler) SetWalletService(ws *WalletService) {
handler.service = ws
}
// old TODO deprecated
func (handler *RechargeHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
// do nothing
return
}
func (handler *RechargeHandler) Recharge(c context.Context, rechargeParam *model.RechargeOrPayParam) (resp *model.MelonseedResp, err error) {
// 校验参数
if rechargeParam.Uid <= 0 ||
rechargeParam.CoinNum <= 0 ||
rechargeParam.ExtendTid == "" ||
!model.IsValidCoinType(rechargeParam.CoinType) ||
!model.IsValidPlatform(rechargeParam.Platform) {
err = ecode.RequestErr
return
}
sysCoinType := model.GetSysCoinType(rechargeParam.CoinType, rechargeParam.Platform)
sysCoinTypeNo := model.GetCoinTypeNumber(sysCoinType)
if !model.IsLocalCoin(sysCoinTypeNo) {
err = ecode.RequestErr
return
}
// 锁住tid
err = handler.service.lockTransactionId(rechargeParam.TransactionId)
if err != nil {
return
}
coinStream := model.CoinStreamRecord{}
model.InjectFieldToCoinStream(&coinStream, rechargeParam)
coinStream.DeltaCoinNum = rechargeParam.CoinNum
coinStream.CoinType = sysCoinTypeNo
coinStream.OpType = int32(model.RECHARGETYPE)
// 初始状态
coinStream.OrgCoinNum = -1
coinStream.OpResult = model.STREAM_OP_RESULT_ADD_FAILED
userLock := handler.service.getUserLock()
// 锁用户
lockErr := userLock.lock(rechargeParam.Uid)
if lockErr != nil {
model.SetReasonByLockErr(lockErr, &coinStream)
err = lockErr
handler.service.s.dao.NewCoinStreamRecord(c, &coinStream)
return
}
defer userLock.release()
// 实际的db操作
wallet, err := handler.recharge(rechargeParam.Uid, rechargeParam.Platform, sysCoinTypeNo, rechargeParam.CoinNum, &coinStream)
if err == nil {
handler.service.s.dao.DelWalletCache(c, rechargeParam.Uid)
resp = model.GetMelonByDetailWithSnapShot(wallet, rechargeParam.Platform)
handler.service.s.pubWalletChangeWithDetailSnapShot(c,
rechargeParam.Uid, "recharge", rechargeParam.CoinNum, rechargeParam.CoinType, rechargeParam.Platform, "", 0, wallet)
}
return
}
func (handler *RechargeHandler) recharge(uid int64, platform string, sysCoinTypeNo int32, coinNum int64, coinStream *model.CoinStreamRecord) (resp *model.DetailWithSnapShot, err error) {
dao := handler.service.s.dao
v, err := dao.DoTx(handler.service.c, func(conn *sql.Tx) (v interface{}, err error) {
return handler.rechargeInTx(conn, uid, platform, sysCoinTypeNo, coinNum, coinStream)
})
if err == nil {
resp = v.(*model.DetailWithSnapShot)
}
return
}
func (handler *RechargeHandler) rechargeInTx(tx *sql.Tx, uid int64, platform string, sysCoinTypeNo int32, coinNum int64, coinStream *model.CoinStreamRecord) (resp *model.DetailWithSnapShot, err error) {
dao := handler.service.s.dao
// 获取数据 for update
wallet, err := dao.WalletForUpdate(tx, uid)
if err != nil {
return
}
// 加钱并且做双余额处理
_, err = dao.RechargeCoinInTx(tx, uid, sysCoinTypeNo, coinNum, wallet)
if err != nil {
return
}
// 写流水
coinStream.OrgCoinNum = model.GetCoinByDetailWithSnapShot(sysCoinTypeNo, wallet)
coinStream.OpResult = model.STREAM_OP_RESULT_ADD_SUCC
_, err = dao.NewCoinStreamRecordInTx(tx, coinStream)
if err != nil {
return
}
model.ModifyCoinInDetailWithSnapShot(wallet, sysCoinTypeNo, coinNum)
resp = wallet
return
}
func (s *Service) Recharge(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
arg, _ := params[1].(*model.RechargeOrPayForm)
rechargeParam := buildRechargeOrPayParam(platform, arg, uid, basicParam)
ws := new(WalletService)
ws.c = c
ws.s = s
handler := getRechargeHandler(ws)
handler.SetWalletService(ws)
return handler.Recharge(c, rechargeParam)
}
func getRechargeHandler(ws *WalletService) *RechargeHandler {
handler := RechargeHandler{}
handler.SetWalletService(ws)
return &handler
}

View File

@@ -0,0 +1,659 @@
package service
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/pkg/errors"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/sync/errgroup"
)
func getTestExtendTid() string {
return fmt.Sprintf("test-service:ex:%d", r.Int31n(1000000))
}
func getTestRechargeOrPayForm(uid int64, coinType string, coinNum int64, tid string) *model.RechargeOrPayForm {
return &model.RechargeOrPayForm{
Uid: uid,
CoinType: coinType,
CoinNum: coinNum,
ExtendTid: getTestExtendTid(),
Timestamp: time.Now().Unix(),
TransactionId: tid,
}
}
func getTestExchangeForm(uid int64, srcCoinType string, srcCoinNum int64, destCoinType string, destCoinNum int64, tid string) *model.ExchangeForm {
return &model.ExchangeForm{
Uid: uid,
SrcCoinType: srcCoinType,
SrcCoinNum: srcCoinNum,
ExtendTid: getTestExtendTid(),
Timestamp: time.Now().Unix(),
TransactionId: tid,
DestCoinNum: destCoinNum,
DestCoinType: destCoinType,
}
}
func getTestTidForCall(t *testing.T, serviceType int32) string {
bp := getTestDefaultBasicParam("")
v, err := s.GetTid(ctx, bp, 0, serviceType, getTestParamsJson())
if err != nil {
t.FailNow()
}
tidResp := v.(*model.TidResp)
tid := tidResp.TransactionId
return tid
}
var (
testLocalValidCoinType = []string{"gold", "silver"}
)
// 增加一个初始化用户的功能 方便测试
type TestUser struct {
uid int64
coin int64
originGold int64
originSilver int64
originIapGold int64
}
func initTestUser() *TestUser {
uid := getTestRandUid()
var coin int64 = 1000
wallet, _ := s.dao.Melonseed(ctx, uid)
needGold := coin - wallet.Gold
s.dao.AddGold(ctx, uid, int(needGold))
needIapGold := coin - wallet.IapGold
s.dao.AddIapGold(ctx, uid, int(needIapGold))
needSilver := coin - wallet.Silver
s.dao.AddSilver(ctx, uid, int(needSilver))
s.dao.ChangeCostBase(ctx, uid, coin)
s.dao.DelWalletCache(ctx, uid)
return &TestUser{
uid: uid,
coin: coin,
originGold: wallet.Gold,
originSilver: wallet.Silver,
originIapGold: wallet.IapGold,
}
}
func (u *TestUser) rest() {
uid := u.uid
wallet, _ := s.dao.Melonseed(ctx, uid)
needGold := u.originGold - wallet.Gold
s.dao.AddGold(ctx, uid, int(needGold))
needIapGold := u.originIapGold - wallet.IapGold
s.dao.AddIapGold(ctx, uid, int(needIapGold))
needSilver := u.originSilver - wallet.Silver
s.dao.AddSilver(ctx, uid, int(needSilver))
s.dao.DelWalletCache(ctx, uid)
}
func testWithTestUser(f func(u *TestUser)) func() {
once.Do(startService)
u := initTestUser()
return func() {
f(u)
u.rest()
}
}
func queryRecharge(t *testing.T, uid int64, platform string, coinType string, coinNum int64) (success bool, tid string) {
tid = getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
_, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, coinNum, tid))
if err != nil {
t.FailNow()
}
success = true
return
}
func TestService_Recharge(t *testing.T) {
Convey("normal", t, testWith(func() {
Convey("basic", func() {
var num int64 = 1000
for _, platform := range testValidPlatform {
uid := getTestRandUid()
beforeWallet := getTestWallet(t, uid, platform)
beforeDetail := getTestAll(t, uid, platform)
for _, coinType := range testLocalValidCoinType {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
}
afterWallet := getTestWallet(t, uid, platform)
afterDetail := getTestAll(t, uid, platform)
So(atoiForTest(afterWallet.Gold)-atoiForTest(beforeWallet.Gold), ShouldEqual, num)
So(atoiForTest(afterWallet.Silver)-atoiForTest(beforeWallet.Silver), ShouldEqual, num)
So(atoiForTest(afterDetail.Gold)-atoiForTest(beforeDetail.Gold), ShouldEqual, num)
So(atoiForTest(afterDetail.Silver)-atoiForTest(beforeDetail.Silver), ShouldEqual, num)
So(atoiForTest(afterDetail.GoldRechargeCnt)-atoiForTest(beforeDetail.GoldRechargeCnt), ShouldEqual, num)
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, 0)
}
})
}))
Convey("params", t, testWith(func() {
Convey("invalid uid", func() {
invalidUids := []int64{-1, 0, -100}
platform := "ios"
coinType := "gold"
var num int64 = 100
for _, uid := range invalidUids {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(err, ShouldNotBeNil)
So(errors.Cause(err).(ecode.Codes), ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
}
})
Convey("invalid coinNum", func() {
uid := getTestRandUid()
platform := "pc"
coinType := "silver"
invalidCoinNums := []int64{0, -1}
for _, num := range invalidCoinNums {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(err, ShouldNotBeNil)
So(errors.Cause(err).(ecode.Codes), ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
}
})
Convey("invalid coinType", func() {
uid := getTestRandUid()
platform := "pc"
var num int64 = 100
invalidCoinTypes := []string{"unknown", "metal"}
for _, coinType := range invalidCoinTypes {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(err, ShouldNotBeNil)
So(errors.Cause(err).(ecode.Codes), ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
}
})
Convey("invalid ExtendTid", func() {
uid := getTestRandUid()
platform := "pc"
var num int64 = 100
coinType := "gold"
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(uid, coinType, num, tid)
form.ExtendTid = ""
v, err := s.Recharge(ctx, bp, uid, platform, form)
So(err, ShouldNotBeNil)
So(errors.Cause(err).(ecode.Codes), ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
})
Convey("invalid tid", func() {
uid := getTestRandUid()
platform := "pc"
var num int64 = 100
coinType := "gold"
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
bp.TransactionId = ""
form := getTestRechargeOrPayForm(uid, coinType, num, tid)
form.TransactionId = ""
v, err := s.Recharge(ctx, bp, uid, platform, form)
So(err, ShouldNotBeNil)
So(errors.Cause(err).(ecode.Codes), ShouldEqual, ecode.RequestErr)
So(v, ShouldBeNil)
})
Convey("same tid twice", func() {
uid := getTestRandUid()
platform := "pc"
var num int64 = 100
coinType := "gold"
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
form := getTestRechargeOrPayForm(uid, coinType, num, tid)
_, err := s.Recharge(ctx, bp, uid, platform, form)
So(err, ShouldBeNil)
_, err = s.Recharge(ctx, bp, uid, platform, form)
So(errors.Cause(err).(ecode.Codes), ShouldEqual, ecode.TargetBlocked)
})
}))
Convey("multi", t, func() {
Convey("basic", func() {
var num int64 = 1000
times := 10
uid := getTestRandUid()
for _, platform := range testValidPlatform {
for _, coinType := range testLocalValidCoinType {
beforeWallet := getTestWallet(t, uid, platform)
beforeDetail := getTestAll(t, uid, platform)
s.dao.UpdateSnapShotTime(ctx, uid, "")
var detail1 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail1, _ = s.dao.WalletForUpdate(tx, uid)
t.Logf("detail1: %+v", detail1)
So(detail1.SnapShotTime, ShouldEqual, "0001-01-01 00:00:00")
tx.Rollback()
var lock sync.Mutex
var wg sync.WaitGroup
rechargeRespMap := make(map[string]*ServiceResForTest)
for i := 0; i < times; i++ {
wg.Add(1)
go func(index int) {
localService := New(conf.Conf)
bp := getTestDefaultBasicParam("")
v, _ := localService.GetTid(ctx, bp, 0, int32(model.RECHARGETYPE), getTestParamsJson())
tidResp := v.(*model.TidResp)
tid := tidResp.TransactionId
bp = getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
lock.Lock()
rechargeRespMap[tid] = &ServiceResForTest{V: v, Err: err}
lock.Unlock()
wg.Done()
}(i)
time.Sleep(time.Millisecond * 30)
}
wg.Wait()
validErrs := map[error]bool{ecode.TargetBlocked: true, nil: true}
successNum := 0
for tid, resp := range rechargeRespMap {
_, ok := validErrs[resp.Err]
So(ok, ShouldBeTrue)
record, recordErr := s.dao.GetCoinStreamByTid(ctx, tid)
So(recordErr, ShouldBeNil)
So(record.DeltaCoinNum, ShouldEqual, num)
So(record.OpType, ShouldEqual, int32(model.RECHARGETYPE))
sysCoinType := 0
if coinType == "gold" {
if platform == "ios" {
sysCoinType = 2
} else {
sysCoinType = 1
}
}
So(record.CoinType, ShouldEqual, sysCoinType)
success, queryResp, err := queryQueryWithUid(t, tid, uid)
So(success, ShouldBeTrue)
So(err, ShouldBeNil)
if resp.Err == nil {
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_ADD_SUCC)
So(queryResp.Status, ShouldEqual, TX_STATUS_SUCC)
successNum++
} else {
So(record.OpResult, ShouldEqual, model.STREAM_OP_RESULT_ADD_FAILED)
So(record.OpReason, ShouldEqual, model.STREAM_OP_REASON_LOCK_FAILED)
So(queryResp.Status, ShouldEqual, TX_STATUS_FAILED)
}
}
So(len(rechargeRespMap), ShouldEqual, times)
So(successNum, ShouldBeGreaterThan, 0)
t.Logf("multi successNum:%d", successNum)
var detail2 *model.DetailWithSnapShot
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, uid)
t.Logf("detail2: %+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, detail1.Gold)
So(detail2.SnapShotIapGold, ShouldEqual, detail1.IapGold)
So(detail2.SnapShotSilver, ShouldEqual, detail1.Silver)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
afterWallet := getTestWallet(t, uid, platform)
afterDetail := getTestAll(t, uid, platform)
successCount := successNum * int(num)
if coinType == "gold" {
So(atoiForTest(afterWallet.Gold)-atoiForTest(beforeWallet.Gold), ShouldEqual, successCount)
So(atoiForTest(afterDetail.Gold)-atoiForTest(beforeDetail.Gold), ShouldEqual, successCount)
So(atoiForTest(afterDetail.Gold)-atoiForTest(beforeDetail.Gold), ShouldEqual, successCount)
So(atoiForTest(afterDetail.GoldRechargeCnt)-atoiForTest(beforeDetail.GoldRechargeCnt), ShouldEqual, successCount)
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, 0)
} else if coinType == "silver" {
So(atoiForTest(afterWallet.Silver)-atoiForTest(beforeWallet.Silver), ShouldEqual, successCount)
So(atoiForTest(afterDetail.Silver)-atoiForTest(beforeDetail.Silver), ShouldEqual, successCount)
// silver 不统计充值
So(atoiForTest(afterDetail.GoldRechargeCnt)-atoiForTest(beforeDetail.GoldRechargeCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.GoldPayCnt)-atoiForTest(beforeDetail.GoldPayCnt), ShouldEqual, 0)
So(atoiForTest(afterDetail.SilverPayCnt)-atoiForTest(beforeDetail.SilverPayCnt), ShouldEqual, 0)
}
}
}
})
})
Convey("iphone", t, func() {
uid := getTestRandUid()
platform := "pc"
coinType := "gold"
beforeWallet, err := s.dao.Melonseed(ctx, uid)
beforePcResp := getTestWallet(t, uid, platform)
beforeIosResp := getTestWallet(t, uid, "ios")
So(err, ShouldBeNil)
var num int64 = 1000
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(v, ShouldNotBeNil)
So(err, ShouldBeNil)
afterAddPcWallet, err := s.dao.Melonseed(ctx, uid)
So(err, ShouldBeNil)
So(beforeWallet.IapGold, ShouldEqual, afterAddPcWallet.IapGold)
resp := getTestWallet(t, uid, platform)
So(atoiForTest(resp.Gold)-atoiForTest(beforePcResp.Gold), ShouldEqual, int(num))
platform = "ios"
tid = getTestTidForCall(t, int32(model.RECHARGETYPE))
bp = getTestDefaultBasicParam(tid)
v, err = s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(v, ShouldNotBeNil)
So(err, ShouldBeNil)
afterAddIosWallet, err := s.dao.Melonseed(ctx, uid)
So(err, ShouldBeNil)
So(beforeWallet.IapGold+num, ShouldEqual, afterAddIosWallet.IapGold) // ios 则加到数据库的iap_gold字段
resp = getTestWallet(t, uid, platform)
So(atoiForTest(resp.Gold)-atoiForTest(beforeIosResp.Gold), ShouldEqual, int(num))
})
}
func Test_TestUser(t *testing.T) {
Convey("set and reset", t, testWith(func() {
u := initTestUser()
platform := "pc"
resp := getTestWallet(t, u.uid, platform)
So(u.coin, ShouldEqual, atoiForTest(resp.Gold))
So(u.coin, ShouldEqual, atoiForTest(resp.Silver))
platform = "ios"
resp = getTestWallet(t, u.uid, platform)
So(u.coin, ShouldEqual, atoiForTest(resp.Gold))
u.rest()
}))
Convey("testWithTestUser", t, testWithTestUser(func(u *TestUser) {
platform := "pc"
resp := getTestWallet(t, u.uid, platform)
So(u.coin, ShouldEqual, atoiForTest(resp.Gold))
So(u.coin, ShouldEqual, atoiForTest(resp.Silver))
platform = "ios"
resp = getTestWallet(t, u.uid, platform)
So(u.coin, ShouldEqual, atoiForTest(resp.Gold))
var num int64 = 100
success, _ := queryRecharge(t, u.uid, platform, "gold", num)
So(success, ShouldBeTrue)
resp = getTestWallet(t, u.uid, platform)
So(u.coin+num, ShouldEqual, atoiForTest(resp.Gold))
}))
}
func TestService_RechargeDoubleCoin(t *testing.T) {
times := []string{""}
times = append(times, time.Now().Add(-time.Second*86400).Format("2006-01-02 15:04:05"))
for _, htime := range times {
Convey("default_"+htime, t, testWithTestUser(func(u *TestUser) {
platform := "pc"
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
s.dao.UpdateSnapShotTime(ctx, u.uid, htime)
t.Logf("htime:%s", htime)
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "gold", 1, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
var detail1 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail1, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("%+v", detail1)
So(detail1.SnapShotGold, ShouldEqual, u.coin)
So(detail1.SnapShotIapGold, ShouldEqual, u.coin)
So(detail1.SnapShotSilver, ShouldEqual, u.coin)
So(detail1.SnapShotTime, ShouldNotEqual, "")
tx.Rollback()
tid = getTestTidForCall(t, int32(model.RECHARGETYPE))
platform = "ios"
v, err = s.Recharge(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "gold", 1, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
tid = getTestTidForCall(t, int32(model.RECHARGETYPE))
platform = "android"
v, err = s.Recharge(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "silver", 1, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
var detail2 *model.DetailWithSnapShot
tx, _ = s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
t.Logf("%+v", detail2)
So(detail2.SnapShotGold, ShouldEqual, u.coin)
So(detail2.SnapShotIapGold, ShouldEqual, u.coin)
So(detail2.SnapShotSilver, ShouldEqual, u.coin)
So(detail2.SnapShotTime, ShouldEqual, detail1.SnapShotTime)
tx.Rollback()
}))
}
}
func TestService_RechargeForNewUser(t *testing.T) {
Convey("new user", t, testWith(func() {
var uid int64
for {
uid = getTestRandUid()
_, err := s.dao.DetailWithoutDefault(ctx, uid)
if err != nil {
if err == sql.ErrNoRows {
break
} else {
t.Errorf("select wrong :%s", err.Error())
t.FailNow()
}
}
}
platform := "pc"
v, err := s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
resp := v.(*model.MelonseedResp)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(atoiForTest(resp.Gold), ShouldEqual, 0)
So(atoiForTest(resp.Silver), ShouldEqual, 0)
_, err = s.dao.DetailWithoutDefault(ctx, uid)
So(err, ShouldEqual, sql.ErrNoRows)
var wg errgroup.Group
times := 2
st := 0
for i := 0; i < times; i++ {
wg.Go(func() error {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
_, err := s.Recharge(context.Background(), bp, uid, platform, getTestRechargeOrPayForm(uid, "gold", 1, tid))
if err == nil {
st++
}
return err
})
}
wg.Wait()
So(st, ShouldBeGreaterThan, 0)
t.Logf("st : %d", st)
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, uid)
t.Logf("detail2:%+v", detail2)
So(detail2.Gold, ShouldEqual, st)
So(detail2.SnapShotGold, ShouldEqual, 0)
So(detail2.SnapShotIapGold, ShouldEqual, 0)
So(detail2.SnapShotSilver, ShouldEqual, 0)
So(detail2.SnapShotTime, ShouldNotEqual, "0001-01-01 00:00:00")
tx.Rollback()
v, err = s.Get(ctx, getTestDefaultBasicParam(""), uid, platform, 0)
resp = v.(*model.MelonseedResp)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(atoiForTest(resp.Gold), ShouldEqual, st)
So(atoiForTest(resp.Silver), ShouldEqual, 0)
}))
}
func TestService_RechargeBasicParam(t *testing.T) {
Convey("default", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
v, err := s.Recharge(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "gold", 1, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
record, err := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(err, ShouldBeNil)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 0)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 2)
So(record.BizSource, ShouldEqual, "")
So(record.DeltaCoinNum, ShouldEqual, 1)
}
}))
Convey("advanced", t, testWithTestUser(func(u *TestUser) {
for _, platform := range testValidPlatform {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
bp := getTestDefaultBasicParam(tid)
bp.Reason = 1
bp.BizSource = "live"
bp.BizCode = "send-gift"
bp.Area = 1
bp.MetaData = "zz"
v, err := s.Recharge(ctx, bp, u.uid, platform, getTestRechargeOrPayForm(u.uid, "gold", 1, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
record, err := s.dao.GetCoinStreamByUidTid(ctx, u.uid, tid)
So(err, ShouldBeNil)
oP := model.GetPlatformNo(platform)
So(record.Platform, ShouldEqual, oP)
So(record.Reserved1, ShouldEqual, 1)
So(record.Uid, ShouldEqual, u.uid)
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType("gold", platform)))
So(record.OpResult, ShouldEqual, 2)
So(record.BizSource, ShouldEqual, bp.BizSource)
So(record.BizCode, ShouldEqual, bp.BizCode)
So(record.Area, ShouldEqual, bp.Area)
So(record.MetaData, ShouldEqual, bp.MetaData)
So(record.DeltaCoinNum, ShouldEqual, 1)
}
}))
}

View File

@@ -0,0 +1,125 @@
package service
import (
"context"
"encoding/json"
"go-common/app/service/live/wallet/model"
"go-common/library/database/sql"
"go-common/library/ecode"
"go-common/library/log"
)
type RecordCoinStreamHandler struct {
}
func (handler *RecordCoinStreamHandler) NeedCheckUid() bool {
return true
}
const ItemNumCount = 50
func (handler *RecordCoinStreamHandler) NeedTransactionMutex() bool {
return false
}
func (handler *RecordCoinStreamHandler) BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
platform, _ := params[0].(string)
if !model.IsValidPlatform(platform) {
err = ecode.RequestErr
return
}
arg, _ := params[1].(*model.RecordCoinStreamForm)
var recordCoinStreamItems []*model.RecordCoinStreamItem
jsonErr := json.Unmarshal([]byte(arg.Data), &recordCoinStreamItems)
if jsonErr != nil || len(recordCoinStreamItems) == 0 || len(recordCoinStreamItems) > ItemNumCount {
err = ecode.RequestErr
return
}
for _, m := range recordCoinStreamItems {
if !m.IsValid() {
err = ecode.RequestErr
return
}
}
goldPay := 0
goldRecharge := 0
silverPay := 0
var detail *model.DetailWithSnapShot
_, err = ws.s.dao.DoTx(ws.c, func(conn *sql.Tx) (v interface{}, err error) {
detail, err = ws.s.dao.WalletForUpdate(conn, uid)
if err != nil {
return
}
for _, m := range recordCoinStreamItems {
sysCoinType := model.GetSysCoinType(m.CoinType, platform)
sysCoinTypeNo := model.GetCoinTypeNumber(sysCoinType)
record := model.NewCoinStream(uid, m.TransactionId, m.ExtendTid, sysCoinTypeNo, m.CoinNum, m.GetOpType(),
m.Timestamp, basicParam.BizCode, basicParam.Area, basicParam.Source, basicParam.BizSource, basicParam.MetaData)
model.AddMoreParam2CoinStream(record, basicParam, platform)
record.Reserved1 = m.Reserved1
record.OrgCoinNum = m.GetOrgCoinNum()
record.OpResult = m.GetOpResult()
_, err = ws.s.dao.NewCoinStreamRecordInTx(conn, record)
if err != nil {
break
}
if m.IsPayType() {
if sysCoinTypeNo == model.SysCoinTypeIosGold || sysCoinTypeNo == model.SysCoinTypeGold {
goldPay = goldPay + int(0-m.CoinNum)
} else if sysCoinTypeNo == model.SysCoinTypeSilver {
silverPay = silverPay + int(0-m.CoinNum)
}
} else if m.IsRechargeType() && (sysCoinTypeNo == model.SysCoinTypeIosGold || sysCoinTypeNo == model.SysCoinTypeGold) {
goldRecharge = goldRecharge + int(m.CoinNum)
}
}
_, err = ws.s.dao.ModifyCntInTx(conn, uid, goldPay, goldRecharge, silverPay)
return
})
if err != nil {
return
}
// 事务执行以后 需要发databus 原因在于成就系统依赖于该消息来算用户消费总数如果不发则成就系统无法及时知道用户的消费总数增加
// 后续成就系统可以通过消费cannal 的方式来重构 后则无需发消息队列
// ps : 流水可能有多条 但是消息队列只发一条
var action string
var number int64
var coinType string
needPub := true
if goldPay > 0 || silverPay > 0 {
action = "pay"
if goldPay > 0 {
coinType = "gold"
number = int64(goldPay)
} else {
coinType = "silver"
number = int64(silverPay)
}
} else if goldRecharge > 0 {
coinType = "gold"
number = int64(goldRecharge)
action = "recharge"
} else {
needPub = false
}
detail.GoldRechargeCnt += int64(goldRecharge)
detail.GoldPayCnt += int64(goldPay)
detail.SilverPayCnt += int64(silverPay)
if needPub {
log.Info("RecordCoinStream#ExecSuccess#Pub#action:%s#coinType:%s#number:%d#uid:%d", action, coinType, number, uid)
ws.s.pubWalletChangeWithDetailSnapShot(ws.c,
uid, action, number, coinType, platform, "", 0, detail)
}
return
}
func (s *Service) RecordCoinStream(c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
handler := RecordCoinStreamHandler{}
return s.execByHandler(&handler, c, basicParam, uid, params...)
}

View File

@@ -0,0 +1,207 @@
package service
import (
"encoding/json"
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"testing"
"time"
)
func getTestRecordCoinStreamItem(coinType string, coinNum int64, tid string, orgCoinNum int64, serviceType string) *model.RecordCoinStreamItem {
return &model.RecordCoinStreamItem{
CoinType: coinType,
CoinNum: coinNum,
ExtendTid: getTestExtendTid(),
Timestamp: time.Now().Unix(),
TransactionId: tid,
Type: serviceType,
OrgCoinNum: orgCoinNum,
}
}
func getDetalWithSnapShot(u *TestUser) *model.DetailWithSnapShot {
var detail2 *model.DetailWithSnapShot
tx, _ := s.dao.BeginTx(ctx)
detail2, _ = s.dao.WalletForUpdate(tx, u.uid)
tx.Rollback()
return detail2
}
func queryRecordCoinStream(t *testing.T, uid int64, platform string, tid string, streamList []*model.RecordCoinStreamItem) error {
bp := getTestDefaultBasicParam(tid)
bp.Version = 23
bp.Source = "kkk"
steamListBytes, _ := json.Marshal(streamList)
form := &model.RecordCoinStreamForm{Uid: uid, Data: string(steamListBytes)}
_, err := s.RecordCoinStream(ctx, bp, uid, platform, form)
return err
}
func TestService_RecordCoinStream(t *testing.T) {
Convey("normal", t, testWithTestUser(func(u *TestUser) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "gold"
var num int64 = 100
d1 := getDetalWithSnapShot(u)
streamList := make([]*model.RecordCoinStreamItem, 0)
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, 0-num, tid, u.coin, "pay"))
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, num, tid, u.coin-num, "recharge"))
streamList = append(streamList, getTestRecordCoinStreamItem("silver", 1024, tid, u.coin, "recharge"))
streamList = append(streamList, getTestRecordCoinStreamItem("silver", -1023, tid, u.coin, "pay"))
streamList[0].Reserved1 = 1
streamList[1].Reserved1 = 2
err := queryRecordCoinStream(t, u.uid, platform, tid, streamList)
So(err, ShouldBeNil)
record := assertNormalCoinStream(t, platform, "gold", tid, 3, -1*num, model.PAYTYPE, model.STREAM_OP_RESULT_SUB_SUCC, TX_STATUS_SUCC)
t.Logf("record:%+v", record)
So(record.OrgCoinNum, ShouldEqual, u.coin)
So(record.Version, ShouldEqual, 23)
assertNormalCoinStream(t, platform, "gold", tid, 2, num, model.RECHARGETYPE, model.STREAM_OP_RESULT_ADD_SUCC, TX_STATUS_SUCC)
record, err = s.dao.GetCoinStreamByTidAndOffset(ctx, tid, 2)
So(err, ShouldBeNil)
So(record.OpResult, ShouldEqual, 2)
So(record.Platform, ShouldEqual, model.GetPlatformNo(platform))
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType(coinType, platform)))
So(record.Reserved1, ShouldEqual, 2)
record, err = s.dao.GetCoinStreamByTidAndOffset(ctx, tid, 3)
So(err, ShouldBeNil)
So(record.OpResult, ShouldEqual, 1)
So(record.Platform, ShouldEqual, model.GetPlatformNo(platform))
So(record.CoinType, ShouldEqual, model.GetCoinTypeNumber(model.GetSysCoinType(coinType, platform)))
So(record.Reserved1, ShouldEqual, 1)
d2 := getDetalWithSnapShot(u)
So(num, ShouldEqual, d2.GoldRechargeCnt-d1.GoldRechargeCnt)
So(1023, ShouldEqual, d2.SilverPayCnt-d1.SilverPayCnt)
}))
Convey("params", t, testWith(func() {
Convey("data err", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
data := "unknown"
form := &model.RecordCoinStreamForm{Uid: uid, Data: data}
bp := getTestDefaultBasicParam(tid)
_, err := s.RecordCoinStream(ctx, bp, uid, platform, form)
So(err, ShouldEqual, ecode.RequestErr)
}))
Convey("data empty", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
streamList := make([]*model.RecordCoinStreamItem, 0)
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}))
Convey("data too much", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "gold"
var num int64 = 100
streamList := make([]*model.RecordCoinStreamItem, 0)
for i := 0; i < 30; i++ {
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, 0-num, tid, 1000, "pay"))
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, num, tid, 900, "recharge"))
}
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}))
}))
Convey("logic", t, testWith(func() {
Convey("wrong orgCoinNum", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "gold"
var num int64 = 100
streamList := make([]*model.RecordCoinStreamItem, 0)
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, 0-num, tid, -100, "pay"))
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}))
Convey("wrong validType", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "gold"
var num int64 = 100
streamList := make([]*model.RecordCoinStreamItem, 0)
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, 0-num, tid, 100, "exchange"))
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}))
Convey("wrong validCoinType", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "unknown"
var num int64 = 100
streamList := make([]*model.RecordCoinStreamItem, 0)
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, 0-num, tid, 100, "pay"))
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}))
Convey("pay but num greater or equal 0", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "gold"
for _, num := range []int64{0, 1, 100} {
streamList := make([]*model.RecordCoinStreamItem, 0)
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, num, tid, 100, "pay"))
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}
}))
Convey("recharge but num less or equal 0", testWithRandUid(func(uid int64) {
tid := getTestTidForCall(t, int32(model.EXCHANGETYPE))
platform := "pc"
coinType := "gold"
for _, num := range []int64{0, -1, -100} {
streamList := make([]*model.RecordCoinStreamItem, 0)
streamList = append(streamList, getTestRecordCoinStreamItem(coinType, num, tid, 100, "recharge"))
err := queryRecordCoinStream(t, uid, platform, tid, streamList)
So(err, ShouldEqual, ecode.RequestErr)
}
}))
}))
}

View File

@@ -0,0 +1,110 @@
package service
import (
"context"
"go-common/app/service/live/wallet/conf"
"go-common/app/service/live/wallet/dao"
"go-common/app/service/live/wallet/model"
"go-common/library/cache"
"go-common/library/log"
)
// Service struct
type Service struct {
c *conf.Config
dao *dao.Dao
runCache *cache.Cache
}
// New init
func New(c *conf.Config) (s *Service) {
s = &Service{
c: c,
dao: dao.New(c),
runCache: cache.New(1, 1024),
}
return s
}
// Ping Service
func (s *Service) Ping(c context.Context) (err error) {
return s.dao.Ping(c)
}
// Close Service
func (s *Service) Close() {
s.dao.Close()
}
func (s *Service) pubWalletChange(c context.Context, uid int64, action string, number int64, coinType string, platform string, destCoinType string, destNumber int64) error {
detail, err := s.dao.Detail(c, uid)
if err != nil {
return err
}
gold := detail.Gold
if platform == "ios" {
gold = detail.IapGold
}
msg := model.WalletChangeMsg{
Uid: uid,
Action: action,
Number: number,
CoinType: coinType,
Gold: gold,
Silver: detail.Silver,
GoldRechargeCnt: detail.GoldRechargeCnt,
GoldPayCnt: detail.GoldPayCnt,
SilverPayCnt: detail.SilverPayCnt,
Platfrom: platform,
DestCoinType: destCoinType,
DestNumber: destNumber,
CostBase: detail.CostBase,
}
err = s.dao.Pub(c, uid, &msg)
return err
}
func (s *Service) execByHandler(handler Handler, c context.Context, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
ws := new(WalletService)
ws.c = c
ws.s = s
ws.SetServiceHandler(handler)
return ws.Execute(basicParam, uid, params...)
}
func (s *Service) pubWalletChangeWithDetailSnapShot(c context.Context, uid int64, action string, number int64, coinType string, platform string, destCoinType string, destNumber int64, detail *model.DetailWithSnapShot) error {
f := func(loc string, ctx context.Context) {
gold := detail.Gold
if platform == "ios" {
gold = detail.IapGold
}
msg := model.WalletChangeMsg{
Uid: uid,
Action: action,
Number: number,
CoinType: coinType,
Gold: gold,
Silver: detail.Silver,
GoldRechargeCnt: detail.GoldRechargeCnt,
GoldPayCnt: detail.GoldPayCnt,
SilverPayCnt: detail.SilverPayCnt,
Platfrom: platform,
DestCoinType: destCoinType,
DestNumber: destNumber,
CostBase: detail.CostBase,
}
err := s.dao.Pub(ctx, uid, &msg)
if err != nil {
log.Error("SubError# loc:%s value:%+v", loc, msg)
}
}
se := s.runCache.Save(func() {
f("cache", context.Background())
})
if se != nil {
log.Error("runCache is full")
f("service", c)
}
return nil
}

View File

@@ -0,0 +1,13 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestService_Ping(t *testing.T) {
Convey("normal", t, testWith(func() {
err := s.Ping(ctx)
So(err, ShouldBeNil)
}))
}

View File

@@ -0,0 +1,130 @@
package service
import (
"context"
"go-common/app/service/live/wallet/model"
"go-common/library/ecode"
"go-common/library/log"
)
type Handler interface {
NeedTransactionMutex() bool
NeedCheckUid() bool
BizExecute(ws *WalletService, basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error)
}
type WalletService struct {
handler Handler
s *Service
c context.Context
uid int64
locked bool
lockValue string
}
func (ws *WalletService) SetServiceHandler(handler Handler) {
ws.handler = handler
}
func (ws *WalletService) lockTransactionId(transactionId string) (err error) {
if transactionId == "" {
err = ecode.RequestErr
return
}
err = ws.s.dao.LockTransactionId(ws.c, transactionId)
if err != nil {
if ws.s.dao.IsLockFailedError(err) {
err = ecode.TargetBlocked
} else {
err = ecode.ServerErr
}
log.Error("wallet lock tid failed : %s, err:%s", transactionId, err.Error())
return
}
return
}
func (ws *WalletService) Execute(basicParam *model.BasicParam, uid int64, params ...interface{}) (v interface{}, err error) {
// 基本检测
if ws.handler.NeedCheckUid() && uid <= 0 {
err = ecode.RequestErr
return
}
needTransactionMutex := ws.handler.NeedTransactionMutex()
if needTransactionMutex {
if basicParam.TransactionId == "" {
err = ecode.RequestErr
return
}
err = ws.s.dao.LockTransactionId(ws.c, basicParam.TransactionId)
if err != nil {
if ws.s.dao.IsLockFailedError(err) {
err = ecode.TargetBlocked
} else {
err = ecode.ServerErr
}
log.Error("wallet lock tid failed : %s, err:%s", basicParam.TransactionId, err.Error())
return
}
}
ws.uid = uid
v, err = ws.handler.BizExecute(ws, basicParam, uid, params...)
ws.unLockUser()
return
}
func (ws *WalletService) unLockUser() {
if ws.locked {
ws.s.dao.UnLockUser(ws.c, ws.uid, ws.lockValue)
}
ws.locked = false
ws.uid = 0
}
func (ws *WalletService) lockUser() error {
err, gotLocked, lockValue := ws.s.dao.LockUser(ws.c, ws.uid)
if gotLocked {
ws.lockValue = lockValue
ws.locked = true
} else if err == nil {
err = ecode.TargetBlocked
} else {
err = ecode.ServerErr
}
if err != nil {
log.Error("wallet user lock failed : %d", ws.uid)
}
return err
}
func (ws *WalletService) lockSpecificUser(uid int64) error {
ws.uid = uid
return ws.lockUser()
}
func (ws *WalletService) getUserLock() UserLock {
//r = &RedisUserLock{ws: ws}
r := &NopUserLock{}
return r
}
func buildRechargeOrPayParam(platform string, arg *model.RechargeOrPayForm, uid int64, basicParam *model.BasicParam) *model.RechargeOrPayParam {
rechargeParam := model.RechargeOrPayParam{
Uid: uid, CoinType: arg.CoinType, CoinNum: arg.CoinNum, ExtendTid: arg.ExtendTid, TransactionId: arg.TransactionId,
Timestamp: arg.Timestamp, BizCode: basicParam.BizCode, Area: basicParam.Area, Source: basicParam.Source, MetaData: basicParam.MetaData,
BizSource: basicParam.BizSource, Reason: basicParam.Reason, Version: basicParam.Version, Platform: platform,
}
return &rechargeParam
}
func buildExchangeParam(platform string, arg *model.ExchangeForm, uid int64, basicParam *model.BasicParam) *model.ExchangeParam {
param := model.ExchangeParam{
Uid: uid, SrcCoinType: arg.SrcCoinType, SrcCoinNum: arg.SrcCoinNum, DestCoinType: arg.DestCoinType, DestCoinNum: arg.DestCoinNum, ExtendTid: arg.ExtendTid, TransactionId: arg.TransactionId,
Timestamp: arg.Timestamp, BizCode: basicParam.BizCode, Area: basicParam.Area, Source: basicParam.Source, MetaData: basicParam.MetaData,
BizSource: basicParam.BizSource, Reason: basicParam.Reason, Version: basicParam.Version, Platform: platform,
}
return &param
}

View File

@@ -0,0 +1,37 @@
package service
import (
. "github.com/smartystreets/goconvey/convey"
"go-common/app/service/live/wallet/model"
"testing"
)
func TestBasicParams(t *testing.T) {
Convey("recharge", t, testWith(func() {
tid := getTestTidForCall(t, int32(model.RECHARGETYPE))
platform := "pc"
uid := getTestRandUid()
coinType := "gold"
var num int64 = 100
var area int64 = 10
bizCode := "test/test"
source := "test_source"
bizSource := "test_biz_source"
metaData := "test_meta"
bp := getTestBasicParam(tid, area, bizCode, source, bizSource, metaData)
v, err := s.Recharge(ctx, bp, uid, platform, getTestRechargeOrPayForm(uid, coinType, num, tid))
So(err, ShouldBeNil)
So(v, ShouldNotBeNil)
coinRecord, recordErr := s.dao.GetCoinStreamByTid(ctx, tid)
So(recordErr, ShouldBeNil)
So(coinRecord.MetaData, ShouldEqual, metaData)
So(coinRecord.Area, ShouldEqual, area)
So(coinRecord.BizCode, ShouldEqual, bizCode)
So(coinRecord.Source, ShouldEqual, source)
So(coinRecord.BizSource, ShouldEqual, bizSource)
}))
}