336 lines
9.6 KiB
Go
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
|
|
}
|