diff --git a/server/internal/logic/sys/app_member.go b/server/internal/logic/sys/app_member.go index 4158f70..10ea9c3 100644 --- a/server/internal/logic/sys/app_member.go +++ b/server/internal/logic/sys/app_member.go @@ -210,7 +210,11 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er } } else { data[mc.MemberNo] = "MB" + gtime.Now().Format("YmdHis") + grand.S(5) - data[mc.InviteCode] = strings.ToUpper(grand.S(8)) + inviteCode, e := newAppMemberInviteCode(ctx, s.memberModel(ctx)) + if e != nil { + return e + } + data[mc.InviteCode] = inviteCode data[mc.MemberStatus] = consts.AppMemberStatusNormal data[mc.CreatedBy] = userId 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() diff --git a/server/internal/logic/sys/app_member_auth.go b/server/internal/logic/sys/app_member_auth.go index 1e60c37..e5df831 100644 --- a/server/internal/logic/sys/app_member_auth.go +++ b/server/internal/logic/sys/app_member_auth.go @@ -3,7 +3,6 @@ package sys import ( "context" "fmt" - "strings" "hotgo/internal/consts" "hotgo/internal/dao" @@ -54,19 +53,15 @@ func (s *sSysAppMemberAuth) Register(ctx context.Context, in *sysin.AppMemberReg return gerror.New("手机号已存在") } - parentId, level, tree := uint64(0), 1, "" - if in.InviteCode != "" { - var parent *entity.AppMember - if e = s.memberModel(ctx).Where(mc.InviteCode, in.InviteCode).Scan(&parent); e != nil { - return gerror.Wrap(e, "查询邀请人失败") - } - if parent == nil { - return gerror.New("邀请码不存在") - } - parentId = parent.Id - level = parent.Level + 1 - tree = parent.Tree + "tr_" + fmt.Sprint(parent.Id) + " " + var parent *entity.AppMember + if e = s.memberModel(ctx).Where(mc.InviteCode, in.InviteCode).Where(mc.MemberStatus, consts.AppMemberStatusNormal).Scan(&parent); e != nil { + return gerror.Wrap(e, "查询邀请人失败") } + if parent == nil { + return gerror.New("邀请码不存在") + } + parentId, level := parent.Id, parent.Level+1 + tree := parent.Tree + "tr_" + fmt.Sprint(parent.Id) + " " levelId := uint64(0) var firstLevel *entity.AppMemberLevel @@ -81,11 +76,15 @@ func (s *sSysAppMemberAuth) Register(ctx context.Context, in *sysin.AppMemberReg return gerror.Wrap(e, "生成密码摘要失败") } memberNo := "MB" + gtime.Now().Format("YmdHis") + grand.S(5) + inviteCode, e := newAppMemberInviteCode(ctx, s.memberModel(ctx)) + if e != nil { + return e + } memberData := g.Map{ mc.TenantId: 0, mc.MemberNo: memberNo, mc.Username: in.Username, mc.Mobile: in.Mobile, mc.PasswordHash: string(hashed), mc.LevelId: levelId, mc.ParentId: parentId, - mc.Level: level, mc.Tree: tree, mc.InviteCode: strings.ToUpper(grand.S(8)), + mc.Level: level, mc.Tree: tree, mc.InviteCode: inviteCode, mc.MemberStatus: consts.AppMemberStatusNormal, mc.RiskStatus: consts.AppMemberRiskNormal, mc.AutoDispatch: consts.AppMemberDispatchManual, mc.CreditScore: 100, mc.IsTestAccount: 2, } diff --git a/server/internal/logic/sys/app_member_invite.go b/server/internal/logic/sys/app_member_invite.go new file mode 100644 index 0000000..e869220 --- /dev/null +++ b/server/internal/logic/sys/app_member_invite.go @@ -0,0 +1,31 @@ +package sys + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + + "hotgo/internal/dao" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" +) + +func newAppMemberInviteCode(ctx context.Context, members *gdb.Model) (string, error) { + for attempt := 0; attempt < 20; attempt++ { + number, err := rand.Int(rand.Reader, big.NewInt(1000000)) + if err != nil { + return "", gerror.Wrap(err, "生成邀请码失败") + } + code := fmt.Sprintf("%06d", number.Int64()) + count, err := members.Unscoped().Where(dao.AppMember.Columns().InviteCode, code).Count() + if err != nil { + return "", gerror.Wrap(err, "检查邀请码失败") + } + if count == 0 { + return code, nil + } + } + return "", gerror.New("邀请码暂时无法生成,请稍后重试") +} diff --git a/server/internal/model/input/sysin/app_member_auth.go b/server/internal/model/input/sysin/app_member_auth.go index 782d07d..b9f5e7b 100644 --- a/server/internal/model/input/sysin/app_member_auth.go +++ b/server/internal/model/input/sysin/app_member_auth.go @@ -2,6 +2,7 @@ package sysin import ( "context" + "regexp" "strings" "github.com/gogf/gf/v2/errors/gerror" @@ -11,7 +12,7 @@ type AppMemberRegisterInp struct { Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"` Mobile string `json:"mobile" v:"required|phone-loose#手机号不能为空|手机号格式不正确"` Password string `json:"password" v:"required|length:8,64#密码不能为空|密码长度为8到64个字符"` - InviteCode string `json:"inviteCode" v:"max-length:32#邀请码不能超过32个字符"` + InviteCode string `json:"inviteCode" v:"required#请输入邀请码"` RegisterOrigin string `json:"registerOrigin" v:"max-length:512#注册来源不能超过512个字符"` RegisterHost string `json:"registerHost" v:"max-length:255#注册域名不能超过255个字符"` } @@ -20,6 +21,9 @@ func (in *AppMemberRegisterInp) Filter(ctx context.Context) error { in.Username = strings.TrimSpace(in.Username) in.Mobile = strings.TrimSpace(in.Mobile) in.InviteCode = strings.TrimSpace(in.InviteCode) + if !regexp.MustCompile(`^[0-9]{6}$`).MatchString(in.InviteCode) { + return gerror.New("邀请码必须为6位数字") + } if !containsLetterAndNumber(in.Password) { return gerror.New("密码至少8位且必须同时包含字母和数字") } diff --git a/server/storage/data/migrations/20261015_003_app_member_invite_code_six_digits.sql b/server/storage/data/migrations/20261015_003_app_member_invite_code_six_digits.sql new file mode 100644 index 0000000..b8fc4ee --- /dev/null +++ b/server/storage/data/migrations/20261015_003_app_member_invite_code_six_digits.sql @@ -0,0 +1,28 @@ +-- 将现有会员邀请码迁移为唯一的六位数字,保留原码映射供核查和回退。 +-- 执行前确认每个租户会员数不超过 900000,且没有占用 100000 起始区间的六位数字码。 +-- 本迁移只调整邀请码;团队关系由 parent_id 和 tree 保存,不受影响。 + +CREATE TABLE IF NOT EXISTS `hg_app_member_invite_code_backup_20261015` ( + `tenant_id` bigint unsigned NOT NULL, + `member_id` bigint unsigned NOT NULL, + `old_code` varchar(32) NOT NULL, + `new_code` char(6) NOT NULL, + `migrated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`tenant_id`, `member_id`), + UNIQUE KEY `uk_invite_code_backup_new` (`tenant_id`, `new_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员邀请码六位数字迁移备份'; + +START TRANSACTION; + +INSERT INTO `hg_app_member_invite_code_backup_20261015` + (`tenant_id`, `member_id`, `old_code`, `new_code`) +SELECT `tenant_id`, `id`, `invite_code`, + LPAD(100000 + ROW_NUMBER() OVER (PARTITION BY `tenant_id` ORDER BY `id`) - 1, 6, '0') +FROM `hg_app_member`; + +UPDATE `hg_app_member` AS m +JOIN `hg_app_member_invite_code_backup_20261015` AS b + ON b.`tenant_id` = m.`tenant_id` AND b.`member_id` = m.`id` +SET m.`invite_code` = b.`new_code`; + +COMMIT;