package app import ( "context" "fmt" "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/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" ) const ( appRefundStatusSuccess = "SUCCESS" appRefundStatusProcessing = "PROCESSING" appRefundStatusAbnormal = "ABNORMAL" appRefundStatusClosed = "CLOSED" ) func (s *sAppCard) BalanceRechargeRefund(ctx context.Context, param *model.AppBalanceRechargeRefundParam) (*model.AppOrderModel, error) { if param == nil { param = &model.AppBalanceRechargeRefundParam{} } orderSn := strings.TrimSpace(param.OrderSn) if orderSn == "" { return nil, gerror.New("订单号不能为空") } user, err := appCurrentUser(ctx) if err != nil { return nil, err } cfg, err := appLoadWechatConfig(ctx) if err != nil { return nil, err } if !cfg.Enabled { return nil, gerror.New("微信支付未启用") } if err = appValidateWechatCommonConfig(cfg); err != nil { return nil, err } if strings.TrimSpace(cfg.RefundNotifyUrl) == "" { return nil, gerror.New("微信退款回调地址未配置") } client, err := appWechatClient(ctx, cfg) if err != nil { return nil, err } refundSvc := refunddomestic.RefundsApiService{Client: client} now := gtime.Now() refundReason := strings.TrimSpace(param.RefundReason) if refundReason == "" { refundReason = "用户发起余额充值退款" } outRefundNo := appBalanceRechargeRefundNo(orderSn) var payOrderSn string var refundAmount float64 var refundAmountFen int64 err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { order, err := s.lockAppBalanceRechargeRefundOrder(ctx, tx, user, orderSn) if err != nil { return err } if appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no") == outRefundNo { return gerror.New("该订单已提交过退款申请,请等待处理结果") } refundAmount = appRoundAmount(order.TotalAmount) if refundAmount <= 0 { return gerror.New("订单退款金额不正确") } refundAmountFen = appWechatAmountFen(refundAmount) if refundAmountFen <= 0 { return gerror.New("订单退款金额不正确") } payOrderSn = strings.TrimSpace(order.PayOrderSn) if _, err = s.deductAppBalance( ctx, tx, user, order.OrderSn, refundAmount, "微信充值退款扣回", now, ); err != nil { return err } if _, err = dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, order.Id). Data(do.TOrder{ ReturnStatus: consts.OrderReturnStatusPending, UnrefundAmount: refundAmount, Remark: "小程序余额充值退款申请中", AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, appRefundStatusProcessing, "", refundReason, true), UpdatedBy: user.UserId, UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新退款申请状态失败") } return nil }) if err != nil { return nil, err } refundResp, _, err := refundSvc.Create(ctx, refunddomestic.CreateRequest{ TransactionId: core.String(payOrderSn), OutTradeNo: core.String(orderSn), OutRefundNo: core.String(outRefundNo), Reason: core.String(refundReason), NotifyUrl: core.String(cfg.RefundNotifyUrl), Amount: &refunddomestic.AmountReq{ Refund: core.Int64(refundAmountFen), Total: core.Int64(refundAmountFen), Currency: core.String("CNY"), }, }) if err != nil { if rollbackErr := s.rollbackAppBalanceRechargeRefund(ctx, orderSn, outRefundNo, "FAILED", "", refundReason, refundAmount, gtime.Now()); rollbackErr != nil { g.Log().Errorf(ctx, "余额充值退款申请失败后回滚余额失败: order=%s refundNo=%s error=%v", orderSn, outRefundNo, rollbackErr) } return nil, gerror.Wrap(err, "提交微信退款申请失败") } refundStatus := appWechatRefundStatus(refundResp) switch refundStatus { case "", appRefundStatusProcessing, appRefundStatusSuccess: if err = s.applyAppBalanceRechargeRefundAccepted(ctx, orderSn, outRefundNo, refundStatus, appWechatRefundId(refundResp), refundReason, refundAmount, gtime.Now()); err != nil { return nil, err } default: if rollbackErr := s.rollbackAppBalanceRechargeRefund(ctx, orderSn, outRefundNo, refundStatus, appWechatRefundId(refundResp), refundReason, refundAmount, gtime.Now()); rollbackErr != nil { g.Log().Errorf(ctx, "微信退款申请未受理后回滚余额失败: order=%s refundNo=%s status=%s error=%v", orderSn, outRefundNo, refundStatus, rollbackErr) } return nil, gerror.Newf("微信退款申请未受理:%s", refundStatus) } return service.AppOrder().Detail(ctx, &model.AppOrderDetailParam{OrderSn: orderSn}) } func (s *sAppCard) applyAppBalanceRechargeRefundAccepted(ctx context.Context, orderSn string, outRefundNo string, refundStatus string, refundId string, refundReason string, refundAmount float64, now *gtime.Time) error { return dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.TOrder if err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().OrderSn, orderSn). LockUpdate(). Limit(1). Scan(&order); err != nil { return gerror.Wrap(err, "锁定退款订单失败") } if order == nil || order.Id == 0 { return gerror.New("退款订单不存在") } if order.ReturnStatus != consts.OrderReturnStatusPending && order.ReturnStatus != consts.OrderReturnStatusRunning { return nil } metaRefundNo := appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no") if metaRefundNo != "" && outRefundNo != "" && metaRefundNo != outRefundNo { return nil } returnStatus := consts.OrderReturnStatusPending if refundStatus == appRefundStatusProcessing { returnStatus = consts.OrderReturnStatusRunning } else if refundStatus == appRefundStatusSuccess { returnStatus = consts.OrderReturnStatusSuccess } unrefundAmount := refundAmount remark := "小程序余额充值退款申请中" if returnStatus == consts.OrderReturnStatusSuccess { unrefundAmount = 0 remark = "小程序余额充值已退款" } if unrefundAmount <= 0 && returnStatus != consts.OrderReturnStatusSuccess { unrefundAmount = appRoundAmount(order.UnrefundAmount) } if _, err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, order.Id). Data(do.TOrder{ ReturnStatus: returnStatus, UnrefundAmount: unrefundAmount, Remark: remark, AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, refundReason, true), UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新退款申请状态失败") } return nil }) } func (s *sAppCard) rollbackAppBalanceRechargeRefund(ctx context.Context, orderSn string, outRefundNo string, refundStatus string, refundId string, refundReason string, refundAmount float64, now *gtime.Time) error { return dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.TOrder if err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().OrderSn, orderSn). LockUpdate(). Limit(1). Scan(&order); err != nil { return gerror.Wrap(err, "锁定退款订单失败") } if order == nil || order.Id == 0 { return nil } if order.ReturnStatus != consts.OrderReturnStatusPending && order.ReturnStatus != consts.OrderReturnStatusRunning { return nil } metaRefundNo := appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no") if metaRefundNo != "" && outRefundNo != "" && metaRefundNo != outRefundNo { return nil } if appBalanceRechargeRefundBalanceDeducted(order.AdminNote) { orderUser, err := s.loadAppBalanceRechargeOrderUser(ctx, tx, order) if err != nil { return err } if refundAmount <= 0 { refundAmount = appRoundAmount(order.UnrefundAmount) } if refundAmount <= 0 { refundAmount = appRoundAmount(order.TotalAmount) } if _, err = s.creditAppBalanceWithChannel( ctx, tx, orderUser, order.OrderSn, refundAmount, "微信退款失败余额回退", consts.BalanceTransactionChannelWechat, outRefundNo, now, ); err != nil { return err } } if _, err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, order.Id). Data(do.TOrder{ ReturnStatus: consts.OrderReturnStatusPartial, UnrefundAmount: 0, Remark: "小程序余额充值退款异常,余额已回退", AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, refundReason, false), UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新退款异常状态失败") } return nil }) } func (s *sAppCard) lockAppBalanceRechargeRefundOrder(ctx context.Context, tx gdb.TX, user *entity.SysUser, orderSn string) (*entity.TOrder, error) { var order *entity.TOrder if err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().TenantId, user.TenantId). Where(dao.TOrder.Columns().OrderSn, orderSn). LockUpdate(). Limit(1). Scan(&order); err != nil { return nil, gerror.Wrap(err, "锁定充值订单失败") } if order == nil || order.Id == 0 { return nil, gerror.New("充值订单不存在") } if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil { return nil, err } if order.OrderType != consts.OrderTypeBalanceRecharge { return nil, gerror.New("仅余额充值订单支持退款") } if order.PayStatus != consts.PayStatusPaid || order.OrderStatus != consts.OrderStatusCompleted { return nil, gerror.New("仅已支付完成的充值订单支持退款") } if order.PayType != consts.PayTypeOnline || !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(order.PayOrderSn)), "WX") { return nil, gerror.New("仅微信支付充值订单支持退款") } switch order.ReturnStatus { case consts.OrderReturnStatusPending, consts.OrderReturnStatusRunning: return nil, gerror.New("退款申请正在处理中") case consts.OrderReturnStatusSuccess: return nil, gerror.New("该订单已退款") case consts.OrderReturnStatusPartial: return nil, gerror.New("该订单退款异常,请联系管理员处理") } return order, nil } func (s *sAppCard) applyAppBalanceRechargeRefundNotify(ctx context.Context, refund *refunddomestic.Refund) error { if refund == nil { return nil } orderSn := strings.TrimSpace(appStringValue(refund.OutTradeNo)) if orderSn == "" { return gerror.New("微信退款通知订单号为空") } outRefundNo := strings.TrimSpace(appStringValue(refund.OutRefundNo)) if outRefundNo == "" { outRefundNo = appBalanceRechargeRefundNo(orderSn) } refundStatus := appWechatRefundStatus(refund) refundId := appStringValue(refund.RefundId) refundAmount := appRoundAmount(float64(appWechatRefundAmountFen(refund)) / 100) now := gtime.Now() return dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.TOrder if err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().OrderSn, orderSn). LockUpdate(). Limit(1). Scan(&order); err != nil { return gerror.Wrap(err, "锁定退款订单失败") } if order == nil || order.Id == 0 { g.Log().Warningf(ctx, "微信退款通知订单不存在: order=%s refundNo=%s", orderSn, outRefundNo) return nil } if order.OrderType != consts.OrderTypeBalanceRecharge { return nil } if order.ReturnStatus != consts.OrderReturnStatusPending && order.ReturnStatus != consts.OrderReturnStatusRunning { return nil } metaRefundNo := appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_no") if metaRefundNo != "" && outRefundNo != "" && metaRefundNo != outRefundNo { return nil } switch refundStatus { case appRefundStatusSuccess: if !appBalanceRechargeRefundBalanceDeducted(order.AdminNote) { user, err := s.loadAppBalanceRechargeOrderUser(ctx, tx, order) if err != nil { return err } if refundAmount <= 0 { refundAmount = appRoundAmount(order.UnrefundAmount) } if refundAmount <= 0 { refundAmount = appRoundAmount(order.TotalAmount) } if _, err = s.deductAppBalance( ctx, tx, user, order.OrderSn, refundAmount, "微信充值退款扣回", now, ); err != nil { return err } } if _, err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, order.Id). Data(do.TOrder{ ReturnStatus: consts.OrderReturnStatusSuccess, UnrefundAmount: 0, Remark: "小程序余额充值已退款", AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_reason"), true), UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新退款成功状态失败") } case "", appRefundStatusProcessing: if _, err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, order.Id). Data(do.TOrder{ ReturnStatus: consts.OrderReturnStatusRunning, AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, appRefundStatusProcessing, refundId, appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_reason"), appBalanceRechargeRefundBalanceDeducted(order.AdminNote)), UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新退款处理中状态失败") } case appRefundStatusAbnormal, appRefundStatusClosed: if appBalanceRechargeRefundBalanceDeducted(order.AdminNote) { user, err := s.loadAppBalanceRechargeOrderUser(ctx, tx, order) if err != nil { return err } if refundAmount <= 0 { refundAmount = appRoundAmount(order.UnrefundAmount) } if refundAmount <= 0 { refundAmount = appRoundAmount(order.TotalAmount) } if _, err = s.creditAppBalanceWithChannel( ctx, tx, user, order.OrderSn, refundAmount, "微信退款失败余额回退", consts.BalanceTransactionChannelWechat, outRefundNo, now, ); err != nil { return err } } if _, err := dao.TOrder.Ctx(ctx).TX(tx). Where(dao.TOrder.Columns().Id, order.Id). Data(do.TOrder{ ReturnStatus: consts.OrderReturnStatusPartial, UnrefundAmount: 0, Remark: "小程序余额充值退款异常,余额已回退", AdminNote: appBalanceRechargeRefundNoteWithBalance(outRefundNo, refundStatus, refundId, appBalanceRechargeRefundNoteValue(order.AdminNote, "refund_reason"), false), UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新退款异常状态失败") } default: g.Log().Warningf(ctx, "微信退款通知状态暂不处理: order=%s refundNo=%s status=%s", orderSn, outRefundNo, refundStatus) } return nil }) } func appBalanceRechargeRefundNo(orderSn string) string { orderSn = strings.TrimSpace(orderSn) if strings.HasPrefix(orderSn, "RF") { return orderSn } return "RF" + orderSn } func appWechatRefundStatus(refund *refunddomestic.Refund) string { if refund == nil || refund.Status == nil { return "" } return strings.ToUpper(strings.TrimSpace(string(*refund.Status))) } func appWechatRefundId(refund *refunddomestic.Refund) string { if refund == nil { return "" } return appStringValue(refund.RefundId) } func appWechatRefundAmountFen(refund *refunddomestic.Refund) int64 { if refund == nil || refund.Amount == nil { return 0 } if refund.Amount.PayerRefund != nil && *refund.Amount.PayerRefund > 0 { return *refund.Amount.PayerRefund } if refund.Amount.Refund != nil && *refund.Amount.Refund > 0 { return *refund.Amount.Refund } return 0 } func appBalanceRechargeRefundNote(outRefundNo string, status string, refundId string, reason string) string { return fmt.Sprintf( "refund_no=%s;refund_status=%s;refund_id=%s;refund_reason=%s", strings.TrimSpace(outRefundNo), strings.ToLower(strings.TrimSpace(status)), strings.TrimSpace(refundId), strings.ReplaceAll(strings.TrimSpace(reason), ";", " "), ) } func appBalanceRechargeRefundNoteWithBalance(outRefundNo string, status string, refundId string, reason string, balanceDeducted bool) string { note := appBalanceRechargeRefundNote(outRefundNo, status, refundId, reason) if balanceDeducted { return note + ";refund_balance_deducted=1" } return note + ";refund_balance_deducted=0" } func appBalanceRechargeRefundBalanceDeducted(note string) bool { value := strings.ToLower(appBalanceRechargeRefundNoteValue(note, "refund_balance_deducted")) return value == "1" || value == "true" || value == "yes" } func appBalanceRechargeRefundNoteValue(note string, key string) string { key = strings.TrimSpace(key) if key == "" { return "" } prefix := key + "=" for _, part := range strings.Split(note, ";") { part = strings.TrimSpace(part) if strings.HasPrefix(part, prefix) { return strings.TrimSpace(strings.TrimPrefix(part, prefix)) } } return "" }