package app import ( "context" "fmt" "math" "strings" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" ) type appBalanceRechargePaymentResult struct { Order *entity.TOrder BalanceAfter float64 PayTradeNo string PayProvider string } func (s *sAppCard) completeAppBalanceRechargePayment(ctx context.Context, orderSn string, loginUser *entity.SysUser, payTradeNo string, payProvider string, transactionChannel int, expectedAmount float64, paidAt *gtime.Time) (*appBalanceRechargePaymentResult, error) { orderSn = strings.TrimSpace(orderSn) if orderSn == "" { return nil, gerror.New("订单号不能为空") } payTradeNo = strings.TrimSpace(payTradeNo) payProvider = strings.TrimSpace(payProvider) if payProvider == "" { payProvider = "online" } if paidAt == nil { paidAt = gtime.Now() } var result *appBalanceRechargePaymentResult err := dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { query := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().OrderSn, orderSn). LockUpdate(). Limit(1) if loginUser != nil { query = query.Where(dao.TOrder.Columns().TenantId, loginUser.TenantId) } var lockedOrder *entity.TOrder if err := query.Scan(&lockedOrder); err != nil { return gerror.Wrap(err, "锁定充值订单失败") } if lockedOrder == nil || lockedOrder.Id == 0 { return gerror.New("充值订单不存在") } if lockedOrder.OrderType != consts.OrderTypeBalanceRecharge { return gerror.New("订单不是余额充值订单") } user := loginUser if user == nil { var err error user, err = s.loadAppBalanceRechargeOrderUser(ctx, tx, lockedOrder) if err != nil { return err } } else if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, lockedOrder); err != nil { return err } amount := appRoundAmount(lockedOrder.TotalAmount) if amount <= 0 { return gerror.New("充值订单金额不正确") } if expectedAmount > 0 && math.Abs(appRoundAmount(expectedAmount)-amount) >= 0.01 { return gerror.Newf("支付金额与订单金额不一致,订单 %.2f 元,支付 %.2f 元", amount, expectedAmount) } account, err := s.lockAppBalanceAccount(ctx, tx, user) if err != nil { return err } if lockedOrder.PayStatus == consts.PayStatusPaid { if payTradeNo == "" { payTradeNo = strings.TrimSpace(lockedOrder.PayOrderSn) } result = &appBalanceRechargePaymentResult{ Order: lockedOrder, BalanceAfter: appRoundAmount(account.Balance), PayTradeNo: payTradeNo, PayProvider: payProvider, } return nil } if lockedOrder.PayStatus != consts.PayStatusUnpaid || lockedOrder.OrderStatus != consts.OrderStatusPendingPay { return gerror.New("充值订单状态不允许支付") } creditedAccount, err := s.creditAppBalanceWithChannel(ctx, tx, user, lockedOrder.OrderSn, amount, fmt.Sprintf("%s充值", payProvider), transactionChannel, payTradeNo, paidAt) if err != nil { return err } if _, err = dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, lockedOrder.Id). Data(do.TOrder{ OrderStatus: consts.OrderStatusCompleted, PayType: consts.PayTypeOnline, PayStatus: consts.PayStatusPaid, PayOrderSn: payTradeNo, PayTime: paidAt, PaidAmount: amount, OnlinePaidAmount: amount, UnpaidAmount: 0, Remark: fmt.Sprintf("小程序余额充值%s支付成功", payProvider), UpdatedBy: user.UserId, UpdatedAt: paidAt, }). Update(); err != nil { return gerror.Wrap(err, "更新充值订单支付状态失败") } if err = s.updateAppBalanceRechargeOrderItemPayment(ctx, tx, user, lockedOrder, creditedAccount, payProvider, payTradeNo, paidAt); err != nil { return err } lockedOrder.OrderStatus = consts.OrderStatusCompleted lockedOrder.PayType = consts.PayTypeOnline lockedOrder.PayStatus = consts.PayStatusPaid lockedOrder.PayOrderSn = payTradeNo lockedOrder.PayTime = paidAt lockedOrder.PaidAmount = amount lockedOrder.OnlinePaidAmount = amount lockedOrder.UnpaidAmount = 0 result = &appBalanceRechargePaymentResult{ Order: lockedOrder, BalanceAfter: appRoundAmount(creditedAccount.Balance), PayTradeNo: payTradeNo, PayProvider: payProvider, } return nil }) if err != nil { return nil, err } if result == nil || result.Order == nil { return nil, gerror.New("支付结果不完整") } _ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{ EventType: appAsyncEventBalanceRechargePaid, OrderSn: result.Order.OrderSn, PayProvider: result.PayProvider, PayTradeNo: result.PayTradeNo, TenantId: result.Order.TenantId, UserId: result.Order.CreatedBy, DeptId: result.Order.CreatedDept, Trigger: "payment_complete", }) return result, nil } func (s *sAppCard) BalanceRechargeMockPay(ctx context.Context, param *model.AppBalanceRechargeMockPayParam) (*model.AppBalanceRechargeStatusModel, error) { if param == nil { param = &model.AppBalanceRechargeMockPayParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } if !appMockPaymentEnabled(ctx) { return nil, gerror.New("模拟支付未启用") } orderSn := strings.TrimSpace(param.OrderSn) if orderSn == "" { return nil, gerror.New("订单号不能为空") } now := gtime.Now() payTradeNo := appCardBatchNo("MOCK", user.UserId, now) if _, err = s.completeAppBalanceRechargePayment(ctx, orderSn, user, payTradeNo, "mock", consts.BalanceTransactionChannelMock, 0, now); err != nil { return nil, err } return s.BalanceRechargeStatus(ctx, &model.AppBalanceRechargeStatusParam{OrderSn: orderSn}) } func (s *sAppCard) loadAppBalanceRechargeOrderUser(ctx context.Context, tx gdb.TX, order *entity.TOrder) (*entity.SysUser, error) { if order == nil || order.CreatedBy <= 0 { return nil, gerror.New("充值订单创建人信息不完整") } var user *entity.SysUser if err := dao.SysUser.Ctx(ctx).TX(tx). Where(dao.SysUser.Columns().TenantId, order.TenantId). Where(dao.SysUser.Columns().UserId, order.CreatedBy). Limit(1). Scan(&user); err != nil { return nil, gerror.Wrap(err, "查询充值订单用户失败") } if user == nil || user.UserId == 0 { return nil, gerror.New("充值订单用户不存在") } if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil { return nil, err } return user, nil } func (s *sAppCard) validateAppBalanceRechargeOrderOwner(ctx context.Context, user *entity.SysUser, order *entity.TOrder) error { if user == nil || order == nil { return gerror.New("充值订单信息不完整") } if order.CreatedBy != user.UserId { if order.CustomerType != consts.CustomerTypeTerminal || order.EndUserId != user.UserId { return gerror.New("无权操作该充值订单") } } if s.isTerminalUser(ctx, user) { if order.CustomerType != consts.CustomerTypeTerminal || order.EndUserId != user.UserId { return gerror.New("无权操作该充值订单") } return nil } if user.DeptId <= 0 { return gerror.New("当前用户未绑定机构,无法操作充值订单") } if order.CustomerType != consts.CustomerTypeAgent || order.CustomerAgentId != user.DeptId { return gerror.New("无权操作该充值订单") } return nil } func (s *sAppCard) updateAppBalanceRechargeOrderItemPayment(ctx context.Context, tx gdb.TX, user *entity.SysUser, order *entity.TOrder, account *appBalanceAccount, payProvider string, payTradeNo string, paidAt *gtime.Time) error { if user == nil || order == nil || account == nil { return gerror.New("充值订单项支付快照信息不完整") } extraSkuData := appRenewMarshalJSON(map[string]any{ "business": "miniapp_balance_recharge", "accountType": account.AccountLabel, "accountId": account.AccountId, "amount": appRoundAmount(order.TotalAmount), "payChannel": payProvider, "payTradeNo": payTradeNo, "paidAt": appRenewFormatTime(paidAt), }) if _, err := dao.TOrderItem.Ctx(ctx).TX(tx). Where(dao.TOrderItem.Columns().OrderSn, order.OrderSn). Data(do.TOrderItem{ ExtraSkuData: extraSkuData, UpdatedBy: user.UserId, UpdatedAt: paidAt, }). Update(); err != nil { return gerror.Wrap(err, "更新充值订单项支付快照失败") } return nil } func (s *sAppCard) BalanceRechargeStatus(ctx context.Context, param *model.AppBalanceRechargeStatusParam) (*model.AppBalanceRechargeStatusModel, error) { if param == nil { param = &model.AppBalanceRechargeStatusParam{} } orderSn := strings.TrimSpace(param.OrderSn) if orderSn == "" { return nil, gerror.New("订单号不能为空") } user, err := appCurrentUser(ctx) if err != nil { return nil, err } var order *entity.TOrder var balanceAfter float64 err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().TenantId, user.TenantId). Where(dao.TOrder.Columns().OrderSn, orderSn). Limit(1). Scan(&order); err != nil { return gerror.Wrap(err, "查询充值订单失败") } if order == nil || order.Id == 0 { return gerror.New("充值订单不存在") } if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil { return err } account, err := s.lockAppBalanceAccount(ctx, tx, user) if err != nil { return err } balanceAfter = appRoundAmount(account.Balance) return nil }) if err != nil { return nil, err } status := "pending_pay" if order.PayStatus == consts.PayStatusPaid { status = "paid" } paymentExpireAt, paymentRemainingSeconds, paymentExpired := appBalanceRechargePaymentTiming(ctx, order) if paymentExpired { status = "expired" } var payRequest *appWechatPayRequest payProvider := appBalancePayProviderFromTradeNo(order.PayOrderSn) if order.PayStatus == consts.PayStatusUnpaid && !paymentExpired && appWechatEnabled(ctx) { payProvider = "wechat" if appNormalizeWechatPayScene(param.PayScene) == "mini" && strings.TrimSpace(param.WxCode) != "" { if err = service.AppAuth().BindWechatOpenIdByLoginCode(ctx, user, param.WxCode); err != nil { return nil, err } } payRequest, err = appBuildWechatPayRequest(ctx, order, param.PayScene, user) if err != nil { return nil, err } } return &model.AppBalanceRechargeStatusModel{ OrderId: order.Id, OrderSn: order.OrderSn, TotalAmount: appRoundAmount(order.TotalAmount), PriceText: appFormatMoney(order.TotalAmount), OrderStatus: order.OrderStatus, PayStatus: order.PayStatus, Status: status, PaymentExpireAt: paymentExpireAt, PaymentRemainingSeconds: paymentRemainingSeconds, PaymentExpired: paymentExpired, BalanceAfter: balanceAfter, BalanceAfterText: appFormatMoney(balanceAfter), PayProvider: payProvider, PayTradeNo: order.PayOrderSn, PayUrl: appWechatPayUrl(payRequest), PayMethod: appWechatPayMethod(payRequest), PayGateway: "", PayParams: appWechatPayParams(payRequest), }, nil } func appBalancePayProviderFromTradeNo(tradeNo string) string { if strings.TrimSpace(tradeNo) == "" { return "" } upper := strings.ToUpper(tradeNo) if strings.HasPrefix(upper, "MOCK") { return "mock" } if strings.HasPrefix(upper, "WX") { return "wechat" } return "wechat" }