feat: refine member and trade management
This commit is contained in:
@@ -133,6 +133,12 @@ func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (r
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) error {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
return s.edit(ctx, in)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) edit(ctx context.Context, in *sysin.AppMemberEditInp) error {
|
||||
if in.Id == 0 {
|
||||
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
|
||||
}
|
||||
@@ -233,7 +239,82 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er
|
||||
return err
|
||||
}
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.edit", TargetType: "app_member", TargetId: in.Id, Before: before, After: g.Map{"username": in.Username, "mobileChanged": in.Mobile != "", "parentId": in.ParentId, "riskStatus": in.RiskStatus, "autoDispatch": in.AutoDispatch, "memberCategory": in.MemberCategory}})
|
||||
if in.AvailableAmount != nil {
|
||||
if err = s.setAvailableAmount(ctx, in.Id, *in.AvailableAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.edit", TargetType: "app_member", TargetId: in.Id, Before: before, After: g.Map{"username": in.Username, "mobileChanged": in.Mobile != "", "parentId": in.ParentId, "riskStatus": in.RiskStatus, "autoDispatch": in.AutoDispatch, "memberCategory": in.MemberCategory, "availableAmount": in.AvailableAmount}})
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) setAvailableAmount(ctx context.Context, memberId uint64, target int64) error {
|
||||
wc := dao.WalletAccount.Columns()
|
||||
var account *entity.WalletAccount
|
||||
if err := s.walletModel(ctx).Where(wc.MemberId, memberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
|
||||
return gerror.Wrap(err, "读取会员钱包失败")
|
||||
}
|
||||
if account == nil {
|
||||
if _, err := s.walletModel(ctx).
|
||||
Fields(wc.MemberId, wc.Currency).
|
||||
Hook(hook.SaveTenant).
|
||||
Data(g.Map{wc.MemberId: memberId, wc.Currency: "CNY"}).
|
||||
Insert(); err != nil {
|
||||
return gerror.Wrap(err, "初始化会员钱包失败")
|
||||
}
|
||||
if err := s.walletModel(ctx).Where(wc.MemberId, memberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
|
||||
return gerror.Wrap(err, "读取会员钱包失败")
|
||||
}
|
||||
}
|
||||
if account == nil {
|
||||
return gerror.New("会员钱包不存在")
|
||||
}
|
||||
if account.AvailableAmount == target {
|
||||
return nil
|
||||
}
|
||||
|
||||
direction := consts.WalletDirectionIncome
|
||||
amount := target - account.AvailableAmount
|
||||
if amount < 0 {
|
||||
direction = consts.WalletDirectionExpense
|
||||
amount = -amount
|
||||
}
|
||||
now := gtime.Now()
|
||||
ledgerNo := "WL" + now.Format("YmdHis") + grand.S(7)
|
||||
bizNo := "ME" + now.Format("YmdHis") + grand.S(7)
|
||||
idempotencyKey := fmt.Sprintf("member-edit-%d-%d-%s", memberId, now.UnixMilli(), grand.S(6))
|
||||
lc := dao.WalletLedger.Columns()
|
||||
if _, 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: ledgerNo, lc.AccountId: account.Id, lc.MemberId: memberId,
|
||||
lc.BizType: "member_edit", lc.BizNo: bizNo, lc.EntryType: fmt.Sprint(consts.WalletAdjustOther),
|
||||
lc.Direction: direction, lc.Amount: amount, lc.BeforeAmount: account.AvailableAmount,
|
||||
lc.AfterAmount: target, lc.Currency: account.Currency, lc.IdempotencyKey: idempotencyKey,
|
||||
lc.Remark: "后台编辑会员余额", lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入账变流水失败")
|
||||
}
|
||||
|
||||
walletData := g.Map{wc.AvailableAmount: target, wc.Version: gdb.Raw(wc.Version + "+1")}
|
||||
if direction == consts.WalletDirectionIncome {
|
||||
walletData[wc.TotalIncome] = gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(amount))
|
||||
} else {
|
||||
walletData[wc.TotalExpense] = gdb.Raw(wc.TotalExpense + "+" + fmt.Sprint(amount))
|
||||
}
|
||||
result, err := s.walletModel(ctx).WherePri(account.Id).Where(wc.Version, account.Version).Data(walletData).Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新会员余额失败")
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows != 1 {
|
||||
return gerror.New("会员余额已变化,请重试")
|
||||
}
|
||||
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
|
||||
Module: "wallet", Action: "wallet.balance.edit", TargetType: "wallet_account", TargetId: account.Id,
|
||||
Before: g.Map{"availableAmount": account.AvailableAmount}, After: g.Map{"availableAmount": target, "ledgerNo": ledgerNo},
|
||||
Reason: "后台编辑会员余额",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysAppMember) saveProfile(ctx context.Context, in *sysin.AppMemberEditInp) error {
|
||||
|
||||
@@ -41,7 +41,8 @@ func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
|
||||
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()
|
||||
realName := fmt.Sprintf("COALESCE(NULLIF(%s,''),(SELECT %s FROM %s pa WHERE pa.%s=%s AND pa.%s IS NULL ORDER BY pa.%s ASC,pa.%s DESC LIMIT 1),'') 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 := 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))
|
||||
}
|
||||
|
||||
@@ -74,32 +74,36 @@ type AppMemberViewModel struct {
|
||||
}
|
||||
|
||||
type AppMemberEditInp struct {
|
||||
Id uint64 `json:"id"`
|
||||
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
|
||||
Mobile string `json:"mobile" v:"phone-loose#手机号格式不正确"`
|
||||
Password string `json:"password"`
|
||||
TradePassword string `json:"tradePassword"`
|
||||
RealName string `json:"realName" v:"max-length:64#姓名不能超过64个字符"`
|
||||
IdNo string `json:"idNo" v:"max-length:32#证件号不能超过32个字符"`
|
||||
ParentId uint64 `json:"parentId"`
|
||||
LevelId uint64 `json:"levelId"`
|
||||
RiskStatus int `json:"riskStatus" d:"1"`
|
||||
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
|
||||
AutoDispatch int `json:"autoDispatch" d:"1"`
|
||||
MemberCategory int `json:"memberCategory"`
|
||||
IsTestAccount int `json:"isTestAccount"`
|
||||
Id uint64 `json:"id"`
|
||||
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
|
||||
Mobile string `json:"mobile" v:"phone-loose#手机号格式不正确"`
|
||||
Password string `json:"password"`
|
||||
TradePassword string `json:"tradePassword"`
|
||||
RealName string `json:"realName" v:"max-length:64#姓名不能超过64个字符"`
|
||||
IdNo string `json:"idNo" v:"max-length:32#证件号不能超过32个字符"`
|
||||
ParentId uint64 `json:"parentId"`
|
||||
LevelId uint64 `json:"levelId"`
|
||||
RiskStatus int `json:"riskStatus" d:"1"`
|
||||
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
|
||||
AutoDispatch int `json:"autoDispatch" d:"1"`
|
||||
MemberCategory int `json:"memberCategory"`
|
||||
IsTestAccount int `json:"isTestAccount"`
|
||||
AvailableAmount *int64 `json:"availableAmount"`
|
||||
}
|
||||
|
||||
func (in *AppMemberEditInp) Filter(ctx context.Context) error {
|
||||
if in.Id == 0 {
|
||||
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
|
||||
}
|
||||
if in.Password != "" && (len(in.Password) < 8 || !containsLetterAndNumber(in.Password)) {
|
||||
return gerror.New("密码至少8位且必须同时包含字母和数字")
|
||||
if in.Password != "" && (len([]rune(in.Password)) < 6 || len([]byte(in.Password)) > 64) {
|
||||
return gerror.New("密码长度为6到64个字符")
|
||||
}
|
||||
if in.TradePassword != "" && len(in.TradePassword) < 6 {
|
||||
return gerror.New("交易密码不能少于6位")
|
||||
}
|
||||
if in.AvailableAmount != nil && *in.AvailableAmount < 0 {
|
||||
return gerror.New("账户余额不能小于0")
|
||||
}
|
||||
if !dict.HasOptionKey(consts.AppMemberRiskStatusOptions, in.RiskStatus) {
|
||||
return gerror.New("风控状态不合法")
|
||||
}
|
||||
@@ -118,18 +122,6 @@ func (in *AppMemberEditInp) Filter(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func containsLetterAndNumber(value string) bool {
|
||||
letter, number := false, false
|
||||
for _, r := range value {
|
||||
if r >= '0' && r <= '9' {
|
||||
number = true
|
||||
}
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
|
||||
letter = true
|
||||
}
|
||||
}
|
||||
return letter && number
|
||||
}
|
||||
|
||||
type AppMemberDeleteInp struct {
|
||||
Id uint64 `json:"id" v:"required|min:1#会员ID不能为空|会员ID不能为空"`
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type AppMemberRegisterInp struct {
|
||||
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
|
||||
Mobile string `json:"mobile" v:"required|phone-loose#手机号不能为空|手机号格式不正确"`
|
||||
Password string `json:"password" v:"required|length:8,64#密码不能为空|密码长度为8到64个字符"`
|
||||
Password string `json:"password" v:"required|length:6,64#密码不能为空|密码长度为6到64个字符"`
|
||||
InviteCode string `json:"inviteCode" v:"required#请输入邀请码"`
|
||||
RegisterOrigin string `json:"registerOrigin" v:"max-length:512#注册来源不能超过512个字符"`
|
||||
RegisterHost string `json:"registerHost" v:"max-length:255#注册域名不能超过255个字符"`
|
||||
@@ -24,9 +24,6 @@ func (in *AppMemberRegisterInp) Filter(ctx context.Context) error {
|
||||
if !regexp.MustCompile(`^[0-9]{6}$`).MatchString(in.InviteCode) {
|
||||
return gerror.New("邀请码必须为6位数字")
|
||||
}
|
||||
if !containsLetterAndNumber(in.Password) {
|
||||
return gerror.New("密码至少8位且必须同时包含字母和数字")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user