From 40bfa4cba0ac905fb4ca66bae0c836de63675fb5 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Tue, 13 Oct 2026 14:19:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E4=BC=9A=E5=91=98?= =?UTF-8?q?=E4=BA=A4=E6=98=93=E4=B8=8E=E4=BB=BB=E5=8A=A1=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/internal/dao/internal/app_member.go | 8 +- server/internal/logic/sys/app_member.go | 36 +- server/internal/logic/sys/recharge_order.go | 9 +- server/internal/logic/sys/task_submission.go | 12 +- server/internal/logic/sys/trade_order.go | 70 +- server/internal/logic/sys/withdrawal_order.go | 9 +- server/internal/model/do/app_member.go | 4 +- server/internal/model/entity/app_member.go | 4 +- .../internal/model/input/sysin/app_member.go | 8 +- .../model/input/sysin/product_task.go | 3 +- server/internal/model/input/sysin/trade.go | 7 +- .../20261008_005_member_management.sql | 15 +- ...261013_001_payment_account_auto_verify.sql | 9 + ...3_002_payment_account_prototype_fields.sql | 7 + .../20261013_003_auto_dispatch_default.sql | 2 + .../20261013_004_member_notice_read.sql | 11 + .../seeds/20261008_business_demo_data.sql | 4 +- web/src/views/app-member/list/index.vue | 1234 ++++++++++++++--- web/src/views/app-member/list/model.ts | 4 +- web/src/views/app-member/wallet/model.ts | 2 +- .../views/product-task/submission/index.vue | 10 +- .../views/product-task/submission/model.ts | 2 +- web/src/views/trade/order/index.vue | 17 +- web/src/views/trade/order/model.ts | 6 +- web/src/views/trade/recharge/model.ts | 2 +- 25 files changed, 1198 insertions(+), 297 deletions(-) create mode 100644 server/storage/data/migrations/20261013_001_payment_account_auto_verify.sql create mode 100644 server/storage/data/migrations/20261013_002_payment_account_prototype_fields.sql create mode 100644 server/storage/data/migrations/20261013_003_auto_dispatch_default.sql create mode 100644 server/storage/data/migrations/20261013_004_member_notice_read.sql diff --git a/server/internal/dao/internal/app_member.go b/server/internal/dao/internal/app_member.go index abe7214..aba6866 100644 --- a/server/internal/dao/internal/app_member.go +++ b/server/internal/dao/internal/app_member.go @@ -25,9 +25,7 @@ type AppMemberColumns struct { TenantId string // 租户ID MemberNo string // 会员编号 Username string // 用户名 - MobileCipher string // 手机号密文(AES-GCM) - MobileHash string // 手机号检索哈希(HMAC-SHA256) - MobileMask string // 手机号脱敏值 + Mobile string // PasswordHash string // 登录密码哈希(bcrypt) TradePasswordHash string // 交易密码哈希 LevelId string // 会员等级ID @@ -58,9 +56,7 @@ var appMemberColumns = AppMemberColumns{ TenantId: "tenant_id", MemberNo: "member_no", Username: "username", - MobileCipher: "mobile_cipher", - MobileHash: "mobile_hash", - MobileMask: "mobile_mask", + Mobile: "mobile", PasswordHash: "password_hash", TradePasswordHash: "trade_password_hash", LevelId: "level_id", diff --git a/server/internal/logic/sys/app_member.go b/server/internal/logic/sys/app_member.go index b3d7293..0c287e2 100644 --- a/server/internal/logic/sys/app_member.go +++ b/server/internal/logic/sys/app_member.go @@ -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.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("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)), 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), @@ -72,11 +72,7 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%") } if in.Mobile != "" { - hash, e := encrypt.HashField(ctx, in.Mobile) - if e != nil { - return nil, 0, e - } - mod = mod.Where(col("m", mc.MobileHash), hash) + mod = mod.Where(col("m", mc.Mobile), in.Mobile) } if in.MemberStatus > 0 { 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() 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( - 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) if err != nil { 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} if in.Mobile != "" { - cipher, e := encrypt.EncryptField(ctx, in.Mobile) - if e != nil { - return e - } - 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 + count, err = s.memberModel(ctx).Where(mc.Mobile, in.Mobile).WhereNot(mc.Id, in.Id).Count() + if err != nil { + return err } if count > 0 { return gerror.New("手机号已存在") } - data[mc.MobileCipher] = cipher - data[mc.MobileHash] = hash - data[mc.MobileMask] = encrypt.MaskMobile(in.Mobile) + data[mc.Mobile] = in.Mobile } if in.Password != "" { 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.MemberStatus] = consts.AppMemberStatusNormal 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 { return gerror.Wrap(e, "创建会员失败") } @@ -442,7 +428,7 @@ func (s *sSysAppMember) Sensitive(ctx context.Context, in *sysin.AppMemberSensit case "member": var member *entity.AppMember 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 _ = s.profileModel(ctx).Where(dao.AppMemberProfile.Columns().MemberId, in.TargetId).Scan(&p) 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() token := "tr_" + fmt.Sprint(in.MemberId) + " " 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) if err != nil { 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) { 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 { mod = mod.Where(col("a", ac.MemberId), in.MemberId) } diff --git a/server/internal/logic/sys/recharge_order.go b/server/internal/logic/sys/recharge_order.go index 9251e39..4bb6983 100644 --- a/server/internal/logic/sys/recharge_order.go +++ b/server/internal/logic/sys/recharge_order.go @@ -12,7 +12,6 @@ import ( "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" - "hotgo/utility/encrypt" "github.com/gogf/gf/v2/database/gdb" "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 { rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns() return s.baseModel(ctx). - Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.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) { @@ -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+"%") } if in.Mobile != "" { - hash, err := encrypt.HashField(ctx, in.Mobile) - if err != nil { - return nil, err - } - mod = mod.Where(col("m", mc.MobileHash), hash) + mod = mod.Where(col("m", mc.Mobile), in.Mobile) } if in.RechargeStatus > 0 { mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus) diff --git a/server/internal/logic/sys/task_submission.go b/server/internal/logic/sys/task_submission.go index df2991b..6da11cf 100644 --- a/server/internal/logic/sys/task_submission.go +++ b/server/internal/logic/sys/task_submission.go @@ -12,7 +12,6 @@ import ( "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" - "hotgo/utility/encrypt" "github.com/gogf/gf/v2/database/gdb" "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 { 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) { sc, mc := dao.TaskSubmission.Columns(), dao.AppMember.Columns() + if in.TradeOrderId > 0 { + mod = mod.Where(col("s", sc.TradeOrderId), in.TradeOrderId) + } if in.Username != "" { mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%") } if in.Mobile != "" { - hash, err := encrypt.HashField(ctx, in.Mobile) - if err != nil { - return nil, err - } - mod = mod.Where(col("m", mc.MobileHash), hash) + mod = mod.Where(col("m", mc.Mobile), in.Mobile) } if in.SubmissionStatus > 0 { mod = mod.Where(col("s", sc.SubmissionStatus), in.SubmissionStatus) diff --git a/server/internal/logic/sys/trade_order.go b/server/internal/logic/sys/trade_order.go index 3f6bbe0..26f016a 100644 --- a/server/internal/logic/sys/trade_order.go +++ b/server/internal/logic/sys/trade_order.go @@ -9,10 +9,10 @@ import ( "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/hgorm/handler" + "hotgo/internal/library/hgorm/hook" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" - "hotgo/utility/encrypt" "github.com/gogf/gf/v2/database/gdb" "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 { - 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) + 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"). 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.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)). - 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) { @@ -50,11 +51,7 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%") } if in.Mobile != "" { - hash, e := encrypt.HashField(ctx, in.Mobile) - if e != nil { - return nil, 0, e - } - mod = mod.Where(col("m", mc.MobileHash), hash) + mod = mod.Where(col("m", mc.Mobile), in.Mobile) } if in.OrderStatus > 0 { 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}). Fields(pc.Id, pc.Name, pc.CoverUrl, pc.PriceAmount). Where(pc.PublishStatus, consts.BizPublishPublished). + Where(pc.Currency, "CNY"). WhereNull(pc.DeletedAt). Where("("+pc.StartAt+" IS NULL OR "+pc.StartAt+" <= ?)", 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 { return gerror.New("商品不存在、已下架或无权使用") } + if product.Currency != "CNY" { + return gerror.New("任务订单只能匹配 CNY 商品") + } now := gtime.Now() if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) { return gerror.New("商品当前不在可匹配时间范围内") @@ -186,7 +187,38 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp return gerror.New("会员等级生效版本不存在") } 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 + 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{ "source": "admin_manual_match", "productId": product.Id, "productNo": product.ProductNo, "unitPriceAmount": product.PriceAmount, "quantity": in.Quantity, @@ -195,10 +227,10 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp }) oc := dao.TradeOrder.Columns() 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.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), } 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) { var order *entity.TradeOrder if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil { diff --git a/server/internal/logic/sys/withdrawal_order.go b/server/internal/logic/sys/withdrawal_order.go index 894f2c7..b61a835 100644 --- a/server/internal/logic/sys/withdrawal_order.go +++ b/server/internal/logic/sys/withdrawal_order.go @@ -12,7 +12,6 @@ import ( "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" - "hotgo/utility/encrypt" "github.com/gogf/gf/v2/database/gdb" "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.AppMemberPaymentAccount.Table()+" pa", col("pa", pc.Id)+"="+col("o", wc.PaymentAccountId)). 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) { @@ -51,11 +50,7 @@ func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrde mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%") } if in.Mobile != "" { - hash, e := encrypt.HashField(ctx, in.Mobile) - if e != nil { - return nil, 0, e - } - mod = mod.Where(col("m", mc.MobileHash), hash) + mod = mod.Where(col("m", mc.Mobile), in.Mobile) } if in.WithdrawalStatus > 0 { mod = mod.Where(col("o", wc.WithdrawalStatus), in.WithdrawalStatus) diff --git a/server/internal/model/do/app_member.go b/server/internal/model/do/app_member.go index 8c01122..4dfea9c 100644 --- a/server/internal/model/do/app_member.go +++ b/server/internal/model/do/app_member.go @@ -16,9 +16,7 @@ type AppMember struct { TenantId any // 租户ID MemberNo any // 会员编号 Username any // 用户名 - MobileCipher any // 手机号密文(AES-GCM) - MobileHash any // 手机号检索哈希(HMAC-SHA256) - MobileMask any // 手机号脱敏值 + Mobile any // PasswordHash any // 登录密码哈希(bcrypt) TradePasswordHash any // 交易密码哈希 LevelId any // 会员等级ID diff --git a/server/internal/model/entity/app_member.go b/server/internal/model/entity/app_member.go index b7633c0..f74182a 100644 --- a/server/internal/model/entity/app_member.go +++ b/server/internal/model/entity/app_member.go @@ -14,9 +14,7 @@ type AppMember struct { TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"` MemberNo string `json:"memberNo" orm:"member_no" description:"会员编号"` Username string `json:"username" orm:"username" description:"用户名"` - MobileCipher string `json:"mobileCipher" orm:"mobile_cipher" description:"手机号密文(AES-GCM)"` - MobileHash string `json:"mobileHash" orm:"mobile_hash" description:"手机号检索哈希(HMAC-SHA256)"` - MobileMask string `json:"mobileMask" orm:"mobile_mask" description:"手机号脱敏值"` + Mobile string `json:"mobile" orm:"mobile" description:""` PasswordHash string `json:"passwordHash" orm:"password_hash" description:"登录密码哈希(bcrypt)"` TradePasswordHash string `json:"tradePasswordHash" orm:"trade_password_hash" description:"交易密码哈希"` LevelId uint64 `json:"levelId" orm:"level_id" description:"会员等级ID"` diff --git a/server/internal/model/input/sysin/app_member.go b/server/internal/model/input/sysin/app_member.go index 033f801..5e5d88f 100644 --- a/server/internal/model/input/sysin/app_member.go +++ b/server/internal/model/input/sysin/app_member.go @@ -30,7 +30,7 @@ type AppMemberListModel struct { Id uint64 `json:"id"` MemberNo string `json:"memberNo"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` RealNameMask string `json:"realNameMask"` AvailableAmount int64 `json:"availableAmount"` FrozenAmount int64 `json:"frozenAmount"` @@ -79,7 +79,7 @@ type AppMemberEditInp struct { 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:"2"` + AutoDispatch int `json:"autoDispatch" d:"1"` MemberCategory int `json:"memberCategory"` IsTestAccount int `json:"isTestAccount"` } @@ -280,7 +280,7 @@ func (in *AppMemberTeamInp) Filter(ctx context.Context) error { return nil } type AppMemberTeamModel struct { Id uint64 `json:"id"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` ParentId uint64 `json:"parentId"` RelativeLevel int `json:"relativeLevel"` TeamCount int `json:"teamCount"` @@ -352,7 +352,7 @@ type WalletAdjustmentListModel struct { RequestNo string `json:"requestNo"` MemberId uint64 `json:"memberId"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` AdjustType int `json:"adjustType"` Direction int `json:"direction"` Amount int64 `json:"amount"` diff --git a/server/internal/model/input/sysin/product_task.go b/server/internal/model/input/sysin/product_task.go index 58c50c0..57c6e1d 100644 --- a/server/internal/model/input/sysin/product_task.go +++ b/server/internal/model/input/sysin/product_task.go @@ -218,6 +218,7 @@ func (in *TaskDeleteInp) Filter(ctx context.Context) error { return nil } type TaskSubmissionListInp struct { form.PageReq + TradeOrderId uint64 `json:"tradeOrderId"` Username string `json:"username"` Mobile string `json:"mobile"` SubmissionStatus int `json:"submissionStatus"` @@ -234,7 +235,7 @@ type TaskSubmissionListModel struct { TaskTitle string `json:"taskTitle"` MemberId uint64 `json:"memberId"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` LevelName string `json:"levelName"` EvidenceText string `json:"evidenceText"` EvidenceAttachmentIds *gjson.Json `json:"evidenceAttachmentIds"` diff --git a/server/internal/model/input/sysin/trade.go b/server/internal/model/input/sysin/trade.go index 25cc754..ff2bc7e 100644 --- a/server/internal/model/input/sysin/trade.go +++ b/server/internal/model/input/sysin/trade.go @@ -30,7 +30,7 @@ type TradeOrderListModel struct { OrderNo string `json:"orderNo"` MemberId uint64 `json:"memberId"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` RealNameMask string `json:"realNameMask"` AutoDispatch int `json:"autoDispatch"` AvailableAmount int64 `json:"availableAmount"` @@ -53,6 +53,7 @@ type TradeOrderListModel struct { CreatedAt *gtime.Time `json:"createdAt"` ApprovalStatus int `json:"approvalStatus"` CanMatch bool `json:"canMatch"` + CanReview bool `json:"canReview"` CanApply bool `json:"canApply"` CanExecute bool `json:"canExecute"` } @@ -133,7 +134,7 @@ type RechargeOrderListModel struct { RechargeNo string `json:"rechargeNo"` MemberId uint64 `json:"memberId"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` RealNameMask string `json:"realNameMask"` AvailableAmount int64 `json:"availableAmount"` ChannelId uint64 `json:"channelId"` @@ -213,7 +214,7 @@ type WithdrawalOrderListModel struct { WithdrawalNo string `json:"withdrawalNo"` MemberId uint64 `json:"memberId"` Username string `json:"username"` - MobileMask string `json:"mobileMask"` + Mobile string `json:"mobile"` AvailableAmount int64 `json:"availableAmount"` FrozenAmount int64 `json:"frozenAmount"` RequestAmount int64 `json:"requestAmount"` diff --git a/server/storage/data/migrations/20261008_005_member_management.sql b/server/storage/data/migrations/20261008_005_member_management.sql index febc156..2c6e416 100644 --- a/server/storage/data/migrations/20261008_005_member_management.sql +++ b/server/storage/data/migrations/20261008_005_member_management.sql @@ -1,14 +1,12 @@ -- 第六章:会员、敏感资料、团队、钱包调账与会员通知。 --- MySQL 8.0+;金额统一使用分,敏感字段仅保存密文、检索哈希和脱敏值。 +-- MySQL 8.0+;金额统一使用分。 CREATE TABLE IF NOT EXISTS `hg_app_member` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `tenant_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', `member_no` varchar(32) NOT NULL COMMENT '会员编号', `username` varchar(64) NOT NULL COMMENT '用户名', - `mobile_cipher` text NOT NULL COMMENT '手机号密文(AES-GCM)', - `mobile_hash` char(64) NOT NULL COMMENT '手机号检索哈希(HMAC-SHA256)', - `mobile_mask` varchar(32) NOT NULL COMMENT '手机号脱敏值', + `mobile` varchar(20) NOT NULL COMMENT '手机号', `password_hash` varchar(100) NOT NULL COMMENT '登录密码哈希(bcrypt)', `trade_password_hash` varchar(100) NOT NULL DEFAULT '' COMMENT '交易密码哈希', `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`), UNIQUE KEY `uk_app_member_no` (`tenant_id`,`member_no`), 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`), 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`), @@ -96,13 +94,18 @@ CREATE TABLE IF NOT EXISTS `hg_app_member_payment_account` ( `bank_name` varchar(100) NOT NULL DEFAULT '' COMMENT '银行名称', `holder_cipher` text 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_mask` varchar(32) NOT NULL DEFAULT '' COMMENT '预留手机号脱敏值', `account_no_cipher` text NOT NULL COMMENT '账号密文', `account_no_hash` char(64) NOT NULL COMMENT '账号检索哈希', `account_no_mask` varchar(80) NOT NULL COMMENT '账号脱敏值', `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 '默认账户', `created_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建者', `updated_by` bigint unsigned NOT NULL DEFAULT 0 COMMENT '更新者', diff --git a/server/storage/data/migrations/20261013_001_payment_account_auto_verify.sql b/server/storage/data/migrations/20261013_001_payment_account_auto_verify.sql new file mode 100644 index 0000000..0f18293 --- /dev/null +++ b/server/storage/data/migrations/20261013_001_payment_account_auto_verify.sql @@ -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; diff --git a/server/storage/data/migrations/20261013_002_payment_account_prototype_fields.sql b/server/storage/data/migrations/20261013_002_payment_account_prototype_fields.sql new file mode 100644 index 0000000..7ad744a --- /dev/null +++ b/server/storage/data/migrations/20261013_002_payment_account_prototype_fields.sql @@ -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`; diff --git a/server/storage/data/migrations/20261013_003_auto_dispatch_default.sql b/server/storage/data/migrations/20261013_003_auto_dispatch_default.sql new file mode 100644 index 0000000..ada29e8 --- /dev/null +++ b/server/storage/data/migrations/20261013_003_auto_dispatch_default.sql @@ -0,0 +1,2 @@ +ALTER TABLE `hg_app_member` + MODIFY COLUMN `auto_dispatch` tinyint NOT NULL DEFAULT 1 COMMENT '派单模式:1自动派单 2人工匹配'; diff --git a/server/storage/data/migrations/20261013_004_member_notice_read.sql b/server/storage/data/migrations/20261013_004_member_notice_read.sql new file mode 100644 index 0000000..a8438b8 --- /dev/null +++ b/server/storage/data/migrations/20261013_004_member_notice_read.sql @@ -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='会员_通知阅读记录'; diff --git a/server/storage/data/seeds/20261008_business_demo_data.sql b/server/storage/data/seeds/20261008_business_demo_data.sql index aa840eb..58c765b 100644 --- a/server/storage/data/seeds/20261008_business_demo_data.sql +++ b/server/storage/data/seeds/20261008_business_demo_data.sql @@ -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.'); 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')), - 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, 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, diff --git a/web/src/views/app-member/list/index.vue b/web/src/views/app-member/list/index.vue index 1992efd..fdbb31c 100644 --- a/web/src/views/app-member/list/index.vue +++ b/web/src/views/app-member/list/index.vue @@ -1,292 +1,1130 @@ diff --git a/web/src/views/app-member/list/model.ts b/web/src/views/app-member/list/model.ts index 836a662..e1eac4b 100644 --- a/web/src/views/app-member/list/model.ts +++ b/web/src/views/app-member/list/model.ts @@ -7,7 +7,7 @@ import { defRangeShortcuts } from '@/utils/dateUtil'; const dict = useDictStore(); 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; inviteCode: string; memberStatus: number; riskStatus: number; autoDispatch: number; memberCategory: number; isTestAccount: number; lastLoginAt?: string; lastLoginIpMask: string; lastLoginRegion: string; @@ -30,7 +30,7 @@ export const columns = [ { title: 'ID', key: 'id', width: 80 }, { title: '会员编号', key: 'memberNo', width: 170 }, { title: '账号', key: 'username', width: 130 }, - { title: '手机号', key: 'mobileMask', width: 130 }, + { title: '手机号', key: 'mobile', width: 130 }, { title: '绑卡姓名', key: 'realNameMask', width: 100 }, { title: '可用余额', key: 'availableAmount', width: 120, render: (row: MemberRecord) => money(row.availableAmount) }, { title: '冻结余额', key: 'frozenAmount', width: 120, render: (row: MemberRecord) => money(row.frozenAmount) }, diff --git a/web/src/views/app-member/wallet/model.ts b/web/src/views/app-member/wallet/model.ts index 901cdf1..0c1d6de 100644 --- a/web/src/views/app-member/wallet/model.ts +++ b/web/src/views/app-member/wallet/model.ts @@ -12,7 +12,7 @@ export const schemas = ref([ const tag = (type, value) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' }); export const columns = [ { 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: 'direction', width: 90, render: (r) => tag('walletDirection', r.direction) }, { title: '金额', key: 'amount', width: 120, render: (r) => `¥${((r.amount || 0) / 100).toFixed(2)}` }, diff --git a/web/src/views/product-task/submission/index.vue b/web/src/views/product-task/submission/index.vue index 452d04a..6a40a43 100644 --- a/web/src/views/product-task/submission/index.vue +++ b/web/src/views/product-task/submission/index.vue @@ -27,7 +27,7 @@ {{ detail.taskTitle }} - {{ detail.username }} / {{ detail.mobileMask }} + {{ detail.username }} / {{ detail.mobile }} ¥{{ ((detail.commissionAmount || 0) / 100).toFixed(2) }} @@ -110,6 +110,7 @@