2026-05-12 09:53:20 +08:00

226 lines
7.3 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 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(&regionID)
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
}