2026-07-09 13:02:49 +08:00

405 lines
14 KiB
Go

package countryregion
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
)
const CountryCodeRenameJobType = "country-code-rename"
var errCountryRenameJobConflict = errors.New("country code rename job already exists")
type countryCodeRenameJobStore interface {
CreateJob(job *model.AdminJob) error
ListActiveJobsByType(jobType string) ([]model.AdminJob, error)
RenameCountryCodeAdminReferences(appCode string, oldCountryCode string, newCountryCode string, nowMS int64) (repository.CountryCodeAdminReferenceCounts, error)
}
type countryCodeRoomClient interface {
RenameOwnerCountryCode(ctx context.Context, req roomclient.RenameOwnerCountryCodeRequest) (roomclient.RenameOwnerCountryCodeResult, error)
}
type countryCodeRenameResponse struct {
JobID uint `json:"jobId"`
Status string `json:"status"`
CountryID int64 `json:"countryId"`
OldCountryCode string `json:"oldCountryCode"`
NewCountryCode string `json:"newCountryCode"`
}
type countryCodeRenameJobPayload struct {
AppCode string `json:"appCode"`
CountryID int64 `json:"countryId"`
OldCountryCode string `json:"oldCountryCode"`
NewCountryCode string `json:"newCountryCode"`
CountryName string `json:"countryName"`
CountryDisplayName string `json:"countryDisplayName"`
SortOrder int32 `json:"sortOrder"`
ISOAlpha3 string `json:"isoAlpha3"`
ISONumeric string `json:"isoNumeric"`
PhoneCountryCode string `json:"phoneCountryCode"`
Flag string `json:"flag"`
ActorID int64 `json:"actorId"`
ActorName string `json:"actorName"`
RequestID string `json:"requestId"`
}
type countryCodeRenameResult struct {
CountryID int64 `json:"countryId"`
OldCountryCode string `json:"oldCountryCode"`
NewCountryCode string `json:"newCountryCode"`
Users int64 `json:"users"`
RegionMappings int64 `json:"regionMappings"`
RebuildTasks int64 `json:"rebuildTasks"`
Banners int64 `json:"banners"`
SplashScreens int64 `json:"splashScreens"`
RoomListEntries int64 `json:"roomListEntries"`
RoomSnapshots int64 `json:"roomSnapshots"`
CompletedAtMs int64 `json:"completedAtMs"`
}
type countryCodeRenameUserCounts struct {
Users int64
RegionMappings int64
RebuildTasks int64
}
func (s *Service) RenameCountryCode(ctx context.Context, actorID int64, actorName string, countryID int64, requestID string, req renameCountryCodeRequest) (*countryCodeRenameResponse, error) {
if s.userDB == nil {
return nil, errors.New("user db is not configured")
}
if s.jobStore == nil {
return nil, errors.New("job store is not configured")
}
req = normalizeRenameCountryCodeRequest(req)
if countryID <= 0 || strings.TrimSpace(requestID) == "" || !validCountryRenameRequest(req) {
return nil, errCountryInvalid
}
tx, err := s.userDB.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
country, err := queryCountryForUpdate(ctx, tx, countryID)
if err != nil {
return nil, err
}
oldCountryCode := strings.ToUpper(strings.TrimSpace(country.CountryCode))
if oldCountryCode == req.CountryCode {
return nil, errCountryInvalid
}
if err := ensureCountryCodeAvailable(ctx, tx, countryID, req.CountryCode); err != nil {
return nil, err
}
if err := s.ensureNoActiveCountryCodeRenameJob(ctx, appctx.FromContext(ctx), countryID, oldCountryCode, req.CountryCode); err != nil {
return nil, err
}
payload := countryCodeRenameJobPayload{
AppCode: appctx.FromContext(ctx),
CountryID: countryID,
OldCountryCode: oldCountryCode,
NewCountryCode: req.CountryCode,
CountryName: req.CountryName,
CountryDisplayName: req.CountryDisplayName,
SortOrder: req.SortOrder,
ISOAlpha3: req.ISOAlpha3,
ISONumeric: req.ISONumeric,
PhoneCountryCode: req.PhoneCountryCode,
Flag: req.Flag,
ActorID: actorID,
ActorName: strings.TrimSpace(actorName),
RequestID: strings.TrimSpace(requestID),
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
job := model.AdminJob{
Type: CountryCodeRenameJobType,
Status: model.JobStatusPending,
PayloadJSON: string(body),
CreatedBy: uint(actorID),
CreatedByName: strings.TrimSpace(actorName),
}
if err := s.jobStore.CreateJob(&job); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &countryCodeRenameResponse{
JobID: job.ID,
Status: job.Status,
CountryID: countryID,
OldCountryCode: oldCountryCode,
NewCountryCode: req.CountryCode,
}, nil
}
func (s *Service) HandleCountryCodeRenameJob(ctx context.Context, job *model.AdminJob) (string, error) {
if job == nil {
return "", errors.New("job is required")
}
if s.userDB == nil {
return "", errors.New("user db is not configured")
}
if s.jobStore == nil {
return "", errors.New("job store is not configured")
}
if s.roomClient == nil {
return "", errors.New("room service client is not configured")
}
var payload countryCodeRenameJobPayload
if err := json.Unmarshal([]byte(job.PayloadJSON), &payload); err != nil {
return "", err
}
payload = normalizeCountryCodeRenamePayload(payload)
if !validCountryCodeRenamePayload(payload) {
return "", errCountryInvalid
}
ctx = appctx.WithContext(ctx, payload.AppCode)
userCounts, err := s.renameCountryCodeInUserDB(ctx, payload)
if err != nil {
return "", err
}
nowMS := time.Now().UTC().UnixMilli()
adminCounts, err := s.jobStore.RenameCountryCodeAdminReferences(payload.AppCode, payload.OldCountryCode, payload.NewCountryCode, nowMS)
if err != nil {
return "", err
}
roomCounts, err := s.roomClient.RenameOwnerCountryCode(ctx, roomclient.RenameOwnerCountryCodeRequest{
RequestID: firstNonEmptyString(payload.RequestID, fmt.Sprintf("country-code-rename-job-%d", job.ID)),
OldCountryCode: payload.OldCountryCode,
NewCountryCode: payload.NewCountryCode,
AdminID: uint64(payload.ActorID),
})
if err != nil {
return "", err
}
result := countryCodeRenameResult{
CountryID: payload.CountryID,
OldCountryCode: payload.OldCountryCode,
NewCountryCode: payload.NewCountryCode,
Users: userCounts.Users,
RegionMappings: userCounts.RegionMappings,
RebuildTasks: userCounts.RebuildTasks,
Banners: adminCounts.Banners,
SplashScreens: adminCounts.SplashScreens,
RoomListEntries: roomCounts.RoomListEntries,
RoomSnapshots: roomCounts.RoomSnapshots,
CompletedAtMs: time.Now().UTC().UnixMilli(),
}
body, err := json.Marshal(result)
if err != nil {
return "", err
}
return string(body), nil
}
func (s *Service) renameCountryCodeInUserDB(ctx context.Context, payload countryCodeRenameJobPayload) (countryCodeRenameUserCounts, error) {
tx, err := s.userDB.BeginTx(ctx, nil)
if err != nil {
return countryCodeRenameUserCounts{}, err
}
defer tx.Rollback()
country, err := queryCountryForUpdate(ctx, tx, payload.CountryID)
if err != nil {
return countryCodeRenameUserCounts{}, err
}
currentCode := strings.ToUpper(strings.TrimSpace(country.CountryCode))
if currentCode != payload.OldCountryCode && currentCode != payload.NewCountryCode {
return countryCodeRenameUserCounts{}, errCountryConflict
}
if err := ensureCountryCodeAvailable(ctx, tx, payload.CountryID, payload.NewCountryCode); err != nil {
return countryCodeRenameUserCounts{}, err
}
nowMS := time.Now().UTC().UnixMilli()
if _, err := tx.ExecContext(ctx, `
UPDATE countries
SET country_code = ?, country_name = ?, country_display_name = ?,
iso_alpha3 = ?, iso_numeric = ?, phone_country_code = ?, flag = ?, sort_order = ?,
updated_by_user_id = ?, updated_at_ms = ?
WHERE app_code = ? AND country_id = ?
`, payload.NewCountryCode, payload.CountryName, payload.CountryDisplayName, nullableString(payload.ISOAlpha3), nullableString(payload.ISONumeric), nullableString(payload.PhoneCountryCode), payload.Flag, payload.SortOrder, nullableInt64(payload.ActorID), nowMS, payload.AppCode, payload.CountryID); err != nil {
return countryCodeRenameUserCounts{}, mapCountryWriteError(err)
}
regionResult, err := tx.ExecContext(ctx, `
UPDATE region_countries
SET country_code = ?, updated_by_user_id = ?, updated_at_ms = ?
WHERE app_code = ? AND country_code = ?
`, payload.NewCountryCode, nullableInt64(payload.ActorID), nowMS, payload.AppCode, payload.OldCountryCode)
if err != nil {
return countryCodeRenameUserCounts{}, err
}
userResult, err := tx.ExecContext(ctx, `
UPDATE users
SET country = ?, updated_at_ms = ?
WHERE app_code = ? AND country = ?
`, payload.NewCountryCode, nowMS, payload.AppCode, payload.OldCountryCode)
if err != nil {
return countryCodeRenameUserCounts{}, err
}
taskResult, err := tx.ExecContext(ctx, `
UPDATE user_region_rebuild_tasks
SET country_code = ?, updated_at_ms = ?
WHERE app_code = ? AND country_code = ? AND status IN (?, ?)
`, payload.NewCountryCode, nowMS, payload.AppCode, payload.OldCountryCode, regionRebuildTaskStatusPending, regionRebuildTaskStatusRunning)
if err != nil {
return countryCodeRenameUserCounts{}, err
}
if err := tx.Commit(); err != nil {
return countryCodeRenameUserCounts{}, err
}
regionRows, _ := regionResult.RowsAffected()
userRows, _ := userResult.RowsAffected()
taskRows, _ := taskResult.RowsAffected()
return countryCodeRenameUserCounts{Users: userRows, RegionMappings: regionRows, RebuildTasks: taskRows}, nil
}
func queryCountryForUpdate(ctx context.Context, tx *sql.Tx, countryID int64) (*userclientCountryForRename, error) {
var country userclientCountryForRename
if err := tx.QueryRowContext(ctx, `
SELECT country_id, country_code
FROM countries
WHERE app_code = ? AND country_id = ?
FOR UPDATE
`, appctx.FromContext(ctx), countryID).Scan(&country.CountryID, &country.CountryCode); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errCountryNotFound
}
return nil, err
}
return &country, nil
}
type userclientCountryForRename struct {
CountryID int64
CountryCode string
}
func ensureCountryCodeAvailable(ctx context.Context, tx *sql.Tx, countryID int64, countryCode string) error {
var existingID int64
err := tx.QueryRowContext(ctx, `
SELECT country_id
FROM countries
WHERE app_code = ? AND country_code = ? AND country_id <> ?
LIMIT 1
FOR UPDATE
`, appctx.FromContext(ctx), countryCode, countryID).Scan(&existingID)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
if err != nil {
return err
}
return errCountryConflict
}
func (s *Service) ensureNoActiveCountryCodeRenameJob(ctx context.Context, appCode string, countryID int64, oldCountryCode string, newCountryCode string) error {
jobs, err := s.jobStore.ListActiveJobsByType(CountryCodeRenameJobType)
if err != nil {
return err
}
for _, job := range jobs {
var payload countryCodeRenameJobPayload
if err := json.Unmarshal([]byte(job.PayloadJSON), &payload); err != nil {
continue
}
payload = normalizeCountryCodeRenamePayload(payload)
if payload.AppCode != appctx.Normalize(appCode) {
continue
}
if payload.CountryID == countryID ||
payload.OldCountryCode == oldCountryCode ||
payload.OldCountryCode == newCountryCode ||
payload.NewCountryCode == oldCountryCode ||
payload.NewCountryCode == newCountryCode {
return errCountryRenameJobConflict
}
}
return nil
}
func normalizeRenameCountryCodeRequest(req renameCountryCodeRequest) renameCountryCodeRequest {
req.CountryName = strings.TrimSpace(req.CountryName)
req.CountryCode = strings.ToUpper(strings.TrimSpace(req.CountryCode))
req.CountryDisplayName = strings.TrimSpace(req.CountryDisplayName)
req.ISOAlpha3 = strings.ToUpper(strings.TrimSpace(req.ISOAlpha3))
req.ISONumeric = strings.TrimSpace(req.ISONumeric)
req.PhoneCountryCode = strings.TrimSpace(req.PhoneCountryCode)
req.Flag = strings.TrimSpace(req.Flag)
return req
}
func validCountryRenameRequest(req renameCountryCodeRequest) bool {
return validCountryCreateRequest(createCountryRequest{
CountryName: req.CountryName,
CountryCode: req.CountryCode,
CountryDisplayName: req.CountryDisplayName,
SortOrder: req.SortOrder,
ISOAlpha3: req.ISOAlpha3,
ISONumeric: req.ISONumeric,
PhoneCountryCode: req.PhoneCountryCode,
Flag: req.Flag,
})
}
func normalizeCountryCodeRenamePayload(payload countryCodeRenameJobPayload) countryCodeRenameJobPayload {
payload.AppCode = appctx.Normalize(payload.AppCode)
payload.OldCountryCode = strings.ToUpper(strings.TrimSpace(payload.OldCountryCode))
payload.NewCountryCode = strings.ToUpper(strings.TrimSpace(payload.NewCountryCode))
payload.CountryName = strings.TrimSpace(payload.CountryName)
payload.CountryDisplayName = strings.TrimSpace(payload.CountryDisplayName)
payload.ISOAlpha3 = strings.ToUpper(strings.TrimSpace(payload.ISOAlpha3))
payload.ISONumeric = strings.TrimSpace(payload.ISONumeric)
payload.PhoneCountryCode = strings.TrimSpace(payload.PhoneCountryCode)
payload.Flag = strings.TrimSpace(payload.Flag)
payload.ActorName = strings.TrimSpace(payload.ActorName)
payload.RequestID = strings.TrimSpace(payload.RequestID)
return payload
}
func validCountryCodeRenamePayload(payload countryCodeRenameJobPayload) bool {
return payload.CountryID > 0 &&
payload.ActorID >= 0 &&
payload.OldCountryCode != payload.NewCountryCode &&
countryCodePattern.MatchString(payload.OldCountryCode) &&
validCountryRenameRequest(renameCountryCodeRequest{
CountryName: payload.CountryName,
CountryCode: payload.NewCountryCode,
CountryDisplayName: payload.CountryDisplayName,
SortOrder: payload.SortOrder,
ISOAlpha3: payload.ISOAlpha3,
ISONumeric: payload.ISONumeric,
PhoneCountryCode: payload.PhoneCountryCode,
Flag: payload.Flag,
})
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}