feat: publish member notices and balance-aware order settlement
Deploy olons-admin production / deploy (push) Successful in 8m50s

This commit is contained in:
richjohnrambo
2026-10-22 21:56:06 +08:00
parent 5a5540785d
commit 70ef3d9d04
10 changed files with 457 additions and 163 deletions
+146 -4
View File
@@ -65,7 +65,7 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
LeftJoin(dao.UrlPool.Table()+" up", col("up", uc.Id)+"="+col("ub", bc.UrlPoolId)). LeftJoin(dao.UrlPool.Table()+" up", col("up", uc.Id)+"="+col("ub", bc.UrlPoolId)).
Fields( Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.Mobile), col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.Mobile),
col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("p", pc.RealNameCipher), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)),
fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.ParentId), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.ParentId),
fmt.Sprintf("COALESCE(%s,'') parent_username", col("parent", mc.Username)), col("m", mc.InviteCode), fmt.Sprintf("COALESCE(%s,'') parent_username", col("parent", mc.Username)), col("m", mc.InviteCode),
col("m", mc.MemberStatus), col("m", mc.RiskStatus), col("m", mc.AutoDispatch), col("m", mc.IsTestAccount), col("m", mc.MemberStatus), col("m", mc.RiskStatus), col("m", mc.AutoDispatch), col("m", mc.IsTestAccount),
@@ -109,6 +109,19 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
if err != nil { if err != nil {
return nil, 0, gerror.Wrap(err, "获取会员列表失败") return nil, 0, gerror.Wrap(err, "获取会员列表失败")
} }
for _, row := range list {
if row.RealNameCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.RealNameCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "会员真实姓名解密失败,列表回退为脱敏值 memberId:%d err:%+v", row.Id, decryptErr)
row.RealName = row.RealNameMask
continue
}
row.RealName = plain
} else {
row.RealName = row.RealNameMask
}
}
return return
} }
@@ -126,6 +139,34 @@ func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (r
if res.Id == 0 { if res.Id == 0 {
return nil, gerror.New("会员不存在") return nil, gerror.New("会员不存在")
} }
var profile *entity.AppMemberProfile
if err = s.profileModel(ctx).Where(dao.AppMemberProfile.Columns().MemberId, in.Id).Scan(&profile); err != nil {
return nil, gerror.Wrap(err, "读取会员实名资料失败")
}
if profile != nil {
if profile.RealNameCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, profile.RealNameCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "会员姓名解密失败,详情回退为脱敏值 memberId:%d err:%+v", in.Id, decryptErr)
res.RealName = profile.RealNameMask
} else {
res.RealName = plain
}
} else {
res.RealName = profile.RealNameMask
}
if profile.IdNoCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, profile.IdNoCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "会员证件号解密失败,详情回退为脱敏值 memberId:%d err:%+v", in.Id, decryptErr)
res.IdNo = profile.IdNoMask
} else {
res.IdNo = plain
}
} else {
res.IdNo = profile.IdNoMask
}
}
res.PaymentAccountCount, _ = s.paymentModel(ctx).Where(dao.AppMemberPaymentAccount.Columns().MemberId, in.Id).Count() res.PaymentAccountCount, _ = s.paymentModel(ctx).Where(dao.AppMemberPaymentAccount.Columns().MemberId, in.Id).Count()
res.AddressCount, _ = s.addressModel(ctx).Where(dao.AppMemberAddress.Columns().MemberId, in.Id).Count() res.AddressCount, _ = s.addressModel(ctx).Where(dao.AppMemberAddress.Columns().MemberId, in.Id).Count()
token := "tr_" + fmt.Sprint(in.Id) + " " token := "tr_" + fmt.Sprint(in.Id) + " "
@@ -407,10 +448,68 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
err = s.paymentModel(ctx).As("p"). err = s.paymentModel(ctx).As("p").
LeftJoin(dao.SysAttachment.Table()+" wq", col("wq", ac.Id)+"="+col("p", c.WechatQrAttachmentId)). LeftJoin(dao.SysAttachment.Table()+" wq", col("wq", ac.Id)+"="+col("p", c.WechatQrAttachmentId)).
LeftJoin(dao.SysAttachment.Table()+" aq", col("aq", ac.Id)+"="+col("p", c.AlipayQrAttachmentId)). LeftJoin(dao.SysAttachment.Table()+" aq", col("aq", ac.Id)+"="+col("p", c.AlipayQrAttachmentId)).
Fields(col("p", c.Id), col("p", c.MemberId), col("p", c.AccountType), col("p", c.BankCode), col("p", c.BankName), col("p", c.HolderMask), col("p", c.IdentityNoMask), col("p", c.MobileMask), col("p", c.AccountNoMask), col("p", c.DigitalRmbCodeMask), col("p", c.QrAttachmentId), col("p", c.WechatQrAttachmentId), col("p", c.AlipayQrAttachmentId), fmt.Sprintf("COALESCE(%s,'') wechat_qr_url", col("wq", ac.FileUrl)), fmt.Sprintf("COALESCE(%s,'') alipay_qr_url", col("aq", ac.FileUrl)), col("p", c.VerifyStatus), col("p", c.IsDefault), col("p", c.CreatedAt), col("p", c.UpdatedAt)). Fields(col("p", c.Id), col("p", c.MemberId), col("p", c.AccountType), col("p", c.BankCode), col("p", c.BankName), col("p", c.HolderCipher), col("p", c.IdentityNoCipher), col("p", c.MobileCipher), col("p", c.AccountNoCipher), col("p", c.DigitalRmbCodeCipher), col("p", c.HolderMask), col("p", c.IdentityNoMask), col("p", c.MobileMask), col("p", c.AccountNoMask), col("p", c.DigitalRmbCodeMask), col("p", c.QrAttachmentId), col("p", c.WechatQrAttachmentId), col("p", c.AlipayQrAttachmentId), fmt.Sprintf("COALESCE(%s,'') wechat_qr_url", col("wq", ac.FileUrl)), fmt.Sprintf("COALESCE(%s,'') alipay_qr_url", col("aq", ac.FileUrl)), col("p", c.VerifyStatus), col("p", c.IsDefault), col("p", c.CreatedAt), col("p", c.UpdatedAt)).
Where(col("p", c.MemberId), in.MemberId).OrderDesc(col("p", c.IsDefault)).OrderDesc(col("p", c.Id)).Scan(&list) Where(col("p", c.MemberId), in.MemberId).OrderDesc(col("p", c.IsDefault)).OrderDesc(col("p", c.Id)).Scan(&list)
if err != nil { if err != nil {
err = gerror.Wrap(err, "获取收款账户失败") err = gerror.Wrap(err, "获取收款账户失败")
return
}
for _, row := range list {
if row.HolderCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.HolderCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收款人姓名解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Holder = row.HolderMask
} else {
row.Holder = plain
}
} else {
row.Holder = row.HolderMask
}
if row.IdentityNoCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.IdentityNoCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收款人证件号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.IdentityNo = row.IdentityNoMask
} else {
row.IdentityNo = plain
}
} else {
row.IdentityNo = row.IdentityNoMask
}
if row.MobileCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.MobileCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收款手机号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Mobile = row.MobileMask
} else {
row.Mobile = plain
}
} else {
row.Mobile = row.MobileMask
}
if row.AccountNoCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.AccountNoCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收款账号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.AccountNo = row.AccountNoMask
} else {
row.AccountNo = plain
}
} else {
row.AccountNo = row.AccountNoMask
}
if row.DigitalRmbCodeCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.DigitalRmbCodeCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "数字钱包编码解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.DigitalRMBCode = row.DigitalRMBCodeMask
} else {
row.DigitalRMBCode = plain
}
} else {
row.DigitalRMBCode = row.DigitalRMBCodeMask
}
} }
return return
} }
@@ -517,9 +616,45 @@ func (s *sSysAppMember) PaymentDelete(ctx context.Context, in *sysin.AppMemberPa
func (s *sSysAppMember) AddressList(ctx context.Context, in *sysin.AppMemberAddressListInp) (list []*sysin.AppMemberAddressModel, err error) { func (s *sSysAppMember) AddressList(ctx context.Context, in *sysin.AppMemberAddressListInp) (list []*sysin.AppMemberAddressModel, err error) {
c := dao.AppMemberAddress.Columns() c := dao.AppMemberAddress.Columns()
err = s.addressModel(ctx).Fields(c.Id, c.MemberId, c.ReceiverMask, c.MobileMask, c.Province, c.City, c.District, c.AddressMask, c.IsDefault, c.CreatedAt, c.UpdatedAt).Where(c.MemberId, in.MemberId).OrderDesc(c.IsDefault).OrderDesc(c.Id).Scan(&list) err = s.addressModel(ctx).Fields(c.Id, c.MemberId, c.ReceiverCipher, c.MobileCipher, c.AddressCipher, c.ReceiverMask, c.MobileMask, c.Province, c.City, c.District, c.AddressMask, c.IsDefault, c.CreatedAt, c.UpdatedAt).Where(c.MemberId, in.MemberId).OrderDesc(c.IsDefault).OrderDesc(c.Id).Scan(&list)
if err != nil { if err != nil {
err = gerror.Wrap(err, "获取会员地址失败") err = gerror.Wrap(err, "获取会员地址失败")
return
}
for _, row := range list {
if row.ReceiverCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.ReceiverCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收货人姓名解密失败,地址信息回退为脱敏值 memberId:%d addressId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Receiver = row.ReceiverMask
} else {
row.Receiver = plain
}
} else {
row.Receiver = row.ReceiverMask
}
if row.MobileCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.MobileCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收货手机号解密失败,地址信息回退为脱敏值 memberId:%d addressId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Mobile = row.MobileMask
} else {
row.Mobile = plain
}
} else {
row.Mobile = row.MobileMask
}
if row.AddressCipher != "" {
plain, decryptErr := encrypt.DecryptField(ctx, row.AddressCipher)
if decryptErr != nil {
g.Log().Warningf(ctx, "收货地址解密失败,地址信息回退为脱敏值 memberId:%d addressId:%d err:%+v", row.MemberId, row.Id, decryptErr)
row.Address = row.AddressMask
} else {
row.Address = plain
}
} else {
row.Address = row.AddressMask
}
} }
return return
} }
@@ -874,11 +1009,18 @@ func (s *sSysAppMember) NoticeList(ctx context.Context, in *sysin.MemberNoticeLi
return return
} }
func (s *sSysAppMember) NoticeEdit(ctx context.Context, in *sysin.MemberNoticeEditInp) error { func (s *sSysAppMember) NoticeEdit(ctx context.Context, in *sysin.MemberNoticeEditInp) error {
memberCount, err := s.memberModel(ctx).WherePri(in.MemberId).Count()
if err != nil {
return gerror.Wrap(err, "校验公告接收会员失败")
}
if memberCount == 0 {
return gerror.New("接收会员不存在或不属于当前租户")
}
if in.SourceType == consts.MemberNoticeSimulation && g.Cfg().MustGet(ctx, "system.mode").String() == "product" { if in.SourceType == consts.MemberNoticeSimulation && g.Cfg().MustGet(ctx, "system.mode").String() == "product" {
return gerror.New("生产环境禁止创建运营模拟通知") return gerror.New("生产环境禁止创建运营模拟通知")
} }
c := dao.AppMemberNotice.Columns() c := dao.AppMemberNotice.Columns()
_, err := s.noticeModel(ctx).Fields(c.MemberId, c.Title, c.Content, c.SourceType, c.StartAt, c.EndAt, c.NoticeStatus, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.MemberId: in.MemberId, c.Title: in.Title, c.Content: in.Content, c.SourceType: in.SourceType, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.NoticeStatus: consts.MemberNoticeActive, c.CreatedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Insert() _, err = s.noticeModel(ctx).Fields(c.MemberId, c.Title, c.Content, c.SourceType, c.StartAt, c.EndAt, c.NoticeStatus, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.MemberId: in.MemberId, c.Title: in.Title, c.Content: in.Content, c.SourceType: in.SourceType, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.NoticeStatus: consts.MemberNoticeActive, c.CreatedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Insert()
if err != nil { if err != nil {
return gerror.Wrap(err, "创建会员通知失败") return gerror.Wrap(err, "创建会员通知失败")
} }
@@ -3,6 +3,7 @@ package sys
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"hotgo/internal/consts" "hotgo/internal/consts"
"hotgo/internal/dao" "hotgo/internal/dao"
@@ -173,6 +174,10 @@ func (s *sSysRechargeOrder) Review(ctx context.Context, in *sysin.RechargeReview
rc.UpdatedBy: contexts.GetUserId(ctx), rc.UpdatedBy: contexts.GetUserId(ctx),
} }
if in.Decision == consts.FinanceDecisionApprove { if in.Decision == consts.FinanceDecisionApprove {
tradeOrders, lockErr := NewSysTradeOrder().lockBalancePendingOrders(ctx, order.MemberId)
if lockErr != nil {
return lockErr
}
wc := dao.WalletAccount.Columns() wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount 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 { 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 {
@@ -181,6 +186,9 @@ func (s *sSysRechargeOrder) Review(ctx context.Context, in *sysin.RechargeReview
if account == nil || account.AccountStatus != 1 { if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可入账") return gerror.New("会员钱包不存在或不可入账")
} }
if in.PaidAmount > 0 && account.AvailableAmount > math.MaxInt64-in.PaidAmount {
return gerror.New("充值金额超出钱包处理范围")
}
after := account.AvailableAmount + in.PaidAmount after := account.AvailableAmount + in.PaidAmount
lc := dao.WalletLedger.Columns() lc := dao.WalletLedger.Columns()
result, insertErr := dao.WalletLedger.Ctx(ctx). result, insertErr := dao.WalletLedger.Ctx(ctx).
@@ -205,6 +213,9 @@ func (s *sSysRechargeOrder) Review(ctx context.Context, in *sysin.RechargeReview
if rows != 1 { if rows != 1 {
return gerror.New("钱包余额已变化,请重试") return gerror.New("钱包余额已变化,请重试")
} }
if err = NewSysTradeOrder().completeBalancePendingOrders(ctx, tradeOrders); err != nil {
return err
}
status = consts.RechargeSuccess status = consts.RechargeSuccess
updateData[rc.RechargeStatus] = status updateData[rc.RechargeStatus] = status
updateData[rc.PaidAmount] = in.PaidAmount updateData[rc.PaidAmount] = in.PaidAmount
@@ -251,6 +262,10 @@ func (s *sSysRechargeOrder) Execute(ctx context.Context, in *sysin.FinanceExecut
now := gtime.Now() now := gtime.Now()
ledgerId := uint64(0) ledgerId := uint64(0)
if input.Decision == consts.FinanceDecisionApprove { if input.Decision == consts.FinanceDecisionApprove {
tradeOrders, lockErr := NewSysTradeOrder().lockBalancePendingOrders(ctx, order.MemberId)
if lockErr != nil {
return lockErr
}
wc := dao.WalletAccount.Columns() wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount 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 { 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 {
@@ -259,6 +274,9 @@ func (s *sSysRechargeOrder) Execute(ctx context.Context, in *sysin.FinanceExecut
if account == nil || account.AccountStatus != 1 { if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可入账") return gerror.New("会员钱包不存在或不可入账")
} }
if input.PaidAmount > 0 && account.AvailableAmount > math.MaxInt64-input.PaidAmount {
return gerror.New("充值金额超出钱包处理范围")
}
after := account.AvailableAmount + input.PaidAmount after := account.AvailableAmount + input.PaidAmount
lc := dao.WalletLedger.Columns() 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() 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()
@@ -275,6 +293,9 @@ func (s *sSysRechargeOrder) Execute(ctx context.Context, in *sysin.FinanceExecut
if rows != 1 { if rows != 1 {
return gerror.New("钱包余额已变化,请重试") return gerror.New("钱包余额已变化,请重试")
} }
if err = NewSysTradeOrder().completeBalancePendingOrders(ctx, tradeOrders); err != nil {
return err
}
_, 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() _, 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 { if err != nil {
return err return err
+151 -31
View File
@@ -77,7 +77,7 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp)
} }
for _, row := range list { for _, row := range list {
row.CanMatch = canAdminMatchOrder(row.AutoDispatch, row.OrderStatus, row.ApprovalRequestId, row.ScheduledMatchAt, time.Now()) row.CanMatch = canAdminMatchOrder(row.AutoDispatch, row.OrderStatus, row.ApprovalRequestId, row.ScheduledMatchAt, time.Now())
manualPending := row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderPending && row.FreezeStatus == consts.TradeFreezeNormal manualPending := row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderPending && row.FreezeStatus == consts.TradeFreezeNormal && row.PaymentStatus != "balance_pending"
row.CanChangeAmount = manualPending row.CanChangeAmount = manualPending
row.CanForcePay = manualPending row.CanForcePay = manualPending
row.CanFreeze = manualPending row.CanFreeze = manualPending
@@ -276,19 +276,34 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
snapshotData["member_level_id"], snapshotData["level_version_id"] = levelId, levelVersion.Id snapshotData["member_level_id"], snapshotData["level_version_id"] = levelId, levelVersion.Id
snapshotData["commission_rate"], snapshotData["manual_payment_status"] = levelVersion.CommissionRate, "settled" snapshotData["commission_rate"], snapshotData["manual_payment_status"] = levelVersion.CommissionRate, "settled"
snapshotData["confirmation_required"], snapshotData["deadline_at"] = false, "" snapshotData["confirmation_required"], snapshotData["deadline_at"] = false, ""
snapshotData["settled_by"], snapshotData["settled_at"] = "admin", time.Now().UTC().Format(time.RFC3339) settlement, err := s.settleMatchedOrder(ctx, order, dispatchMode, totalAmount, commissionAmount, now)
snapshot := gjson.New(snapshotData)
commissionLedgerId, err := s.settleMatchedOrder(ctx, order, dispatchMode, totalAmount, commissionAmount, now)
if err != nil { if err != nil {
return err return err
} }
balancePending := !settlement.Settled
orderStatus, message := consts.TradeOrderCompleted, "后台已匹配并完成订单,本金已返还,佣金已结算"
var completedAt *gtime.Time = now
if balancePending {
orderStatus = consts.TradeOrderPending
message = balanceShortageMessage(settlement.AvailableAmount, totalAmount)
completedAt = nil
snapshotData["balance_shortage_pending"] = true
snapshotData["balance_available_amount"] = settlement.AvailableAmount
snapshotData["balance_shortfall_amount"] = settlement.ShortfallAmount
snapshotData["manual_payment_status"] = "balance_pending"
} else {
snapshotData["balance_shortage_pending"] = false
snapshotData["balance_shortfall_amount"] = int64(0)
snapshotData["settled_by"], snapshotData["settled_at"] = "admin", time.Now().UTC().Format(time.RFC3339)
}
snapshot := gjson.New(snapshotData)
oc := dao.TradeOrder.Columns() oc := dao.TradeOrder.Columns()
data := g.Map{ data := g.Map{
oc.ProductId: product.Id, oc.ProductName: product.Name, oc.ProductId: product.Id, oc.ProductName: product.Name,
oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount, oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount,
oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId, oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId,
oc.RuleSnapshot: snapshot, oc.Message: "后台已匹配并完成订单,余额及佣金已结算", oc.OrderStatus: consts.TradeOrderCompleted, oc.RuleSnapshot: snapshot, oc.Message: message, oc.OrderStatus: orderStatus,
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.CompletedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx), oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.CompletedAt: completedAt, oc.UpdatedBy: contexts.GetUserId(ctx),
} }
result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderMatching).Data(data).Update() result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderMatching).Data(data).Update()
if err != nil { if err != nil {
@@ -298,43 +313,54 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
if rows != 1 { if rows != 1 {
return gerror.New("订单状态已变化,请刷新后重试") return gerror.New("订单状态已变化,请刷新后重试")
} }
if err = s.insertMatchedSubmission(ctx, order, levelId, commissionAmount, commissionLedgerId, now, dispatchMode); err != nil { if !balancePending {
return err if err = s.insertMatchedSubmission(ctx, order, levelId, commissionAmount, settlement.CommissionLedgerId, now, dispatchMode); err != nil {
return err
}
} }
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{ return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
Module: "trade", Action: "trade_order.match", TargetType: "trade_order", TargetId: order.Id, Module: "trade", Action: "trade_order.match", TargetType: "trade_order", TargetId: order.Id,
Before: order, After: data, ChangeJson: gjson.New(g.Map{"productId": product.Id, "quantity": in.Quantity, "totalAmount": totalAmount}), Before: order, After: data, ChangeJson: gjson.New(g.Map{"productId": product.Id, "quantity": in.Quantity, "totalAmount": totalAmount}),
Reason: "后台人工匹配商品", Reason: message,
}) })
}) })
} }
func (s *sSysTradeOrder) settleMatchedOrder(ctx context.Context, order *entity.TradeOrder, dispatchMode string, totalAmount, commissionAmount int64, now *gtime.Time) (uint64, error) { type matchedOrderSettlement struct {
CommissionLedgerId uint64
AvailableAmount int64
ShortfallAmount int64
Settled bool
}
func (s *sSysTradeOrder) settleMatchedOrder(ctx context.Context, order *entity.TradeOrder, dispatchMode string, totalAmount, commissionAmount int64, now *gtime.Time) (*matchedOrderSettlement, error) {
wc := dao.WalletAccount.Columns() wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount var account *entity.WalletAccount
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}). if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
Where(wc.MemberId, order.MemberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil { Where(wc.MemberId, order.MemberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
return 0, gerror.Wrap(err, "读取会员钱包失败") return nil, gerror.Wrap(err, "读取会员钱包失败")
} }
if account == nil || account.AccountStatus != 1 { if account == nil || account.AccountStatus != 1 {
return 0, gerror.New("会员钱包不存在或状态异常") return nil, gerror.New("会员钱包不存在或状态异常")
} }
if totalAmount <= 0 || account.AvailableAmount < math.MinInt64+totalAmount { if totalAmount <= 0 || account.AvailableAmount < math.MinInt64+totalAmount {
return 0, gerror.New("订单金额超出钱包处理范围") return nil, gerror.New("订单金额超出钱包处理范围")
} }
if commissionAmount < 0 || account.AvailableAmount > math.MaxInt64-commissionAmount { if commissionAmount < 0 || account.AvailableAmount > math.MaxInt64-commissionAmount {
return 0, gerror.New("订单佣金超出钱包处理范围") return nil, gerror.New("订单佣金超出钱包处理范围")
} }
if dispatchMode == "auto" && account.AvailableAmount < totalAmount { if account.AvailableAmount < totalAmount {
return 0, gerror.New("自动派单商品金额不能超过会员可用余额") if account.AvailableAmount < 0 && totalAmount > math.MaxInt64+account.AvailableAmount {
return nil, gerror.New("余额差额超出可计算范围")
}
return &matchedOrderSettlement{AvailableAmount: account.AvailableAmount, ShortfallAmount: totalAmount - account.AvailableAmount}, nil
} }
principalAfter := account.AvailableAmount - totalAmount principalAfter := account.AvailableAmount - totalAmount
settledAfter := principalAfter + commissionAmount settledAfter := account.AvailableAmount + commissionAmount
if dispatchMode == "auto" { if dispatchMode == "auto" {
settledAfter = account.AvailableAmount + commissionAmount
if account.FrozenAmount > math.MaxInt64-totalAmount { if account.FrozenAmount > math.MaxInt64-totalAmount {
return 0, gerror.New("订单金额超出钱包冻结范围") return nil, gerror.New("订单金额超出钱包冻结范围")
} }
} }
accountData := g.Map{ accountData := g.Map{
@@ -342,15 +368,12 @@ func (s *sSysTradeOrder) settleMatchedOrder(ctx context.Context, order *entity.T
wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(commissionAmount)), wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(commissionAmount)),
wc.Version: gdb.Raw(wc.Version + "+1"), wc.Version: gdb.Raw(wc.Version + "+1"),
} }
if dispatchMode == "manual" {
accountData[wc.TotalExpense] = gdb.Raw(wc.TotalExpense + "+" + fmt.Sprint(totalAmount))
}
result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(accountData).Update() result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(accountData).Update()
if err != nil { if err != nil {
return 0, gerror.Wrap(err, "结算订单余额失败") return nil, gerror.Wrap(err, "结算订单余额失败")
} }
if affected, _ := result.RowsAffected(); affected != 1 { if affected, _ := result.RowsAffected(); affected != 1 {
return 0, gerror.New("会员余额已变化,请刷新后重试") return nil, gerror.New("会员余额已变化,请刷新后重试")
} }
if dispatchMode == "auto" { if dispatchMode == "auto" {
@@ -368,22 +391,116 @@ func (s *sSysTradeOrder) settleMatchedOrder(ctx context.Context, order *entity.T
} }
for _, entry := range entries { for _, entry := range entries {
if _, err = insertMatchedOrderLedger(ctx, order, account, "WL"+fmt.Sprint(order.Id)+entry.code, entry.bizType, order.OrderNo, entry.entryType, entry.direction, totalAmount, entry.before, entry.after, entry.suffix, entry.remark, now); err != nil { if _, err = insertMatchedOrderLedger(ctx, order, account, "WL"+fmt.Sprint(order.Id)+entry.code, entry.bizType, order.OrderNo, entry.entryType, entry.direction, totalAmount, entry.before, entry.after, entry.suffix, entry.remark, now); err != nil {
return 0, err return nil, err
} }
} }
} else { } else {
if _, err = insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMP", order.Id), "task_principal_expense", order.OrderNo, "available", consts.WalletDirectionExpense, totalAmount, account.AvailableAmount, principalAfter, "admin-manual:principal", "手动单匹配完成扣除订单金额(允许负余额)", now); err != nil { if _, err = insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMP", order.Id), "task_principal_expense", order.OrderNo, "available", consts.WalletDirectionExpense, totalAmount, account.AvailableAmount, principalAfter, "admin-manual:principal", "手动单匹配完成扣除订单金额(允许负余额)", now); err != nil {
return 0, err return nil, err
}
if _, err = insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMR", order.Id), "task_principal_return", order.OrderNo, "available", consts.WalletDirectionIncome, totalAmount, principalAfter, account.AvailableAmount, "admin-manual:return-available", "手动单完成返还订单本金", now); err != nil {
return nil, err
} }
} }
if commissionAmount == 0 { if commissionAmount == 0 {
return 0, nil return &matchedOrderSettlement{AvailableAmount: account.AvailableAmount, Settled: true}, nil
} }
commissionBefore := principalAfter ledgerId, err := insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMC", order.Id), "commission", fmt.Sprintf("TSADMIN%d", order.Id), "available", consts.WalletDirectionIncome, commissionAmount, account.AvailableAmount, settledAfter, "admin-match:commission", "后台匹配订单完成返佣", now)
if dispatchMode == "auto" { if err != nil {
commissionBefore = account.AvailableAmount return nil, err
} }
return insertMatchedOrderLedger(ctx, order, account, fmt.Sprintf("WL%dMC", order.Id), "commission", fmt.Sprintf("TSADMIN%d", order.Id), "available", consts.WalletDirectionIncome, commissionAmount, commissionBefore, settledAfter, "admin-match:commission", "后台匹配订单完成返佣", now) return &matchedOrderSettlement{CommissionLedgerId: ledgerId, AvailableAmount: settledAfter, Settled: true}, nil
}
func balanceShortageMessage(availableAmount, requiredAmount int64) string {
shortfall := requiredAmount - availableAmount
return fmt.Sprintf("余额不足:当前可用 ¥%s,订单需要 ¥%s,还差 ¥%s。充值审核通过且余额足够后,订单将自动完成。",
formatSignedTradeCents(availableAmount), formatCents(requiredAmount), formatCents(shortfall))
}
func formatSignedTradeCents(amount int64) string {
if amount >= 0 {
return formatCents(amount)
}
magnitude := uint64(-(amount + 1)) + 1
return fmt.Sprintf("-%d.%02d", magnitude/100, magnitude%100)
}
func (s *sSysTradeOrder) lockBalancePendingOrders(ctx context.Context, memberId uint64) ([]*entity.TradeOrder, error) {
oc := dao.TradeOrder.Columns()
orders := make([]*entity.TradeOrder, 0)
err := s.model(ctx).
Where(oc.MemberId, memberId).
Where(oc.OrderStatus, consts.TradeOrderPending).
Where("JSON_UNQUOTE(JSON_EXTRACT("+oc.RuleSnapshot+", '$.balance_shortage_pending')) = ?", "true").
OrderAsc(oc.Id).LockUpdate().Scan(&orders)
if err != nil {
return nil, gerror.Wrap(err, "读取余额不足待处理订单失败")
}
return orders, nil
}
func (s *sSysTradeOrder) completeBalancePendingOrders(ctx context.Context, orders []*entity.TradeOrder) error {
if len(orders) == 0 {
return nil
}
oc := dao.TradeOrder.Columns()
for _, order := range orders {
if order == nil || order.OrderStatus != consts.TradeOrderPending || order.TotalAmount <= 0 {
continue
}
snapshot := cloneOrderSnapshot(order)
dispatchMode := gjson.New(snapshot).Get("dispatch_mode").String()
if dispatchMode == "" {
dispatchMode = "manual"
}
now := gtime.Now()
settlement, err := s.settleMatchedOrder(ctx, order, dispatchMode, order.TotalAmount, order.CommissionAmount, now)
if err != nil {
return err
}
data := g.Map{oc.UpdatedBy: contexts.GetUserId(ctx)}
if !settlement.Settled {
snapshot["balance_available_amount"] = settlement.AvailableAmount
snapshot["balance_shortfall_amount"] = settlement.ShortfallAmount
data[oc.RuleSnapshot] = gjson.New(snapshot)
data[oc.Message] = balanceShortageMessage(settlement.AvailableAmount, order.TotalAmount)
if _, err = s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderPending).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新余额不足提示失败")
}
continue
}
snapshot["balance_shortage_pending"] = false
snapshot["balance_available_amount"] = settlement.AvailableAmount
snapshot["balance_shortfall_amount"] = int64(0)
snapshot["manual_payment_status"] = "settled"
snapshot["settled_by"] = "recharge_auto"
snapshot["settled_at"] = time.Now().UTC().Format(time.RFC3339)
message := "充值审核通过后余额充足,订单已自动完成,本金已返还,佣金已结算"
data[oc.RuleSnapshot] = gjson.New(snapshot)
data[oc.Message] = message
data[oc.OrderStatus] = consts.TradeOrderCompleted
data[oc.CompletedAt] = now
result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderPending).Data(data).Update()
if err != nil {
return gerror.Wrap(err, "自动完成余额已充足的订单失败")
}
if affected, _ := result.RowsAffected(); affected != 1 {
return gerror.New("待处理订单状态已变化,请重试")
}
if err = s.insertMatchedSubmission(ctx, order, order.MemberLevelId, order.CommissionAmount, settlement.CommissionLedgerId, now, dispatchMode); err != nil {
return err
}
if err = service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
Module: "trade", Action: "trade_order.complete_after_recharge", TargetType: "trade_order", TargetId: order.Id,
Before: order, After: data, ChangeJson: gjson.New(g.Map{"availableAmount": settlement.AvailableAmount, "totalAmount": order.TotalAmount}),
Reason: message,
}); err != nil {
return err
}
}
return nil
} }
func (s *sSysTradeOrder) insertMatchedSubmission(ctx context.Context, order *entity.TradeOrder, levelId uint64, commissionAmount int64, ledgerId uint64, now *gtime.Time, dispatchMode string) error { func (s *sSysTradeOrder) insertMatchedSubmission(ctx context.Context, order *entity.TradeOrder, levelId uint64, commissionAmount int64, ledgerId uint64, now *gtime.Time, dispatchMode string) error {
@@ -595,6 +712,9 @@ func validateManualPaymentOrder(order *entity.TradeOrder, requireUnpaid bool) er
if order.OrderStatus != consts.TradeOrderPending || order.FreezeStatus != consts.TradeFreezeNormal || order.RuleSnapshot == nil || order.RuleSnapshot.Get("dispatch_mode").String() != "manual" { if order.OrderStatus != consts.TradeOrderPending || order.FreezeStatus != consts.TradeFreezeNormal || order.RuleSnapshot == nil || order.RuleSnapshot.Get("dispatch_mode").String() != "manual" {
return gerror.New("当前订单状态不允许此操作") return gerror.New("当前订单状态不允许此操作")
} }
if order.RuleSnapshot.Get("balance_shortage_pending").Bool() {
return gerror.New("订单因余额不足待处理,充值审核通过且余额足够后将自动完成")
}
if requireUnpaid && order.RuleSnapshot.Get("manual_payment_status").String() == "paid" { if requireUnpaid && order.RuleSnapshot.Get("manual_payment_status").String() == "paid" {
return gerror.New("订单已经付款") return gerror.New("订单已经付款")
} }
@@ -1,6 +1,7 @@
package sys package sys
import ( import (
"strings"
"testing" "testing"
"time" "time"
@@ -35,3 +36,12 @@ func TestCanAdminMatchOrder(t *testing.T) {
}) })
} }
} }
func TestBalanceShortageMessageShowsExactDifference(t *testing.T) {
message := balanceShortageMessage(30000, 50000)
for _, expected := range []string{"余额不足", "¥300.00", "¥500.00", "还差 ¥200.00", "充值审核通过"} {
if !strings.Contains(message, expected) {
t.Fatalf("balanceShortageMessage() = %q, missing %q", message, expected)
}
}
}
+36 -11
View File
@@ -33,6 +33,8 @@ type AppMemberListModel struct {
MemberNo string `json:"memberNo"` MemberNo string `json:"memberNo"`
Username string `json:"username"` Username string `json:"username"`
Mobile string `json:"mobile"` Mobile string `json:"mobile"`
RealName string `json:"realName"`
RealNameCipher string `json:"-"`
RealNameMask string `json:"realNameMask"` RealNameMask string `json:"realNameMask"`
AvailableAmount int64 `json:"availableAmount"` AvailableAmount int64 `json:"availableAmount"`
FrozenAmount int64 `json:"frozenAmount"` FrozenAmount int64 `json:"frozenAmount"`
@@ -63,6 +65,7 @@ func (in *AppMemberViewInp) Filter(ctx context.Context) error { return nil }
type AppMemberViewModel struct { type AppMemberViewModel struct {
AppMemberListModel AppMemberListModel
IdNo string `json:"idNo"`
LevelId uint64 `json:"levelId"` LevelId uint64 `json:"levelId"`
Level int `json:"level"` Level int `json:"level"`
CreditScore int `json:"creditScore"` CreditScore int `json:"creditScore"`
@@ -158,11 +161,21 @@ type AppMemberPaymentModel struct {
AccountType int `json:"accountType"` AccountType int `json:"accountType"`
BankCode string `json:"bankCode"` BankCode string `json:"bankCode"`
BankName string `json:"bankName"` BankName string `json:"bankName"`
Holder string `json:"holder"`
IdentityNo string `json:"identityNo"`
Mobile string `json:"mobile"`
AccountNo string `json:"accountNo"`
DigitalRMBCode string `json:"digitalRmbCode"`
HolderMask string `json:"holderMask"` HolderMask string `json:"holderMask"`
HolderCipher string `json:"-"`
IdentityNoMask string `json:"identityNoMask"` IdentityNoMask string `json:"identityNoMask"`
IdentityNoCipher string `json:"-"`
MobileMask string `json:"mobileMask"` MobileMask string `json:"mobileMask"`
MobileCipher string `json:"-"`
AccountNoMask string `json:"accountNoMask"` AccountNoMask string `json:"accountNoMask"`
AccountNoCipher string `json:"-"`
DigitalRMBCodeMask string `json:"digitalRmbCodeMask"` DigitalRMBCodeMask string `json:"digitalRmbCodeMask"`
DigitalRmbCodeCipher string `json:"-"`
QrAttachmentId uint64 `json:"qrAttachmentId"` QrAttachmentId uint64 `json:"qrAttachmentId"`
WechatQrAttachmentId uint64 `json:"wechatQrAttachmentId"` WechatQrAttachmentId uint64 `json:"wechatQrAttachmentId"`
AlipayQrAttachmentId uint64 `json:"alipayQrAttachmentId"` AlipayQrAttachmentId uint64 `json:"alipayQrAttachmentId"`
@@ -216,17 +229,23 @@ type AppMemberAddressListInp struct {
func (in *AppMemberAddressListInp) Filter(ctx context.Context) error { return nil } func (in *AppMemberAddressListInp) Filter(ctx context.Context) error { return nil }
type AppMemberAddressModel struct { type AppMemberAddressModel struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
MemberId uint64 `json:"memberId"` MemberId uint64 `json:"memberId"`
ReceiverMask string `json:"receiverMask"` Receiver string `json:"receiver"`
MobileMask string `json:"mobileMask"` Mobile string `json:"mobile"`
Province string `json:"province"` Address string `json:"address"`
City string `json:"city"` ReceiverCipher string `json:"-"`
District string `json:"district"` MobileCipher string `json:"-"`
AddressMask string `json:"addressMask"` AddressCipher string `json:"-"`
IsDefault int `json:"isDefault"` ReceiverMask string `json:"receiverMask"`
CreatedAt *gtime.Time `json:"createdAt"` MobileMask string `json:"mobileMask"`
UpdatedAt *gtime.Time `json:"updatedAt"` Province string `json:"province"`
City string `json:"city"`
District string `json:"district"`
AddressMask string `json:"addressMask"`
IsDefault int `json:"isDefault"`
CreatedAt *gtime.Time `json:"createdAt"`
UpdatedAt *gtime.Time `json:"updatedAt"`
} }
type AppMemberAddressEditInp struct { type AppMemberAddressEditInp struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
@@ -414,6 +433,12 @@ type MemberNoticeEditInp struct {
} }
func (in *MemberNoticeEditInp) Filter(ctx context.Context) error { func (in *MemberNoticeEditInp) Filter(ctx context.Context) error {
if in.MemberId == 0 {
return gerror.New("请选择接收公告的会员")
}
if in.StartAt == nil || in.EndAt == nil {
return gerror.New("请设置公告有效期")
}
if in.StartAt.After(in.EndAt) { if in.StartAt.After(in.EndAt) {
return gerror.New("结束时间必须晚于开始时间") return gerror.New("结束时间必须晚于开始时间")
} }
@@ -0,0 +1,20 @@
package sysin
import (
"context"
"testing"
)
func TestMemberNoticeEditInpRequiresRecipient(t *testing.T) {
in := &MemberNoticeEditInp{}
if err := in.Filter(context.Background()); err == nil {
t.Fatal("expected a missing recipient to be rejected")
}
}
func TestMemberNoticeEditInpRequiresValidityRange(t *testing.T) {
in := &MemberNoticeEditInp{MemberId: 1}
if err := in.Filter(context.Background()); err == nil {
t.Fatal("expected a missing validity range to be rejected")
}
}
+56 -109
View File
@@ -136,9 +136,9 @@
<n-descriptions-item label="会员编号">{{ detail.memberNo }}</n-descriptions-item> <n-descriptions-item label="会员编号">{{ detail.memberNo }}</n-descriptions-item>
<n-descriptions-item label="手机号">{{ detail.mobile }}</n-descriptions-item> <n-descriptions-item label="手机号">{{ detail.mobile }}</n-descriptions-item>
<n-descriptions-item label="真实姓名">{{ <n-descriptions-item label="真实姓名">{{
detail.realNameMask || '-' detail.realName || detail.realNameMask || '-'
}}</n-descriptions-item> }}</n-descriptions-item>
<n-descriptions-item label="证件号">{{ detail.idNoMask || '-' }}</n-descriptions-item> <n-descriptions-item label="证件号">{{ detail.idNo || detail.idNoMask || '-' }}</n-descriptions-item>
<n-descriptions-item label="团队层级">{{ detail.level }}</n-descriptions-item> <n-descriptions-item label="团队层级">{{ detail.level }}</n-descriptions-item>
<n-descriptions-item label="可用余额">{{ <n-descriptions-item label="可用余额">{{
money(detail.availableAmount) money(detail.availableAmount)
@@ -165,12 +165,6 @@
>收货地址</n-button >收货地址</n-button
> >
<n-button v-if="hasPermission(['/appMember/team'])" @click="openTeam">团队查询</n-button> <n-button v-if="hasPermission(['/appMember/team'])" @click="openTeam">团队查询</n-button>
<n-button
v-if="hasPermission(['/appMember/sensitive'])"
type="warning"
@click="openSensitive('member', detail.id)"
>授权查看明文</n-button
>
</n-space> </n-space>
</n-spin> </n-spin>
</n-modal> </n-modal>
@@ -184,14 +178,8 @@
<n-spin :show="relatedLoading"> <n-spin :show="relatedLoading">
<n-form label-placement="left" label-width="110"> <n-form label-placement="left" label-width="110">
<template v-if="relatedType === 'payment'"> <template v-if="relatedType === 'payment'">
<n-alert v-if="paymentForm.id" type="info" :show-icon="false"> <n-alert v-if="paymentForm.id" type="warning" :show-icon="false">
当前默认显示脱敏资料;留空保存不会覆盖原敏感字段或收款码。 敏感资料已明文展示;请仅用于当前业务处理。留空保存不会覆盖原敏感字段或收款码。
<n-button
v-if="hasPermission(['/appMember/sensitive'])"
text
type="primary"
@click="openSensitive('payment', paymentForm.id)"
>授权查看明文</n-button>
</n-alert> </n-alert>
<n-form-item label="开户人" <n-form-item label="开户人"
><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'" ><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'"
@@ -284,35 +272,6 @@
<n-empty v-if="!ledgerLoading && ledgerRows.length === 0" description="暂无账变记录" /> <n-empty v-if="!ledgerLoading && ledgerRows.length === 0" description="暂无账变记录" />
</n-modal> </n-modal>
<n-modal
v-model:show="showSensitive"
preset="dialog"
title="授权查看敏感信息"
:show-icon="false"
>
<n-alert type="error"
>明文仅用于当前业务目的,请勿复制传播。本次操作将记录查看人、对象、用途和结果。</n-alert
>
<n-form label-placement="top">
<n-form-item label="用途编码"
><n-input
v-model:value="sensitiveForm.purposeCode"
placeholder="如 customer_service_verify"
/></n-form-item>
<n-form-item label="用途说明"
><n-input v-model:value="sensitiveForm.purposeText" type="textarea"
/></n-form-item>
<n-checkbox v-model:checked="sensitiveForm.confirm">我确认业务需要并接受留痕</n-checkbox>
</n-form>
<n-code v-if="sensitiveResult" :code="sensitiveResult" language="json" word-wrap />
<template #action
><n-space
><n-button @click="showSensitive = false">关闭</n-button
><n-button type="error" @click="revealSensitive">确认查看</n-button></n-space
></template
>
</n-modal>
<n-modal v-model:show="showStatus" preset="dialog" title="信息" :show-icon="false"> <n-modal v-model:show="showStatus" preset="dialog" title="信息" :show-icon="false">
<div>确定{{ statusForm.toStatus === 1 ? '启用' : '禁用' }}此用户吗?</div> <div>确定{{ statusForm.toStatus === 1 ? '启用' : '禁用' }}此用户吗?</div>
<template #action <template #action
@@ -341,8 +300,11 @@
> >
</n-modal> </n-modal>
<n-modal v-model:show="showNotice" preset="card" title="发送弹幕" class="related-modal"> <n-modal v-model:show="showNotice" preset="card" title="发送系统通知" class="related-modal">
<n-form label-placement="left" label-width="100"> <n-form label-placement="left" label-width="100">
<n-alert type="info" :show-icon="false">
仅当前会员可见;会员在任意已登录页面通常15秒内收到系统通知弹窗,首页公告栏继续轮询。
</n-alert>
<n-form-item label="用户ID" <n-form-item label="用户ID"
><n-input :value="String(noticeMember?.id || '')" disabled ><n-input :value="String(noticeMember?.id || '')" disabled
/></n-form-item> /></n-form-item>
@@ -352,8 +314,18 @@
<n-form-item label="用户手机" <n-form-item label="用户手机"
><n-input :value="noticeMember?.mobile || ''" disabled ><n-input :value="noticeMember?.mobile || ''" disabled
/></n-form-item> /></n-form-item>
<n-form-item label="发送内容" <n-form-item label="通知标题"
><n-input v-model:value="noticeForm.content" placeholder="请输入发送内容" ><n-input v-model:value="noticeForm.title" maxlength="120" placeholder="请输入通知标题"
/></n-form-item>
<n-form-item label="通知内容"
><n-input v-model:value="noticeForm.content" type="textarea" maxlength="500" placeholder="请输入通知内容"
/></n-form-item>
<n-form-item label="有效期"
><n-date-picker
v-model:formatted-value="noticeDateRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/></n-form-item> /></n-form-item>
</n-form> </n-form>
<template #action <template #action
@@ -389,7 +361,6 @@
AddressList, AddressList,
AddressEdit, AddressEdit,
AddressDelete, AddressDelete,
Sensitive,
Team, Team,
StatusRequest, StatusRequest,
LedgerList, LedgerList,
@@ -429,7 +400,7 @@
editFormRef = ref<any>(), editFormRef = ref<any>(),
memberTableWrapRef = ref<HTMLElement | null>(null); memberTableWrapRef = ref<HTMLElement | null>(null);
const [register] = useForm({ const [register] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' }, gridProps: { cols: '1 s:2 m:4 l:6 xl:8 2xl:8' },
labelWidth: 80, labelWidth: 80,
schemas, schemas,
}); });
@@ -439,7 +410,6 @@
showRelatedForm = ref(false), showRelatedForm = ref(false),
showTeam = ref(false), showTeam = ref(false),
showLedger = ref(false), showLedger = ref(false),
showSensitive = ref(false),
showStatus = ref(false), showStatus = ref(false),
showWallet = ref(false), showWallet = ref(false),
showNotice = ref(false), showNotice = ref(false),
@@ -454,20 +424,12 @@
teamRows = ref<any[]>([]), teamRows = ref<any[]>([]),
relatedType = ref<'payment' | 'address'>('payment'), relatedType = ref<'payment' | 'address'>('payment'),
teamLevel = ref(3), teamLevel = ref(3),
sensitiveResult = ref(''),
walletYuan = ref<number | null>(null), walletYuan = ref<number | null>(null),
memberLevelOptions = ref<any[]>([]), memberLevelOptions = ref<any[]>([]),
noticeMember = ref<MemberRecord | null>(null); noticeMember = ref<MemberRecord | null>(null);
const editForm = reactive<any>({}), const editForm = reactive<any>({}),
paymentForm = reactive<any>({}), paymentForm = reactive<any>({}),
addressForm = reactive<any>({}), addressForm = reactive<any>({}),
sensitiveForm = reactive<any>({
targetType: 'member',
targetId: 0,
purposeCode: '',
purposeText: '',
confirm: false,
}),
statusForm = reactive<any>({ memberId: 0, toStatus: 2, reasonCode: '', reasonText: '' }), statusForm = reactive<any>({ memberId: 0, toStatus: 2, reasonCode: '', reasonText: '' }),
walletForm = reactive<any>({ walletForm = reactive<any>({
memberId: 0, memberId: 0,
@@ -508,8 +470,8 @@
trigger: ['input', 'blur'], trigger: ['input', 'blur'],
}, },
}; };
const noticeForm = reactive({ memberId: 0, title: '', content: '', sourceType: 2 }); const noticeForm = reactive({ memberId: 0, title: '', content: '', sourceType: 1 });
const noticeDateRange = ref<[string, string]>(['', '']); const noticeDateRange = ref<[string, string] | null>(null);
const ledgerRows = ref<any[]>([]), const ledgerRows = ref<any[]>([]),
ledgerMember = ref<MemberRecord | null>(null); ledgerMember = ref<MemberRecord | null>(null);
const reset = (obj, value) => { const reset = (obj, value) => {
@@ -580,7 +542,7 @@
auth: ['/appMember/walletRequest'], auth: ['/appMember/walletRequest'],
}, },
{ {
label: '弹幕', label: '弹窗',
color: '#009688', color: '#009688',
onClick: () => openNotice(record), onClick: () => openNotice(record),
auth: ['/appMember/noticeEdit'], auth: ['/appMember/noticeEdit'],
@@ -772,16 +734,16 @@
reset(paymentForm, { reset(paymentForm, {
memberId, memberId,
id: bank?.id || 0, id: bank?.id || 0,
holder: '', holder: bank?.holder || bank?.holderMask || '',
holderMask: bank?.holderMask || '', holderMask: bank?.holderMask || '',
identityNo: '', identityNo: bank?.identityNo || bank?.identityNoMask || '',
identityNoMask: bank?.identityNoMask || '', identityNoMask: bank?.identityNoMask || '',
bankName: bank?.bankName || '', bankName: bank?.bankName || '',
mobile: '', mobile: bank?.mobile || bank?.mobileMask || '',
mobileMask: bank?.mobileMask || '', mobileMask: bank?.mobileMask || '',
bankAccount: '', bankAccount: bank?.accountNo || bank?.accountNoMask || '',
accountNoMask: bank?.accountNoMask || '', accountNoMask: bank?.accountNoMask || '',
digitalWallet: '', digitalWallet: bank?.digitalRmbCode || bank?.digitalRmbCodeMask || '',
digitalRmbCodeMask: bank?.digitalRmbCodeMask || '', digitalRmbCodeMask: bank?.digitalRmbCodeMask || '',
alipayQrCode: bank?.alipayQrUrl || '', alipayQrCode: bank?.alipayQrUrl || '',
wechatQrCode: bank?.wechatQrUrl || '', wechatQrCode: bank?.wechatQrUrl || '',
@@ -794,8 +756,8 @@
reset(addressForm, { reset(addressForm, {
id: row?.id || 0, id: row?.id || 0,
memberId, memberId,
receiver: '', receiver: row?.receiver || row?.receiverMask || '',
mobile: '', mobile: row?.mobile || row?.mobileMask || '',
region, region,
originalRegion: region, originalRegion: region,
province: row?.province || '', province: row?.province || '',
@@ -839,7 +801,7 @@
province: '', province: '',
city: '', city: '',
district: '', district: '',
address: '', address: row?.address || row?.addressMask || '',
isDefault: 2, isDefault: 2,
}); });
} }
@@ -893,9 +855,11 @@
memberId: row.memberId, memberId: row.memberId,
accountType: row.accountType, accountType: row.accountType,
bankName: row.bankName, bankName: row.bankName,
holder: '', holder: row.holder || row.holderMask || '',
mobile: '', identityNo: row.identityNo || row.identityNoMask || '',
accountNo: '', mobile: row.mobile || row.mobileMask || '',
accountNo: row.accountNo || row.accountNoMask || '',
digitalRmbCode: row.digitalRmbCode || row.digitalRmbCodeMask || '',
verifyStatus: row.verifyStatus, verifyStatus: row.verifyStatus,
isDefault: row.isDefault, isDefault: row.isDefault,
}); });
@@ -903,30 +867,15 @@
reset(addressForm, { reset(addressForm, {
id: row.id, id: row.id,
memberId: row.memberId, memberId: row.memberId,
receiver: '', receiver: row.receiver || row.receiverMask || '',
mobile: '', mobile: row.mobile || row.mobileMask || '',
province: row.province, province: row.province,
city: row.city, city: row.city,
district: row.district, district: row.district,
address: '', address: row.address || row.addressMask || '',
isDefault: row.isDefault, isDefault: row.isDefault,
}); });
} }
function openSensitive(type, targetId) {
reset(sensitiveForm, {
targetType: type,
targetId,
purposeCode: '',
purposeText: '',
confirm: false,
});
sensitiveResult.value = '';
showSensitive.value = true;
}
async function revealSensitive() {
const r: any = await Sensitive(sensitiveForm);
sensitiveResult.value = JSON.stringify({ accessNo: r.accessNo, ...r.fields }, null, 2);
}
async function openTeam(memberId = detail.value.id) { async function openTeam(memberId = detail.value.id) {
detail.value = { ...detail.value, id: memberId }; detail.value = { ...detail.value, id: memberId };
showTeam.value = true; showTeam.value = true;
@@ -1000,13 +949,21 @@
const start = new Date(), const start = new Date(),
end = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000); end = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
noticeMember.value = row; noticeMember.value = row;
reset(noticeForm, { memberId: row.id, title: '会员弹幕', content: '', sourceType: 2 }); reset(noticeForm, { memberId: row.id, title: '系统通知', content: '', sourceType: 1 });
noticeDateRange.value = [formatDate(start), formatDate(end)]; noticeDateRange.value = [formatDate(start), formatDate(end)];
showNotice.value = true; showNotice.value = true;
} }
async function submitNotice() { async function submitNotice() {
if (!noticeForm.title.trim()) {
message.warning('请填写通知标题');
return;
}
if (!noticeForm.content.trim()) { if (!noticeForm.content.trim()) {
message.warning('请填写弹幕内容'); message.warning('请填写通知内容');
return;
}
if (!noticeDateRange.value) {
message.warning('请选择通知有效期');
return; return;
} }
await NoticeEdit({ await NoticeEdit({
@@ -1014,7 +971,7 @@
startAt: noticeDateRange.value[0], startAt: noticeDateRange.value[0],
endAt: noticeDateRange.value[1], endAt: noticeDateRange.value[1],
}); });
message.success('弹幕已发送'); message.success('系统通知已发送');
showNotice.value = false; showNotice.value = false;
} }
const relatedColumns = computed(() => const relatedColumns = computed(() =>
@@ -1025,8 +982,8 @@
key: 'accountType', key: 'accountType',
render: (r) => dict.getLabel('paymentAccountType', r.accountType), render: (r) => dict.getLabel('paymentAccountType', r.accountType),
}, },
{ title: '持有人', key: 'holderMask' }, { title: '持有人', key: 'holder' },
{ title: '账号', key: 'accountNoMask' }, { title: '账号', key: 'accountNo' },
{ title: '银行', key: 'bankName' }, { title: '银行', key: 'bankName' },
{ {
title: '操作', title: '操作',
@@ -1034,11 +991,6 @@
render: (r) => render: (r) =>
h(NSpace, {}, () => [ h(NSpace, {}, () => [
h(NButton, { size: 'small', onClick: () => editRelated(r) }, () => '编辑'), h(NButton, { size: 'small', onClick: () => editRelated(r) }, () => '编辑'),
h(
NButton,
{ size: 'small', onClick: () => openSensitive('payment', r.id) },
() => '查看明文'
),
h( h(
NButton, NButton,
{ size: 'small', type: 'error', onClick: () => deleteRelated(r.id) }, { size: 'small', type: 'error', onClick: () => deleteRelated(r.id) },
@@ -1048,21 +1000,16 @@
}, },
] ]
: [ : [
{ title: '收货人', key: 'receiverMask' }, { title: '收货人', key: 'receiver' },
{ title: '手机号', key: 'mobileMask' }, { title: '手机号', key: 'mobile' },
{ title: '地区', key: 'province', render: (r) => r.province + r.city + r.district }, { title: '地区', key: 'province', render: (r) => r.province + r.city + r.district },
{ title: '地址', key: 'addressMask' }, { title: '地址', key: 'address' },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
render: (r) => render: (r) =>
h(NSpace, {}, () => [ h(NSpace, {}, () => [
h(NButton, { size: 'small', onClick: () => editRelated(r) }, () => '编辑'), h(NButton, { size: 'small', onClick: () => editRelated(r) }, () => '编辑'),
h(
NButton,
{ size: 'small', onClick: () => openSensitive('address', r.id) },
() => '查看明文'
),
h( h(
NButton, NButton,
{ size: 'small', type: 'error', onClick: () => deleteRelated(r.id) }, { size: 'small', type: 'error', onClick: () => deleteRelated(r.id) },
+2 -2
View File
@@ -7,7 +7,7 @@ import { defRangeShortcuts } from '@/utils/dateUtil';
const dict = useDictStore(); const dict = useDictStore();
export interface MemberRecord { export interface MemberRecord {
id: number; memberNo: string; username: string; mobile: string; realNameMask: string; id: number; memberNo: string; username: string; mobile: string; realName: string; realNameMask: string;
availableAmount: number; frozenAmount: number; parentId: number; parentUsername: string; availableAmount: number; frozenAmount: number; parentId: number; parentUsername: string;
inviteCode: string; memberStatus: number; riskStatus: number; autoDispatch: number; memberCategory: number; inviteCode: string; memberStatus: number; riskStatus: number; autoDispatch: number; memberCategory: number;
isTestAccount: number; lastLoginAt?: string; lastLoginIpMask: string; lastLoginRegion: string; isTestAccount: number; lastLoginAt?: string; lastLoginIpMask: string; lastLoginRegion: string;
@@ -34,7 +34,7 @@ export const columns = [
{ title: '会员编号', key: 'memberNo', width: 170 }, { title: '会员编号', key: 'memberNo', width: 170 },
{ title: '账号', key: 'username', width: 130 }, { title: '账号', key: 'username', width: 130 },
{ title: '手机号', key: 'mobile', width: 130 }, { title: '手机号', key: 'mobile', width: 130 },
{ title: '绑卡姓名', key: 'realNameMask', width: 100 }, { title: '真实姓名', key: 'realName', width: 100 },
{ title: '可用余额', key: 'availableAmount', width: 120, render: (row: MemberRecord) => money(row.availableAmount) }, { title: '可用余额', key: 'availableAmount', width: 120, render: (row: MemberRecord) => money(row.availableAmount) },
{ title: '冻结余额', key: 'frozenAmount', width: 120, render: (row: MemberRecord) => money(row.frozenAmount) }, { title: '冻结余额', key: 'frozenAmount', width: 120, render: (row: MemberRecord) => money(row.frozenAmount) },
{ title: '上级用户', key: 'parentUsername', width: 120, render: (row: MemberRecord) => row.parentUsername || '-' }, { title: '上级用户', key: 'parentUsername', width: 120, render: (row: MemberRecord) => row.parentUsername || '-' },
+8 -3
View File
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="n-layout-page-header"><n-card :bordered="false" title="订单列表">手动单匹配后立即完成并允许余额为负;自动单可在预约时间前人工匹配,逾期未处理则由系统自动匹配。</n-card></div> <div class="n-layout-page-header"><n-card :bordered="false" title="订单列表">匹配时会校验会员可用余额;余额不足的订单保留为待处理,充值审核通过且余额足够后自动完成并结算本金及佣金。</n-card></div>
<n-card :bordered="false" class="proCard"> <n-card :bordered="false" class="proCard">
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" /> <BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row)=>row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000"> <BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row)=>row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
@@ -45,6 +45,9 @@
<n-form-item label="商品数量"> <n-form-item label="商品数量">
<n-input-number v-model:value="matchForm.quantity" :min="1" :max="999" /> <n-input-number v-model:value="matchForm.quantity" :min="1" :max="999" />
</n-form-item> </n-form-item>
<n-alert v-if="matchShortfall > 0" type="warning" :bordered="false">
会员余额不足:当前可用 {{ yuan(matchMember.availableAmount) }},订单金额 {{ yuan(selectedProduct.priceAmount * matchForm.quantity) }},还差 {{ yuan(matchShortfall) }}。订单将保留为待处理,充值审核通过且余额足够后自动完成。
</n-alert>
</n-form> </n-form>
</n-spin> </n-spin>
<template #action> <template #action>
@@ -99,6 +102,7 @@ const approvalColumns=[{title:'节点',key:'stepNo',width:70},{title:'操作人I
const matchLoading=ref(false),productLoading=ref(false),matchSubmitting=ref(false),products=ref<any[]>([]),selectedProduct=ref<any>(); const matchLoading=ref(false),productLoading=ref(false),matchSubmitting=ref(false),products=ref<any[]>([]),selectedProduct=ref<any>();
const matchMember=reactive({id:0,username:'',availableAmount:0,frozenAmount:0}); const matchMember=reactive({id:0,username:'',availableAmount:0,frozenAmount:0});
const matchForm=reactive({id:0,productId:0,quantity:1}); const matchForm=reactive({id:0,productId:0,quantity:1});
const matchShortfall=computed(()=>Math.max(0,(selectedProduct.value?.priceAmount||0)*matchForm.quantity-matchMember.availableAmount));
const amountMember=reactive({username:'',availableAmount:0}); const amountMember=reactive({username:'',availableAmount:0});
const amountForm=reactive<{id:number;amount:number|null;quantity:number}>({id:0,amount:null,quantity:1}); const amountForm=reactive<{id:number;amount:number|null;quantity:number}>({id:0,amount:null,quantity:1});
const amountSubmitting=ref(false); const amountSubmitting=ref(false);
@@ -123,7 +127,7 @@ function clearProductFilter(){Object.assign(productFilter,{name:'',priceMin:null
function selectProduct(row){selectedProduct.value=row;matchForm.productId=row.id} function selectProduct(row){selectedProduct.value=row;matchForm.productId=row.id}
async function confirmMatch(){ async function confirmMatch(){
if(!matchForm.productId){message.error('请先选择商品');return} if(!matchForm.productId){message.error('请先选择商品');return}
matchSubmitting.value=true;try{await TradeOrderMatch(matchForm);message.success('订单已匹配并完成');showMatch.value=false;reloadTable()}finally{matchSubmitting.value=false} matchSubmitting.value=true;try{await TradeOrderMatch(matchForm);message.success('订单已匹配;如余额不足将保持待处理,充值审核通过且余额足够后自动完成');showMatch.value=false;reloadTable()}finally{matchSubmitting.value=false}
} }
async function view(row){detail.value=await TradeOrderView({id:row.id});showDetail.value=true} async function view(row){detail.value=await TradeOrderView({id:row.id});showDetail.value=true}
function openAmount(row){Object.assign(amountMember,{username:row.username,availableAmount:row.availableAmount});Object.assign(amountForm,{id:row.id,amount:Number(row.totalAmount||0)/100,quantity:row.quantity||1});showAmount.value=true} function openAmount(row){Object.assign(amountMember,{username:row.username,availableAmount:row.availableAmount});Object.assign(amountForm,{id:row.id,amount:Number(row.totalAmount||0)/100,quantity:row.quantity||1});showAmount.value=true}
@@ -137,7 +141,8 @@ const actionColumn=reactive({width:300,title:'操作',key:'action',fixed:'right'
if(row.canForcePay)actions.push({label:'强制付款',type:'primary',onClick:()=>forcePay(row)}); if(row.canForcePay)actions.push({label:'强制付款',type:'primary',onClick:()=>forcePay(row)});
if(row.canFreeze)actions.push({label:'冻结订单',type:'warning',onClick:()=>freezeOrder(row)}); if(row.canFreeze)actions.push({label:'冻结订单',type:'warning',onClick:()=>freezeOrder(row)});
const hasManualActions=row.canChangeAmount||row.canForcePay||row.canFreeze; const hasManualActions=row.canChangeAmount||row.canForcePay||row.canFreeze;
if(!hasManualActions&&row.orderType===2&&row.orderStatus===2&&row.canReview)actions.push({label:'审核任务',type:'warning',onClick:()=>router.push({name:'task_submission_list',query:{tradeOrderId:String(row.id)}})}); if(!hasManualActions&&row.paymentStatus==='balance_pending')actions.push({label:'余额不足待处理',type:'error',disabled:true,title:row.message||'充值审核通过且余额足够后将自动完成'});
else if(!hasManualActions&&row.orderType===2&&row.orderStatus===2&&row.canReview)actions.push({label:'审核任务',type:'warning',onClick:()=>router.push({name:'task_submission_list',query:{tradeOrderId:String(row.id)}})});
else if(!hasManualActions&&row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员付款',disabled:true,title:'等待会员完成付款'}); else if(!hasManualActions&&row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员付款',disabled:true,title:'等待会员完成付款'});
if(!hasManualActions)actions.push({label:'详情',onClick:()=>view(row)}); if(!hasManualActions)actions.push({label:'详情',onClick:()=>view(row)});
return h(TableAction as any,{style:'button',actions}); return h(TableAction as any,{style:'button',actions});
+7 -3
View File
@@ -30,9 +30,13 @@ export const columns = [
{ title: '佣金', key: 'commissionAmount', width: 110, render: (r) => yuan(r.commissionAmount) }, { title: '佣金', key: 'commissionAmount', width: 110, render: (r) => yuan(r.commissionAmount) },
{ title: '下单时间', key: 'createdAt', width: 180 }, { title: '下单时间', key: 'createdAt', width: 180 },
{ title: '解冻时间', key: 'unfrozenAt', width: 180 }, { title: '解冻时间', key: 'unfrozenAt', width: 180 },
{ title: '消息', key: 'message', width: 180, ellipsis: { tooltip: true } }, { title: '消息', key: 'message', width: 280, ellipsis: { tooltip: true }, render: (r) => r.paymentStatus === 'balance_pending'
{ title: '订单状态', key: 'orderStatus', width: 110, render: (r) => r.orderType === 2 && r.orderStatus === 2 ? h(NTag, { type: 'error', bordered: false }, { default: () => r.message || '余额不足,待处理' })
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待付款' }) : r.message },
{ title: '订单状态', key: 'orderStatus', width: 130, render: (r) => r.paymentStatus === 'balance_pending'
? h(NTag, { type: 'error', bordered: false }, { default: () => '余额不足待处理' })
: r.orderType === 2 && r.orderStatus === 2
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待付款' })
: tag('tradeOrderStatus', r.orderStatus) }, : tag('tradeOrderStatus', r.orderStatus) },
{ title: '冻结状态', key: 'freezeStatus', width: 110, render: (r) => tag('tradeFreezeStatus', r.freezeStatus) }, { title: '冻结状态', key: 'freezeStatus', width: 110, render: (r) => tag('tradeFreezeStatus', r.freezeStatus) },
]; ];