feat: 完善会员交易与任务管理流程
This commit is contained in:
@@ -25,9 +25,7 @@ type AppMemberColumns struct {
|
|||||||
TenantId string // 租户ID
|
TenantId string // 租户ID
|
||||||
MemberNo string // 会员编号
|
MemberNo string // 会员编号
|
||||||
Username string // 用户名
|
Username string // 用户名
|
||||||
MobileCipher string // 手机号密文(AES-GCM)
|
Mobile string //
|
||||||
MobileHash string // 手机号检索哈希(HMAC-SHA256)
|
|
||||||
MobileMask string // 手机号脱敏值
|
|
||||||
PasswordHash string // 登录密码哈希(bcrypt)
|
PasswordHash string // 登录密码哈希(bcrypt)
|
||||||
TradePasswordHash string // 交易密码哈希
|
TradePasswordHash string // 交易密码哈希
|
||||||
LevelId string // 会员等级ID
|
LevelId string // 会员等级ID
|
||||||
@@ -58,9 +56,7 @@ var appMemberColumns = AppMemberColumns{
|
|||||||
TenantId: "tenant_id",
|
TenantId: "tenant_id",
|
||||||
MemberNo: "member_no",
|
MemberNo: "member_no",
|
||||||
Username: "username",
|
Username: "username",
|
||||||
MobileCipher: "mobile_cipher",
|
Mobile: "mobile",
|
||||||
MobileHash: "mobile_hash",
|
|
||||||
MobileMask: "mobile_mask",
|
|
||||||
PasswordHash: "password_hash",
|
PasswordHash: "password_hash",
|
||||||
TradePasswordHash: "trade_password_hash",
|
TradePasswordHash: "trade_password_hash",
|
||||||
LevelId: "level_id",
|
LevelId: "level_id",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
|
|||||||
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").
|
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").
|
||||||
LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).
|
LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).
|
||||||
Fields(
|
Fields(
|
||||||
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.MobileMask),
|
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.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),
|
||||||
@@ -72,11 +72,7 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
|
|||||||
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
||||||
}
|
}
|
||||||
if in.Mobile != "" {
|
if in.Mobile != "" {
|
||||||
hash, e := encrypt.HashField(ctx, in.Mobile)
|
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
|
||||||
if e != nil {
|
|
||||||
return nil, 0, e
|
|
||||||
}
|
|
||||||
mod = mod.Where(col("m", mc.MobileHash), hash)
|
|
||||||
}
|
}
|
||||||
if in.MemberStatus > 0 {
|
if in.MemberStatus > 0 {
|
||||||
mod = mod.Where(col("m", mc.MemberStatus), in.MemberStatus)
|
mod = mod.Where(col("m", mc.MemberStatus), in.MemberStatus)
|
||||||
@@ -103,7 +99,7 @@ func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (r
|
|||||||
mc, pc, wc := dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns()
|
mc, pc, wc := dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns()
|
||||||
res = new(sysin.AppMemberViewModel)
|
res = new(sysin.AppMemberViewModel)
|
||||||
err = s.memberModel(ctx).As("m").LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("m", mc.Id)).LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).Fields(
|
err = s.memberModel(ctx).As("m").LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("m", mc.Id)).LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).Fields(
|
||||||
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("p", pc.IdNoMask), col("p", pc.KycStatus), 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,'') 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), fmt.Sprintf("CASE WHEN %s=1 THEN %d ELSE %d END member_category", col("m", mc.IsTestAccount), consts.AppMemberCategoryFake, consts.AppMemberCategoryReal), col("m", mc.LastLoginAt), col("m", mc.LastLoginIpMask), col("m", mc.LastLoginRegion), col("m", mc.CreatedAt), col("m", mc.LevelId), col("m", mc.Level), col("m", mc.CreditScore),
|
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), col("p", pc.IdNoMask), col("p", pc.KycStatus), 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,'') 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), fmt.Sprintf("CASE WHEN %s=1 THEN %d ELSE %d END member_category", col("m", mc.IsTestAccount), consts.AppMemberCategoryFake, consts.AppMemberCategoryReal), col("m", mc.LastLoginAt), col("m", mc.LastLoginIpMask), col("m", mc.LastLoginRegion), col("m", mc.CreatedAt), col("m", mc.LevelId), col("m", mc.Level), col("m", mc.CreditScore),
|
||||||
).Where(col("m", mc.Id), in.Id).Scan(res)
|
).Where(col("m", mc.Id), in.Id).Scan(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gerror.Wrap(err, "获取会员详情失败")
|
return nil, gerror.Wrap(err, "获取会员详情失败")
|
||||||
@@ -146,24 +142,14 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er
|
|||||||
}
|
}
|
||||||
data := g.Map{mc.Username: in.Username, mc.LevelId: in.LevelId, mc.ParentId: in.ParentId, mc.RiskStatus: in.RiskStatus, mc.CreditScore: in.CreditScore, mc.AutoDispatch: in.AutoDispatch, mc.IsTestAccount: isTestAccount, mc.UpdatedBy: userId}
|
data := g.Map{mc.Username: in.Username, mc.LevelId: in.LevelId, mc.ParentId: in.ParentId, mc.RiskStatus: in.RiskStatus, mc.CreditScore: in.CreditScore, mc.AutoDispatch: in.AutoDispatch, mc.IsTestAccount: isTestAccount, mc.UpdatedBy: userId}
|
||||||
if in.Mobile != "" {
|
if in.Mobile != "" {
|
||||||
cipher, e := encrypt.EncryptField(ctx, in.Mobile)
|
count, err = s.memberModel(ctx).Where(mc.Mobile, in.Mobile).WhereNot(mc.Id, in.Id).Count()
|
||||||
if e != nil {
|
if err != nil {
|
||||||
return e
|
return err
|
||||||
}
|
|
||||||
hash, e := encrypt.HashField(ctx, in.Mobile)
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
count, e = s.memberModel(ctx).Where(mc.MobileHash, hash).WhereNot(mc.Id, in.Id).Count()
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
return gerror.New("手机号已存在")
|
return gerror.New("手机号已存在")
|
||||||
}
|
}
|
||||||
data[mc.MobileCipher] = cipher
|
data[mc.Mobile] = in.Mobile
|
||||||
data[mc.MobileHash] = hash
|
|
||||||
data[mc.MobileMask] = encrypt.MaskMobile(in.Mobile)
|
|
||||||
}
|
}
|
||||||
if in.Password != "" {
|
if in.Password != "" {
|
||||||
hashed, e := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
|
hashed, e := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
|
||||||
@@ -206,7 +192,7 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er
|
|||||||
data[mc.InviteCode] = strings.ToUpper(grand.S(8))
|
data[mc.InviteCode] = strings.ToUpper(grand.S(8))
|
||||||
data[mc.MemberStatus] = consts.AppMemberStatusNormal
|
data[mc.MemberStatus] = consts.AppMemberStatusNormal
|
||||||
data[mc.CreatedBy] = userId
|
data[mc.CreatedBy] = userId
|
||||||
result, e := s.memberModel(ctx).Fields(mc.MemberNo, mc.Username, mc.MobileCipher, mc.MobileHash, mc.MobileMask, mc.PasswordHash, mc.TradePasswordHash, mc.LevelId, mc.ParentId, mc.Level, mc.Tree, mc.InviteCode, mc.MemberStatus, mc.RiskStatus, mc.AutoDispatch, mc.CreditScore, mc.IsTestAccount, mc.CreatedBy, mc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
|
result, e := s.memberModel(ctx).Fields(mc.MemberNo, mc.Username, mc.Mobile, mc.PasswordHash, mc.TradePasswordHash, mc.LevelId, mc.ParentId, mc.Level, mc.Tree, mc.InviteCode, mc.MemberStatus, mc.RiskStatus, mc.AutoDispatch, mc.CreditScore, mc.IsTestAccount, mc.CreatedBy, mc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return gerror.Wrap(e, "创建会员失败")
|
return gerror.Wrap(e, "创建会员失败")
|
||||||
}
|
}
|
||||||
@@ -442,7 +428,7 @@ func (s *sSysAppMember) Sensitive(ctx context.Context, in *sysin.AppMemberSensit
|
|||||||
case "member":
|
case "member":
|
||||||
var member *entity.AppMember
|
var member *entity.AppMember
|
||||||
if err = s.memberModel(ctx).WherePri(in.TargetId).Scan(&member); err == nil && member != nil {
|
if err = s.memberModel(ctx).WherePri(in.TargetId).Scan(&member); err == nil && member != nil {
|
||||||
res.Fields["mobile"], err = encrypt.DecryptField(ctx, member.MobileCipher)
|
res.Fields["mobile"] = member.Mobile
|
||||||
var p *entity.AppMemberProfile
|
var p *entity.AppMemberProfile
|
||||||
_ = s.profileModel(ctx).Where(dao.AppMemberProfile.Columns().MemberId, in.TargetId).Scan(&p)
|
_ = s.profileModel(ctx).Where(dao.AppMemberProfile.Columns().MemberId, in.TargetId).Scan(&p)
|
||||||
if p != nil {
|
if p != nil {
|
||||||
@@ -494,7 +480,7 @@ func (s *sSysAppMember) Team(ctx context.Context, in *sysin.AppMemberTeamInp) (l
|
|||||||
mc, wc := dao.AppMember.Columns(), dao.WalletAccount.Columns()
|
mc, wc := dao.AppMember.Columns(), dao.WalletAccount.Columns()
|
||||||
token := "tr_" + fmt.Sprint(in.MemberId) + " "
|
token := "tr_" + fmt.Sprint(in.MemberId) + " "
|
||||||
maxAbsolute := root.Level + in.MaxLevel
|
maxAbsolute := root.Level + in.MaxLevel
|
||||||
mod := s.memberModel(ctx).As("m").LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").Fields(col("m", mc.Id), col("m", mc.Username), col("m", mc.MobileMask), col("m", mc.ParentId), fmt.Sprintf("%s-%d relative_level", col("m", mc.Level), root.Level), 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.MemberStatus), col("m", mc.CreatedAt)).WhereLike(col("m", mc.Tree), "%"+token+"%").WhereLTE(col("m", mc.Level), maxAbsolute)
|
mod := s.memberModel(ctx).As("m").LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").Fields(col("m", mc.Id), col("m", mc.Username), col("m", mc.Mobile), col("m", mc.ParentId), fmt.Sprintf("%s-%d relative_level", col("m", mc.Level), root.Level), 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.MemberStatus), col("m", mc.CreatedAt)).WhereLike(col("m", mc.Tree), "%"+token+"%").WhereLTE(col("m", mc.Level), maxAbsolute)
|
||||||
err = mod.Page(in.Page, in.PerPage).OrderAsc(col("m", mc.Level)).OrderAsc(col("m", mc.Id)).ScanAndCount(&list, &total, false)
|
err = mod.Page(in.Page, in.PerPage).OrderAsc(col("m", mc.Level)).OrderAsc(col("m", mc.Id)).ScanAndCount(&list, &total, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gerror.Wrap(err, "查询会员团队失败")
|
err = gerror.Wrap(err, "查询会员团队失败")
|
||||||
@@ -569,7 +555,7 @@ func (s *sSysAppMember) StatusExecute(ctx context.Context, in *sysin.AppMemberSt
|
|||||||
|
|
||||||
func (s *sSysAppMember) WalletList(ctx context.Context, in *sysin.WalletAdjustmentListInp) (list []*sysin.WalletAdjustmentListModel, total int, err error) {
|
func (s *sSysAppMember) WalletList(ctx context.Context, in *sysin.WalletAdjustmentListInp) (list []*sysin.WalletAdjustmentListModel, total int, err error) {
|
||||||
ac, mc := dao.WalletAdjustmentRequest.Columns(), dao.AppMember.Columns()
|
ac, mc := dao.WalletAdjustmentRequest.Columns(), dao.AppMember.Columns()
|
||||||
mod := s.adjustmentModel(ctx).As("a").LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("a", ac.MemberId)).Fields(col("a", ac.Id), col("a", ac.RequestNo), col("a", ac.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("a", ac.AdjustType), col("a", ac.Direction), col("a", ac.Amount), col("a", ac.ReasonCode), col("a", ac.ReasonText), col("a", ac.RequestStatus), col("a", ac.ApprovalRequestId), col("a", ac.LedgerId), col("a", ac.CreatedAt))
|
mod := s.adjustmentModel(ctx).As("a").LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("a", ac.MemberId)).Fields(col("a", ac.Id), col("a", ac.RequestNo), col("a", ac.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("a", ac.AdjustType), col("a", ac.Direction), col("a", ac.Amount), col("a", ac.ReasonCode), col("a", ac.ReasonText), col("a", ac.RequestStatus), col("a", ac.ApprovalRequestId), col("a", ac.LedgerId), col("a", ac.CreatedAt))
|
||||||
if in.MemberId > 0 {
|
if in.MemberId > 0 {
|
||||||
mod = mod.Where(col("a", ac.MemberId), in.MemberId)
|
mod = mod.Where(col("a", ac.MemberId), in.MemberId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
"hotgo/utility/encrypt"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
@@ -42,7 +41,7 @@ func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
|
|||||||
func (s *sSysRechargeOrder) listModel(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()
|
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
|
||||||
return s.baseModel(ctx).
|
return s.baseModel(ctx).
|
||||||
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
|
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) {
|
func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) {
|
||||||
@@ -54,11 +53,7 @@ func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysi
|
|||||||
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
||||||
}
|
}
|
||||||
if in.Mobile != "" {
|
if in.Mobile != "" {
|
||||||
hash, err := encrypt.HashField(ctx, in.Mobile)
|
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mod = mod.Where(col("m", mc.MobileHash), hash)
|
|
||||||
}
|
}
|
||||||
if in.RechargeStatus > 0 {
|
if in.RechargeStatus > 0 {
|
||||||
mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus)
|
mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
"hotgo/utility/encrypt"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
@@ -30,19 +29,18 @@ func (s *sSysTaskSubmission) Model(ctx context.Context) *gdb.Model {
|
|||||||
}
|
}
|
||||||
func (s *sSysTaskSubmission) listModel(ctx context.Context) *gdb.Model {
|
func (s *sSysTaskSubmission) listModel(ctx context.Context) *gdb.Model {
|
||||||
sc, tc, mc, lc := dao.TaskSubmission.Columns(), dao.Task.Columns(), dao.AppMember.Columns(), dao.AppMemberLevel.Columns()
|
sc, tc, mc, lc := dao.TaskSubmission.Columns(), dao.Task.Columns(), dao.AppMember.Columns(), dao.AppMemberLevel.Columns()
|
||||||
return s.Model(ctx).As("s").LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), col("s", sc.TaskId), fmt.Sprintf("COALESCE(%s,'') task_title", col("t", tc.Title)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
|
return s.Model(ctx).As("s").LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), col("s", sc.TaskId), fmt.Sprintf("COALESCE(%s,'') task_title", col("t", tc.Title)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
|
||||||
}
|
}
|
||||||
func (s *sSysTaskSubmission) filter(ctx context.Context, mod *gdb.Model, in *sysin.TaskSubmissionListInp) (*gdb.Model, error) {
|
func (s *sSysTaskSubmission) filter(ctx context.Context, mod *gdb.Model, in *sysin.TaskSubmissionListInp) (*gdb.Model, error) {
|
||||||
sc, mc := dao.TaskSubmission.Columns(), dao.AppMember.Columns()
|
sc, mc := dao.TaskSubmission.Columns(), dao.AppMember.Columns()
|
||||||
|
if in.TradeOrderId > 0 {
|
||||||
|
mod = mod.Where(col("s", sc.TradeOrderId), in.TradeOrderId)
|
||||||
|
}
|
||||||
if in.Username != "" {
|
if in.Username != "" {
|
||||||
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
||||||
}
|
}
|
||||||
if in.Mobile != "" {
|
if in.Mobile != "" {
|
||||||
hash, err := encrypt.HashField(ctx, in.Mobile)
|
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mod = mod.Where(col("m", mc.MobileHash), hash)
|
|
||||||
}
|
}
|
||||||
if in.SubmissionStatus > 0 {
|
if in.SubmissionStatus > 0 {
|
||||||
mod = mod.Where(col("s", sc.SubmissionStatus), in.SubmissionStatus)
|
mod = mod.Where(col("s", sc.SubmissionStatus), in.SubmissionStatus)
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"hotgo/internal/dao"
|
"hotgo/internal/dao"
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
"hotgo/internal/library/hgorm/handler"
|
"hotgo/internal/library/hgorm/handler"
|
||||||
|
"hotgo/internal/library/hgorm/hook"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
"hotgo/utility/encrypt"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
@@ -30,14 +30,15 @@ func (s *sSysTradeOrder) model(ctx context.Context) *gdb.Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model {
|
func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model {
|
||||||
oc, mc, pc, wc, ac := dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.ApprovalRequest.Columns()
|
oc, mc, pc, wc, ac, sc := dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.ApprovalRequest.Columns(), dao.TaskSubmission.Columns()
|
||||||
todayCount := fmt.Sprintf("(SELECT COUNT(*) FROM %s td WHERE td.%s=%s AND td.%s>=CURDATE()) today_trade_count", dao.TradeOrder.Table(), oc.MemberId, col("o", oc.MemberId), oc.CreatedAt)
|
todayCount := fmt.Sprintf("(SELECT COUNT(*) FROM %s td WHERE td.%s=%s AND td.%s>=CURDATE()) today_trade_count", dao.TradeOrder.Table(), oc.MemberId, col("o", oc.MemberId), oc.CreatedAt)
|
||||||
|
canReview := fmt.Sprintf("EXISTS(SELECT 1 FROM %s ts WHERE ts.%s=%s AND ts.%s=%d) can_review", dao.TaskSubmission.Table(), sc.TradeOrderId, col("o", oc.Id), sc.SubmissionStatus, consts.TaskSubmissionPending)
|
||||||
return s.model(ctx).As("o").
|
return s.model(ctx).As("o").
|
||||||
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)).
|
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)).
|
||||||
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("o", oc.MemberId)).
|
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("o", oc.MemberId)).
|
||||||
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("o", oc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
|
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("o", oc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
|
||||||
LeftJoin(dao.ApprovalRequest.Table()+" a", col("a", ac.Id)+"="+col("o", oc.ApprovalRequestId)).
|
LeftJoin(dao.ApprovalRequest.Table()+" a", col("a", ac.Id)+"="+col("o", oc.ApprovalRequestId)).
|
||||||
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("m", mc.AutoDispatch), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
|
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), col("m", mc.AutoDispatch), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, canReview, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) (list []*sysin.TradeOrderListModel, total int, err error) {
|
func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) (list []*sysin.TradeOrderListModel, total int, err error) {
|
||||||
@@ -50,11 +51,7 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp)
|
|||||||
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
||||||
}
|
}
|
||||||
if in.Mobile != "" {
|
if in.Mobile != "" {
|
||||||
hash, e := encrypt.HashField(ctx, in.Mobile)
|
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
|
||||||
if e != nil {
|
|
||||||
return nil, 0, e
|
|
||||||
}
|
|
||||||
mod = mod.Where(col("m", mc.MobileHash), hash)
|
|
||||||
}
|
}
|
||||||
if in.OrderStatus > 0 {
|
if in.OrderStatus > 0 {
|
||||||
mod = mod.Where(col("o", oc.OrderStatus), in.OrderStatus)
|
mod = mod.Where(col("o", oc.OrderStatus), in.OrderStatus)
|
||||||
@@ -97,6 +94,7 @@ func (s *sSysTradeOrder) MatchProducts(ctx context.Context, in *sysin.TradeOrder
|
|||||||
mod := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}).
|
mod := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}).
|
||||||
Fields(pc.Id, pc.Name, pc.CoverUrl, pc.PriceAmount).
|
Fields(pc.Id, pc.Name, pc.CoverUrl, pc.PriceAmount).
|
||||||
Where(pc.PublishStatus, consts.BizPublishPublished).
|
Where(pc.PublishStatus, consts.BizPublishPublished).
|
||||||
|
Where(pc.Currency, "CNY").
|
||||||
WhereNull(pc.DeletedAt).
|
WhereNull(pc.DeletedAt).
|
||||||
Where("("+pc.StartAt+" IS NULL OR "+pc.StartAt+" <= ?)", gtime.Now()).
|
Where("("+pc.StartAt+" IS NULL OR "+pc.StartAt+" <= ?)", gtime.Now()).
|
||||||
Where("("+pc.EndAt+" IS NULL OR "+pc.EndAt+" >= ?)", gtime.Now())
|
Where("("+pc.EndAt+" IS NULL OR "+pc.EndAt+" >= ?)", gtime.Now())
|
||||||
@@ -153,6 +151,9 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
|||||||
if product == nil {
|
if product == nil {
|
||||||
return gerror.New("商品不存在、已下架或无权使用")
|
return gerror.New("商品不存在、已下架或无权使用")
|
||||||
}
|
}
|
||||||
|
if product.Currency != "CNY" {
|
||||||
|
return gerror.New("任务订单只能匹配 CNY 商品")
|
||||||
|
}
|
||||||
now := gtime.Now()
|
now := gtime.Now()
|
||||||
if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) {
|
if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) {
|
||||||
return gerror.New("商品当前不在可匹配时间范围内")
|
return gerror.New("商品当前不在可匹配时间范围内")
|
||||||
@@ -186,7 +187,38 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
|||||||
return gerror.New("会员等级生效版本不存在")
|
return gerror.New("会员等级生效版本不存在")
|
||||||
}
|
}
|
||||||
totalAmount := product.PriceAmount * int64(in.Quantity)
|
totalAmount := product.PriceAmount * int64(in.Quantity)
|
||||||
|
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, product.Currency).LockUpdate().Scan(&account); err != nil {
|
||||||
|
return gerror.Wrap(err, "读取会员钱包失败")
|
||||||
|
}
|
||||||
|
if account == nil || account.AccountStatus != 1 {
|
||||||
|
return gerror.New("会员钱包不存在或状态异常")
|
||||||
|
}
|
||||||
|
if totalAmount > account.AvailableAmount {
|
||||||
|
return gerror.New("匹配商品总额不能超过会员可用余额")
|
||||||
|
}
|
||||||
commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000
|
commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000
|
||||||
|
afterAvailable := account.AvailableAmount - totalAmount
|
||||||
|
afterFrozen := account.FrozenAmount + totalAmount
|
||||||
|
walletResult, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
|
||||||
|
WherePri(account.Id).Where(wc.Version, account.Version).
|
||||||
|
Data(g.Map{wc.AvailableAmount: afterAvailable, wc.FrozenAmount: afterFrozen, wc.Version: gdb.Raw(wc.Version + "+1")}).Update()
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "扣除订单本金失败")
|
||||||
|
}
|
||||||
|
if affected, _ := walletResult.RowsAffected(); affected != 1 {
|
||||||
|
return gerror.New("会员余额已变化,请刷新后重试")
|
||||||
|
}
|
||||||
|
if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dMA", order.Id), "available", consts.WalletDirectionExpense,
|
||||||
|
totalAmount, account.AvailableAmount, afterAvailable, "match:available", "人工匹配扣除可用余额"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dMF", order.Id), "frozen", consts.WalletDirectionIncome,
|
||||||
|
totalAmount, account.FrozenAmount, afterFrozen, "match:frozen", "人工匹配冻结订单本金"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
snapshot := gjson.New(g.Map{
|
snapshot := gjson.New(g.Map{
|
||||||
"source": "admin_manual_match", "productId": product.Id, "productNo": product.ProductNo,
|
"source": "admin_manual_match", "productId": product.Id, "productNo": product.ProductNo,
|
||||||
"unitPriceAmount": product.PriceAmount, "quantity": in.Quantity,
|
"unitPriceAmount": product.PriceAmount, "quantity": in.Quantity,
|
||||||
@@ -195,10 +227,10 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
|||||||
})
|
})
|
||||||
oc := dao.TradeOrder.Columns()
|
oc := dao.TradeOrder.Columns()
|
||||||
data := g.Map{
|
data := g.Map{
|
||||||
oc.ProductId: product.Id, oc.ProductName: product.Name, oc.OrderType: consts.TradeOrderTypeManual,
|
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.TradeOrderPending,
|
oc.RuleSnapshot: snapshot, oc.Message: "后台人工匹配商品,订单本金已扣除", oc.OrderStatus: consts.TradeOrderPending,
|
||||||
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx),
|
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, 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()
|
||||||
@@ -217,6 +249,24 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insertTradeMatchLedger(ctx context.Context, order *entity.TradeOrder, account *entity.WalletAccount,
|
||||||
|
ledgerNo, entryType string, direction int, amount, beforeAmount, afterAmount int64, suffix, remark string) error {
|
||||||
|
lc := dao.WalletLedger.Columns()
|
||||||
|
_, 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: order.MemberId,
|
||||||
|
lc.BizType: "task_principal_hold", lc.BizNo: order.OrderNo, lc.EntryType: entryType, lc.Direction: direction,
|
||||||
|
lc.Amount: amount, lc.BeforeAmount: beforeAmount, lc.AfterAmount: afterAmount, lc.Currency: account.Currency,
|
||||||
|
lc.IdempotencyKey: fmt.Sprintf("task:%d:%s", order.Id, suffix), lc.Remark: remark,
|
||||||
|
lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: gtime.Now()}).Insert()
|
||||||
|
if err != nil {
|
||||||
|
return gerror.Wrap(err, "记录订单扣款流水失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sSysTradeOrder) Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) {
|
func (s *sSysTradeOrder) Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) {
|
||||||
var order *entity.TradeOrder
|
var order *entity.TradeOrder
|
||||||
if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil {
|
if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
"hotgo/utility/encrypt"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
@@ -38,7 +37,7 @@ func (s *sSysWithdrawalOrder) listModel(ctx context.Context) *gdb.Model {
|
|||||||
LeftJoin(dao.WalletAccount.Table()+" w", col("w", ac.MemberId)+"="+col("o", wc.MemberId)+" AND "+col("w", ac.Currency)+"='CNY'").
|
LeftJoin(dao.WalletAccount.Table()+" w", col("w", ac.MemberId)+"="+col("o", wc.MemberId)+" AND "+col("w", ac.Currency)+"='CNY'").
|
||||||
LeftJoin(dao.AppMemberPaymentAccount.Table()+" pa", col("pa", pc.Id)+"="+col("o", wc.PaymentAccountId)).
|
LeftJoin(dao.AppMemberPaymentAccount.Table()+" pa", col("pa", pc.Id)+"="+col("o", wc.PaymentAccountId)).
|
||||||
LeftJoin(dao.SysAttachment.Table()+" sa", col("sa", sc.Id)+"="+col("pa", pc.QrAttachmentId)).
|
LeftJoin(dao.SysAttachment.Table()+" sa", col("sa", sc.Id)+"="+col("pa", pc.QrAttachmentId)).
|
||||||
Fields(col("o", wc.Id), col("o", wc.WithdrawalNo), col("o", wc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", ac.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", ac.FrozenAmount)), col("o", wc.RequestAmount), col("o", wc.FeeAmount), col("o", wc.ActualAmount), col("o", wc.Currency), col("o", wc.AccountMask), col("o", wc.BankName), col("o", wc.HolderMask), fmt.Sprintf("COALESCE(%s,0) payment_account_type", col("pa", pc.AccountType)), fmt.Sprintf("COALESCE(%s,'') qr_code_url", col("sa", sc.FileUrl)), col("o", wc.RiskResult), col("o", wc.WithdrawalStatus), col("o", wc.ReviewReasonCode), col("o", wc.ReviewReason), col("o", wc.ApprovalRequestId), col("o", wc.ExternalNo), col("o", wc.FreezeLedgerId), col("o", wc.FinalLedgerId), col("o", wc.ReviewedAt), col("o", wc.ProcessedAt), col("o", wc.CompletedAt), col("o", wc.CreatedAt))
|
Fields(col("o", wc.Id), col("o", wc.WithdrawalNo), col("o", wc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", ac.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", ac.FrozenAmount)), col("o", wc.RequestAmount), col("o", wc.FeeAmount), col("o", wc.ActualAmount), col("o", wc.Currency), col("o", wc.AccountMask), col("o", wc.BankName), col("o", wc.HolderMask), fmt.Sprintf("COALESCE(%s,0) payment_account_type", col("pa", pc.AccountType)), fmt.Sprintf("COALESCE(%s,'') qr_code_url", col("sa", sc.FileUrl)), col("o", wc.RiskResult), col("o", wc.WithdrawalStatus), col("o", wc.ReviewReasonCode), col("o", wc.ReviewReason), col("o", wc.ApprovalRequestId), col("o", wc.ExternalNo), col("o", wc.FreezeLedgerId), col("o", wc.FinalLedgerId), col("o", wc.ReviewedAt), col("o", wc.ProcessedAt), col("o", wc.CompletedAt), col("o", wc.CreatedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrderListInp) (list []*sysin.WithdrawalOrderListModel, total int, err error) {
|
func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrderListInp) (list []*sysin.WithdrawalOrderListModel, total int, err error) {
|
||||||
@@ -51,11 +50,7 @@ func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrde
|
|||||||
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
|
||||||
}
|
}
|
||||||
if in.Mobile != "" {
|
if in.Mobile != "" {
|
||||||
hash, e := encrypt.HashField(ctx, in.Mobile)
|
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
|
||||||
if e != nil {
|
|
||||||
return nil, 0, e
|
|
||||||
}
|
|
||||||
mod = mod.Where(col("m", mc.MobileHash), hash)
|
|
||||||
}
|
}
|
||||||
if in.WithdrawalStatus > 0 {
|
if in.WithdrawalStatus > 0 {
|
||||||
mod = mod.Where(col("o", wc.WithdrawalStatus), in.WithdrawalStatus)
|
mod = mod.Where(col("o", wc.WithdrawalStatus), in.WithdrawalStatus)
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ type AppMember struct {
|
|||||||
TenantId any // 租户ID
|
TenantId any // 租户ID
|
||||||
MemberNo any // 会员编号
|
MemberNo any // 会员编号
|
||||||
Username any // 用户名
|
Username any // 用户名
|
||||||
MobileCipher any // 手机号密文(AES-GCM)
|
Mobile any //
|
||||||
MobileHash any // 手机号检索哈希(HMAC-SHA256)
|
|
||||||
MobileMask any // 手机号脱敏值
|
|
||||||
PasswordHash any // 登录密码哈希(bcrypt)
|
PasswordHash any // 登录密码哈希(bcrypt)
|
||||||
TradePasswordHash any // 交易密码哈希
|
TradePasswordHash any // 交易密码哈希
|
||||||
LevelId any // 会员等级ID
|
LevelId any // 会员等级ID
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ type AppMember struct {
|
|||||||
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
|
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
|
||||||
MemberNo string `json:"memberNo" orm:"member_no" description:"会员编号"`
|
MemberNo string `json:"memberNo" orm:"member_no" description:"会员编号"`
|
||||||
Username string `json:"username" orm:"username" description:"用户名"`
|
Username string `json:"username" orm:"username" description:"用户名"`
|
||||||
MobileCipher string `json:"mobileCipher" orm:"mobile_cipher" description:"手机号密文(AES-GCM)"`
|
Mobile string `json:"mobile" orm:"mobile" description:""`
|
||||||
MobileHash string `json:"mobileHash" orm:"mobile_hash" description:"手机号检索哈希(HMAC-SHA256)"`
|
|
||||||
MobileMask string `json:"mobileMask" orm:"mobile_mask" description:"手机号脱敏值"`
|
|
||||||
PasswordHash string `json:"passwordHash" orm:"password_hash" description:"登录密码哈希(bcrypt)"`
|
PasswordHash string `json:"passwordHash" orm:"password_hash" description:"登录密码哈希(bcrypt)"`
|
||||||
TradePasswordHash string `json:"tradePasswordHash" orm:"trade_password_hash" description:"交易密码哈希"`
|
TradePasswordHash string `json:"tradePasswordHash" orm:"trade_password_hash" description:"交易密码哈希"`
|
||||||
LevelId uint64 `json:"levelId" orm:"level_id" description:"会员等级ID"`
|
LevelId uint64 `json:"levelId" orm:"level_id" description:"会员等级ID"`
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type AppMemberListModel struct {
|
|||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
MemberNo string `json:"memberNo"`
|
MemberNo string `json:"memberNo"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
RealNameMask string `json:"realNameMask"`
|
RealNameMask string `json:"realNameMask"`
|
||||||
AvailableAmount int64 `json:"availableAmount"`
|
AvailableAmount int64 `json:"availableAmount"`
|
||||||
FrozenAmount int64 `json:"frozenAmount"`
|
FrozenAmount int64 `json:"frozenAmount"`
|
||||||
@@ -79,7 +79,7 @@ type AppMemberEditInp struct {
|
|||||||
LevelId uint64 `json:"levelId"`
|
LevelId uint64 `json:"levelId"`
|
||||||
RiskStatus int `json:"riskStatus" d:"1"`
|
RiskStatus int `json:"riskStatus" d:"1"`
|
||||||
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
|
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
|
||||||
AutoDispatch int `json:"autoDispatch" d:"2"`
|
AutoDispatch int `json:"autoDispatch" d:"1"`
|
||||||
MemberCategory int `json:"memberCategory"`
|
MemberCategory int `json:"memberCategory"`
|
||||||
IsTestAccount int `json:"isTestAccount"`
|
IsTestAccount int `json:"isTestAccount"`
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ func (in *AppMemberTeamInp) Filter(ctx context.Context) error { return nil }
|
|||||||
type AppMemberTeamModel struct {
|
type AppMemberTeamModel struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
ParentId uint64 `json:"parentId"`
|
ParentId uint64 `json:"parentId"`
|
||||||
RelativeLevel int `json:"relativeLevel"`
|
RelativeLevel int `json:"relativeLevel"`
|
||||||
TeamCount int `json:"teamCount"`
|
TeamCount int `json:"teamCount"`
|
||||||
@@ -352,7 +352,7 @@ type WalletAdjustmentListModel struct {
|
|||||||
RequestNo string `json:"requestNo"`
|
RequestNo string `json:"requestNo"`
|
||||||
MemberId uint64 `json:"memberId"`
|
MemberId uint64 `json:"memberId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
AdjustType int `json:"adjustType"`
|
AdjustType int `json:"adjustType"`
|
||||||
Direction int `json:"direction"`
|
Direction int `json:"direction"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ func (in *TaskDeleteInp) Filter(ctx context.Context) error { return nil }
|
|||||||
|
|
||||||
type TaskSubmissionListInp struct {
|
type TaskSubmissionListInp struct {
|
||||||
form.PageReq
|
form.PageReq
|
||||||
|
TradeOrderId uint64 `json:"tradeOrderId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
SubmissionStatus int `json:"submissionStatus"`
|
SubmissionStatus int `json:"submissionStatus"`
|
||||||
@@ -234,7 +235,7 @@ type TaskSubmissionListModel struct {
|
|||||||
TaskTitle string `json:"taskTitle"`
|
TaskTitle string `json:"taskTitle"`
|
||||||
MemberId uint64 `json:"memberId"`
|
MemberId uint64 `json:"memberId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
LevelName string `json:"levelName"`
|
LevelName string `json:"levelName"`
|
||||||
EvidenceText string `json:"evidenceText"`
|
EvidenceText string `json:"evidenceText"`
|
||||||
EvidenceAttachmentIds *gjson.Json `json:"evidenceAttachmentIds"`
|
EvidenceAttachmentIds *gjson.Json `json:"evidenceAttachmentIds"`
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type TradeOrderListModel struct {
|
|||||||
OrderNo string `json:"orderNo"`
|
OrderNo string `json:"orderNo"`
|
||||||
MemberId uint64 `json:"memberId"`
|
MemberId uint64 `json:"memberId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
RealNameMask string `json:"realNameMask"`
|
RealNameMask string `json:"realNameMask"`
|
||||||
AutoDispatch int `json:"autoDispatch"`
|
AutoDispatch int `json:"autoDispatch"`
|
||||||
AvailableAmount int64 `json:"availableAmount"`
|
AvailableAmount int64 `json:"availableAmount"`
|
||||||
@@ -53,6 +53,7 @@ type TradeOrderListModel struct {
|
|||||||
CreatedAt *gtime.Time `json:"createdAt"`
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
ApprovalStatus int `json:"approvalStatus"`
|
ApprovalStatus int `json:"approvalStatus"`
|
||||||
CanMatch bool `json:"canMatch"`
|
CanMatch bool `json:"canMatch"`
|
||||||
|
CanReview bool `json:"canReview"`
|
||||||
CanApply bool `json:"canApply"`
|
CanApply bool `json:"canApply"`
|
||||||
CanExecute bool `json:"canExecute"`
|
CanExecute bool `json:"canExecute"`
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ type RechargeOrderListModel struct {
|
|||||||
RechargeNo string `json:"rechargeNo"`
|
RechargeNo string `json:"rechargeNo"`
|
||||||
MemberId uint64 `json:"memberId"`
|
MemberId uint64 `json:"memberId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
RealNameMask string `json:"realNameMask"`
|
RealNameMask string `json:"realNameMask"`
|
||||||
AvailableAmount int64 `json:"availableAmount"`
|
AvailableAmount int64 `json:"availableAmount"`
|
||||||
ChannelId uint64 `json:"channelId"`
|
ChannelId uint64 `json:"channelId"`
|
||||||
@@ -213,7 +214,7 @@ type WithdrawalOrderListModel struct {
|
|||||||
WithdrawalNo string `json:"withdrawalNo"`
|
WithdrawalNo string `json:"withdrawalNo"`
|
||||||
MemberId uint64 `json:"memberId"`
|
MemberId uint64 `json:"memberId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
MobileMask string `json:"mobileMask"`
|
Mobile string `json:"mobile"`
|
||||||
AvailableAmount int64 `json:"availableAmount"`
|
AvailableAmount int64 `json:"availableAmount"`
|
||||||
FrozenAmount int64 `json:"frozenAmount"`
|
FrozenAmount int64 `json:"frozenAmount"`
|
||||||
RequestAmount int64 `json:"requestAmount"`
|
RequestAmount int64 `json:"requestAmount"`
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
-- 第六章:会员、敏感资料、团队、钱包调账与会员通知。
|
-- 第六章:会员、敏感资料、团队、钱包调账与会员通知。
|
||||||
-- MySQL 8.0+;金额统一使用分,敏感字段仅保存密文、检索哈希和脱敏值。
|
-- MySQL 8.0+;金额统一使用分。
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `hg_app_member` (
|
CREATE TABLE IF NOT EXISTS `hg_app_member` (
|
||||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
`tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
`tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
`member_no` varchar(32) NOT NULL COMMENT '会员编号',
|
`member_no` varchar(32) NOT NULL COMMENT '会员编号',
|
||||||
`username` varchar(64) NOT NULL COMMENT '用户名',
|
`username` varchar(64) NOT NULL COMMENT '用户名',
|
||||||
`mobile_cipher` text NOT NULL COMMENT '手机号密文(AES-GCM)',
|
`mobile` varchar(20) NOT NULL COMMENT '手机号',
|
||||||
`mobile_hash` char(64) NOT NULL COMMENT '手机号检索哈希(HMAC-SHA256)',
|
|
||||||
`mobile_mask` varchar(32) NOT NULL COMMENT '手机号脱敏值',
|
|
||||||
`password_hash` varchar(100) NOT NULL COMMENT '登录密码哈希(bcrypt)',
|
`password_hash` varchar(100) NOT NULL COMMENT '登录密码哈希(bcrypt)',
|
||||||
`trade_password_hash` varchar(100) NOT NULL DEFAULT '' COMMENT '交易密码哈希',
|
`trade_password_hash` varchar(100) NOT NULL DEFAULT '' COMMENT '交易密码哈希',
|
||||||
`level_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '会员等级ID',
|
`level_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '会员等级ID',
|
||||||
@@ -34,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `hg_app_member` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_app_member_no` (`tenant_id`,`member_no`),
|
UNIQUE KEY `uk_app_member_no` (`tenant_id`,`member_no`),
|
||||||
UNIQUE KEY `uk_app_member_username` (`tenant_id`,`username`),
|
UNIQUE KEY `uk_app_member_username` (`tenant_id`,`username`),
|
||||||
UNIQUE KEY `uk_app_member_mobile_hash` (`tenant_id`,`mobile_hash`),
|
UNIQUE KEY `uk_app_member_mobile` (`tenant_id`,`mobile`),
|
||||||
UNIQUE KEY `uk_app_member_invite_code` (`tenant_id`,`invite_code`),
|
UNIQUE KEY `uk_app_member_invite_code` (`tenant_id`,`invite_code`),
|
||||||
KEY `idx_app_member_team` (`tenant_id`,`parent_id`,`level`,`created_at`),
|
KEY `idx_app_member_team` (`tenant_id`,`parent_id`,`level`,`created_at`),
|
||||||
KEY `idx_app_member_status` (`tenant_id`,`member_status`,`auto_dispatch`,`created_at`),
|
KEY `idx_app_member_status` (`tenant_id`,`member_status`,`auto_dispatch`,`created_at`),
|
||||||
@@ -96,13 +94,18 @@ CREATE TABLE IF NOT EXISTS `hg_app_member_payment_account` (
|
|||||||
`bank_name` varchar(100) NOT NULL DEFAULT '' COMMENT '银行名称',
|
`bank_name` varchar(100) NOT NULL DEFAULT '' COMMENT '银行名称',
|
||||||
`holder_cipher` text NOT NULL COMMENT '持有人密文',
|
`holder_cipher` text NOT NULL COMMENT '持有人密文',
|
||||||
`holder_mask` varchar(64) NOT NULL COMMENT '持有人脱敏值',
|
`holder_mask` varchar(64) NOT NULL COMMENT '持有人脱敏值',
|
||||||
|
`identity_no_cipher` text COMMENT '身份证号密文',
|
||||||
|
`identity_no_hash` char(64) NOT NULL DEFAULT '' COMMENT '身份证号检索哈希',
|
||||||
|
`identity_no_mask` varchar(32) NOT NULL DEFAULT '' COMMENT '身份证号脱敏值',
|
||||||
`mobile_cipher` text COMMENT '预留手机号密文',
|
`mobile_cipher` text COMMENT '预留手机号密文',
|
||||||
`mobile_mask` varchar(32) NOT NULL DEFAULT '' COMMENT '预留手机号脱敏值',
|
`mobile_mask` varchar(32) NOT NULL DEFAULT '' COMMENT '预留手机号脱敏值',
|
||||||
`account_no_cipher` text NOT NULL COMMENT '账号密文',
|
`account_no_cipher` text NOT NULL COMMENT '账号密文',
|
||||||
`account_no_hash` char(64) NOT NULL COMMENT '账号检索哈希',
|
`account_no_hash` char(64) NOT NULL COMMENT '账号检索哈希',
|
||||||
`account_no_mask` varchar(80) NOT NULL COMMENT '账号脱敏值',
|
`account_no_mask` varchar(80) NOT NULL COMMENT '账号脱敏值',
|
||||||
`qr_attachment_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '收款码附件ID',
|
`qr_attachment_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '收款码附件ID',
|
||||||
`verify_status` tinyint NOT NULL DEFAULT 1 COMMENT '核验状态',
|
`wechat_qr_attachment_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '微信二维码附件ID',
|
||||||
|
`alipay_qr_attachment_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '支付宝二维码附件ID',
|
||||||
|
`verify_status` tinyint NOT NULL DEFAULT 2 COMMENT '核验状态',
|
||||||
`is_default` tinyint NOT NULL DEFAULT 2 COMMENT '默认账户',
|
`is_default` tinyint NOT NULL DEFAULT 2 COMMENT '默认账户',
|
||||||
`created_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建者',
|
`created_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建者',
|
||||||
`updated_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '更新者',
|
`updated_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '更新者',
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- 当前产品没有银行卡后台审核流程,绑定成功后直接核验通过。
|
||||||
|
ALTER TABLE `hg_app_member_payment_account`
|
||||||
|
MODIFY COLUMN `verify_status` tinyint NOT NULL DEFAULT 2 COMMENT '核验状态';
|
||||||
|
|
||||||
|
UPDATE `hg_app_member_payment_account`
|
||||||
|
SET `verify_status` = 2,
|
||||||
|
`updated_at` = CURRENT_TIMESTAMP(3)
|
||||||
|
WHERE `verify_status` = 1
|
||||||
|
AND `deleted_at` IS NULL;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 按移动端原型补齐身份证号和微信/支付宝双二维码字段。
|
||||||
|
ALTER TABLE `hg_app_member_payment_account`
|
||||||
|
ADD COLUMN `identity_no_cipher` text NULL COMMENT '身份证号密文' AFTER `holder_mask`,
|
||||||
|
ADD COLUMN `identity_no_hash` char(64) NOT NULL DEFAULT '' COMMENT '身份证号检索哈希' AFTER `identity_no_cipher`,
|
||||||
|
ADD COLUMN `identity_no_mask` varchar(32) NOT NULL DEFAULT '' COMMENT '身份证号脱敏值' AFTER `identity_no_hash`,
|
||||||
|
ADD COLUMN `wechat_qr_attachment_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '微信二维码附件ID' AFTER `qr_attachment_id`,
|
||||||
|
ADD COLUMN `alipay_qr_attachment_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '支付宝二维码附件ID' AFTER `wechat_qr_attachment_id`;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `hg_app_member`
|
||||||
|
MODIFY COLUMN `auto_dispatch` tinyint NOT NULL DEFAULT 1 COMMENT '派单模式:1自动派单 2人工匹配';
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `hg_app_member_notice_read` (
|
||||||
|
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||||
|
`notice_id` bigint unsigned NOT NULL COMMENT '通知ID',
|
||||||
|
`member_id` bigint unsigned NOT NULL COMMENT '会员ID',
|
||||||
|
`read_at` datetime(3) NOT NULL COMMENT '阅读时间',
|
||||||
|
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_member_notice_read` (`tenant_id`,`notice_id`,`member_id`),
|
||||||
|
KEY `idx_member_notice_read_member` (`tenant_id`,`member_id`,`read_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员_通知阅读记录';
|
||||||
@@ -197,9 +197,9 @@ WHERE l.id=@level_silver_id AND l.current_version_id=0;
|
|||||||
SET @demo_password_hash = COALESCE((SELECT password_hash FROM hg_app_member ORDER BY id LIMIT 1),'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.');
|
SET @demo_password_hash = COALESCE((SELECT password_hash FROM hg_app_member ORDER BY id LIMIT 1),'$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.');
|
||||||
|
|
||||||
INSERT INTO hg_app_member
|
INSERT INTO hg_app_member
|
||||||
(`tenant_id`,`member_no`,`username`,`mobile_cipher`,`mobile_hash`,`mobile_mask`,`password_hash`,`trade_password_hash`,`level_id`,`parent_id`,`level`,`tree`,`invite_code`,`member_status`,`risk_status`,`auto_dispatch`,`credit_score`,`is_test_account`,`last_login_at`,`last_login_ip_cipher`,`last_login_ip_mask`,`last_login_region`,`created_by`,`updated_by`,`deleted_by`,`created_at`,`updated_at`,`deleted_at`)
|
(`tenant_id`,`member_no`,`username`,`mobile`,`password_hash`,`trade_password_hash`,`level_id`,`parent_id`,`level`,`tree`,`invite_code`,`member_status`,`risk_status`,`auto_dispatch`,`credit_score`,`is_test_account`,`last_login_at`,`last_login_ip_cipher`,`last_login_ip_mask`,`last_login_region`,`created_by`,`updated_by`,`deleted_by`,`created_at`,`updated_at`,`deleted_at`)
|
||||||
SELECT @tenant_id,CONCAT('DEMO-MB-',LPAD(x.seq,2,'0')),CONCAT('demo_user_',LPAD(x.seq,2,'0')),
|
SELECT @tenant_id,CONCAT('DEMO-MB-',LPAD(x.seq,2,'0')),CONCAT('demo_user_',LPAD(x.seq,2,'0')),
|
||||||
CONCAT('DEMO_MOBILE_CIPHER_',x.seq),SHA2(CONCAT('1380000100',x.seq),256),CONCAT('138****10',LPAD(x.seq,2,'0')),
|
CONCAT('138000010',LPAD(x.seq,2,'0')),
|
||||||
@demo_password_hash,'',x.level_id,0,1,'',CONCAT('DM',LPAD(x.seq,6,'0')),x.member_status,x.risk_status,
|
@demo_password_hash,'',x.level_id,0,1,'',CONCAT('DM',LPAD(x.seq,6,'0')),x.member_status,x.risk_status,
|
||||||
IF(MOD(x.seq,2)=1,1,2),100-x.seq*5,1,DATE_SUB(@now,INTERVAL x.seq HOUR),'DEMO_IP_CIPHER','127.0.*.*','中国 上海',
|
IF(MOD(x.seq,2)=1,1,2),100-x.seq*5,1,DATE_SUB(@now,INTERVAL x.seq HOUR),'DEMO_IP_CIPHER','127.0.*.*','中国 上海',
|
||||||
@admin_id,@admin_id,IF(x.member_status=3,@admin_id,0),DATE_SUB(@now,INTERVAL x.seq*3 DAY),@now,
|
@admin_id,@admin_id,IF(x.member_status=3,@admin_id,0),DATE_SUB(@now,INTERVAL x.seq*3 DAY),@now,
|
||||||
|
|||||||
+1036
-198
File diff suppressed because it is too large
Load Diff
@@ -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; mobileMask: string; realNameMask: string;
|
id: number; memberNo: string; username: string; mobile: 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;
|
||||||
@@ -30,7 +30,7 @@ export const columns = [
|
|||||||
{ title: 'ID', key: 'id', width: 80 },
|
{ title: 'ID', key: 'id', width: 80 },
|
||||||
{ title: '会员编号', key: 'memberNo', width: 170 },
|
{ title: '会员编号', key: 'memberNo', width: 170 },
|
||||||
{ title: '账号', key: 'username', width: 130 },
|
{ title: '账号', key: 'username', width: 130 },
|
||||||
{ title: '手机号', key: 'mobileMask', width: 130 },
|
{ title: '手机号', key: 'mobile', width: 130 },
|
||||||
{ title: '绑卡姓名', key: 'realNameMask', width: 100 },
|
{ title: '绑卡姓名', key: 'realNameMask', 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) },
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const schemas = ref<FormSchema[]>([
|
|||||||
const tag = (type, value) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
|
const tag = (type, value) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
|
||||||
export const columns = [
|
export const columns = [
|
||||||
{ title: '申请单号', key: 'requestNo', width: 210 }, { title: '会员ID', key: 'memberId', width: 90 },
|
{ title: '申请单号', key: 'requestNo', width: 210 }, { title: '会员ID', key: 'memberId', width: 90 },
|
||||||
{ title: '用户名', key: 'username', width: 120 }, { title: '手机号', key: 'mobileMask', width: 130 },
|
{ title: '用户名', key: 'username', width: 120 }, { title: '手机号', key: 'mobile', width: 130 },
|
||||||
{ title: '类型', key: 'adjustType', width: 110, render: (r) => tag('walletAdjustType', r.adjustType) },
|
{ title: '类型', key: 'adjustType', width: 110, render: (r) => tag('walletAdjustType', r.adjustType) },
|
||||||
{ title: '方向', key: 'direction', width: 90, render: (r) => tag('walletDirection', r.direction) },
|
{ title: '方向', key: 'direction', width: 90, render: (r) => tag('walletDirection', r.direction) },
|
||||||
{ title: '金额', key: 'amount', width: 120, render: (r) => `¥${((r.amount || 0) / 100).toFixed(2)}` },
|
{ title: '金额', key: 'amount', width: 120, render: (r) => `¥${((r.amount || 0) / 100).toFixed(2)}` },
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<n-descriptions v-if="detail.id" bordered :column="2">
|
<n-descriptions v-if="detail.id" bordered :column="2">
|
||||||
<n-descriptions-item label="任务">{{ detail.taskTitle }}</n-descriptions-item>
|
<n-descriptions-item label="任务">{{ detail.taskTitle }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="会员">
|
<n-descriptions-item label="会员">
|
||||||
{{ detail.username }} / {{ detail.mobileMask }}
|
{{ detail.username }} / {{ detail.mobile }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="佣金">
|
<n-descriptions-item label="佣金">
|
||||||
¥{{ ((detail.commissionAmount || 0) / 100).toFixed(2) }}
|
¥{{ ((detail.commissionAmount || 0) / 100).toFixed(2) }}
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { BasicForm, useForm } from '@/components/Form';
|
import { BasicForm, useForm } from '@/components/Form';
|
||||||
import { BasicTable, TableAction } from '@/components/Table';
|
import { BasicTable, TableAction } from '@/components/Table';
|
||||||
@@ -118,6 +119,7 @@
|
|||||||
import { columns, loadOptions, schemas } from './model';
|
import { columns, loadOptions, schemas } from './model';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
const route = useRoute();
|
||||||
const dict = useDictStore();
|
const dict = useDictStore();
|
||||||
const searchRef = ref();
|
const searchRef = ref();
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
@@ -152,7 +154,11 @@
|
|||||||
schemas,
|
schemas,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadTable = (params) => SubmissionList({ ...searchRef.value?.formModel, ...params });
|
const loadTable = (params) => SubmissionList({
|
||||||
|
...searchRef.value?.formModel,
|
||||||
|
...params,
|
||||||
|
tradeOrderId: Number(route.query.tradeOrderId) || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
tableRef.value?.reload();
|
tableRef.value?.reload();
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const tag = (k, v) =>
|
|||||||
export const columns = [
|
export const columns = [
|
||||||
{ title: '记录ID', key: 'id', width: 90 },
|
{ title: '记录ID', key: 'id', width: 90 },
|
||||||
{ title: '用户名', key: 'username', width: 130 },
|
{ title: '用户名', key: 'username', width: 130 },
|
||||||
{ title: '手机号', key: 'mobileMask', width: 130 },
|
{ title: '手机号', key: 'mobile', width: 130 },
|
||||||
{ title: '任务标题', key: 'taskTitle', width: 180 },
|
{ title: '任务标题', key: 'taskTitle', width: 180 },
|
||||||
{ title: '等级', key: 'levelName', width: 110 },
|
{ title: '等级', key: 'levelName', width: 110 },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showDetail" preset="card" title="订单详情" class="max-w-900px">
|
<n-modal v-model:show="showDetail" preset="card" title="订单详情" class="max-w-900px">
|
||||||
<n-descriptions v-if="detail" bordered label-placement="left" :column="2">
|
<n-descriptions v-if="detail" bordered label-placement="left" :column="2">
|
||||||
<n-descriptions-item label="订单号">{{ detail.orderNo }}</n-descriptions-item><n-descriptions-item label="会员">{{ detail.username }} / {{ detail.mobileMask }}</n-descriptions-item>
|
<n-descriptions-item label="订单号">{{ detail.orderNo }}</n-descriptions-item><n-descriptions-item label="会员">{{ detail.username }} / {{ detail.mobile }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="商品/任务">{{ detail.productName || '-' }}</n-descriptions-item><n-descriptions-item label="规则等级ID">{{ detail.memberLevelId }}</n-descriptions-item>
|
<n-descriptions-item label="商品/任务">{{ detail.productName || '-' }}</n-descriptions-item><n-descriptions-item label="规则等级ID">{{ detail.memberLevelId }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="订单金额">{{ yuan(detail.totalAmount) }}</n-descriptions-item><n-descriptions-item label="佣金">{{ yuan(detail.commissionAmount) }}</n-descriptions-item>
|
<n-descriptions-item label="订单金额">{{ yuan(detail.totalAmount) }}</n-descriptions-item><n-descriptions-item label="佣金">{{ yuan(detail.commissionAmount) }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="规则快照" :span="2"><n-code :code="JSON.stringify(detail.ruleSnapshot || {}, null, 2)" language="json" word-wrap /></n-descriptions-item>
|
<n-descriptions-item label="规则快照" :span="2"><n-code :code="JSON.stringify(detail.ruleSnapshot || {}, null, 2)" language="json" word-wrap /></n-descriptions-item>
|
||||||
@@ -66,10 +66,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { NButton, NImage, NTag, useMessage } from 'naive-ui';
|
import { NButton, NImage, NTag, useMessage } from 'naive-ui';
|
||||||
import { BasicTable, TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { adaTableScrollX } from '@/utils/hotgo';
|
import { BasicTable, TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { adaTableScrollX } from '@/utils/hotgo';
|
||||||
import { TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
|
import { TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
|
||||||
const message=useMessage();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
|
const message=useMessage();const router=useRouter();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
|
||||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:5'},labelWidth:80,schemas});const loadDataTable=(res)=>TradeOrderList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const yuan=(v)=>`¥${((v||0)/100).toFixed(2)}`;
|
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:5'},labelWidth:80,schemas});const loadDataTable=(res)=>TradeOrderList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const yuan=(v)=>`¥${((v||0)/100).toFixed(2)}`;
|
||||||
const yuanPlain=(v)=>((v||0)/100).toFixed(2);
|
const yuanPlain=(v)=>((v||0)/100).toFixed(2);
|
||||||
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>();
|
||||||
@@ -99,10 +100,14 @@ async function confirmMatch(){
|
|||||||
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}
|
||||||
const actionColumn=reactive({width:180,title:'操作',key:'action',fixed:'right',render:(row)=>h(TableAction as any,{style:'button',actions:[
|
const actionColumn=reactive({width:210,title:'操作',key:'action',fixed:'right',render:(row)=>{
|
||||||
{label:'匹配订单',type:'primary',disabled:!row.canMatch,title:row.canMatch?'匹配订单':'仅“匹配中”的订单可操作',onClick:()=>openMatch(row)},
|
const actions:any[]=[];
|
||||||
{label:'详情',onClick:()=>view(row)},
|
if(row.canMatch)actions.push({label:'匹配订单',type:'primary',onClick:()=>openMatch(row)});
|
||||||
]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
if(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(row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员提交',disabled:true,title:'会员提交任务凭证后即可审核'});
|
||||||
|
actions.push({label:'详情',onClick:()=>view(row)});
|
||||||
|
return h(TableAction as any,{style:'button',actions});
|
||||||
|
}});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||||
watch(autoRefresh,(enabled)=>{
|
watch(autoRefresh,(enabled)=>{
|
||||||
clearInterval(timer);refreshCountdown.value=10;
|
clearInterval(timer);refreshCountdown.value=10;
|
||||||
if(!enabled)return;
|
if(!enabled)return;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const columns = [
|
|||||||
{ title: '订单号', key: 'orderNo', width: 210 },
|
{ title: '订单号', key: 'orderNo', width: 210 },
|
||||||
{ title: '绑卡姓名', key: 'realNameMask', width: 110 },
|
{ title: '绑卡姓名', key: 'realNameMask', width: 110 },
|
||||||
{ title: '用户名', key: 'username', width: 120 },
|
{ title: '用户名', key: 'username', width: 120 },
|
||||||
{ title: '手机号', key: 'mobileMask', width: 130 },
|
{ title: '手机号', key: 'mobile', width: 130 },
|
||||||
{ title: '自动派单', key: 'autoDispatch', width: 100, render: (r) => tag('tradeAutoDispatch', r.autoDispatch) },
|
{ title: '自动派单', key: 'autoDispatch', width: 100, render: (r) => tag('tradeAutoDispatch', r.autoDispatch) },
|
||||||
{ title: '余额', key: 'availableAmount', width: 120, render: (r) => yuan(r.availableAmount) },
|
{ title: '余额', key: 'availableAmount', width: 120, render: (r) => yuan(r.availableAmount) },
|
||||||
{ title: '当日交易', key: 'todayTradeCount', width: 90 },
|
{ title: '当日交易', key: 'todayTradeCount', width: 90 },
|
||||||
@@ -30,7 +30,9 @@ export const columns = [
|
|||||||
{ 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: 180, ellipsis: { tooltip: true } },
|
||||||
{ title: '订单状态', key: 'orderStatus', width: 100, render: (r) => tag('tradeOrderStatus', r.orderStatus) },
|
{ title: '订单状态', key: 'orderStatus', width: 110, render: (r) => r.orderType === 2 && r.orderStatus === 2
|
||||||
|
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待会员提交' })
|
||||||
|
: 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) },
|
||||||
];
|
];
|
||||||
export const loadOptions = () => dict.loadOptions(['tradeOrderType', 'tradeOrderStatus', 'tradeFreezeStatus', 'tradeAutoDispatch', 'trade_exception_reason']);
|
export const loadOptions = () => dict.loadOptions(['tradeOrderType', 'tradeOrderStatus', 'tradeFreezeStatus', 'tradeAutoDispatch', 'trade_exception_reason']);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const yuan = (value) => `¥${((value || 0) / 100).toFixed(2)}`;
|
|||||||
export const columns = [
|
export const columns = [
|
||||||
{ title: '订单号', key: 'rechargeNo', width: 210 },
|
{ title: '订单号', key: 'rechargeNo', width: 210 },
|
||||||
{ title: '真实姓名', key: 'realNameMask', width: 110 },
|
{ title: '真实姓名', key: 'realNameMask', width: 110 },
|
||||||
{ title: '手机号', key: 'mobileMask', width: 130 },
|
{ title: '手机号', key: 'mobile', width: 130 },
|
||||||
{ title: '用户余额', key: 'availableAmount', width: 120, render: (row) => yuan(row.availableAmount) },
|
{ title: '用户余额', key: 'availableAmount', width: 120, render: (row) => yuan(row.availableAmount) },
|
||||||
{ title: '交易金额', key: 'requestAmount', width: 120, render: (row) => yuan(row.requestAmount) },
|
{ title: '交易金额', key: 'requestAmount', width: 120, render: (row) => yuan(row.requestAmount) },
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user