-
Notifications
You must be signed in to change notification settings - Fork 9
支付环境下复杂的嵌套事务
Tuuz edited this page Jun 20, 2022
·
5 revisions
BalanceModel.go
type Interface struct {
Db gorose.IOrm
}
func (self *Interface) Api_find(uid, fid, cid interface{}) gorose.Data {
db := self.Db.Table(table)
where := map[string]interface{}{
"uid": uid,
"fid": fid,
"cid": cid,
}
db.Where(where)
db.LockForUpdate()
ret, err := db.Find()
if err != nil {
Log.Dbrr(err, tuuz.FUNCTION_ALL())
return nil
} else {
return ret
}
}
BalanceModel.go
func (self *Interface) Api_incr(uid, fid, cid, incr_balance interface{}) bool {
db := self.Db.Table(table)
where := map[string]interface{}{
"uid": uid,
"fid": fid,
"cid": cid,
}
db.Where(where)
db.LockForUpdate()
_, err := db.Increment("balance", incr_balance)
if err != nil {
Log.Dbrr(err, tuuz.FUNCTION_ALL())
return false
} else {
return true
}
}
BalanceRecordModel.go
type Interface struct {
Db gorose.IOrm
}
func (self *Interface) Api_find_last(uid, fid, cid interface{}) gorose.Data {
db := self.Db.Table(table)
where := map[string]interface{}{
"uid": uid,
"fid": fid,
"cid": cid,
}
db.Where(where)
db.OrderBy("id desc")
db.LockForUpdate()
ret, err := db.Find()
if err != nil {
Log.Dbrr(err, tuuz.FUNCTION_ALL())
return nil
} else {
return ret
}
}
BalanceRecordModel.go
func (self *Interface) Api_insert(uid, fid, cid, Type, order_id, before, amount, after, extra, remark1, remark2 interface{}) bool {
db := self.Db.Table(table)
data := map[string]interface{}{
"uid": uid,
"fid": fid,
"cid": cid,
"type": Type,
"order_id": order_id,
"before": before,
"amount": amount,
"after": after,
"extra": extra,
"remark1": remark1,
"remark2": remark2,
}
db.Data(data)
db.LockForUpdate()
_, err := db.Insert()
if err != nil {
Log.Dbrr(err, tuuz.FUNCTION_ALL())
return false
} else {
return true
}
}
查询余额所用的BalanceAction,后面查询余额的复用功能都需要走这个action
func (self *Interface) App_check_balance(uid, fid, cid interface{}) decimal.Decimal {
var balmodel BalanceModel.Interface
balmodel.Db = self.Db
userbalance := balmodel.Api_find(uid, fid, cid)
if len(userbalance) > 0 {
return Calc.ToDecimal(userbalance["balance"])
} else {
balmodel.Api_insert(uid, fid, cid, 0)
return decimal.Zero
}
}
func (self *Interface) App_single_balance(uid, fid, cid, Type, order_id interface{}, amount decimal.Decimal, extra, remark1, remark2 interface{}) error {
db := self.Db
if order_id == nil {
order_id = Calc.GenerateOrderId()
}
//Begin注解:如果外部已经执行过Begin开启事务后,当你在此处再次执行Begin指令后,相当于这条*db流就开启了子事务,在这条*db流被rollback或者commit之后,该条事务会被当做savepoint进行提交,但是在父事务最终提交前,任一子事务都不会写入数据库
//请放心,这里和Thinkphp的think\Db::startTrans()是一个用法,如果你不理解,按照PHP里面那么写就行
db.Begin()
balance_left := self.App_check_balance(uid, fid, cid)
if balance_left.Add(amount).LessThan(decimal.Zero) {
//请关注最后Rollback注解
db.Rollback()
return errors.New("余额不足")
}
var balance BalanceModel.Interface
balance.Db = db
balance.Api_incr(uid, fid, cid, amount)
//插入变动数据
var balancerecord BalanceRecordModel.Interface
balancerecord.Db = db
last_record := balancerecord.Api_find_last(uid, fid, cid)
after := decimal.Zero
before := "0"
if len(last_record) > 0 {
after = Calc.Bc_add(last_record["after"], amount)
before = last_record["after"].(string)
} else {
after = amount
}
if after.LessThan(decimal.Zero) {
//请关注Rollback注解
db.Rollback()
return errors.New("余额记录不足")
}
if !balancerecord.Api_insert(uid, fid, cid, Type, order_id, before, amount, after, extra, remark1, remark2) {
//Rollback注解:请注意这里执行Rollback后,仅仅会将当前的Begin指令进行恢复,不会影响到之前的事务,该功能仅在GorosePro中支持,请勿在原版框架中使用!!!
db.Rollback()
return errors.New("balance_record添加失败")
}
//Commit注解:请注意,这里Commit只会提交上面从Begin开始的子事务,不会影响到父级事务,该功能仅在GorosePro中支持,请勿在原版框架中使用!!!
db.Commit()
return nil
}
本action干的事情就是调用1次资金减少,再调用1次资金增加从而完成转账,如果有必要,可以向一个专门用于记录转账信息的表中再写入一条转账记录数据(有别于资金记录),这里就是一个完整的带有手续费功能的转账方法
func (self *Interface) App_single_transfer(uid, fid, cid, to_uid, order_id interface{}, amount decimal.Decimal, extra, remark1, remark2 interface{}) (err error) {
if len(FacilityModel.Api_find(fid)) < 1 {
return errors.New("机构不存在")
}
db := self.Db
if order_id == nil {
order_id = Calc.GenerateOrderId()
}
user := UserModel.Api_find(to_uid)
if len(user) < 1 {
err = errors.New("没有找到接收人")
return
}
//这里开始了一个“父级”的事务,一般来说转账方法的上层就已经是Controller了,所以一般本action不会作为子action被执行
coin := CoinModel.Api_find(cid)
if len(coin) < 1 {
db.Rollback()
err = errors.New("未找到币种")
return
}
transfer_fee := coin["transfer_fee"].(float64)
transfer_time_limit := coin["transfer_time_limit"].(int64)
transfer_limit := coin["transfer_limit"].(float64)
transfer_per_limit := coin["transfer_per_limit"].(float64)
var trm TransferRecordModel.Interface
trm.Db = db
todays_time := trm.Api_count_today(uid, Date.Today())
if todays_time > transfer_time_limit {
db.Rollback()
err = errors.New("超过当日最大转出数量")
return
}
todays_amount := trm.Api_sum_today(uid, Date.Today())
if todays_amount.GreaterThan(Calc.ToDecimal(transfer_limit)) {
db.Rollback()
err = errors.New("超过当日最大转出数量")
return
}
if decimal.NewFromFloat(transfer_per_limit).LessThan(amount) {
db.Rollback()
err = errors.New("超过单笔最大限制")
return
}
fee := Calc.Bc_mul(transfer_fee, amount)
after_amount := Calc.Bc_min(amount, fee)
//这个方法是扣款方法,在这func下就是转出的意思
err = self.App_single_balance(uid, fid, cid, 22, order_id, amount.Abs().Neg(), extra, "用户转账转出", remark2)
if err != nil {
//如果这个执行了Rollback,就会把当前的父级的事务给结束掉
db.Rollback()
//如果上面执行了Rollback,这里通常要return把程序停掉
return
}
//这个方法就是加钱的方法,在这个func下就是转入的意思
err = self.App_single_balance(to_uid, fid, cid, 21, order_id, after_amount.Abs(), extra, "用户收款转入", remark2)
if err != nil {
//这里同上
db.Rollback()
return
}
//最后在完成后,插入一条转账记录
if !trm.Api_insert(uid, cid, 0, order_id, amount, fee, after_amount.Abs(), 1, 1, "") {
//这里如果插入记录失败了,虽然这是一个model但是依然可以执行rollback,只要这个父级的rollback被执行并return,之前所有的子级事务全部都会被回档,不会写入数据库
db.Rollback()
err = errors.New("插入转账记录失败")
return
}
//如果一切顺利,这里执行Commit之后,之前两个资金的Action+最后一条转账记录将会被确认写入到数据库
db.Commit()
return
}
到此一个复杂的解耦嵌套事务就结束了
- 尽量避免相同数据库下同时调用修改和查询接口,例如:
func (self *Interface)Api_insert()
和
func Api_Find()
非常容易造成死锁!
同时存在在一个事务内,不要再继承db之后再调用非db类型(需要重新init)的方法,例如:
id:=UserMode.Api_find()
var usr UserModel.Interface
usr=tuuz.Db()
if usr.Api_insert(){
//....执行代码
}
id:=UserMode.Api_find()
这样执行有极大的概率会在生产环境中出错,造成不可预料的死锁,所以千万要避免掉!