155 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package luckygift
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"hyapp-luck-gateway/internal/integration/luckygiftclient"
"hyapp-luck-gateway/internal/requestid"
"hyapp-luck-gateway/internal/response"
)
type Handler struct {
client luckygiftclient.Client
allowedApps map[string]struct{}
requestTimeout time.Duration
now func() time.Time
}
func New(client luckygiftclient.Client, requestTimeout time.Duration, allowedApps map[string]struct{}) *Handler {
if requestTimeout <= 0 {
requestTimeout = 3 * time.Second
}
return &Handler{client: client, allowedApps: cloneAllowedApps(allowedApps), requestTimeout: requestTimeout, now: time.Now}
}
type sendRequest struct {
AppCode string `json:"app_code"`
RequestID string `json:"request_id"`
ExternalUserID string `json:"external_user_id"`
GiftCount int64 `json:"gift_count"`
UnitAmount int64 `json:"unit_amount"`
Currency string `json:"currency"`
Metadata map[string]any `json:"metadata"`
}
func (h *Handler) Send(w http.ResponseWriter, r *http.Request) {
requestID := requestid.FromContext(r.Context())
var req sendRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
response.Fail(w, http.StatusBadRequest, response.CodeBadRequest, requestID, "invalid lucky gift send body")
return
}
if err := req.validate(); err != nil {
response.Fail(w, http.StatusBadRequest, response.CodeBadRequest, requestID, err.Error())
return
}
appCode := normalizeAppCode(req.AppCode)
if !h.isAllowedApp(appCode) {
response.Fail(w, http.StatusForbidden, response.CodeForbidden, requestID, "app_code is not allowed")
return
}
ctx, cancel := context.WithTimeout(r.Context(), h.requestTimeout)
defer cancel()
result, err := h.client.SendLuckyGift(ctx, luckygiftclient.SendCommand{
TraceRequestID: requestID,
AppCode: appCode,
RequestID: strings.TrimSpace(req.RequestID),
ExternalUserID: strings.TrimSpace(req.ExternalUserID),
GiftCount: req.GiftCount,
UnitAmount: req.UnitAmount,
TotalAmount: req.GiftCount * req.UnitAmount,
Currency: strings.TrimSpace(req.Currency),
MetadataJSON: metadataJSON(req.Metadata),
// 外部 HTTP 契约不暴露 pool_id/paid_at_mspool 由 owner service 默认规则选择,支付完成时间使用网关服务端 UTC 时间,
// 避免外部 App 通过请求体影响奖池分桶、预算日或风控窗口。
PaidAtMS: normalizePaidAt(h.now),
})
if err != nil {
response.Fail(w, http.StatusInternalServerError, response.CodeServerError, requestID, "send lucky gift failed")
return
}
response.OK(w, requestID, result)
}
func (r sendRequest) validate() error {
if normalizeAppCode(r.AppCode) == "" {
return errors.New("app_code is required")
}
if len(normalizeAppCode(r.AppCode)) > 32 {
return errors.New("app_code is too long")
}
if strings.TrimSpace(r.RequestID) == "" {
return errors.New("request_id is required")
}
if len(strings.TrimSpace(r.RequestID)) > 128 {
return errors.New("request_id is too long")
}
if strings.TrimSpace(r.ExternalUserID) == "" {
return errors.New("external_user_id is required")
}
if len(strings.TrimSpace(r.ExternalUserID)) > 128 {
return errors.New("external_user_id is too long")
}
if r.GiftCount <= 0 || r.UnitAmount <= 0 {
return errors.New("gift_count and unit_amount must be positive")
}
// 网关只接受数量和单价total_amount 由服务端计算,避免外部 App SDK 或恶意调用方传入不一致账面金额。
if r.GiftCount > (1<<63-1)/r.UnitAmount {
return errors.New("gift amount is too large")
}
if currency := strings.ToUpper(strings.TrimSpace(r.Currency)); currency != "" && currency != "COIN" {
// 当前 RTP、奖池和风控都以金币等价单位推进未配置汇率前拒绝其他币种避免污染奖池水位。
return errors.New("currency must be COIN")
}
if len(strings.TrimSpace(r.Currency)) > 16 {
return errors.New("currency is too long")
}
return nil
}
func metadataJSON(value map[string]any) string {
if len(value) == 0 {
return "{}"
}
body, err := json.Marshal(value)
if err != nil {
// JSON decoder 已保证 map 可序列化;失败时返回空对象,让 owner service 不因展示元数据阻断抽奖。
return "{}"
}
return string(body)
}
func normalizePaidAt(now func() time.Time) int64 {
return now().UTC().UnixMilli()
}
func (h *Handler) isAllowedApp(appCode string) bool {
if len(h.allowedApps) == 0 {
return false
}
_, ok := h.allowedApps[normalizeAppCode(appCode)]
return ok
}
func cloneAllowedApps(input map[string]struct{}) map[string]struct{} {
out := make(map[string]struct{}, len(input))
for app := range input {
if normalized := normalizeAppCode(app); normalized != "" {
out[normalized] = struct{}{}
}
}
return out
}
func normalizeAppCode(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}