package user import ( "context" "strings" "unicode/utf8" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) const ( maxCountryNameLength = 128 maxCountryDisplayNameLength = 128 maxCountryFlagLength = 16 maxRegionNameLength = 128 ) // CreateCountry 创建国家主数据;country_code 创建后不可变。 func (s *Service) CreateCountry(ctx context.Context, command userdomain.CreateCountryCommand) (userdomain.Country, error) { command = normalizeCreateCountryCommand(command) if err := validateCountryPayload(command.CountryName, command.CountryCode, command.ISOAlpha3, command.ISONumeric, command.CountryDisplayName, command.PhoneCountryCode, command.Flag); err != nil { return userdomain.Country{}, err } if strings.TrimSpace(command.RequestID) == "" { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.countryAdminRepository == nil { return userdomain.Country{}, xerr.New(xerr.Unavailable, "country repository is not configured") } command.NowMs = s.now().UnixMilli() return s.countryAdminRepository.CreateCountry(ctx, command) } // UpdateCountry 修改国家展示字段;国家码不允许原地修改。 func (s *Service) UpdateCountry(ctx context.Context, command userdomain.UpdateCountryCommand) (userdomain.Country, error) { command = normalizeUpdateCountryCommand(command) if command.CountryID <= 0 { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "country_id is required") } if err := validateCountryMetadata(command.CountryName, command.ISOAlpha3, command.ISONumeric, command.CountryDisplayName, command.PhoneCountryCode, command.Flag); err != nil { return userdomain.Country{}, err } if strings.TrimSpace(command.RequestID) == "" { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.countryAdminRepository == nil { return userdomain.Country{}, xerr.New(xerr.Unavailable, "country repository is not configured") } command.NowMs = s.now().UnixMilli() return s.countryAdminRepository.UpdateCountry(ctx, command) } // ListCountries 返回国家主数据;enabled 为空时返回全部国家。 func (s *Service) ListCountries(ctx context.Context, enabled *bool) ([]userdomain.Country, error) { if s.countryAdminRepository == nil { return nil, xerr.New(xerr.Unavailable, "country repository is not configured") } return s.countryAdminRepository.ListCountries(ctx, userdomain.CountryFilter{Enabled: enabled}) } // ListRegistrationCountries 返回 App 注册页可选国家列表。 func (s *Service) ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error) { if s.countryRegionRepository == nil { return nil, xerr.New(xerr.Unavailable, "country repository is not configured") } return s.countryRegionRepository.ListRegistrationCountries(ctx) } // EnableCountry 重新开放国家;历史区域映射和历史用户国家快照不需要改写。 func (s *Service) EnableCountry(ctx context.Context, countryID int64, operatorUserID int64, requestID string) (userdomain.Country, error) { requestID = strings.TrimSpace(requestID) if countryID <= 0 { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "country_id is required") } if requestID == "" { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.countryAdminRepository == nil { return userdomain.Country{}, xerr.New(xerr.Unavailable, "country repository is not configured") } return s.countryAdminRepository.EnableCountry(ctx, userdomain.EnableCountryCommand{ CountryID: countryID, OperatorUserID: operatorUserID, RequestID: requestID, NowMs: s.now().UnixMilli(), }) } // DisableCountry 只关闭 App 选择入口;不删除国家、不清空历史用户,也不释放区域配置。 func (s *Service) DisableCountry(ctx context.Context, countryID int64, operatorUserID int64, requestID string) (userdomain.Country, error) { requestID = strings.TrimSpace(requestID) if countryID <= 0 { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "country_id is required") } if requestID == "" { return userdomain.Country{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.countryAdminRepository == nil { return userdomain.Country{}, xerr.New(xerr.Unavailable, "country repository is not configured") } return s.countryAdminRepository.DisableCountry(ctx, userdomain.DisableCountryCommand{ CountryID: countryID, OperatorUserID: operatorUserID, RequestID: requestID, NowMs: s.now().UnixMilli(), }) } // CreateRegion 创建区域并建立国家归属;一个国家只能存在一个 active 区域归属。 func (s *Service) CreateRegion(ctx context.Context, regionCode string, name string, countries []string, sortOrder int32, operatorUserID int64, requestID string) (userdomain.Region, error) { regionCode = userdomain.NormalizeRegionCode(regionCode) name = strings.TrimSpace(name) requestID = strings.TrimSpace(requestID) countries, err := normalizeCountryList(countries) if err != nil { return userdomain.Region{}, err } if err := validateRegionPayload(regionCode, name); err != nil { return userdomain.Region{}, err } if requestID == "" { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.regionAdminRepository == nil { return userdomain.Region{}, xerr.New(xerr.Unavailable, "region repository is not configured") } return s.regionAdminRepository.CreateRegion(ctx, userdomain.CreateRegionCommand{ RegionCode: regionCode, Name: name, Countries: countries, SortOrder: sortOrder, OperatorUserID: operatorUserID, RequestID: requestID, NowMs: s.now().UnixMilli(), }) } // UpdateRegion 修改区域展示字段,不改变国家归属。 func (s *Service) UpdateRegion(ctx context.Context, regionID int64, regionCode string, name string, sortOrder int32, operatorUserID int64, requestID string) (userdomain.Region, error) { regionCode = userdomain.NormalizeRegionCode(regionCode) name = strings.TrimSpace(name) requestID = strings.TrimSpace(requestID) if regionID <= 0 { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "region_id is required") } if err := validateRegionPayload(regionCode, name); err != nil { return userdomain.Region{}, err } if requestID == "" { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.regionAdminRepository == nil { return userdomain.Region{}, xerr.New(xerr.Unavailable, "region repository is not configured") } return s.regionAdminRepository.UpdateRegion(ctx, userdomain.UpdateRegionCommand{ RegionID: regionID, RegionCode: regionCode, Name: name, SortOrder: sortOrder, OperatorUserID: operatorUserID, RequestID: requestID, NowMs: s.now().UnixMilli(), }) } // ListRegions 返回区域主数据,status 为空时返回全部。 func (s *Service) ListRegions(ctx context.Context, status string) ([]userdomain.Region, error) { status = strings.TrimSpace(status) if status != "" && status != userdomain.RegionStatusActive && status != userdomain.RegionStatusDisabled { return nil, xerr.New(xerr.InvalidArgument, "region status is invalid") } if s.regionAdminRepository == nil { return nil, xerr.New(xerr.Unavailable, "region repository is not configured") } return s.regionAdminRepository.ListRegions(ctx, userdomain.RegionFilter{Status: status}) } // GetRegion 返回单个区域及其 active 国家列表。 func (s *Service) GetRegion(ctx context.Context, regionID int64) (userdomain.Region, error) { if regionID <= 0 { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "region_id is required") } if s.regionAdminRepository == nil { return userdomain.Region{}, xerr.New(xerr.Unavailable, "region repository is not configured") } return s.regionAdminRepository.GetRegion(ctx, regionID) } // ReplaceRegionCountries 原子替换区域国家归属,并为受影响国家创建用户区域重算任务。 func (s *Service) ReplaceRegionCountries(ctx context.Context, regionID int64, countries []string, operatorUserID int64, requestID string) (userdomain.Region, error) { requestID = strings.TrimSpace(requestID) if regionID <= 0 { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "region_id is required") } normalizedCountries, err := normalizeCountryList(countries) if err != nil { return userdomain.Region{}, err } if requestID == "" { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.regionAdminRepository == nil { return userdomain.Region{}, xerr.New(xerr.Unavailable, "region repository is not configured") } return s.regionAdminRepository.ReplaceRegionCountries(ctx, userdomain.ReplaceRegionCountriesCommand{ RegionID: regionID, Countries: normalizedCountries, OperatorUserID: operatorUserID, RequestID: requestID, NowMs: s.now().UnixMilli(), }) } // DisableRegion 停用区域并释放 active 国家归属。 func (s *Service) DisableRegion(ctx context.Context, regionID int64, operatorUserID int64, requestID string) (userdomain.Region, error) { requestID = strings.TrimSpace(requestID) if regionID <= 0 { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "region_id is required") } if requestID == "" { return userdomain.Region{}, xerr.New(xerr.InvalidArgument, "request_id is required") } if s.regionAdminRepository == nil { return userdomain.Region{}, xerr.New(xerr.Unavailable, "region repository is not configured") } return s.regionAdminRepository.DisableRegion(ctx, userdomain.DisableRegionCommand{ RegionID: regionID, OperatorUserID: operatorUserID, RequestID: requestID, NowMs: s.now().UnixMilli(), }) } func normalizeCreateCountryCommand(command userdomain.CreateCountryCommand) userdomain.CreateCountryCommand { command.CountryName = strings.TrimSpace(command.CountryName) command.CountryCode = userdomain.NormalizeCountryCode(command.CountryCode) command.ISOAlpha3 = userdomain.NormalizeISOAlpha3(command.ISOAlpha3) command.ISONumeric = userdomain.NormalizeISONumeric(command.ISONumeric) command.CountryDisplayName = strings.TrimSpace(command.CountryDisplayName) command.PhoneCountryCode = userdomain.NormalizePhoneCountryCode(command.PhoneCountryCode) command.Flag = strings.TrimSpace(command.Flag) command.RequestID = strings.TrimSpace(command.RequestID) return command } func normalizeUpdateCountryCommand(command userdomain.UpdateCountryCommand) userdomain.UpdateCountryCommand { command.CountryName = strings.TrimSpace(command.CountryName) command.ISOAlpha3 = userdomain.NormalizeISOAlpha3(command.ISOAlpha3) command.ISONumeric = userdomain.NormalizeISONumeric(command.ISONumeric) command.CountryDisplayName = strings.TrimSpace(command.CountryDisplayName) command.PhoneCountryCode = userdomain.NormalizePhoneCountryCode(command.PhoneCountryCode) command.Flag = strings.TrimSpace(command.Flag) command.RequestID = strings.TrimSpace(command.RequestID) return command } func validateCountryPayload(countryName string, countryCode string, isoAlpha3 string, isoNumeric string, countryDisplayName string, phoneCountryCode string, flag string) error { if !userdomain.ValidCountryCode(countryCode) { return xerr.New(xerr.InvalidArgument, "country_code must use 2 to 3 uppercase letters") } return validateCountryMetadata(countryName, isoAlpha3, isoNumeric, countryDisplayName, phoneCountryCode, flag) } func validateCountryMetadata(countryName string, isoAlpha3 string, isoNumeric string, countryDisplayName string, phoneCountryCode string, flag string) error { if countryName == "" || utf8.RuneCountInString(countryName) > maxCountryNameLength { return xerr.New(xerr.InvalidArgument, "country_name length is invalid") } if !userdomain.ValidISOAlpha3(isoAlpha3) { return xerr.New(xerr.InvalidArgument, "iso_alpha3 is invalid") } if !userdomain.ValidISONumeric(isoNumeric) { return xerr.New(xerr.InvalidArgument, "iso_numeric is invalid") } if countryDisplayName == "" || utf8.RuneCountInString(countryDisplayName) > maxCountryDisplayNameLength { return xerr.New(xerr.InvalidArgument, "country_display_name length is invalid") } if !userdomain.ValidPhoneCountryCode(phoneCountryCode) { return xerr.New(xerr.InvalidArgument, "phone_country_code is invalid") } if utf8.RuneCountInString(flag) > maxCountryFlagLength { return xerr.New(xerr.InvalidArgument, "flag is too long") } return nil } func validateRegionPayload(regionCode string, name string) error { if !userdomain.ValidRegionCode(regionCode) { return xerr.New(xerr.InvalidArgument, "region_code is invalid") } if name == "" || utf8.RuneCountInString(name) > maxRegionNameLength { return xerr.New(xerr.InvalidArgument, "region name length is invalid") } return nil } func normalizeCountryList(countries []string) ([]string, error) { if len(countries) == 0 { return nil, xerr.New(xerr.InvalidArgument, "countries is required") } seen := make(map[string]struct{}, len(countries)) result := make([]string, 0, len(countries)) for _, country := range countries { country = userdomain.NormalizeCountryCode(country) if !userdomain.ValidCountryCode(country) { return nil, xerr.New(xerr.InvalidArgument, "country_code must use 2 to 3 uppercase letters") } if _, exists := seen[country]; exists { return nil, xerr.New(xerr.InvalidArgument, "countries must not contain duplicates") } seen[country] = struct{}{} result = append(result, country) } return result, nil }