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 }