package sys import ( "context" "fmt" "hotgo/internal/consts" "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/hgorm/handler" "hotgo/internal/library/hgorm/hook" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/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/gogf/gf/v2/util/grand" ) type sSysWithdrawalOrder struct{} func NewSysWithdrawalOrder() *sSysWithdrawalOrder { return &sSysWithdrawalOrder{} } func init() { service.RegisterSysWithdrawalOrder(NewSysWithdrawalOrder()) } func (s *sSysWithdrawalOrder) model(ctx context.Context) *gdb.Model { return handler.Model(dao.WithdrawalOrder.Ctx(ctx), &handler.Option{FilterTenant: true}) } func (s *sSysWithdrawalOrder) listModel(ctx context.Context) *gdb.Model { wc, mc, ac := dao.WithdrawalOrder.Columns(), dao.AppMember.Columns(), dao.WalletAccount.Columns() pc, sc := dao.AppMemberPaymentAccount.Columns(), dao.SysAttachment.Columns() // account_snapshot_cipher is deliberately excluded: full account data never enters ordinary API responses. return s.model(ctx).As("o"). LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", wc.MemberId)). LeftJoin(dao.WalletAccount.Table()+" w", col("w", ac.MemberId)+"="+col("o", wc.MemberId)+" AND "+col("w", ac.Currency)+"='CNY'"). LeftJoin(dao.AppMemberPaymentAccount.Table()+" pa", col("pa", pc.Id)+"="+col("o", wc.PaymentAccountId)). LeftJoin(dao.SysAttachment.Table()+" sa", col("sa", sc.Id)+"="+col("pa", pc.QrAttachmentId)). Fields(col("o", wc.Id), col("o", wc.WithdrawalNo), col("o", wc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", ac.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", ac.FrozenAmount)), col("o", wc.RequestAmount), col("o", wc.FeeAmount), col("o", wc.ActualAmount), col("o", wc.Currency), col("o", wc.AccountMask), col("o", wc.BankName), col("o", wc.HolderMask), fmt.Sprintf("COALESCE(%s,0) payment_account_type", col("pa", pc.AccountType)), fmt.Sprintf("COALESCE(%s,'') qr_code_url", col("sa", sc.FileUrl)), col("o", wc.RiskResult), col("o", wc.WithdrawalStatus), col("o", wc.ReviewReasonCode), col("o", wc.ReviewReason), col("o", wc.ApprovalRequestId), col("o", wc.ExternalNo), col("o", wc.FreezeLedgerId), col("o", wc.FinalLedgerId), col("o", wc.ReviewedAt), col("o", wc.ProcessedAt), col("o", wc.CompletedAt), col("o", wc.CreatedAt)) } func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrderListInp) (list []*sysin.WithdrawalOrderListModel, total int, err error) { wc, mc := dao.WithdrawalOrder.Columns(), dao.AppMember.Columns() mod := s.listModel(ctx) if in.WithdrawalNo != "" { mod = mod.WhereLike(col("o", wc.WithdrawalNo), "%"+in.WithdrawalNo+"%") } if in.Username != "" { mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%") } if in.Mobile != "" { mod = mod.Where(col("m", mc.Mobile), in.Mobile) } if in.WithdrawalStatus > 0 { mod = mod.Where(col("o", wc.WithdrawalStatus), in.WithdrawalStatus) } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(col("o", wc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1]) } err = mod.Page(in.Page, in.PerPage).OrderDesc(col("o", wc.Id)).ScanAndCount(&list, &total, false) if err != nil { err = gerror.Wrap(err, "获取提现订单失败") } return } func (s *sSysWithdrawalOrder) View(ctx context.Context, in *sysin.WithdrawalOrderViewInp) (res *sysin.WithdrawalOrderListModel, err error) { err = s.listModel(ctx).Where(col("o", dao.WithdrawalOrder.Columns().Id), in.Id).Scan(&res) if err != nil { return nil, gerror.Wrap(err, "获取提现详情失败") } if res == nil { return nil, gerror.New("提现订单不存在") } return } func (s *sSysWithdrawalOrder) Review(ctx context.Context, in *sysin.WithdrawalReviewInp) (res *sysin.FinanceApprovalModel, err error) { err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.WithdrawalOrder if scanErr := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); scanErr != nil { return scanErr } if order == nil { return gerror.New("提现订单不存在") } if order.WithdrawalStatus != consts.WithdrawalPending || order.ApprovalRequestId > 0 { return gerror.New("提现单已处理,请刷新列表") } if in.Decision == consts.FinanceDecisionApprove { var member *entity.AppMember if scanErr := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(order.MemberId).Scan(&member); scanErr != nil { return scanErr } if member == nil || member.MemberStatus != consts.AppMemberStatusNormal || member.RiskStatus == consts.AppMemberRiskBlocked { return gerror.New("会员或风控状态不允许提现") } } var ledgerId uint64 var handleErr error status := consts.WithdrawalSuccess if in.Decision == consts.FinanceDecisionApprove { ledgerId, handleErr = s.consumeWithdrawal(ctx, order, in.ReasonText) } else { status = consts.WithdrawalRejected ledgerId, handleErr = s.releaseWithdrawal(ctx, order, "提现审核驳回:"+in.ReasonText) } if handleErr != nil { return handleErr } now := gtime.Now() c := dao.WithdrawalOrder.Columns() if _, updateErr := s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: status, c.ReviewReasonCode: in.ReasonCode, c.ReviewReason: in.ReasonText, c.FinalLedgerId: ledgerId, c.ReviewedAt: now, c.ProcessedAt: now, c.CompletedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); updateErr != nil { return updateErr } return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "withdrawal", Action: "withdrawal.review", TargetType: "withdrawal_order", TargetId: order.Id, Before: order, After: g.Map{"decision": in.Decision, "status": status, "ledgerId": ledgerId}, Reason: in.ReasonText}) }) if err != nil { return nil, err } return &sysin.FinanceApprovalModel{}, nil } func (s *sSysWithdrawalOrder) Execute(ctx context.Context, in *sysin.FinanceExecuteInp) error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var approval *entity.ApprovalRequest if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil { return err } if approval == nil || approval.ApprovalType != "withdrawal_review" { return gerror.New("提现审批不存在") } if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil { return gerror.New("审批尚未通过或已执行") } var input sysin.WithdrawalReviewInp if err := approval.PayloadJson.Scan(&input); err != nil { return gerror.Wrap(err, "解析提现审批数据失败") } var order *entity.WithdrawalOrder if err := s.model(ctx).WherePri(input.Id).LockUpdate().Scan(&order); err != nil { return err } if order == nil || order.ApprovalRequestId != approval.Id || order.WithdrawalStatus != consts.WithdrawalReviewing { return gerror.New("提现订单状态已变化") } c := dao.WithdrawalOrder.Columns() now := gtime.Now() finalLedgerId := uint64(0) if input.Decision == consts.FinanceDecisionApprove { if _, err := s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: consts.WithdrawalProcessing, c.ReviewedAt: now, c.ProcessedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil { return err } } else { id, err := s.releaseWithdrawal(ctx, order, "提现审核驳回:"+input.ReasonText) if err != nil { return err } finalLedgerId = id if _, err = s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: consts.WithdrawalRejected, c.FinalLedgerId: finalLedgerId, c.ReviewedAt: now, c.CompletedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil { return err } } if err := recordFinanceReview(ctx, approval, "withdrawal", order.Id, input.Decision, input.ReasonCode, input.ReasonText, order); err != nil { return err } if err := markApprovalExecuted(ctx, approval); err != nil { return err } return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "withdrawal", Action: "withdrawal.review.execute", TargetType: "withdrawal_order", TargetId: order.Id, Before: order, After: g.Map{"decision": input.Decision, "finalLedgerId": finalLedgerId}, Reason: input.ReasonText, ApprovalNo: approval.ApprovalNo}) }) } func (s *sSysWithdrawalOrder) Complete(ctx context.Context, in *sysin.WithdrawalCompleteInp) error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.WithdrawalOrder if err := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); err != nil { return err } if order == nil { return gerror.New("提现订单不存在") } if order.WithdrawalStatus != consts.WithdrawalProcessing { return gerror.New("仅付款中的提现单可以登记结果") } var ledgerId uint64 var err error status := consts.WithdrawalFailed if in.Success { ledgerId, err = s.consumeWithdrawal(ctx, order, in.ReasonText) status = consts.WithdrawalSuccess } else { ledgerId, err = s.releaseWithdrawal(ctx, order, "付款失败退回:"+in.ReasonText) } if err != nil { return err } c := dao.WithdrawalOrder.Columns() if _, err = s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: status, c.ExternalNo: in.ExternalNo, c.FinalLedgerId: ledgerId, c.CompletedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil { return err } return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "withdrawal", Action: "withdrawal.payment.complete", TargetType: "withdrawal_order", TargetId: order.Id, Before: order, After: g.Map{"status": status, "externalNo": in.ExternalNo, "ledgerId": ledgerId}, Reason: in.ReasonText}) }) } func (s *sSysWithdrawalOrder) wallet(ctx context.Context, order *entity.WithdrawalOrder) (*entity.WalletAccount, error) { c := dao.WalletAccount.Columns() var account *entity.WalletAccount if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(c.MemberId, order.MemberId).Where(c.Currency, order.Currency).LockUpdate().Scan(&account); err != nil { return nil, err } if account == nil { return nil, gerror.New("会员钱包不存在") } if account.FrozenAmount < order.RequestAmount { return nil, gerror.New("提现冻结金额不足,不能完成处理") } return account, nil } func (s *sSysWithdrawalOrder) consumeWithdrawal(ctx context.Context, order *entity.WithdrawalOrder, remark string) (uint64, error) { account, err := s.wallet(ctx, order) if err != nil { return 0, err } ledgerId, err := insertWithdrawalLedger(ctx, account, order, "withdraw", "frozen", consts.WalletDirectionExpense, order.RequestAmount, account.FrozenAmount, account.FrozenAmount-order.RequestAmount, fmt.Sprintf("withdraw:%d:consume", order.Id), remark) if err != nil { return 0, err } c := dao.WalletAccount.Columns() result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(c.Version, account.Version).Data(g.Map{c.FrozenAmount: account.FrozenAmount - order.RequestAmount, c.TotalExpense: gdb.Raw(c.TotalExpense + "+" + fmt.Sprint(order.RequestAmount)), c.Version: gdb.Raw(c.Version + "+1")}).Update() if err != nil { return 0, err } rows, _ := result.RowsAffected() if rows != 1 { return 0, gerror.New("钱包余额已变化,请重试") } return ledgerId, nil } func (s *sSysWithdrawalOrder) releaseWithdrawal(ctx context.Context, order *entity.WithdrawalOrder, remark string) (uint64, error) { account, err := s.wallet(ctx, order) if err != nil { return 0, err } _, err = insertWithdrawalLedger(ctx, account, order, "withdraw_release", "frozen", consts.WalletDirectionExpense, order.RequestAmount, account.FrozenAmount, account.FrozenAmount-order.RequestAmount, fmt.Sprintf("withdraw:%d:release:frozen", order.Id), remark) if err != nil { return 0, err } ledgerId, err := insertWithdrawalLedger(ctx, account, order, "withdraw_release", "available", consts.WalletDirectionIncome, order.RequestAmount, account.AvailableAmount, account.AvailableAmount+order.RequestAmount, fmt.Sprintf("withdraw:%d:release:available", order.Id), remark) if err != nil { return 0, err } c := dao.WalletAccount.Columns() result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(c.Version, account.Version).Data(g.Map{c.FrozenAmount: account.FrozenAmount - order.RequestAmount, c.AvailableAmount: account.AvailableAmount + order.RequestAmount, c.Version: gdb.Raw(c.Version + "+1")}).Update() if err != nil { return 0, err } rows, _ := result.RowsAffected() if rows != 1 { return 0, gerror.New("钱包余额已变化,请重试") } return ledgerId, nil } func insertWithdrawalLedger(ctx context.Context, account *entity.WalletAccount, order *entity.WithdrawalOrder, bizType, entryType string, direction int, amount, before, after int64, idempotencyKey, remark string) (uint64, error) { c := dao.WalletLedger.Columns() result, err := dao.WalletLedger.Ctx(ctx).Fields(c.LedgerNo, c.AccountId, c.MemberId, c.BizType, c.BizNo, c.EntryType, c.Direction, c.Amount, c.BeforeAmount, c.AfterAmount, c.Currency, c.IdempotencyKey, c.Remark, c.OperatorId, c.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{c.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), c.AccountId: account.Id, c.MemberId: order.MemberId, c.BizType: bizType, c.BizNo: order.WithdrawalNo, c.EntryType: entryType, c.Direction: direction, c.Amount: amount, c.BeforeAmount: before, c.AfterAmount: after, c.Currency: account.Currency, c.IdempotencyKey: idempotencyKey, c.Remark: remark, c.OperatorId: contexts.GetUserId(ctx), c.OccurredAt: gtime.Now()}).Insert() if err != nil { return 0, gerror.Wrap(err, "写入提现台账失败") } id, _ := result.LastInsertId() return uint64(id), nil }