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 }