Files
kami_gateway/internal/controllers/scan_controller.go
danial a1db6b764c fix(card_sender): 优化飞鱼查询响应日志输出
- 精简飞鱼查询返回日志内容,移除冗余字段
- 保留原始响应字符串以便排查问题
- 保持对频繁操作消息的判定逻辑不变
- 避免过多日志数据导致日志膨胀

perf(deploy): 优化代理请求数量及协程池容量

- 将Dockerfile中proxyUrl请求数从2改为1,减少请求量
- 扫描控制器中延迟处理、提交限制和提交池容量分别从20、20、300增至50、50、500
- 绑定订单逻辑中发送卡片任务循环次数由10调整为3,优化性能
- 移除多余的错误日志打印,减少冗余日志输出

fix(pool): 修复飞鱼查询日志字段输出问题

- 将日志字段 "respRawData" 类型调整为字符串类型,避免类型不匹配
- 修改日志中布尔字段名为 "operation",更准确表达含义
- 保持日志内容详尽,方便后续问题排查
- 更新 Go 版本至 1.25.5 以保持依赖更新
2025-12-10 22:14:52 +08:00

552 lines
18 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"
"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", 500, gopool.NewConfig())
)
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(ctx, "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
}