Files
ttstd-infinite-canvas/backend/internal/auth/registration.go
✨await 672bb0be73 feat(auth): 注册服务协议 - 名称与条款改为后台可配置并同步注册页展示
后端:
- RegistrationSettingRequest 协议字段改为可选指针,注册开关单独保存不再覆盖协议正文
- 新增 RegistrationAgreement / AgreementTitleForMessage:标题未配置时跟随品牌名生成,读取失败返回空标题而非猜一个不存在的协议名
- PublicAuthSettings 下发 agreementTitle / agreementContent,首用户分支同样携带
- 新增 registration_agreement_test.go 覆盖品牌名兜底、存储合并与失败降级

前端:
- 注册页协议标题与条款改为消费后台配置;读取失败时隐藏协议入口并支持重新读取
- 验证码获取按钮内联到输入框右侧,联系方式为空即禁用;sms_email 双通道合并为单输入框
- 协议弹窗重排:条目标题独立成行加重、空态虚线提示、底部补充同意说明;深浅主题适配
- 协议勾选行改为状态化区域,复选框适配 AntD 6 结构(方框在 .ant-checkbox 本体,修正深色主题白底白勾)
- 后台「服务协议名称与条款」区块补齐表单内边距并对齐区块头/页脚,协议名称限宽

文档:http-api.mdx 更新协议字段语义与失败降级说明;pending-test.mdx 增加手工验证清单
2026-09-24 16:56:44 +08:00

155 lines
4.9 KiB
Go

package auth
import (
"encoding/json"
"errors"
"os"
"strings"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/gorm"
)
const registrationSettingKey = "registration"
// RegistrationSettingRequest 的协议字段用指针区分「未提交」和「明确清空」:
// 注册开关单独切换时不带协议字段,必须保留已保存的协议正文。
type RegistrationSettingRequest struct {
Enabled bool `json:"enabled"`
AgreementTitle *string `json:"agreementTitle"`
AgreementContent *string `json:"agreementContent"`
}
type PublicRegistrationSetting struct {
Enabled bool `json:"enabled"`
AgreementTitle string `json:"agreementTitle"`
AgreementContent string `json:"agreementContent"`
UpdatedBy string `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type registrationSettingValue struct {
Enabled bool `json:"enabled"`
AgreementTitle string `json:"agreementTitle"`
AgreementContent string `json:"agreementContent"`
}
func (s *Service) AdminRegistrationSetting(actor *model.User) (*PublicRegistrationSetting, error) {
if err := s.host.RequireAdmin(actor); err != nil {
return nil, err
}
setting, value, err := s.readRegistrationSetting()
if err != nil {
return nil, err
}
return publicRegistrationSetting(setting, value), nil
}
func (s *Service) UpdateRegistrationSetting(actor *model.User, req RegistrationSettingRequest) (*PublicRegistrationSetting, error) {
if err := s.host.RequireAdmin(actor); err != nil {
return nil, err
}
current, currentValue, err := s.readRegistrationSetting()
if err != nil {
return nil, err
}
nextValue := registrationSettingValue{
Enabled: req.Enabled,
AgreementTitle: currentValue.AgreementTitle,
AgreementContent: currentValue.AgreementContent,
}
if req.AgreementTitle != nil {
nextValue.AgreementTitle = strings.TrimSpace(*req.AgreementTitle)
}
if req.AgreementContent != nil {
nextValue.AgreementContent = strings.TrimSpace(*req.AgreementContent)
}
encoded, err := json.Marshal(nextValue)
if err != nil {
return nil, err
}
setting := model.SystemSetting{Key: registrationSettingKey, ValueJSON: string(encoded), UpdatedBy: actor.ID}
if current != nil {
setting.CreatedAt = current.CreatedAt
}
if err := s.repo.SaveSystemSetting(&setting); err != nil {
return nil, err
}
return publicRegistrationSetting(&setting, nextValue), nil
}
func (s *Service) RegistrationEnabled() (bool, error) {
_, value, err := s.readRegistrationSetting()
return value.Enabled, err
}
// AgreementTitleForMessage 只用于错误提示。协议读取失败时不再返回品牌名兜底标题,
// 否则用户会被要求同意一个后台并不存在的协议名称。
func (s *Service) AgreementTitleForMessage() string {
title, _ := s.RegistrationAgreement()
if strings.TrimSpace(title) == "" {
return "服务协议"
}
return title
}
// RegistrationAgreement 返回展示用的协议标题(已去掉书名号)和条款正文。
// 标题未配置时跟随当前品牌名,保证品牌改名后注册页文案同步。
// 读取失败时返回空标题,由调用方决定是降级展示还是给出失败提示。
func (s *Service) RegistrationAgreement() (string, string) {
_, value, err := s.readRegistrationSetting()
if err != nil {
return "", ""
}
title := strings.Trim(strings.TrimSpace(value.AgreementTitle), "《》")
if title == "" {
title = s.defaultAgreementTitle()
}
return title, value.AgreementContent
}
func (s *Service) defaultAgreementTitle() string {
brand := strings.TrimSpace(s.host.BrandName())
if brand == "" {
brand = DefaultBrandName
}
return brand + "服务协议"
}
func (s *Service) readRegistrationSetting() (*model.SystemSetting, registrationSettingValue, error) {
setting, err := s.repo.SystemSetting(registrationSettingKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, registrationSettingValue{Enabled: registrationEnabledFromEnvironment()}, nil
}
if err != nil {
return nil, registrationSettingValue{}, err
}
value := registrationSettingValue{}
if strings.TrimSpace(setting.ValueJSON) == "" || json.Unmarshal([]byte(setting.ValueJSON), &value) != nil {
return nil, registrationSettingValue{}, errors.New("用户注册配置格式无效")
}
return setting, value, nil
}
func publicRegistrationSetting(setting *model.SystemSetting, value registrationSettingValue) *PublicRegistrationSetting {
result := &PublicRegistrationSetting{
Enabled: value.Enabled,
AgreementTitle: value.AgreementTitle,
AgreementContent: value.AgreementContent,
}
if setting != nil {
result.UpdatedBy = setting.UpdatedBy
result.CreatedAt = setting.CreatedAt
result.UpdatedAt = setting.UpdatedAt
}
return result
}
func registrationEnabledFromEnvironment() bool {
value := strings.ToLower(strings.TrimSpace(os.Getenv("CANVAS_REGISTRATION_ENABLED")))
return value == "1" || value == "true" || value == "yes"
}