308 lines
16 KiB
Go
308 lines
16 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/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()
|
|
ac := dao.AppMemberPaymentAccount.Columns()
|
|
remarkName := fmt.Sprintf("CASE WHEN %s LIKE '姓名:%%' THEN SUBSTRING_INDEX(%s,'姓名:',-1) ELSE '' END", col("r", rc.Remark), col("r", rc.Remark))
|
|
realName := fmt.Sprintf("COALESCE(NULLIF(%s,''),(SELECT NULLIF(%s,'') FROM %s pa WHERE pa.%s=%s AND pa.%s IS NULL ORDER BY pa.%s ASC,pa.%s DESC LIMIT 1),NULLIF(%s,''),'') real_name_mask", col("p", pc.RealNameMask), ac.HolderMask, dao.AppMemberPaymentAccount.Table(), ac.MemberId, col("r", rc.MemberId), ac.DeletedAt, ac.IsDefault, ac.Id, remarkName)
|
|
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), realName, 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
|
|
}
|