2026-04-29 12:43:05 +08:00

111 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user
import (
"context"
"strings"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
)
// UpdateUserProfile 修改用户名、头像和生日;国家修改走独立接口和冷却期。
func (s *Service) UpdateUserProfile(ctx context.Context, userID int64, username *string, avatar *string, birth *string) (userdomain.User, error) {
if userID <= 0 {
// user_id 必须来自 gateway 鉴权上下文service 层仍兜底校验。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
username = trimmedStringPtr(username)
avatar = trimmedStringPtr(avatar)
birth = trimmedStringPtr(birth)
if username == nil && avatar == nil && birth == nil {
// PATCH 语义要求至少有一个字段,否则客户端通常是传参错误。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile update field is required")
}
if username != nil && *username == "" {
// 用户名是展示资料,禁止写成空字符串;头像和生日允许清空。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "username is required")
}
if username != nil && userdomain.ProfileStringTooLong(*username, userdomain.ProfileUsernameMaxRunes) {
// 提前按 users.username 的 VARCHAR 边界拒绝,避免把客户端错误暴露成 MySQL 错误。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "username is too long")
}
if avatar != nil && *avatar != "" {
if userdomain.ProfileStringTooLong(*avatar, userdomain.ProfileAvatarMaxRunes) {
// 头像 URL 直接进入展示资料,长度错误必须稳定返回 INVALID_ARGUMENT。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar is too long")
}
if !userdomain.ValidProfileAvatarURL(*avatar) {
// 非 HTTP(S) 或相对地址不应写入展示资料,避免客户端渲染和安全策略不一致。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar must be an absolute http or https URL")
}
}
if birth != nil && *birth != "" {
if !userdomain.ValidProfileBirthDate(*birth) {
// 生日只保存日期,不接受时区或斜杠格式,避免客户端跨时区偏移。
return userdomain.User{}, xerr.New(xerr.InvalidArgument, "birth must use yyyy-mm-dd")
}
}
if s.userRepository == nil {
return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.UpdateUserProfile(ctx, userdomain.ProfileUpdateCommand{
UserID: userID,
Username: username,
Avatar: avatar,
BirthDate: birth,
UpdatedAtMs: s.now().UnixMilli(),
})
}
// ChangeUserCountry 修改用户国家并写审计日志;同一用户滚动 30 天内只能成功修改一次。
func (s *Service) ChangeUserCountry(ctx context.Context, userID int64, country string, requestID string) (userdomain.User, int64, error) {
country = userdomain.NormalizeCountryCode(country)
if userID <= 0 {
return userdomain.User{}, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if country == "" {
return userdomain.User{}, 0, xerr.New(xerr.InvalidArgument, "country is required")
}
if !userdomain.ValidCountryCode(country) {
return userdomain.User{}, 0, xerr.New(xerr.InvalidArgument, "country must use 2 to 3 uppercase letters")
}
if s.userRepository == nil {
return userdomain.User{}, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
}
if s.countryRegionRepository == nil {
return userdomain.User{}, 0, xerr.New(xerr.Unavailable, "country repository is not configured")
}
resolvedCountry, ok, err := s.countryRegionRepository.ResolveActiveCountryByCode(ctx, country)
if err != nil {
return userdomain.User{}, 0, err
}
if !ok {
return userdomain.User{}, 0, xerr.New(xerr.InvalidArgument, "country is not supported")
}
var regionID int64
if region, ok, err := s.countryRegionRepository.ResolveActiveRegionByCountry(ctx, resolvedCountry.CountryCode); err != nil {
return userdomain.User{}, 0, err
} else if ok {
regionID = region.RegionID
}
nowMs := s.now().UnixMilli()
return s.userRepository.ChangeUserCountry(ctx, userdomain.CountryChangeCommand{
UserID: userID,
NewCountry: resolvedCountry.CountryCode,
NewRegionID: regionID,
RequestID: strings.TrimSpace(requestID),
ChangedAtMs: nowMs,
CooldownMs: s.countryChangeCooldown.Milliseconds(),
})
}
func trimmedStringPtr(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
return &trimmed
}