feat-20231230-001-Robin #25

Merged
chenhao merged 22 commits from feat-20231230-001-Robin into test 2023-12-30 19:49:54 +08:00
8 changed files with 316 additions and 9 deletions
Showing only changes of commit 7d5758d999 - Show all commits

View File

@ -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
}

View File

@ -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")
}

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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{

View File

@ -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