226 lines
7.3 KiB
Go
226 lines
7.3 KiB
Go
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
|
||
}
|