Files
olons_admin/server/internal/logic/sys/withdrawal_order.go
T

277 lines
14 KiB
Go

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
}