336 lines
9.6 KiB
Go

package luckygift
import (
"chatapp3-golang/internal/config"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
// buildLuckyGiftProviderPayload 构造发往三方的请求体。
func buildLuckyGiftProviderPayload(account config.LuckyGiftProviderConfig, req LuckyGiftDrawRequest) (map[string]any, error) {
orderIDs := make([]string, 0, len(req.Orders))
for _, order := range req.Orders {
orderIDs = append(orderIDs, luckyGiftProviderOrderID(order.ID))
}
payload := map[string]any{
"app_id": account.AppID,
"app_channel": account.AppChannel,
"room_id": strconv.FormatInt(req.RoomID, 10),
"user_id": strconv.FormatInt(req.SendUserID, 10),
"gift_id": req.GiftID,
"gift_price": req.GiftPrice,
"gift_num": req.GiftNum,
"gift_is_free": func() int {
if req.GiftIsFree {
return 1
}
return 0
}(),
"order_id": orderIDs,
"timestamp": time.Now().Unix(),
}
signature, err := buildLuckyGiftSignature(payload, account.AppKey)
if err != nil {
return nil, err
}
payload["signature"] = signature
return payload, nil
}
// resolveLuckyGiftProviderEndpoint 校验并补齐三方接口地址。
func resolveLuckyGiftProviderEndpoint(rawURL string) (string, error) {
trimmed := strings.TrimSpace(rawURL)
if trimmed == "" {
return "", fmt.Errorf("provider url is empty")
}
parsed, err := url.Parse(trimmed)
if err != nil {
return "", err
}
if parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("provider url must include scheme and host")
}
const luckyGiftStartGamePath = "/lucky_gift/start_game"
path := strings.TrimRight(parsed.Path, "/")
if path == "" {
parsed.Path = luckyGiftStartGamePath
return parsed.String(), nil
}
if path == luckyGiftStartGamePath {
parsed.Path = luckyGiftStartGamePath
return parsed.String(), nil
}
parsed.Path = path + luckyGiftStartGamePath
return parsed.String(), nil
}
// buildLuckyGiftSignature 按约定的 key 排序规则生成签名。
func buildLuckyGiftSignature(payload map[string]any, appKey string) (string, error) {
keys := make([]string, 0, len(payload))
for key := range payload {
if strings.EqualFold(key, "signature") {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys)+1)
for _, key := range keys {
value, err := stringifyLuckyGiftSignatureValue(key, payload[key])
if err != nil {
return "", err
}
parts = append(parts, key+"="+value)
}
parts = append(parts, "app_key="+strings.TrimSpace(appKey))
sum := md5.Sum([]byte(strings.Join(parts, "&")))
return hex.EncodeToString(sum[:]), nil
}
// stringifyLuckyGiftSignatureValue 把参与签名的字段值转换成稳定字符串。
func stringifyLuckyGiftSignatureValue(key string, value any) (string, error) {
if key == "order_id" {
raw, err := json.Marshal(value)
if err != nil {
return "", err
}
return url.QueryEscape(string(raw)), nil
}
switch typed := value.(type) {
case string:
return typed, nil
case int:
return strconv.Itoa(typed), nil
case int64:
return strconv.FormatInt(typed, 10), nil
case float64:
return strconv.FormatInt(int64(typed), 10), nil
case json.Number:
return typed.String(), nil
default:
return strings.TrimSpace(fmt.Sprint(typed)), nil
}
}
// parseLuckyGiftProviderResponse 解析三方响应,并与本地订单列表做对齐校验。
func parseLuckyGiftProviderResponse(raw string, orders []LuckyGiftDrawOrderInput) (int, []LuckyGiftDrawResult, int64, error) {
payload, err := decodeLuckyGiftJSON(raw)
if err != nil {
return 0, nil, 0, fmt.Errorf("decode provider response: %w", err)
}
code := int(readLuckyGiftInt64(payload["code"]))
if code == luckyGiftProviderFuseCode {
results := zeroLuckyGiftResults(orders)
return code, results, 0, nil
}
if code != luckyGiftProviderSuccessCode {
return code, nil, 0, fmt.Errorf("provider code=%d message=%s", code, readLuckyGiftMessage(payload))
}
data := readLuckyGiftMap(payload["data"])
orderList := readLuckyGiftSlice(data["order_list"])
if len(orderList) == 0 {
orderList = readLuckyGiftSlice(data["orderList"])
}
if len(orderList) == 0 {
orderList = readLuckyGiftSlice(data["OrderList"])
}
if len(orderList) != len(orders) {
return code, nil, 0, fmt.Errorf("provider order_list size mismatch: want=%d got=%d", len(orders), len(orderList))
}
parsed := make(map[string]LuckyGiftDrawResult, len(orderList))
for _, item := range orderList {
row := readLuckyGiftMap(item)
providerOrderID := strings.TrimSpace(readLuckyGiftString(row["order_id"]))
if providerOrderID == "" {
providerOrderID = strings.TrimSpace(readLuckyGiftString(row["orderId"]))
}
if providerOrderID == "" {
return code, nil, 0, fmt.Errorf("provider order_id is empty")
}
if _, exists := parsed[providerOrderID]; exists {
return code, nil, 0, fmt.Errorf("provider order_id duplicated: %s", providerOrderID)
}
parsed[providerOrderID] = LuckyGiftDrawResult{
OrderID: providerOrderID,
AcceptUserID: readLuckyGiftInt64(row["accept_user_id"]),
IsWin: readLuckyGiftBool(row["is_win"], row["isWin"]),
RewardNum: readLuckyGiftInt64(firstLuckyGiftValue(row["reward_num"], row["rewardNum"])),
}
}
results := make([]LuckyGiftDrawResult, 0, len(orders))
var rewardTotal int64
for _, order := range orders {
providerOrderID := luckyGiftProviderOrderID(order.ID)
result, exists := parsed[providerOrderID]
if !exists {
return code, nil, 0, fmt.Errorf("provider order missing: %s", providerOrderID)
}
if result.AcceptUserID != 0 && result.AcceptUserID != order.AcceptUserID {
return code, nil, 0, fmt.Errorf("provider accept_user_id mismatch for order %s", providerOrderID)
}
result.ID = order.ID
result.AcceptUserID = order.AcceptUserID
rewardTotal += result.RewardNum
results = append(results, result)
}
return code, results, rewardTotal, nil
}
// zeroLuckyGiftResults 在三方熔断或未中奖场景下生成全空中奖结果。
func zeroLuckyGiftResults(orders []LuckyGiftDrawOrderInput) []LuckyGiftDrawResult {
results := make([]LuckyGiftDrawResult, 0, len(orders))
for _, order := range orders {
results = append(results, LuckyGiftDrawResult{
ID: order.ID,
OrderID: luckyGiftProviderOrderID(order.ID),
AcceptUserID: order.AcceptUserID,
IsWin: false,
RewardNum: 0,
})
}
return results
}
// luckyGiftProviderOrderID 生成发往三方的订单号。
func luckyGiftProviderOrderID(orderSeed string) string {
return "LG_" + strings.TrimSpace(orderSeed)
}
// luckyGiftWalletEventID 构造钱包幂等事件 ID。
func luckyGiftWalletEventID(sysOrigin, businessID string) string {
return fmt.Sprintf("%s:%s:%s", luckyGiftWalletOrigin, strings.ToUpper(strings.TrimSpace(sysOrigin)), strings.TrimSpace(businessID))
}
// findLuckyGiftConfig 按档位 ID 选择对应的三方配置。
func findLuckyGiftConfig(configs []config.LuckyGiftProviderConfig, standardID int64) (config.LuckyGiftProviderConfig, bool) {
for _, item := range configs {
if item.StandardID == standardID {
return item, true
}
}
for _, item := range configs {
if item.StandardID == 0 {
return item, true
}
}
return config.LuckyGiftProviderConfig{}, false
}
// decodeLuckyGiftJSON 把三方 JSON 报文解析成 map。
func decodeLuckyGiftJSON(raw string) (map[string]any, error) {
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.UseNumber()
var payload map[string]any
if err := decoder.Decode(&payload); err != nil {
return nil, err
}
return payload, nil
}
// readLuckyGiftMap 安全读取 map 子对象。
func readLuckyGiftMap(value any) map[string]any {
if typed, ok := value.(map[string]any); ok {
return typed
}
return map[string]any{}
}
// readLuckyGiftSlice 安全读取数组子对象。
func readLuckyGiftSlice(value any) []any {
if typed, ok := value.([]any); ok {
return typed
}
return nil
}
// readLuckyGiftString 尽量宽松地读取字符串字段。
func readLuckyGiftString(value any) string {
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
case json.Number:
return typed.String()
default:
return strings.TrimSpace(fmt.Sprint(typed))
}
}
// readLuckyGiftInt64 尽量宽松地读取数值字段。
func readLuckyGiftInt64(value any) int64 {
switch typed := value.(type) {
case int:
return int64(typed)
case int64:
return typed
case float64:
return int64(typed)
case json.Number:
parsed, _ := typed.Int64()
return parsed
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
return parsed
default:
parsed, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 64)
return parsed
}
}
// readLuckyGiftBool 尽量宽松地读取布尔字段。
func readLuckyGiftBool(values ...any) bool {
for _, value := range values {
switch typed := value.(type) {
case bool:
return typed
case int:
return typed != 0
case int64:
return typed != 0
case float64:
return typed != 0
case json.Number:
parsed, _ := typed.Int64()
return parsed != 0
case string:
text := strings.TrimSpace(strings.ToLower(typed))
return text == "1" || text == "true" || text == "yes"
}
}
return false
}
// readLuckyGiftMessage 从不同字段名里兜底读取错误消息。
func readLuckyGiftMessage(payload map[string]any) string {
for _, key := range []string{"msg", "message", "errorMsg"} {
if value := strings.TrimSpace(readLuckyGiftString(payload[key])); value != "" {
return value
}
}
return ""
}
// firstLuckyGiftValue 返回第一个非 nil 值,便于兼容三方多种字段名。
func firstLuckyGiftValue(values ...any) any {
for _, value := range values {
if value != nil {
return value
}
}
return nil
}