package region import ( "context" "database/sql" "errors" "strings" mysqldriver "github.com/go-sql-driver/mysql" "hyapp/pkg/appcode" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" "hyapp/services/user-service/internal/storage/mysql/shared" ) func (r *Repository) ResolveEnabledCountryByCode(ctx context.Context, countryCode string) (userdomain.Country, bool, error) { return resolveCountryByCode(ctx, r.db, countryCode, true) } // ListRegistrationCountries 返回注册页可选国家,只包含 enabled 的国家。 func (r *Repository) ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error) { return listCountries(ctx, r.db, userdomain.CountryFilter{Enabled: new(true)}) } // ResolveActiveRegionByCountry 按国家码读取当前 active 区域映射;无显式映射时由 regions.go 返回 GLOBAL。 // UpdateCountry 修改国家展示字段;country_code 不允许原地修改。 func (r *Repository) UpdateCountry(ctx context.Context, command userdomain.UpdateCountryCommand) (userdomain.Country, error) { result, err := r.db.ExecContext(ctx, ` UPDATE countries SET country_name = ?, iso_alpha3 = ?, iso_numeric = ?, country_display_name = ?, phone_country_code = ?, flag = ?, sort_order = ?, updated_by_user_id = ?, updated_at_ms = ? WHERE app_code = ? AND country_id = ? `, command.CountryName, shared.NullableString(command.ISOAlpha3), shared.NullableString(command.ISONumeric), command.CountryDisplayName, shared.NullableString(command.PhoneCountryCode), command.Flag, command.SortOrder, shared.NullableOperator(command.OperatorUserID), command.NowMs, appcode.Normalize(command.AppCode), command.CountryID) if err != nil { return userdomain.Country{}, mapCountryDuplicateError(err) } if affected, err := result.RowsAffected(); err != nil { return userdomain.Country{}, err } else if affected == 0 { return userdomain.Country{}, xerr.New(xerr.CountryNotFound, "country not found") } return queryCountry(ctx, r.db, "WHERE app_code = ? AND country_id = ?", appcode.Normalize(command.AppCode), command.CountryID) } // ListCountries 返回国家主数据,enabled 为空时返回全部。 func (r *Repository) ListCountries(ctx context.Context, filter userdomain.CountryFilter) ([]userdomain.Country, error) { return listCountries(ctx, r.db, filter) } func queryCountry(ctx context.Context, q queryer, clause string, args ...any) (userdomain.Country, error) { row := q.QueryRowContext(ctx, ` SELECT app_code, country_id, country_name, country_code, COALESCE(iso_alpha3, ''), COALESCE(iso_numeric, ''), country_display_name, COALESCE(phone_country_code, ''), COALESCE(flag, ''), enabled, sort_order, COALESCE(created_by_user_id, 0), COALESCE(updated_by_user_id, 0), created_at_ms, updated_at_ms FROM countries `+clause, args...) return scanCountry(row) } func listCountries(ctx context.Context, q queryer, filter userdomain.CountryFilter) ([]userdomain.Country, error) { appCode := appcode.FromContext(ctx) query := ` SELECT app_code, country_id, country_name, country_code, COALESCE(iso_alpha3, ''), COALESCE(iso_numeric, ''), country_display_name, COALESCE(phone_country_code, ''), COALESCE(flag, ''), enabled, sort_order, COALESCE(created_by_user_id, 0), COALESCE(updated_by_user_id, 0), created_at_ms, updated_at_ms FROM countries` args := []any{appCode} conditions := []string{"app_code = ?"} if filter.Enabled != nil { conditions = append(conditions, "enabled = ?") args = append(args, *filter.Enabled) } if len(conditions) > 0 { query += " WHERE " + strings.Join(conditions, " AND ") } query += " ORDER BY sort_order ASC, country_code ASC" rows, err := queryRows(ctx, q, query, args...) if err != nil { return nil, err } defer rows.Close() countries := []userdomain.Country{} for rows.Next() { country, err := scanCountry(rows) if err != nil { return nil, err } countries = append(countries, country) } return countries, rows.Err() } func scanCountry(scanner interface{ Scan(dest ...any) error }) (userdomain.Country, error) { var country userdomain.Country err := scanner.Scan( &country.AppCode, &country.CountryID, &country.CountryName, &country.CountryCode, &country.ISOAlpha3, &country.ISONumeric, &country.CountryDisplayName, &country.PhoneCountryCode, &country.Flag, &country.Enabled, &country.SortOrder, &country.CreatedByUserID, &country.UpdatedByUserID, &country.CreatedAtMs, &country.UpdatedAtMs, ) return country, err } //go:fix inline func boolPtr(value bool) *bool { return new(value) } func canonicalCountryCodes(ctx context.Context, q queryer, countries []string) ([]string, error) { seen := make(map[string]struct{}, len(countries)) result := make([]string, 0, len(countries)) for _, input := range countries { country, ok, err := resolveCountryByCode(ctx, q, input, false) if err != nil { return nil, err } if !ok { return nil, xerr.New(xerr.InvalidArgument, "country is not supported") } if _, exists := seen[country.CountryCode]; exists { // 区域国家列表不能重复,避免重复写入 active 国家归属。 return nil, xerr.New(xerr.InvalidArgument, "countries must not contain duplicate canonical codes") } seen[country.CountryCode] = struct{}{} result = append(result, country.CountryCode) } return result, nil } func resolveCountryByCode(ctx context.Context, q queryer, countryCode string, requireEnabled bool) (userdomain.Country, bool, error) { countryCode = userdomain.NormalizeCountryCode(countryCode) clause := "WHERE app_code = ? AND country_code = ?" args := []any{appcode.FromContext(ctx), countryCode} if requireEnabled { clause += " AND enabled = ?" args = append(args, true) } country, err := queryCountry(ctx, q, clause, args...) if err == sql.ErrNoRows { return userdomain.Country{}, false, nil } if err != nil { return userdomain.Country{}, false, err } return country, true, nil } func ensureCountriesAssignableToRegion(ctx context.Context, q queryer, countries []string, regionID int64) error { for _, country := range countries { mappedRegionID, mapped, err := activeRegionIDByCountry(ctx, q, country) if err != nil { return err } if mapped && mappedRegionID != regionID { return xerr.New(xerr.RegionCountryConflict, "country already belongs to another active region") } } return nil } func activeRegionIDByCountry(ctx context.Context, q queryer, countryCode string) (int64, bool, error) { var regionID int64 err := q.QueryRowContext(ctx, ` SELECT region_id FROM region_countries WHERE app_code = ? AND country_code = ? AND status = ? LIMIT 1 `, appcode.FromContext(ctx), countryCode, userdomain.RegionStatusActive).Scan(®ionID) if err == sql.ErrNoRows { return 0, false, nil } if err != nil { return 0, false, err } return regionID, true, nil } func mapCountryDuplicateError(err error) error { var mysqlErr *mysqldriver.MySQLError if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { return xerr.New(xerr.Conflict, "country_code already exists") } return err } func mapRegionCountryDuplicateError(err error) error { var mysqlErr *mysqldriver.MySQLError if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { return xerr.New(xerr.RegionCountryConflict, "country already belongs to another active region") } return err }