diff --git a/api/tenant/tenant.go b/api/tenant/tenant.go index 99c49eb..9df7d76 100644 --- a/api/tenant/tenant.go +++ b/api/tenant/tenant.go @@ -48,6 +48,7 @@ type ITenantV1 interface { TenantSendCode(ctx context.Context, req *v1.TenantSendCodeReq) (res *v1.TenantSendCodeRes, err error) TenantResetPassword(ctx context.Context, req *v1.TenantResetPasswordReq) (res *v1.TenantResetPasswordRes, err error) TenantChangeEmail(ctx context.Context, req *v1.TenantChangeEmailReq) (res *v1.TenantChangeEmailRes, err error) + TenantSendChangeEmailCode(ctx context.Context, req *v1.TenantSendChangeEmailCodeReq) (res *v1.TenantSendChangeEmailCodeRes, err error) FeedbackCreate(ctx context.Context, req *v1.FeedbackCreateReq) (res *v1.FeedbackCreateRes, err error) FeedbackList(ctx context.Context, req *v1.FeedbackListReq) (res *v1.FeedbackListRes, err error) FeedbackGet(ctx context.Context, req *v1.FeedbackGetReq) (res *v1.FeedbackGetRes, err error) diff --git a/api/tenant/v1/email.go b/api/tenant/v1/email.go index 0272831..eafef1e 100644 --- a/api/tenant/v1/email.go +++ b/api/tenant/v1/email.go @@ -33,3 +33,11 @@ type TenantChangeEmailReq struct { } type TenantChangeEmailRes struct{} + +// TenantSendChangeEmailCodeReq 设置/修改邮箱时发送验证码(需登录,发送前校验租户内邮箱唯一性) +type TenantSendChangeEmailCodeReq struct { + g.Meta `path:"/email/send-change-email-code" method:"post" mime:"json" tags:"租户控制台-邮箱" summary:"发送设置邮箱验证码"` + NewEmail string `json:"new_email" v:"required|email#请输入新邮箱|邮箱格式不正确" dc:"新邮箱"` +} + +type TenantSendChangeEmailCodeRes struct{} diff --git a/api/tenant/v1/member.go b/api/tenant/v1/member.go index d9ec46f..e4c8b47 100644 --- a/api/tenant/v1/member.go +++ b/api/tenant/v1/member.go @@ -98,7 +98,7 @@ type TenantMemberJoinReq struct { Code string `json:"code" v:"required#请提供邀请码" dc:"邀请码"` Username string `json:"username" v:"required|length:3,50#请输入用户名|用户名长度为3-50位" dc:"用户名"` Password string `json:"password" v:"required|length:8,64#请输入密码|密码长度为8-64位" dc:"密码"` - Email string `json:"email" v:"required|email#请输入邮箱|邮箱格式不正确" dc:"邮箱"` + Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` DisplayName string `json:"display_name" dc:"显示名称"` } diff --git a/internal/controller/tenant/tenant_v1_email.go b/internal/controller/tenant/tenant_v1_email.go index 53a5c21..e0b6812 100644 --- a/internal/controller/tenant/tenant_v1_email.go +++ b/internal/controller/tenant/tenant_v1_email.go @@ -16,3 +16,6 @@ func (c *ControllerV1) TenantResetPassword(ctx context.Context, req *v1.TenantRe func (c *ControllerV1) TenantChangeEmail(ctx context.Context, req *v1.TenantChangeEmailReq) (res *v1.TenantChangeEmailRes, err error) { return service.Tenant().ChangeEmail(ctx, req) } +func (c *ControllerV1) TenantSendChangeEmailCode(ctx context.Context, req *v1.TenantSendChangeEmailCodeReq) (res *v1.TenantSendChangeEmailCodeRes, err error) { + return service.Tenant().SendChangeEmailCode(ctx, req) +} diff --git a/internal/logic/tenant/email.go b/internal/logic/tenant/email.go index 177cdff..887e7c0 100644 --- a/internal/logic/tenant/email.go +++ b/internal/logic/tenant/email.go @@ -199,3 +199,31 @@ func (s *sTenant) ChangeEmail(ctx context.Context, req *v1.TenantChangeEmailReq) return nil, nil } + +// SendChangeEmailCode sends a verification code for setting/changing email. +// Authenticated (unlike the public send-code): pre-checks that newEmail is not +// already taken by another member in the same tenant before sending, so we don't +// waste a code (and an email) on an address the user can't actually use. +func (s *sTenant) SendChangeEmailCode(ctx context.Context, req *v1.TenantSendChangeEmailCodeReq) (*v1.TenantSendChangeEmailCodeRes, error) { + tenantID := middleware.GetTenantID(ctx) + userID := middleware.GetUserID(ctx) + newEmail := strings.TrimSpace(strings.ToLower(req.NewEmail)) + + // Pre-check: reject early if the email is already used by another member in this tenant. + count, err := dao.TntUsers.Ctx(ctx). + Where("tenant_id", tenantID). + Where("email", newEmail). + Where("id <> ?", userID). + Count() + if err != nil { + return nil, err + } + if count > 0 { + return nil, common.NewBadRequestError("该邮箱已被其他成员使用") + } + + if err := common.SendVerifyCode(ctx, newEmail, common.VerifyPurposeChangeEmail); err != nil { + return nil, err + } + return nil, nil +} diff --git a/internal/logic/tenant/member.go b/internal/logic/tenant/member.go index 9be0b56..8d6e0f7 100644 --- a/internal/logic/tenant/member.go +++ b/internal/logic/tenant/member.go @@ -177,6 +177,16 @@ func buildInviteURL(ctx context.Context, code string) string { return fmt.Sprintf("%s/#/tenant/join?code=%s", base, code) } +// nullableEmail converts an empty email to nil so an optional email is stored as +// SQL NULL (tnt_users.email is nullable) rather than "" — the latter would occupy +// the UNIQUE(tenant_id, email) slot and block other email-less members in the tenant. +func nullableEmail(email string) any { + if email == "" { + return nil + } + return email +} + // JoinByInvite handles a user joining a tenant via invitation link. func (s *sTenant) JoinByInvite(ctx context.Context, req *v1.TenantMemberJoinReq) (*v1.TenantMemberJoinRes, error) { // Find invitation @@ -268,16 +278,18 @@ func (s *sTenant) JoinByInvite(ctx context.Context, req *v1.TenantMemberJoinReq) return common.NewBusinessError(consts.CodeUsernameExists, consts.MsgUsernameExists) } - // Check email uniqueness within tenant - count, err = tx.Model("tnt_users").Ctx(ctx). - Where("tenant_id", tenantID). - Where("email", email). - Count() - if err != nil { - return err - } - if count > 0 { - return common.NewBusinessError(consts.CodeEmailExists, consts.MsgEmailExists) + // Check email uniqueness within tenant (email is optional — skip when empty) + if email != "" { + count, err = tx.Model("tnt_users").Ctx(ctx). + Where("tenant_id", tenantID). + Where("email", email). + Count() + if err != nil { + return err + } + if count > 0 { + return common.NewBusinessError(consts.CodeEmailExists, consts.MsgEmailExists) + } } // Create user @@ -289,7 +301,7 @@ func (s *sTenant) JoinByInvite(ctx context.Context, req *v1.TenantMemberJoinReq) userResult, err := tx.Model("tnt_users").Ctx(ctx).Data(do.TntUsers{ TenantId: tenantID, Username: username, - Email: email, + Email: nullableEmail(email), PasswordHash: passwordHash, DisplayName: displayName, Role: invitation.Role, @@ -454,7 +466,7 @@ func (s *sTenant) CreateMember(ctx context.Context, req *v1.TenantMemberCreateRe userResult, err := tx.Model("tnt_users").Ctx(ctx).Data(do.TntUsers{ TenantId: tenantID, Username: username, - Email: email, + Email: nullableEmail(email), PasswordHash: passwordHash, DisplayName: displayName, Role: req.Role, diff --git a/internal/service/tenant.go b/internal/service/tenant.go index d34926a..6f1f6c2 100644 --- a/internal/service/tenant.go +++ b/internal/service/tenant.go @@ -94,6 +94,11 @@ type ( ResetPassword(ctx context.Context, req *v1.TenantResetPasswordReq) (*v1.TenantResetPasswordRes, error) // ChangeEmail handles email change for a tenant user. ChangeEmail(ctx context.Context, req *v1.TenantChangeEmailReq) (*v1.TenantChangeEmailRes, error) + // SendChangeEmailCode sends a verification code for setting/changing email. + // Authenticated (unlike the public send-code): pre-checks that newEmail is not + // already taken by another member in the same tenant before sending, so we don't + // waste a code (and an email) on an address the user can't actually use. + SendChangeEmailCode(ctx context.Context, req *v1.TenantSendChangeEmailCodeReq) (*v1.TenantSendChangeEmailCodeRes, error) // CreateFeedback 提交反馈 CreateFeedback(ctx context.Context, req *v1.FeedbackCreateReq) (*v1.FeedbackCreateRes, error) // ListFeedbacks 我的反馈列表 diff --git a/web/tenant/src/views/auth/JoinPage.vue b/web/tenant/src/views/auth/JoinPage.vue index 374cd07..82a5530 100644 --- a/web/tenant/src/views/auth/JoinPage.vue +++ b/web/tenant/src/views/auth/JoinPage.vue @@ -65,22 +65,6 @@ -
- -
-
- -
- -
-
-
@@ -192,7 +176,6 @@ const joinResult = ref({ const form = ref({ username: '', display_name: '', - email: '', password: '', }) @@ -252,7 +235,6 @@ async function handleJoin() { code: route.query.code as string, username: form.value.username, display_name: form.value.display_name || undefined, - email: form.value.email, password: form.value.password, }), }) diff --git a/web/tenant/src/views/service/ModelsPage.vue b/web/tenant/src/views/service/ModelsPage.vue index ed26c55..73217a6 100644 --- a/web/tenant/src/views/service/ModelsPage.vue +++ b/web/tenant/src/views/service/ModelsPage.vue @@ -1,8 +1,15 @@