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/encoding/gjson" "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 sSysRechargeOrder struct{} func NewSysRechargeOrder() *sSysRechargeOrder { return &sSysRechargeOrder{} } func init() { service.RegisterSysRechargeOrder(NewSysRechargeOrder()) } func (s *sSysRechargeOrder) model(ctx context.Context) *gdb.Model { return handler.Model(dao.RechargeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}) } func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model { rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns() return s.model(ctx).As("r"). LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("r", rc.MemberId)). LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("r", rc.MemberId)). LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("r", rc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'"). LeftJoin(dao.PaymentChannel.Table()+" c", col("c", cc.Id)+"="+col("r", rc.ChannelId)) } func (s *sSysRechargeOrder) listModel(ctx context.Context) *gdb.Model { rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns() return s.baseModel(ctx). Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt)) } func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) { rc, mc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.PaymentChannel.Columns() if in.RechargeNo != "" { mod = mod.WhereLike(col("r", rc.RechargeNo), "%"+in.RechargeNo+"%") } 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.RechargeStatus > 0 { mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus) } if in.ChannelId > 0 { mod = mod.Where(col("r", rc.ChannelId), in.ChannelId) } if in.ChannelName != "" { mod = mod.WhereLike(col("c", cc.Name), "%"+in.ChannelName+"%") } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(col("r", rc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1]) } return mod, nil } func (s *sSysRechargeOrder) List(ctx context.Context, in *sysin.RechargeOrderListInp) (list []*sysin.RechargeOrderListModel, total int, err error) { mod, err := s.filter(ctx, s.listModel(ctx), in) if err != nil { return nil, 0, err } err = mod.Page(in.Page, in.PerPage).OrderDesc(col("r", dao.RechargeOrder.Columns().Id)).ScanAndCount(&list, &total, false) if err != nil { err = gerror.Wrap(err, "获取充值订单失败") } return } func (s *sSysRechargeOrder) Summary(ctx context.Context, in *sysin.RechargeOrderListInp) (res *sysin.RechargeSummaryModel, err error) { rc := dao.RechargeOrder.Columns() mod, err := s.filter(ctx, s.baseModel(ctx).Fields( "COUNT(DISTINCT "+col("r", rc.Id)+") total_count", "COALESCE(SUM("+col("r", rc.RequestAmount)+"),0) total_amount", fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) paid_count", col("r", rc.RechargeStatus), consts.RechargeSuccess, col("r", rc.Id)), fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) paid_amount", col("r", rc.RechargeStatus), consts.RechargeSuccess, col("r", rc.PaidAmount)), fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) unpaid_count", col("r", rc.RechargeStatus), consts.RechargeUnpaid, col("r", rc.Id)), fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) unpaid_amount", col("r", rc.RechargeStatus), consts.RechargeUnpaid, col("r", rc.RequestAmount)), fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) review_count", col("r", rc.RechargeStatus), consts.RechargeReview, col("r", rc.Id)), fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) review_amount", col("r", rc.RechargeStatus), consts.RechargeReview, col("r", rc.RequestAmount)), fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) rejected_count", col("r", rc.RechargeStatus), consts.RechargeRejected, col("r", rc.Id)), fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) rejected_amount", col("r", rc.RechargeStatus), consts.RechargeRejected, col("r", rc.RequestAmount)), ), in) if err != nil { return nil, err } res = new(sysin.RechargeSummaryModel) if err = mod.Scan(res); err != nil { return nil, gerror.Wrap(err, "汇总充值订单失败") } return } func (s *sSysRechargeOrder) View(ctx context.Context, in *sysin.RechargeOrderViewInp) (res *sysin.RechargeOrderListModel, err error) { err = s.listModel(ctx).Where(col("r", dao.RechargeOrder.Columns().Id), in.Id).Scan(&res) if err != nil { return nil, gerror.Wrap(err, "获取充值订单详情失败") } if res == nil { return nil, gerror.New("充值订单不存在") } return } func (s *sSysRechargeOrder) Review(ctx context.Context, in *sysin.RechargeReviewInp) (res *sysin.FinanceApprovalModel, err error) { err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.RechargeOrder if scanErr := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); scanErr != nil { return scanErr } if order == nil { return gerror.New("充值订单不存在") } if order.RechargeStatus != consts.RechargeReview { return gerror.New("充值单已处理,请刷新列表") } if in.Decision != consts.FinanceDecisionApprove && in.Decision != consts.FinanceDecisionReject { return gerror.New("审核结论不合法") } if in.Decision == consts.FinanceDecisionApprove { if order.VoucherAttachmentIds == nil && order.RechargeType == consts.RechargeTypeManual { return gerror.New("人工充值必须提供打款凭证") } if in.PaidAmount != order.RequestAmount { return gerror.New("实付金额与申请金额不一致,请驳回或先核对") } 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 { return gerror.New("会员账户状态异常,不能入账") } if in.ExternalNo != "" { count, countErr := s.model(ctx).Where(dao.RechargeOrder.Columns().ExternalNo, in.ExternalNo).WhereNot(dao.RechargeOrder.Columns().Id, order.Id).Count() if countErr != nil { return countErr } if count > 0 { return gerror.New("外部支付单号已存在,疑似重复订单") } } } rc := dao.RechargeOrder.Columns() now := gtime.Now() ledgerId := uint64(0) status := consts.RechargeRejected updateData := g.Map{ rc.RechargeStatus: status, rc.ApprovalRequestId: 0, rc.ReviewReasonCode: in.ReasonCode, rc.ReviewReason: in.ReasonText, rc.ReviewedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx), } if in.Decision == consts.FinanceDecisionApprove { wc := dao.WalletAccount.Columns() var account *entity.WalletAccount if scanErr := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); scanErr != nil { return scanErr } if account == nil || account.AccountStatus != 1 { return gerror.New("会员钱包不存在或不可入账") } after := account.AvailableAmount + in.PaidAmount lc := dao.WalletLedger.Columns() result, insertErr := dao.WalletLedger.Ctx(ctx). Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt). Hook(hook.SaveTenant). Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: order.MemberId, lc.BizType: "recharge", lc.BizNo: order.RechargeNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: in.PaidAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: fmt.Sprintf("recharge:%d", order.Id), lc.Remark: in.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now}). Insert() if insertErr != nil { return gerror.Wrap(insertErr, "写入充值台账失败") } id, _ := result.LastInsertId() ledgerId = uint64(id) walletUpdate, updateErr := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}). WherePri(account.Id). Where(wc.Version, account.Version). Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(in.PaidAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}). Update() if updateErr != nil { return updateErr } rows, _ := walletUpdate.RowsAffected() if rows != 1 { return gerror.New("钱包余额已变化,请重试") } status = consts.RechargeSuccess updateData[rc.RechargeStatus] = status updateData[rc.PaidAmount] = in.PaidAmount updateData[rc.ExternalNo] = in.ExternalNo updateData[rc.LedgerId] = ledgerId updateData[rc.PaidAt] = now updateData[rc.CreditedAt] = now } if _, updateErr := s.model(ctx).WherePri(order.Id).Data(updateData).Update(); updateErr != nil { return updateErr } return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "recharge", Action: "recharge.review", TargetType: "recharge_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 *sSysRechargeOrder) 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 != "recharge_review" { return gerror.New("充值审批不存在") } if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil { return gerror.New("审批尚未通过或已执行") } var input sysin.RechargeReviewInp if err := approval.PayloadJson.Scan(&input); err != nil { return gerror.Wrap(err, "解析充值审批数据失败") } var order *entity.RechargeOrder if err := s.model(ctx).WherePri(input.Id).LockUpdate().Scan(&order); err != nil { return err } if order == nil || order.ApprovalRequestId != approval.Id || order.RechargeStatus != consts.RechargeReview { return gerror.New("充值订单状态已变化") } rc := dao.RechargeOrder.Columns() now := gtime.Now() ledgerId := uint64(0) if input.Decision == consts.FinanceDecisionApprove { wc := dao.WalletAccount.Columns() var account *entity.WalletAccount if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); err != nil { return err } if account == nil || account.AccountStatus != 1 { return gerror.New("会员钱包不存在或不可入账") } after := account.AvailableAmount + input.PaidAmount lc := dao.WalletLedger.Columns() result, err := dao.WalletLedger.Ctx(ctx).Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: order.MemberId, lc.BizType: "recharge", lc.BizNo: order.RechargeNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: input.PaidAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: fmt.Sprintf("recharge:%d", order.Id), lc.Remark: input.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now}).Insert() if err != nil { return gerror.Wrap(err, "写入充值台账失败") } id, _ := result.LastInsertId() ledgerId = uint64(id) update, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(input.PaidAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}).Update() if err != nil { return err } rows, _ := update.RowsAffected() if rows != 1 { return gerror.New("钱包余额已变化,请重试") } _, err = s.model(ctx).WherePri(order.Id).Data(g.Map{rc.RechargeStatus: consts.RechargeSuccess, rc.PaidAmount: input.PaidAmount, rc.ExternalNo: input.ExternalNo, rc.LedgerId: ledgerId, rc.ReviewedAt: now, rc.CreditedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update() if err != nil { return err } } else { if _, err := s.model(ctx).WherePri(order.Id).Data(g.Map{rc.RechargeStatus: consts.RechargeRejected, rc.ReviewedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil { return err } } if err := recordFinanceReview(ctx, approval, "recharge", 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: "recharge", Action: "recharge.review.execute", TargetType: "recharge_order", TargetId: order.Id, Before: order, After: g.Map{"decision": input.Decision, "ledgerId": ledgerId}, Reason: input.ReasonText, ApprovalNo: approval.ApprovalNo}) }) } func recordFinanceReview(ctx context.Context, approval *entity.ApprovalRequest, bizType string, bizId uint64, decision int, reasonCode, reasonText string, snapshot interface{}) error { c := dao.FinanceReview.Columns() _, err := dao.FinanceReview.Ctx(ctx).Fields(c.ApprovalRequestId, c.BizType, c.BizId, c.ReviewStage, c.ReviewerId, c.Decision, c.ReasonCode, c.ReasonText, c.SnapshotJson).Hook(hook.SaveTenant).Data(g.Map{c.ApprovalRequestId: approval.Id, c.BizType: bizType, c.BizId: bizId, c.ReviewStage: 2, c.ReviewerId: contexts.GetUserId(ctx), c.Decision: decision, c.ReasonCode: reasonCode, c.ReasonText: reasonText, c.SnapshotJson: gjson.New(snapshot)}).Insert() return gerror.Wrap(err, "记录财务审核失败") } func markApprovalExecuted(ctx context.Context, approval *entity.ApprovalRequest) error { c := dao.ApprovalRequest.Columns() _, err := service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{c.ApprovalStatus: consts.ApprovalStatusExecuted, c.ExecutedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update() return err }