diff --git a/api/proto/vas/proto/pay.go b/api/proto/vas/proto/pay.go index 75ab2cfb..27ececb6 100644 --- a/api/proto/vas/proto/pay.go +++ b/api/proto/vas/proto/pay.go @@ -127,3 +127,9 @@ type H5DirectUnlockWechatData struct { AlipayParamStr string `json:"alipay_param_str"` // 支付宝 app支付参数 AlipayH5ParamStr string `json:"alipay_h5_param_str"` // 支付宝 h5支付参数 } + +// 支付宝回调参数 +type AlipayCallbackParamIn struct { + OrderId string `json:"order_id"` // 我们自己服务的订单id + AlipayOrderId string `json:"alipay_order_id"` // 支付宝订单id +} diff --git a/app/mix/controller/alipay_callback.go b/app/mix/controller/alipay_callback.go new file mode 100644 index 00000000..81197f04 --- /dev/null +++ b/app/mix/controller/alipay_callback.go @@ -0,0 +1,24 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + vasproto "service/api/proto/vas/proto" + "service/app/mix/service" + "service/library/logger" + "service/library/payclients/alipaycli" +) + +func AlipayCallback(ctx *gin.Context) { + req, _ := ctx.GetRawData() + bm, err := alipaycli.GetDefaultAlipayClient().ParseNotify(ctx.Request) + if err != nil { + logger.Error("ParseNotify fail, req: %v, err: %v", string(req), err) + return + } + + service.DefaultService.AlipayCallback(ctx, &vasproto.AlipayCallbackParamIn{ + OrderId: bm.GetString("out_trade_no"), + AlipayOrderId: bm.GetString("trade_no"), + }) + ctx.String(200, "success") +} diff --git a/app/mix/controller/init.go b/app/mix/controller/init.go index 2d52103b..d40aff13 100644 --- a/app/mix/controller/init.go +++ b/app/mix/controller/init.go @@ -185,6 +185,9 @@ func Init(r *gin.Engine) { vasPayGroup.POST("h5_direct_unlock_wechat", middleware.JSONParamValidator(vasproto.H5DirectUnlockWechatReq{}), H5DirectUnlockWechat) vasPayGroup.POST("h5_get_unlock_wechat_list", middleware.JSONParamValidator(vasproto.GetUnlockWechatListReq{}), GetUnlockWechatList) + extVasPayGroup := r.Group("/ext/vas") + extVasPayGroup.POST("alipay_callback", AlipayCallback) + opVasPayGroup := r.Group("/op/vas", PrepareOp()) opVasPayGroup.POST("create_order", middleware.JSONParamValidator(vasproto.OpCreateOrderReq{}), OpCreateOrder) @@ -454,6 +457,22 @@ func PrepareToC() gin.HandlerFunc { } } +func PrepareExt() gin.HandlerFunc { + return func(ctx *gin.Context) { + var bodyParam map[string]interface{} + buf, err := ioutil.ReadAll(ctx.Request.Body) + err = json.Unmarshal(buf, &bodyParam) + if err != nil { + logger.Error("arg parse fail: %v", err) + ReplyJsonError(ctx, http.StatusBadRequest, "参数解析失败") + return + } + + buf, err = json.Marshal(&bodyParam) + ctx.Set(gin.BodyBytesKey, buf) + } +} + var upGrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true diff --git a/app/mix/dao/mysql.go b/app/mix/dao/mysql.go index 3cb92443..4cb5c1ef 100644 --- a/app/mix/dao/mysql.go +++ b/app/mix/dao/mysql.go @@ -132,6 +132,66 @@ func (m *Mysql) CreateOrder(ctx *gin.Context, tx *sqlx.Tx, order *dbstruct.Order return err } +// 获取订单 +func (m *Mysql) GetOrderById(ctx *gin.Context, tx *sqlx.Tx, id string) (order *dbstruct.Order, err error) { + var tmpOrder dbstruct.Order + if tx != nil { + err = tx.GetContext(ctx, &tmpOrder, fmt.Sprintf("select * from %s where id = ?", TableOrder), id) + } else { + db := m.getDBVas() + err = db.GetContext(ctx, &tmpOrder, fmt.Sprintf("select * from %s where id = ?", TableOrder), id) + } + if err != nil { + return + } + order = &tmpOrder + return +} + +// 获取订单 +func (m *Mysql) GetOrderByOutOrderId(ctx *gin.Context, tx *sqlx.Tx, outOrderId string) (order *dbstruct.Order, err error) { + var tmpOrder dbstruct.Order + if tx != nil { + err = tx.GetContext(ctx, &tmpOrder, fmt.Sprintf("select * from %s where out_order_id = ?", TableOrder), outOrderId) + } else { + db := m.getDBVas() + err = db.GetContext(ctx, &tmpOrder, fmt.Sprintf("select * from %s where out_order_id = ?", TableOrder), outOrderId) + } + if err != nil { + return + } + order = &tmpOrder + return +} + +// 获取订单for update +func (m *Mysql) GetOrderByIdForUpdate(ctx *gin.Context, tx *sqlx.Tx, id string) (order *dbstruct.Order, err error) { + var tmpOrder dbstruct.Order + err = tx.GetContext(ctx, &tmpOrder, fmt.Sprintf("select * from %s where id = ? for update", TableOrder), id) + if err != nil { + return + } + order = &tmpOrder + return +} + +// 更新订单状态 +func (m *Mysql) UpdateOrderStatus(ctx *gin.Context, tx *sqlx.Tx, orderId string, preStatus, aftStatus int32) error { + var err error + sqlStr := "update " + TableOrder + " set order_status=? where id=? and order_status=?" + if tx != nil { + _, err = tx.ExecContext(ctx, sqlStr, aftStatus, orderId, preStatus) + } else { + db := m.getDBVas() + _, err = db.ExecContext(ctx, sqlStr, aftStatus, orderId, preStatus) + } + if err != nil { + logger.Error("UpdateOrderStatus fail, orderId: %v, preStatus: %v, aftStatus: %v, err: %v", orderId, preStatus, aftStatus, err) + return err + } + return err +} + // 获取钱包 for update func (m *Mysql) GetWalletForUpdate(ctx *gin.Context, tx *sqlx.Tx, mid int64) (wallet *dbstruct.Wallet, err error) { var tmpWallet dbstruct.Wallet @@ -271,7 +331,7 @@ func (m *Mysql) CreateCoinOrder(ctx *gin.Context, tx *sqlx.Tx, order *dbstruct.C return err } -// 获取金币订单 for update +// 获取金币订单 func (m *Mysql) GetCoinOrderById(ctx *gin.Context, tx *sqlx.Tx, id string) (order *dbstruct.CoinOrder, err error) { var tmpOrder dbstruct.CoinOrder if tx != nil { diff --git a/app/mix/service/logic/vas.go b/app/mix/service/logic/vas.go index adaf703c..9d4dde4e 100644 --- a/app/mix/service/logic/vas.go +++ b/app/mix/service/logic/vas.go @@ -8,6 +8,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/samber/lo" goproto "google.golang.org/protobuf/proto" + "service/api/base" "service/api/errs" vasproto "service/api/proto/vas/proto" "service/app/mix/dao" @@ -133,7 +134,7 @@ func (v *Vas) CreateOrder(ctx *gin.Context, req *vasproto.CreateOrderReq) (data product.RealPrice = req.CalcPrice } if req.CustomCoins > 0 { - product.RealCoinPrice = req.CustomCoins + product.ValueCoins = req.CustomCoins } // 支付参数 @@ -649,6 +650,9 @@ func (v *Vas) OneStepUnlockContact(ctx *gin.Context, req *vasproto.OneStepUnlock contactProductId = req.ContactProductId // 要解锁的联系方式 ) + // 获取uid的邀请人mid + req.InviterMid = 0 + // 检查钱包 _, has := v.CheckWalletExist(ctx, mid) if !has { @@ -691,6 +695,9 @@ func (v *Vas) OneStepUnlockContact(ctx *gin.Context, req *vasproto.OneStepUnlock case dbstruct.ProductIdContactWechat: // 获取uid微信金币价格 coinPrice = uVasInfo.WechatCoinPrice + case dbstruct.ProductIdH5ContactWechat: + contactProductId = dbstruct.ProductIdContactWechat + coinPrice = uVasInfo.GetH5WechatCoinPrice() default: err = errs.ErrVasInvalidContactProduct return @@ -1356,7 +1363,7 @@ func (v *Vas) H5DirectUnlockWechat(ctx *gin.Context, req *vasproto.H5DirectUnloc if wallet.GetCoins() >= uVas.GetH5WechatCoinPrice() { v.OneStepUnlockContact(ctx, &vasproto.OneStepUnlockContactReq{ BaseRequest: req.BaseRequest, - ContactProductId: dbstruct.ProductIdH5ContactWechat, + ContactProductId: dbstruct.ProductIdContactWechat, Uid: req.Uid, InviterMid: req.InviterMid, }) @@ -1405,3 +1412,165 @@ func (v *Vas) GetUserWechatUnlock(ctx *gin.Context, mid, uid int64) (uu *dbstruc } return } + +// 支付宝【支付】回调 +func (v *Vas) AlipayCallback(ctx *gin.Context, p *vasproto.AlipayCallbackParamIn) { + var ( + orderId = p.OrderId + alipayOrderId = p.AlipayOrderId + afterStatus int32 + ) + + // 获取订单 + checkOrder, err := v.store.GetOrderById(ctx, nil, orderId) + if err != nil { + logger.Error("GetOrderById fail, p: %v, err: %v", util.ToJson(p), err) + return + } + if checkOrder == nil { + logger.Warn("GetOrderById nil, p: %v", util.ToJson(p)) + return + } + // 是否已处理过的订单 + if checkOrder.GetOrderStatus() != dbstruct.VasOrderStatusInit { + logger.Error("repeat deal, p: %v", util.ToJson(p)) + return + } + + // ali_order_id检查 + outOrder, err := v.store.GetOrderByOutOrderId(ctx, nil, alipayOrderId) + if err != nil { + logger.Error("GetOrderByOutOrderId fail, p: %v, err: %v", util.ToJson(p), err) + return + } + if outOrder != nil { + logger.Error("out order exists, p: %v", util.ToJson(p)) + return + } + + // 获取商品 + product, err := v.store.GetProductById(ctx, checkOrder.GetProductId()) + if err != nil { + logger.Error("GetProductById fail, id: %v, err: %v", checkOrder.GetProductId(), err) + return + } + if product == nil { + logger.Error("GetProductById nil, id: %v", checkOrder.GetProductId()) + return + } + + // 钱包 + _, hasWallet := v.CheckWalletExist(ctx, checkOrder.GetMid()) + if !hasWallet { + logger.Error("CheckWalletExist fail, mid: %v", checkOrder.GetMid()) + return + } + + // 开启事务 + tx, err := v.store.VasBegin(ctx) + if err != nil { + logger.Error("vas begin fail, err: %v", err) + return + } + defer func() { + _ = v.AddOplogOrder( + ctx, + &dbstruct.OplogOrder{ + OrderId: orderId, + Mid: checkOrder.GetMid(), + Action: dbstruct.OrderOpLogActionAdd, + Detail: fmt.Sprintf("充值%d金币", checkOrder.GetCoins()), + BeforeStatus: checkOrder.GetOrderStatus(), + AfterStatus: afterStatus, + }, + err, + ) + + if err != nil { + logger.Error("global err, p: %v, order: %v, err: %v", util.ToJson(p), util.ToJson(checkOrder), err) + } + errTx := v.store.DealTxCR(tx, err) + if errTx != nil { + logger.Error("DealTxCR fail, err: %v", errTx) + return + } + + // 后续处理 + switch { + case product.Id == dbstruct.ProductIdH5ContactWechat: + // 解锁 + _, _, _, errIn := v.OneStepUnlockContact(ctx, &vasproto.OneStepUnlockContactReq{ + BaseRequest: base.BaseRequest{ + Mid: checkOrder.GetMid(), + }, + ContactProductId: dbstruct.ProductIdH5ContactWechat, + Uid: checkOrder.GetUid(), + }) + if errIn != nil { + logger.Error("OneStepUnlockContact fail, order: %v", util.ToJson(checkOrder)) + return + } + } + }() + + // 锁住订单 + order, err := v.store.GetOrderByIdForUpdate(ctx, tx, orderId) + if err != nil { + logger.Error("GetOrderByIdForUpdate fail, p: %v", util.ToJson(p)) + return + } + + // 锁住钱包 + wallet, _ := v.store.GetWalletForUpdate(ctx, tx, order.GetMid()) + + // 消费历史 + ch := &dbstruct.ConsumeHistory{ + Mid: goproto.Int64(order.GetMid()), + Type: goproto.Int32(dbstruct.CHTypeCharge), + SType: goproto.Int32(dbstruct.CHSTypeChargeUser), + TypeId: goproto.String(dbstruct.ProductTypeCoins), + OrderId: goproto.String(orderId), + Change: goproto.Int64(order.GetCoins()), + Before: goproto.Int64(wallet.GetCoins()), + After: goproto.Int64(wallet.GetCoins() + order.GetCoins()), + Count: goproto.Int64(order.GetCoins()), + Ct: goproto.Int64(time.Now().Unix()), + } + err = v.store.CreateConsumeHistory(ctx, tx, ch) + if err != nil { + logger.Error("CreateConsumeHistory fail, ch: %v, err: %v", util.ToJson(ch), err) + return + } + + // 更新状态 + err = v.store.UpdateOrderStatus(ctx, tx, orderId, dbstruct.VasOrderStatusInit, dbstruct.VasOrderStatusPaySuccess) + if err != nil { + logger.Error("UpdateOrderStatus fail, p: %v", util.ToJson(p)) + return + } + afterStatus = dbstruct.VasOrderStatusPaySuccess + + // 商品判断 + switch { + case product.Type == dbstruct.ProductTypeCoins: + // 充金币 + err = v.store.IncCoins(ctx, tx, order.GetMid(), order.GetCoins()) + if err != nil { + logger.Error("IncCoins fail, order: %v", util.ToJson(order)) + return + } + err = v.store.UpdateOrderStatus(ctx, tx, orderId, dbstruct.VasOrderStatusPaySuccess, dbstruct.VasOrderStatusFinish) + if err != nil { + logger.Error("UpdateOrderStatus fail, order: %v", util.ToJson(order)) + return + } + afterStatus = dbstruct.VasOrderStatusFinish + case product.Id == dbstruct.ProductIdH5ContactWechat: + // 充金币 + err = v.store.IncCoins(ctx, tx, order.GetMid(), order.GetCoins()) + if err != nil { + logger.Error("IncCoins fail, order: %v", util.ToJson(order)) + return + } + } +} diff --git a/app/mix/service/vasservice.go b/app/mix/service/vasservice.go index da7568e8..18560c2f 100644 --- a/app/mix/service/vasservice.go +++ b/app/mix/service/vasservice.go @@ -339,3 +339,9 @@ func (s *Service) H5DirectUnlockWechat(ctx *gin.Context, req *vasproto.H5DirectU } return } + +// 支付宝回调 +func (s *Service) AlipayCallback(ctx *gin.Context, req *vasproto.AlipayCallbackParamIn) (data *vasproto.H5DirectUnlockWechatData, ec errcode.ErrCode) { + _DefaultVas.AlipayCallback(ctx, req) + return +} diff --git a/dbstruct/vas_mysql.go b/dbstruct/vas_mysql.go index 2bab8a6c..a037aef8 100644 --- a/dbstruct/vas_mysql.go +++ b/dbstruct/vas_mysql.go @@ -6,11 +6,11 @@ import ( // 订单状态 const ( - VasOrderStatusNone = iota - 1 - VasOrderStatusInit // 初始化 - VasOrderStatusPaySuccess // 付款成功(第三方回调成功) - VasOrderStatusFinish // 订单完成,不再参与业务 - VasOrderStatusRefund // 已退款 + VasOrderStatusNone = -1 + VasOrderStatusInit = 0 // 初始化 + VasOrderStatusPaySuccess = 1 // 付款成功(第三方回调成功) + VasOrderStatusFinish = 2 // 订单完成,不再参与业务 + VasOrderStatusRefund = 3 // 已退款 ) var OrderStatusDescMap = map[int32]string{ diff --git a/library/payclients/alipaycli/client.go b/library/payclients/alipaycli/client.go index e708a278..d4f3be41 100644 --- a/library/payclients/alipaycli/client.go +++ b/library/payclients/alipaycli/client.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/go-pay/gopay" "github.com/go-pay/gopay/alipay" + "net/http" "service/bizcommon/util" "service/library/configcenter" "service/library/logger" @@ -34,11 +35,33 @@ func Init(cfg *configcenter.AlipayClientConfig) (err error) { alipayCli.SetNotifyUrl(cfg.NotifyUrl) defaultAlipayClient = &AlipayClient{ - alipayCli, + Client: alipayCli, } return } +// 解析回调参数 +func (c *AlipayClient) ParseNotify(req *http.Request) (notify gopay.BodyMap, err error) { + // 解析参数 + notifyTmp, err := alipay.ParseNotifyToBodyMap(req) + if err != nil { + logger.Error("ParseNotifyToBodyMap fail, req: %v, err: %v", util.ToJson(req), err) + return + } + // 验签 + ok, err := alipay.VerifySign("", notifyTmp) + if err != nil { + logger.Error("VerifySign fail, bm: %v, err: %v", util.ToJson(notifyTmp), err) + return + } + if !ok { + logger.Error("VerifySign fail, not ok, bm: %v", util.ToJson(notifyTmp)) + return + } + notify = notifyTmp + return +} + // 支付宝 app支付 type AppPayParam struct { OutTradeNo string // 商家订单id,我们自己的订单id