2026-05-05 03:05:06 +08:00

209 lines
8.1 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"
"unicode/utf8"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
)
const (
maxCountryNameLength = 128
maxCountryDisplayNameLength = 128
maxCountryFlagLength = 16
maxRegionNameLength = 128
)
// UpdateCountry 修改国家展示字段;国家码不允许原地修改。
func (s *Service) UpdateCountry(ctx context.Context, command userdomain.UpdateCountryCommand) (userdomain.Country, error) {
command = normalizeUpdateCountryCommand(command)
command.AppCode = appcode.FromContext(ctx)
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{AppCode: appcode.FromContext(ctx), 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)
}
// 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{
AppCode: appcode.FromContext(ctx),
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{AppCode: appcode.FromContext(ctx), 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{
AppCode: appcode.FromContext(ctx),
RegionID: regionID,
Countries: normalizedCountries,
OperatorUserID: operatorUserID,
RequestID: requestID,
NowMs: s.now().UnixMilli(),
})
}
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 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 userdomain.IsRetiredRegionCode(regionCode) {
return xerr.New(xerr.InvalidArgument, "region has been retired")
}
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
}