427 lines
17 KiB
Go
427 lines
17 KiB
Go
package baishun
|
||
|
||
import (
|
||
"chatapp3-golang/internal/integration"
|
||
"chatapp3-golang/internal/model"
|
||
"chatapp3-golang/internal/service/taskcenter"
|
||
"chatapp3-golang/internal/utils"
|
||
"context"
|
||
"crypto/md5"
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// HandleToken 处理百顺获取启动 code/token 的回调。
|
||
func (s *BaishunService) HandleToken(ctx context.Context, req BaishunTokenRequest, rawJSON string) baishunStandardResponse {
|
||
runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp)
|
||
if !ok {
|
||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||
}
|
||
var session model.BaishunLaunchSession
|
||
if err := s.repo.DB.WithContext(ctx).
|
||
Where("launch_code = ?", strings.TrimSpace(req.Code)).
|
||
First(&session).Error; err != nil {
|
||
resp := s.failStandard(baishunCodeTokenExpired, "code invalid")
|
||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
if session.Status != baishunSessionCodeIssued || time.Now().After(session.LaunchCodeExpireTime) {
|
||
resp := s.failStandard(baishunCodeTokenExpired, "code expired")
|
||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
if strings.TrimSpace(req.UserID) != "" && strconv.FormatInt(session.UserID, 10) != strings.TrimSpace(req.UserID) {
|
||
resp := s.failStandard(baishunCodeTokenExpired, "code invalid")
|
||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
|
||
ssToken, err := s.randomToken(32)
|
||
if err != nil {
|
||
resp := s.failStandard(baishunCodeServerError, "token create failed")
|
||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second)
|
||
if err := s.repo.DB.WithContext(ctx).
|
||
Model(&model.BaishunLaunchSession{}).
|
||
Where("id = ?", session.ID).
|
||
Updates(map[string]any{
|
||
"ss_token": ssToken,
|
||
"ss_token_expire_time": expire,
|
||
"status": baishunSessionTokenIssued,
|
||
"update_time": time.Now(),
|
||
}).Error; err != nil {
|
||
resp := s.failStandard(baishunCodeServerError, "session update failed")
|
||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
|
||
userInfo := s.readUserInfo(ctx, session.UserID)
|
||
balance := s.readUserBalance(ctx, session.UserID)
|
||
resp := baishunStandardResponse{
|
||
Code: 0,
|
||
Message: "succeed",
|
||
UniqueID: s.uniqueID(),
|
||
Data: map[string]any{
|
||
"ss_token": ssToken,
|
||
"expire_date": expire.UnixMilli(),
|
||
},
|
||
UserInfo: map[string]any{
|
||
"user_id": req.UserID,
|
||
"user_name": userInfo.UserNickname,
|
||
"user_avatar": userInfo.UserAvatar,
|
||
"balance": balance,
|
||
},
|
||
}
|
||
s.saveCallbackLog(ctx, "token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message)
|
||
return resp
|
||
}
|
||
|
||
// HandleProfile 处理百顺获取用户资料的回调。
|
||
func (s *BaishunService) HandleProfile(ctx context.Context, req BaishunProfileRequest, rawJSON string) baishunStandardResponse {
|
||
if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||
}
|
||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||
if !ok {
|
||
resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid")
|
||
s.saveCallbackLog(ctx, "profile", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
userInfo := s.readUserInfo(ctx, session.UserID)
|
||
balance := s.readUserBalance(ctx, session.UserID)
|
||
resp := baishunStandardResponse{
|
||
Code: 0,
|
||
Message: "succeed",
|
||
UniqueID: s.uniqueID(),
|
||
Data: map[string]any{
|
||
"user_id": req.UserID,
|
||
"user_name": userInfo.UserNickname,
|
||
"user_avatar": userInfo.UserAvatar,
|
||
"balance": balance,
|
||
"balance_list": []map[string]any{{"name": "Gold", "currency_type": 0, "currency_amount": balance}},
|
||
"user_type": 1,
|
||
},
|
||
}
|
||
_ = s.repo.DB.WithContext(ctx).
|
||
Model(&model.BaishunLaunchSession{}).
|
||
Where("id = ?", session.ID).
|
||
Updates(map[string]any{"status": baishunSessionActive, "update_time": time.Now()}).Error
|
||
s.saveCallbackLog(ctx, "profile", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message)
|
||
return resp
|
||
}
|
||
|
||
// HandleUpdateToken 处理百顺刷新 ss_token 的回调。
|
||
func (s *BaishunService) HandleUpdateToken(ctx context.Context, req BaishunUpdateTokenRequest, rawJSON string) baishunStandardResponse {
|
||
runtimeCfg, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp)
|
||
if !ok {
|
||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||
}
|
||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||
if !ok {
|
||
resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid")
|
||
s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
newToken, err := s.randomToken(32)
|
||
if err != nil {
|
||
resp := s.failStandard(baishunCodeServerError, "token create failed")
|
||
s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
expire := time.Now().Add(time.Duration(runtimeCfg.ssTokenTTLSeconds()) * time.Second)
|
||
if err := s.repo.DB.WithContext(ctx).
|
||
Model(&model.BaishunLaunchSession{}).
|
||
Where("id = ?", session.ID).
|
||
Updates(map[string]any{
|
||
"ss_token": newToken,
|
||
"ss_token_expire_time": expire,
|
||
"status": baishunSessionActive,
|
||
"update_time": time.Now(),
|
||
}).Error; err != nil {
|
||
resp := s.failStandard(baishunCodeServerError, "session update failed")
|
||
s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
resp := baishunStandardResponse{
|
||
Code: 0,
|
||
Message: "succeed",
|
||
UniqueID: s.uniqueID(),
|
||
Data: map[string]any{
|
||
"ss_token": newToken,
|
||
"expire_date": expire.UnixMilli(),
|
||
},
|
||
}
|
||
s.saveCallbackLog(ctx, "update-token", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Message)
|
||
return resp
|
||
}
|
||
|
||
// HandleChangeBalance 处理百顺扣增币回调,并驱动钱包变更。
|
||
func (s *BaishunService) HandleChangeBalance(ctx context.Context, req BaishunChangeBalanceRequest, rawJSON string) baishunStandardResponse {
|
||
if _, ok := s.validateCommonSignature(ctx, req.AppID, "", req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
||
return s.failStandard(baishunCodeSignatureError, "signature error")
|
||
}
|
||
session, ok := s.findSessionByToken(ctx, req.SSToken, req.UserID)
|
||
if !ok {
|
||
resp := s.failStandard(baishunCodeTokenExpired, "ss_token invalid")
|
||
s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
if req.CurrencyDiff == 0 {
|
||
resp := s.failStandard(baishunCodeServerError, "currency_diff invalid")
|
||
s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
|
||
lockKey := fmt.Sprintf("bs:lock:user:%s:%s", session.SysOrigin, req.UserID)
|
||
lockOK, err := s.repo.Redis.SetNX(ctx, lockKey, "1", 5*time.Second).Result()
|
||
if err == nil && lockOK {
|
||
defer s.repo.Redis.Del(context.Background(), lockKey)
|
||
}
|
||
|
||
var order model.BaishunOrderIdempotency
|
||
err = s.repo.DB.WithContext(ctx).
|
||
Where("sys_origin = ? AND order_id = ?", session.SysOrigin, req.OrderID).
|
||
First(&order).Error
|
||
if err == nil && order.Status == baishunOrderStatusSuccess && strings.TrimSpace(order.ResponseJSON) != "" {
|
||
var resp baishunStandardResponse
|
||
if json.Unmarshal([]byte(order.ResponseJSON), &resp) == nil {
|
||
s.saveCallbackLog(ctx, "change-balance", rawJSON, order.ResponseJSON, session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message)
|
||
return resp
|
||
}
|
||
}
|
||
|
||
walletEventID := fmt.Sprintf("BAISHUN:%s:%s", session.SysOrigin, req.OrderID)
|
||
exists, existsErr := s.java.ExistsGoldEvent(ctx, walletEventID)
|
||
if existsErr == nil && !exists {
|
||
changeErr := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
||
ReceiptType: receiptTypeFromDiff(req.CurrencyDiff),
|
||
UserID: session.UserID,
|
||
SysOrigin: session.SysOrigin,
|
||
EventID: walletEventID,
|
||
Remark: defaultIfBlank(req.DiffMsg, req.MsgType),
|
||
Amount: integration.NewPennyAmountPayloadFromDollar(abs64(req.CurrencyDiff)),
|
||
CloseDelayAsset: false,
|
||
OpUserType: "APP",
|
||
CustomizeOrigin: fmt.Sprintf("BAISHUN_GAME_%d", req.GameID),
|
||
CustomizeOriginDesc: fmt.Sprintf("BAISHUN GAME[%d]", req.GameID),
|
||
})
|
||
if changeErr != nil {
|
||
message := changeErr.Error()
|
||
code := baishunCodeServerError
|
||
if strings.Contains(strings.ToLower(message), "insufficient") || strings.Contains(message, "5000") {
|
||
code = baishunCodeInsufficientAssets
|
||
}
|
||
resp := s.failStandard(code, message)
|
||
s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusFailed, rawJSON, utils.MustJSONString(resp, ""), nil)
|
||
s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusFailed, resp.Code, resp.Message)
|
||
return resp
|
||
}
|
||
}
|
||
|
||
balance := s.readUserBalance(ctx, session.UserID)
|
||
resp := baishunStandardResponse{
|
||
Code: 0,
|
||
Message: "succeed",
|
||
UniqueID: s.uniqueID(),
|
||
Data: map[string]any{
|
||
"currency_balance": balance,
|
||
},
|
||
}
|
||
s.persistOrderRecord(ctx, session.SysOrigin, req, walletEventID, baishunOrderStatusSuccess, rawJSON, utils.MustJSONString(resp, ""), nil)
|
||
s.saveCallbackLog(ctx, "change-balance", rawJSON, utils.MustJSONString(resp, ""), session.SysOrigin, req.UserID, &req.OrderID, &req.GameRoundID, baishunCallbackStatusSuccess, 0, resp.Message)
|
||
s.reportGameConsumeTaskEvent(ctx, session, req)
|
||
return resp
|
||
}
|
||
|
||
func (s *BaishunService) reportGameConsumeTaskEvent(ctx context.Context, session model.BaishunLaunchSession, req BaishunChangeBalanceRequest) {
|
||
if s.taskReporter == nil || req.CurrencyDiff >= 0 {
|
||
return
|
||
}
|
||
deltaValue := abs64(req.CurrencyDiff)
|
||
if deltaValue <= 0 {
|
||
return
|
||
}
|
||
payload := map[string]any{
|
||
"provider": "BAISHUN",
|
||
"orderId": strings.TrimSpace(req.OrderID),
|
||
"gameId": req.GameID,
|
||
"gameRoundId": strings.TrimSpace(req.GameRoundID),
|
||
"msgType": strings.TrimSpace(req.MsgType),
|
||
"diffMsg": strings.TrimSpace(req.DiffMsg),
|
||
}
|
||
if err := s.taskReporter.ReportSimpleEvent(ctx, session.SysOrigin, fmt.Sprintf("BAISHUN_GAME_CONSUME:%s:%s", session.SysOrigin, strings.TrimSpace(req.OrderID)), taskcenter.EventTypeGameConsumeGold, session.UserID, deltaValue, payload); err != nil {
|
||
log.Printf("report baishun task event failed: %v", err)
|
||
}
|
||
}
|
||
|
||
// HandleBalanceInfo 处理百顺查询余额回调。
|
||
func (s *BaishunService) HandleBalanceInfo(ctx context.Context, req BaishunBalanceInfoRequest, rawJSON string) baishunBalanceInfoResponse {
|
||
if _, ok := s.validateCommonSignature(ctx, req.AppID, req.AppChannel, req.Signature, req.SignatureNonce, req.Timestamp); !ok {
|
||
return baishunBalanceInfoResponse{Code: baishunCodeSignatureError, Msg: "signature error"}
|
||
}
|
||
userID, _ := strconv.ParseInt(req.UserID, 10, 64)
|
||
balance := s.readUserBalance(ctx, userID)
|
||
resp := baishunBalanceInfoResponse{
|
||
Code: 0,
|
||
Msg: "success",
|
||
Data: map[string]any{"cur_coin": balance},
|
||
}
|
||
s.saveCallbackLog(ctx, "balance-info", rawJSON, utils.MustJSONString(resp, ""), "", req.UserID, nil, nil, baishunCallbackStatusSuccess, 0, resp.Msg)
|
||
return resp
|
||
}
|
||
|
||
// validateCommonSignature 校验百顺回调签名。
|
||
func (s *BaishunService) validateCommonSignature(ctx context.Context, appID int64, appChannel, signature, nonce string, timestamp int64) (baishunRuntimeConfig, bool) {
|
||
runtimeCfg, err := s.resolveRuntimeConfigByApp(ctx, appID, appChannel)
|
||
if err != nil {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
if runtimeCfg.AppID != 0 && appID != 0 && appID != runtimeCfg.AppID {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
if strings.TrimSpace(appChannel) != "" && runtimeCfg.appChannel() != "" && appChannel != runtimeCfg.appChannel() {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
if strings.TrimSpace(signature) == "" || strings.TrimSpace(nonce) == "" || timestamp == 0 || strings.TrimSpace(runtimeCfg.AppKey) == "" {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
if abs64(time.Now().Unix()-timestamp) > 15 {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
expected := signBAISHUN(runtimeCfg.AppKey, timestamp, nonce)
|
||
if !strings.EqualFold(expected, signature) {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
if !s.rememberNonce(ctx, nonce) {
|
||
return baishunRuntimeConfig{}, false
|
||
}
|
||
return runtimeCfg, true
|
||
}
|
||
|
||
// rememberNonce 记录已用 nonce,防止回调重放。
|
||
func (s *BaishunService) rememberNonce(ctx context.Context, nonce string) bool {
|
||
ok, err := s.repo.Redis.SetNX(ctx, "bs:sign:nonce:"+nonce, "1", 15*time.Second).Result()
|
||
return err == nil && ok
|
||
}
|
||
|
||
// signBAISHUN 按百顺约定生成签名。
|
||
func signBAISHUN(appKey string, timestamp int64, nonce string) string {
|
||
sum := md5.Sum([]byte(nonce + appKey + strconv.FormatInt(timestamp, 10)))
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
// randomNonce 生成随机 nonce。
|
||
func (s *BaishunService) randomNonce() string {
|
||
token, err := s.randomToken(8)
|
||
if err != nil {
|
||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||
}
|
||
return token
|
||
}
|
||
|
||
// randomToken 生成指定长度的随机 token。
|
||
func (s *BaishunService) randomToken(size int) (string, error) {
|
||
buf := make([]byte, size)
|
||
if _, err := rand.Read(buf); err != nil {
|
||
return "", err
|
||
}
|
||
return hex.EncodeToString(buf), nil
|
||
}
|
||
|
||
// uniqueID 生成一条可返回给百顺的唯一 ID。
|
||
func (s *BaishunService) uniqueID() string {
|
||
id, err := utils.NextID()
|
||
if err != nil {
|
||
return strconv.FormatInt(time.Now().UnixNano(), 10)
|
||
}
|
||
return strconv.FormatInt(id, 10)
|
||
}
|
||
|
||
// persistOrderRecord 持久化扣增币回调的幂等记录。
|
||
func (s *BaishunService) persistOrderRecord(ctx context.Context, sysOrigin string, req BaishunChangeBalanceRequest, walletEventID, status, requestJSON, responseJSON string, assetRecordID *int64) {
|
||
id, err := utils.NextID()
|
||
if err != nil {
|
||
return
|
||
}
|
||
record := model.BaishunOrderIdempotency{
|
||
ID: id,
|
||
SysOrigin: sysOrigin,
|
||
OrderID: req.OrderID,
|
||
GameRoundID: req.GameRoundID,
|
||
RoomID: req.RoomID,
|
||
UserID: parseInt64Default(req.UserID),
|
||
VendorGameID: req.GameID,
|
||
CurrencyDiff: req.CurrencyDiff,
|
||
DiffMsg: req.DiffMsg,
|
||
MsgType: req.MsgType,
|
||
CurrencyType: req.CurrencyType,
|
||
WalletEventID: walletEventID,
|
||
WalletAssetRecordID: assetRecordID,
|
||
Status: status,
|
||
RequestJSON: requestJSON,
|
||
ResponseJSON: responseJSON,
|
||
CreateTime: time.Now(),
|
||
UpdateTime: time.Now(),
|
||
}
|
||
_ = s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "sys_origin"}, {Name: "order_id"}},
|
||
DoUpdates: clause.AssignmentColumns([]string{
|
||
"wallet_event_id", "wallet_asset_record_id", "status", "request_json", "response_json", "update_time",
|
||
}),
|
||
}).Create(&record).Error
|
||
}
|
||
|
||
// saveCallbackLog 保存百顺回调审计日志。
|
||
func (s *BaishunService) saveCallbackLog(ctx context.Context, requestType, requestJSON, responseJSON, sysOrigin, userID string, orderID, gameRoundID *string, status string, bizCode int, bizMessage string) {
|
||
id, err := utils.NextID()
|
||
if err != nil {
|
||
return
|
||
}
|
||
logRecord := model.BaishunCallbackLog{
|
||
ID: id,
|
||
SysOrigin: sysOrigin,
|
||
RequestType: requestType,
|
||
RequestJSON: requestJSON,
|
||
ResponseJSON: responseJSON,
|
||
BizCode: strconv.Itoa(bizCode),
|
||
BizMessage: bizMessage,
|
||
Status: status,
|
||
CreateTime: time.Now(),
|
||
UpdateTime: time.Now(),
|
||
}
|
||
if orderID != nil {
|
||
logRecord.OrderID = *orderID
|
||
}
|
||
if gameRoundID != nil {
|
||
logRecord.GameRoundID = *gameRoundID
|
||
}
|
||
if strings.TrimSpace(userID) != "" {
|
||
parsed := parseInt64Default(userID)
|
||
logRecord.UserID = &parsed
|
||
}
|
||
err = s.repo.DB.WithContext(ctx).Create(&logRecord).Error
|
||
if err != nil && isBizMessageTooLongError(err) {
|
||
logRecord.BizMessage = truncateRunes(logRecord.BizMessage, legacyBizMessageRuneLimit)
|
||
_ = s.repo.DB.WithContext(ctx).Create(&logRecord).Error
|
||
}
|
||
}
|
||
|
||
// failStandard 构造百顺标准失败回包。
|
||
func (s *BaishunService) failStandard(code int, message string) baishunStandardResponse {
|
||
return baishunStandardResponse{
|
||
Code: code,
|
||
Message: defaultIfBlank(message, "failed"),
|
||
UniqueID: s.uniqueID(),
|
||
}
|
||
}
|