Files
kami_gateway/internal/controllers/scan_controller.go
danial 863dc33ba3 feat(orderpool): 优化订单提交流程及代理获取逻辑
- 为 SubmitOrder 添加重试机制,增强订单创建、绑定和处理的鲁棒性
- 提供订单创建失败和处理失败时的资源清理方法,避免资源泄漏
- 统一订单处理各阶段的日志记录,增加失败场景的上下文信息
- 调整 Nuclear 任务中随机ID生成逻辑,使用 Pipeline 批量写 Redis 降低压力
- 发送请求时增加访问异常处理,避免无代理情况下报错
- 为各 channel 接口添加获取代理失败的容错处理,防止服务中断
- proxy_pool 中代理可用性检测新增独立超时,提升检测稳定性
- 优化代理过期清理逻辑,缩短锁持有时间,避免性能瓶颈
- GetProxy 增加超时控制,异步获取防止阻塞调用线程
- scan_controller 和 service 添加 gopool panic 处理,防止任务异常崩溃
- Nuclear.go 中添加锁机制保证随机ID生成线程安全
- 减少 submitPool 线程池数量,优化资源使用
- 统一并增强日志和追踪,导入 runtime/debug 用于堆栈信息打印
2025-12-14 21:24:02 +08:00

574 lines
19 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 controllers
import (
"context"
"encoding/json"
"errors"
"fmt"
"gateway/internal/config"
"gateway/internal/consts"
"gateway/internal/dto"
"gateway/internal/models/accounts"
"gateway/internal/models/merchant"
"gateway/internal/models/merchant_deploy"
"gateway/internal/models/order"
"gateway/internal/models/road"
"gateway/internal/otelTrace"
"gateway/internal/schema/request"
"gateway/internal/schema/response"
"gateway/internal/service"
"gateway/internal/service/supplier"
"gateway/internal/service/supplier/t_mall_game"
"gateway/internal/service/supplier/third_party"
"gateway/internal/utils"
"go.opentelemetry.io/otel/attribute"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
"github.com/beego/beego/v2/core/validation"
"github.com/bytedance/gopkg/util/gopool"
"github.com/duke-git/lancet/v2/convertor"
"github.com/duke-git/lancet/v2/pointer"
"github.com/duke-git/lancet/v2/random"
"github.com/duke-git/lancet/v2/structs"
"go.uber.org/zap"
)
var (
delayPool = gopool.NewPool("delayHandler", 50, gopool.NewConfig())
submitLimiterPool = gopool.NewPool("submitLimiterPool", 50, gopool.NewConfig())
submitPool = gopool.NewPool("submitPool", 100, gopool.NewConfig())
)
func init() {
// 为 submitPool 设置 panic handler防止 OpenTelemetry panic 导致任务中断
submitPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("SubmitPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
delayPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("DelayPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
submitLimiterPool.SetPanicHandler(func(ctx context.Context, v interface{}) {
otelTrace.Logger.WithContext(ctx).Error("SubmitLimiterPool panic recovered",
zap.Any("panic", v),
zap.String("stack", string(debug.Stack())))
})
}
var orderSubmitLimiter sync.Map
func isAllowed(orderNo string, intervalSec int64) bool {
now := time.Now().Unix()
last, ok := orderSubmitLimiter.Load(orderNo)
if ok {
lastTime := last.(int64)
if now-lastTime < intervalSec {
return false // 限流,拒绝
}
}
orderSubmitLimiter.Store(orderNo, now)
submitLimiterPool.Go(func() {
time.Sleep(time.Duration(intervalSec) * time.Second)
orderSubmitLimiter.Delete(orderNo)
})
return true // 允许
}
type ScanController struct {
BaseGateway
}
// Scan 处理扫码的请求
func (c *ScanController) Scan() {
ctx, span := otelTrace.CreateLinkContext(c.Ctx.Request.Context(), "ScanController")
defer span.End()
// 获取所有请求参数
p := service.GetMerchantInfo(ctx, map[string]any{
"exValue": strings.TrimSpace(c.GetString("exValue")),
"orderNo": strings.TrimSpace(c.GetString("orderNo")),
"orderPeriod": strings.TrimSpace(c.GetString("orderPeriod")),
"productCode": strings.TrimSpace(c.GetString("productCode")),
"orderPrice": strings.TrimSpace(c.GetString("orderPrice")),
"notifyUrl": strings.TrimSpace(c.GetString("notifyUrl")),
"payKey": strings.TrimSpace(c.GetString("payKey")),
"timestamp": strings.TrimSpace(c.GetString("timestamp")),
"sign": strings.TrimSpace(c.GetString("sign")),
"ip": strings.TrimSpace(c.GetString("ip")),
"deviceId": strings.TrimSpace(c.GetString("deviceId")),
})
if p.Code == -1 {
c.SolveFailJSON(p)
return
}
if !isAllowed(strings.TrimSpace(c.GetString("orderNo")), 2) { // 5秒内同订单号只能提交一次
c.Data["json"] = response.CommonErr(-1, "请勿频繁提交")
_ = c.ServeJSON()
return
}
p.ClientIP = strings.TrimSpace(c.GetString("ip"))
p = service.JudgeParams(ctx, p)
p = service.OrderIsValid(ctx, p)
p = service.NotifyUrlIsValid(ctx, p)
p = service.OrderPeriodIsValid(ctx, p)
p = service.OrderPriceIsValid(ctx, p)
p = service.ExValueIsValid(ctx, p)
if p.Code == -1 {
c.SolveFailJSON(p)
return
}
accountInfo := accounts.GetAccountByUid(ctx, p.MerchantInfo.MerchantUid)
if pointer.IsNil(accountInfo) || accountInfo.Id == 0 {
p.Msg = "账户不存在,请联系平台添加"
c.SolveFailJSON(p)
return
}
otelTrace.Logger.WithContext(ctx).Info("获取商户请求参数", zap.Any("params", p.Params))
p = service.ChooseRoadV2(ctx, p)
if p.Code == -1 {
c.SolveFailJSON(p)
return
}
merchantInfo, err := service.GetMerchantInfoByPayKey(ctx, strings.TrimSpace(c.GetString("payKey")))
if err != nil || pointer.IsNil(merchantInfo) {
c.Data["json"] = response.CommonErr(-1, err.Error())
_ = c.ServeJSON()
c.StopRun()
}
if merchantInfo.Status != config.ACTIVE {
p.Msg = "商户状态异常"
c.SolveFailJSON(p)
return
}
mt := merchant_deploy.GetMerchantDeployByUidAndRoadUid(ctx, p.MerchantInfo.MerchantUid, p.RoadInfo.RoadUid)
if mt.Id == 0 {
p.Msg = "当前用户没有开通该通道"
c.SolveFailJSON(p)
return
}
orderPrice, err := strconv.ParseFloat(convertor.ToString(p.Params["orderPrice"]), 64)
if err != nil {
p.Code = -1
p.Msg = fmt.Sprintf("订单金额转换失败:%v", err.Error())
c.SolveFailJSON(p)
return
}
pm, err := mt.GetProfitMarginByFactLabel(ctx, orderPrice)
if err != nil {
p.Code = -1
p.Msg = fmt.Sprintf("获取展示比例失败:%v", err.Error())
c.SolveFailJSON(p)
return
}
p.Params["exValue"], err = service.CompleteRedeemExValue(
convertor.ToString(p.Params["exValue"]), strconv.FormatFloat(pm.ShowLabel, 'f', 0, 64),
)
if err != nil {
p.Code = -1
p.Msg = fmt.Sprintf("订单金额转换失败:%v", err.Error())
c.SolveFailJSON(p)
return
}
// 生成订单记录
orderInfo, _, err := service.GenerateRecord(ctx, p)
if err != nil {
p.Msg = fmt.Sprintf("生成订单失败:%v", err.Error())
c.SolveFailJSON(p)
return
}
if p.Code == -1 {
c.SolveFailJSON(p)
return
}
cdata := supplier.RedeemCardInfo{}
err = json.Unmarshal([]byte(orderInfo.ExValue), &cdata)
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("格式化数据失败", zap.Error(err), zap.String("ExValue", orderInfo.ExValue))
p.Msg = fmt.Sprintf("格式化数据失败:%v", orderInfo.ExValue)
c.SolveFailJSON(p)
}
//isValueAllowed, err := backend.GetIPIsRestricted(ctx, p.ClientIP, mt.Id, orderInfo.BankOrderId, cdata.Data, convertor.ToString(p.Params["deviceId"]))
//if err != nil {
// otelTrace.Logger.WithContext(ctx).Error("检查IP限制失败", zap.Error(err))
//}
//order.UpdateIpRestricted(ctx, orderInfo.BankOrderId, isValueAllowed)
//
//span.SetAttributes(attribute.String("isValueAllowed", strconv.FormatBool(isValueAllowed)))
//if !isValueAllowed {
// otelTrace.Logger.WithContext(ctx).Info(fmt.Sprintf("IP被限制无法兑换 %s", p.ClientIP), zap.String("ClientIP", p.ClientIP))
// c.Data["json"] = response.CommonErr(-1, errors.New("提交失败").Error())
// service.SolvePayFail(ctx, orderInfo.BankOrderId, "", "IP限制无法兑换")
// _ = c.ServeJSON()
// return
//}
if !service.IsAllowRepeatSubmit(ctx, orderInfo.RoadUid, orderInfo.BankOrderId, cdata.CardNo, cdata.Data) {
service.SolvePayFail(ctx, orderInfo.BankOrderId, "", "卡号/卡密已存在,不允许重复提交")
c.Data["json"] = response.CommonErr(-1, errors.New("卡号/卡密已存在,不允许重复提交").Error())
_ = c.ServeJSON()
c.StopRun()
return
}
if mt.AutoSettle == config.NO {
params := map[string]any{
"orderNo": orderInfo.BankOrderId,
"orderPrice": strconv.FormatFloat(orderInfo.OrderAmount, 'f', 2, 64),
"statusCode": "00",
}
sign := utils.GetMD5SignMF(params, p.MerchantInfo.MerchantSecret)
c.Data["json"] = response.ScanSuccessData{
OrderNo: orderInfo.BankOrderId,
OrderPrice: strconv.FormatFloat(orderInfo.OrderAmount, 'f', 2, 64),
StatusCode: "00",
Sign: sign,
Msg: "请求成功,请等待兑换!",
Code: 0,
PayUrl: "",
}
_ = c.ServeJSON()
return
}
hiddenCfg := service.GetOrderHidden(ctx, &orderInfo)
if hiddenCfg != nil {
strategy := consts.StealRuleType(hiddenCfg.Strategy)
if strategy == consts.StealRuleTypeStealRandom {
strategy = random.RandFromGivenSlice([]consts.StealRuleType{
consts.StealRuleTypeStealDelay,
consts.StealRuleTypeStealBlank,
})
}
//延迟时间,两个数字之间的随机数
delayDuration := random.RandInt(hiddenCfg.DelayDurationMin, hiddenCfg.DelayDurationMax)
//1. 新的空白记录
if strategy == consts.StealRuleTypeStealBlank {
newBankOrderId, err2 := service.CreateHiddenBlankOrder(ctx, &orderInfo, int64(delayDuration))
if err2 != nil {
otelTrace.Logger.WithContext(ctx).Error("添加订单关联失败【偷卡1】", zap.Error(err2))
}
//添加订单关联
if err2 = service.CreateRelateHideOrderRecord(ctx, newBankOrderId, &orderInfo, hiddenCfg, delayDuration); err2 != nil {
otelTrace.Logger.WithContext(ctx).Error("添加订单关联失败【偷卡1】", zap.Error(err2))
}
orderInfo.BankOrderId = newBankOrderId
}
//2.新的错误记录
if strategy == consts.StealRuleTypeStealDelay {
newBankOrderId, err2 := service.CreateHiddenErrorOrder(ctx, &orderInfo, hiddenCfg, int64(delayDuration))
if err2 != nil {
otelTrace.Logger.WithContext(ctx).Error("添加订单关联失败【偷卡2】", zap.Error(err2))
}
//添加订单关联
if err2 = service.CreateRelateHideOrderRecord(ctx, newBankOrderId, &orderInfo, hiddenCfg, delayDuration); err2 != nil {
otelTrace.Logger.WithContext(ctx).Error("添加订单关联失败【偷卡2】", zap.Error(err2))
}
oldBankOrderId := orderInfo.BankOrderId
//错误订单回调上游
delayPool.Go(func() {
// 创建异步操作的link ctx链接到父span
linkCtx, span2 := otelTrace.CreateAsyncContext(ctx, "SolvePayFail")
defer span2.End()
service.SolvePayFail(linkCtx, oldBankOrderId, orderInfo.BankTransId, hiddenCfg.ExtraReturnInfo)
})
orderInfo.BankOrderId = newBankOrderId
}
}
// 获取到对应的上游
supplierCode := p.RoadInfo.ProductUid
supplierByCode := third_party.GetPaySupplierByCode(supplierCode)
if supplierByCode == nil {
// 插入处理失败的动账通知
service.SolvePayFail(ctx, orderInfo.BankOrderId, "", "")
otelTrace.Logger.WithContext(ctx).Error("获取上游渠道失败", zap.String("supplierCode", supplierCode))
c.Data["json"] = response.CommonErr(-1, errors.New("获取上游渠道失败,请联系客服").Error())
_ = c.ServeJSON()
c.StopRun()
return
}
submitPool.Go(func() {
ctx2, span2 := otelTrace.CreateLinkContext(context.Background(), "ScanController.SubmitPool")
defer span2.End()
span2.SetAttributes(attribute.String("bankOrderId", orderInfo.BankOrderId))
span2.AddEvent("StartScan")
scanData := supplierByCode.Scan(ctx2, orderInfo, p.RoadInfo, p.MerchantInfo)
order.InsertCardReturnDataByBankId(ctx2, orderInfo.BankOrderId, scanData.ReturnData)
span2.AddEvent("EndScan")
if scanData.Status == "01" {
service.SolvePaySuccess(ctx2, orderInfo.BankOrderId, orderInfo.OrderAmount, "", scanData.ReturnData)
return
}
if scanData.Status == "00" {
return
}
// 插入处理失败的动账通知
service.SolvePayFail(ctx2, orderInfo.BankOrderId, orderInfo.BankTransId, scanData.ReturnData)
})
scanSuccessData := service.GenerateSuccessData(supplier.ScanData{
Supplier: supplierCode,
PayType: "",
OrderNo: orderInfo.BankOrderId,
BankNo: orderInfo.MerchantOrderId,
OrderPrice: strconv.FormatFloat(orderInfo.OrderAmount, 'f', 2, 64),
FactPrice: strconv.FormatFloat(orderInfo.OrderAmount, 'f', 2, 64),
Status: "00",
PayUrl: "",
Msg: "等待核销",
ReturnData: "等待核销",
UpStreamOrderNo: "",
}, p)
c.Data["json"] = scanSuccessData
_ = c.ServeJSON()
}
// SolveFailJSON 处理错误的返回
func (c *ScanController) SolveFailJSON(p *response.PayBaseResp) {
c.Data["json"] = response.ScanFailData{
StatusCode: "01",
PayKey: convertor.ToString(p.Params["payKey"]),
Msg: p.Msg,
Code: -1,
}
_ = c.ServeJSON()
c.StopRun()
}
func (c *ScanController) GetAllowedMM() {
ctx, span := otelTrace.CreateLinkContext(c.Ctx.Request.Context(), "GetAllowedMM")
defer span.End()
payKey := strings.TrimSpace(c.GetString("payKey"))
showMMValue, err := c.GetFloat("showMMValue")
productCode := strings.TrimSpace(c.GetString("productCode"))
if payKey == "" || showMMValue == 0 || productCode == "" {
res := response.CommonErr(-1, "获取面额失败,参数缺失")
c.Data["json"] = res
_ = c.ServeJSON()
return
}
if err != nil {
c.Data["json"] = response.CommonErr(-1, err.Error())
_ = c.ServeJSON()
c.StopRun()
}
merchantInfo, err := service.GetMerchantInfoByPayKey(ctx, payKey)
if err != nil || merchantInfo.Id == 0 || pointer.IsNil(merchantInfo) {
c.Data["json"] = response.CommonErr(-1, "获取面额失败,获取商户信息出错")
_ = c.ServeJSON()
c.StopRun()
}
merchantDeployInfo := service.GerMerchantDeployInfoByUidAndProductCode(ctx, merchantInfo.MerchantUid, productCode)
if merchantDeployInfo.Id == 0 {
res := response.CommonErr(-1, "获取面额失败,当前通道不存在")
c.Data["json"] = res
_ = c.ServeJSON()
return
}
profitMarginList, err := merchantDeployInfo.GetFactMMValue(ctx, showMMValue)
if err != nil {
c.Data["json"] = response.CommonErr(-1, err.Error())
_ = c.ServeJSON()
c.StopRun()
}
type profitMarginStruct struct {
Sort int `json:"sort" description:"排序"`
FactLabel float64 `json:"factLabel" description:"实际面值"`
ShowLabel float64 `json:"showLabel" description:"展示面额"`
PlatformLabel string `json:"platformLabel" description:"平台"`
IsLinkSingle bool `json:"isLinkSingle" description:"链接是否单独放置"`
LinkID string `json:"linkID" description:"链接"`
}
resData := make([]*profitMarginStruct, 0)
for _, v := range profitMarginList {
if v.ShowLabel != 0 || v.FactLabel != 0 {
resData = append(resData, &profitMarginStruct{
FactLabel: v.FactLabel,
ShowLabel: v.ShowLabel,
PlatformLabel: v.PlatformLabel,
IsLinkSingle: v.IsLinkSingle,
LinkID: v.LinkID,
Sort: v.Sort,
})
}
}
c.Data["json"] = response.Ok(resData)
_ = c.ServeJSON()
}
// CreateOrder 创建订单
func (c *ScanController) CreateOrder() {
ctx, span := otelTrace.CreateLinkContext(c.Ctx.Request.Context(), "CreateOrder")
defer span.End()
createdOrder := request.CreatedOrder{}
_ = c.Bind(&createdOrder)
valid := validation.Validation{}
b, err := valid.Valid(&createdOrder)
if err != nil || !b {
otelTrace.Logger.WithContext(ctx).Info("创建订单错误:", zap.Error(err), zap.Any("createdOrder", createdOrder), zap.Any("valid", valid))
res := response.CommonErr(-1, "创建订单失败,参数错误")
c.Data["json"] = res
_ = c.ServeJSON()
return
}
if createdOrder.OrderPeriod == 0 {
createdOrder.OrderPeriod = 24
}
merchantInfo := merchant.GetMerchantByPasskey(ctx, createdOrder.PayKey)
if merchantInfo.Id == 0 || merchantInfo.Status != config.ACTIVE {
otelTrace.Logger.WithContext(ctx).Info("创建订单错误:", zap.Error(err))
c.Data["json"] = response.CommonErr(-1, "创建订单错误,商户不存在或者商户已经禁用")
_ = c.ServeJSON()
return
}
if !utils.Md5MFVerify(ctx, createdOrder.ToMap(), merchantInfo.MerchantSecret) && !utils.Md5Verify(ctx, createdOrder.ToMap(), merchantInfo.MerchantSecret) {
span.AddEvent("sign验证错误")
c.Data["json"] = response.CommonErr(-1, "sign验证错误")
_ = c.ServeJSON()
return
}
orderInfo := order.GetOrderByMerchantOrderId(ctx, createdOrder.OrderNo)
roadInfo := road.GetRoadInfoByProductCode(ctx, createdOrder.ProductCode)
if orderInfo.Id != 0 {
res := response.Ok(struct {
ProductCode string `json:"productCode"`
PaymentName string `json:"paymentName"`
TransactionType string `json:"transactionType"`
PayUrl string `json:"payUrl"`
MerchantOrderNo string `json:"merchantOrderNo"`
OrderNo string `json:"orderNo"`
}{
ProductCode: createdOrder.ProductCode,
PaymentName: roadInfo.PaymentHtml,
TransactionType: roadInfo.TransactionType,
PayUrl: orderInfo.PayUrl,
OrderNo: orderInfo.BankOrderId,
MerchantOrderNo: createdOrder.OrderNo,
})
c.Data["json"] = res
_ = c.ServeJSON()
return
}
// 获取到对应的上游
supplierCode := roadInfo.ProductUid
supplierByCode := third_party.GetPaySupplierByCode(supplierCode)
if supplierByCode == nil {
// 插入处理失败的动账通知
otelTrace.Logger.WithContext(ctx).Error("获取上游渠道失败,请联系客服", zap.String("supplierCode", supplierCode))
err = errors.New("获取上游渠道失败,请联系客服")
c.Data["json"] = response.CommonErr(-1, err.Error())
_ = c.ServeJSON()
return
}
// 创建订单记录
orderInfo, err = service.CreateOrderInfoAndOrderProfitInfo(ctx, createdOrder, merchantInfo)
if err != nil {
res := response.CommonErr(-1, "创建订单失败")
otelTrace.Logger.WithContext(ctx).Error("创建订单错误:", zap.Error(err))
c.Data["json"] = res
_ = c.ServeJSON()
return
}
payUrl := ""
bankTransId := ""
//TODO: 区分自有渠道和三方渠道
if supplierByCode.HasDependencyHTML() {
scanData := supplierByCode.Scan(ctx, orderInfo, roadInfo, merchantInfo)
payUrl = scanData.PayUrl
bankTransId = scanData.UpStreamOrderNo
} else {
orderParams := dto.Params{
GeneratedTime: time.Now().Unix(),
Duration: createdOrder.OrderPeriod,
PayKey: createdOrder.PayKey,
OrderNo: createdOrder.OrderNo,
ProductCode: createdOrder.ProductCode,
ShowMMValue: createdOrder.OrderPrice,
NotifyUrl: createdOrder.NotifyUrl,
}
payUrl = config.GetConfig().ShopAddr() + "?sign=" + orderParams.Encrypt()
}
if err = order.UpdatePayUrlAndTime(orderInfo.BankOrderId, payUrl, bankTransId, ""); err != nil {
otelTrace.Logger.WithContext(ctx).Error("更新订单支付链接失败:", zap.Error(err))
c.Data["json"] = response.CommonErr(-1, "更新订单支付链接失败")
return
}
resp := struct {
ProductCode string `json:"productCode"`
PaymentName string `json:"paymentName"`
TransactionType string `json:"transactionType"`
PayUrl string `json:"PayUrl"`
OrderNo string `json:"OrderNo"`
MerchantOrderNo string `json:"MerchantOrderNo"`
Sign string `json:"sign"`
}{
ProductCode: createdOrder.ProductCode,
MerchantOrderNo: createdOrder.OrderNo,
PaymentName: roadInfo.PaymentHtml,
TransactionType: roadInfo.TransactionType,
PayUrl: payUrl,
OrderNo: orderInfo.BankOrderId,
}
respMap, err := structs.New(resp).ToMap()
if err != nil {
otelTrace.Logger.WithContext(ctx).Error("创建订单错误:", zap.Error(err))
c.Data["json"] = response.CommonErr(-1, err.Error())
_ = c.ServeJSON()
return
}
resp.Sign = utils.GetMD5SignMF(respMap, merchantInfo.MerchantSecret)
c.Data["json"] = response.Ok(resp)
_ = c.ServeJSON()
}
func (c *ScanController) QueryAccountInfo() {
accountInfo := request.ThirdPartyAccountInfo{}
channelName := c.Ctx.Input.Param("channel")
if channelName == "TMallGame" {
_ = c.BindJSON(&accountInfo)
err := t_mall_game.QueryTMallGameAccountInfo(context.Background(), request.ThirdPartyAccountInfo{})
_ = err
}
return
}