hyapp-server/server/admin/internal/modules/payment/money_region_source.go
2026-07-02 10:53:44 +08:00

351 lines
12 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 payment
import (
"context"
"fmt"
"hash/fnv"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/repository"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
const (
legacySyntheticRegionIDBase int64 = 9_000_000_000_000_000_000
legacySyntheticRegionIDRange uint64 = 223_372_036_854_775_807
)
type MoneyRegionSource interface {
ListMoneyMasterData(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error)
CountryBelongsToRegion(ctx context.Context, appCode string, regionID int64, countryCode string) (bool, bool, error)
}
type MongoMoneyRegionSource struct {
collection *mongo.Collection
config config.MoneyRegionSourceConfig
}
type legacyRegionDocument struct {
ID any `bson:"_id"`
RegionCode string `bson:"regionCode"`
SysOrigin string `bson:"sysOrigin"`
RegionName string `bson:"regionName"`
CountryCodes string `bson:"countryCodes"`
Del bool `bson:"del"`
}
func NewMongoMoneyRegionSource(database *mongo.Database, sourceConfig config.MoneyRegionSourceConfig) MoneyRegionSource {
if database == nil || !sourceConfig.Enabled {
return nil
}
collectionName := strings.TrimSpace(sourceConfig.MongoCollection)
if collectionName == "" {
collectionName = "sys_region_config"
}
sourceConfig.AppCode = appctx.Normalize(sourceConfig.AppCode)
sourceConfig.AppName = strings.TrimSpace(sourceConfig.AppName)
sourceConfig.SysOrigin = strings.ToUpper(strings.TrimSpace(sourceConfig.SysOrigin))
sourceConfig.MongoCollection = collectionName
return &MongoMoneyRegionSource{collection: database.Collection(collectionName), config: sourceConfig}
}
func (s *MongoMoneyRegionSource) ListMoneyMasterData(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
if s == nil || s.collection == nil {
return nil, nil, nil, nil
}
appCode := appctx.Normalize(s.config.AppCode)
if appCode == "" || !moneyAccessAllowsApp(access, appCode) {
return nil, nil, nil, nil
}
filter := bson.M{
"sysOrigin": s.config.SysOrigin,
"del": false,
}
// Yumi/Aslan 的区域事实仍由 legacy likei Mongo 持有admin 只读与 Java 当前查询等价的字段,不复制对方写模型。
cursor, err := s.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "createTime", Value: -1}}))
if err != nil {
return nil, nil, nil, fmt.Errorf("query legacy money regions for %s: %w", appCode, err)
}
defer cursor.Close(ctx)
documents := []legacyRegionDocument{}
for cursor.Next(ctx) {
var document legacyRegionDocument
if err := cursor.Decode(&document); err != nil {
return nil, nil, nil, fmt.Errorf("decode legacy money region for %s: %w", appCode, err)
}
documents = append(documents, document)
}
if err := cursor.Err(); err != nil {
return nil, nil, nil, fmt.Errorf("iterate legacy money regions for %s: %w", appCode, err)
}
return legacyRegionDocumentsToMoneyData(s.config, documents, access)
}
func (s *MongoMoneyRegionSource) CountryBelongsToRegion(ctx context.Context, appCode string, regionID int64, countryCode string) (bool, bool, error) {
if s == nil || s.collection == nil || appctx.Normalize(s.config.AppCode) != appctx.Normalize(appCode) {
return false, false, nil
}
filter := bson.M{
"sysOrigin": s.config.SysOrigin,
"del": false,
}
cursor, err := s.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "createTime", Value: -1}}))
if err != nil {
return false, false, fmt.Errorf("query legacy region countries for %s: %w", appCode, err)
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var document legacyRegionDocument
if err := cursor.Decode(&document); err != nil {
return false, false, fmt.Errorf("decode legacy region countries for %s: %w", appCode, err)
}
documentRegionID, _, err := legacyMoneyRegionID(s.config, document)
if err != nil {
return false, false, err
}
if documentRegionID != regionID {
continue
}
for _, item := range splitLegacyCountryCodes(document.CountryCodes) {
if item == strings.ToUpper(strings.TrimSpace(countryCode)) {
return true, true, nil
}
}
return false, true, nil
}
if err := cursor.Err(); err != nil {
return false, false, fmt.Errorf("iterate legacy region countries for %s: %w", appCode, err)
}
return false, false, nil
}
func (s *MongoMoneyRegionSource) CountryCodesForRegion(ctx context.Context, appCode string, regionID int64) ([]string, bool, error) {
if s == nil || s.collection == nil || appctx.Normalize(s.config.AppCode) != appctx.Normalize(appCode) {
return nil, false, nil
}
filter := bson.M{
"sysOrigin": s.config.SysOrigin,
"del": false,
}
cursor, err := s.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "createTime", Value: -1}}))
if err != nil {
return nil, false, fmt.Errorf("query legacy region countries for %s: %w", appCode, err)
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var document legacyRegionDocument
if err := cursor.Decode(&document); err != nil {
return nil, false, fmt.Errorf("decode legacy region countries for %s: %w", appCode, err)
}
documentRegionID, _, err := legacyMoneyRegionID(s.config, document)
if err != nil {
return nil, false, err
}
if documentRegionID != regionID {
continue
}
// 大屏外接源只认 legacy country_code这里集中复用财务范围的 Mongo 区域口径,避免每个外接 App 自己维护一套区域映射。
return splitLegacyCountryCodes(document.CountryCodes), true, nil
}
if err := cursor.Err(); err != nil {
return nil, false, fmt.Errorf("iterate legacy region countries for %s: %w", appCode, err)
}
return nil, false, nil
}
func legacyRegionDocumentsToMoneyData(sourceConfig config.MoneyRegionSourceConfig, documents []legacyRegionDocument, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
appCode := appctx.Normalize(sourceConfig.AppCode)
if appCode == "" || !moneyAccessAllowsApp(access, appCode) {
return nil, nil, nil, nil
}
appName := strings.TrimSpace(sourceConfig.AppName)
if appName == "" {
appName = appCode
}
sysOrigin := strings.ToUpper(strings.TrimSpace(sourceConfig.SysOrigin))
regions := []moneyRegionDTO{}
countries := []moneyCountryDTO{}
seenRegionIDs := map[int64]string{}
for index, document := range documents {
if document.Del || sysOrigin != "" && !strings.EqualFold(strings.TrimSpace(document.SysOrigin), sysOrigin) {
continue
}
regionID, rawRegionID, err := legacyMoneyRegionID(sourceConfig, document)
if err != nil {
return nil, nil, nil, err
}
if previousRawID, ok := seenRegionIDs[regionID]; ok && previousRawID != rawRegionID {
return nil, nil, nil, fmt.Errorf("legacy money region id collision for %s: %s and %s", appCode, previousRawID, rawRegionID)
}
seenRegionIDs[regionID] = rawRegionID
if !access.All && !access.Allows(appCode, regionID) {
continue
}
regionCode := strings.TrimSpace(document.RegionCode)
if regionCode == "" {
regionCode = strconv.FormatInt(regionID, 10)
}
regionName := strings.TrimSpace(document.RegionName)
if regionName == "" {
regionName = regionCode
}
countryCodes := splitLegacyCountryCodes(document.CountryCodes)
regions = append(regions, moneyRegionDTO{
AppCode: appCode,
RegionID: regionID,
RegionCode: regionCode,
Name: regionName,
Status: "active",
SortOrder: index + 1,
Countries: countryCodes,
})
for countryIndex, countryCode := range countryCodes {
countries = append(countries, moneyCountryDTO{
AppCode: appCode,
CountryCode: countryCode,
CountryName: countryCode,
CountryDisplayName: countryCode,
Enabled: true,
RegionID: regionID,
SortOrder: countryIndex + 1,
})
}
}
return []moneyAppDTO{{
AppCode: appCode,
AppName: appName,
Platform: "legacy",
Status: "active",
}}, regions, countries, nil
}
func legacyMoneyRegionID(sourceConfig config.MoneyRegionSourceConfig, document legacyRegionDocument) (int64, string, error) {
appCode := appctx.Normalize(sourceConfig.AppCode)
rawID := legacyRawRegionID(document.ID)
if rawID == "" {
return 0, "", fmt.Errorf("legacy money region %s has empty id", appCode)
}
if numericID, err := strconv.ParseInt(rawID, 10, 64); err == nil && numericID > 0 {
return numericID, rawID, nil
}
// Java legacy 数据里历史区域多数是雪花数字字符串,但线上已存在 Mongo ObjectIdadmin 授权表只能存 int64
// 因此把 appCode + 原始 _id 哈希到 9e18 以上的保留正整数段,保证同一 legacy 区域跨进程、跨部署稳定。
hash := fnv.New64a()
_, _ = hash.Write([]byte(appCode))
_, _ = hash.Write([]byte{0})
_, _ = hash.Write([]byte(rawID))
return legacySyntheticRegionIDBase + int64(hash.Sum64()%legacySyntheticRegionIDRange), rawID, nil
}
func legacyRawRegionID(value any) string {
switch typed := value.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(typed)
case bson.ObjectID:
return typed.Hex()
case fmt.Stringer:
return strings.TrimSpace(typed.String())
default:
return strings.TrimSpace(fmt.Sprint(typed))
}
}
func (h *Handler) listMoneyRegionSources(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
if h == nil || len(h.moneyRegionSources) == 0 {
return nil, nil, nil, nil
}
apps := []moneyAppDTO{}
regions := []moneyRegionDTO{}
countries := []moneyCountryDTO{}
for _, source := range h.moneyRegionSources {
sourceApps, sourceRegions, sourceCountries, err := source.ListMoneyMasterData(ctx, access)
if err != nil {
return nil, nil, nil, err
}
apps = appendUniqueMoneyApps(apps, sourceApps)
regions = appendUniqueMoneyRegions(regions, sourceRegions)
countries = appendUniqueMoneyCountries(countries, sourceCountries)
}
return apps, regions, countries, nil
}
func splitLegacyCountryCodes(value string) []string {
parts := strings.FieldsFunc(value, func(r rune) bool {
return r == ',' || r == ';' || r == ' ' || r == '\n' || r == '\t' || r == '\r'
})
out := make([]string, 0, len(parts))
seen := map[string]struct{}{}
for _, part := range parts {
code := strings.ToUpper(strings.TrimSpace(part))
if code == "" {
continue
}
if _, ok := seen[code]; ok {
continue
}
seen[code] = struct{}{}
out = append(out, code)
}
return out
}
func appendUniqueMoneyApps(base []moneyAppDTO, extra []moneyAppDTO) []moneyAppDTO {
seen := map[string]struct{}{}
for _, item := range base {
seen[appctx.Normalize(item.AppCode)] = struct{}{}
}
for _, item := range extra {
key := appctx.Normalize(item.AppCode)
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
base = append(base, item)
}
return base
}
func appendUniqueMoneyRegions(base []moneyRegionDTO, extra []moneyRegionDTO) []moneyRegionDTO {
seen := map[string]struct{}{}
for _, item := range base {
seen[fmt.Sprintf("%s:%d", appctx.Normalize(item.AppCode), item.RegionID)] = struct{}{}
}
for _, item := range extra {
key := fmt.Sprintf("%s:%d", appctx.Normalize(item.AppCode), item.RegionID)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
base = append(base, item)
}
return base
}
func appendUniqueMoneyCountries(base []moneyCountryDTO, extra []moneyCountryDTO) []moneyCountryDTO {
seen := map[string]struct{}{}
for _, item := range base {
seen[fmt.Sprintf("%s:%d:%s", appctx.Normalize(item.AppCode), item.RegionID, strings.ToUpper(item.CountryCode))] = struct{}{}
}
for _, item := range extra {
key := fmt.Sprintf("%s:%d:%s", appctx.Normalize(item.AppCode), item.RegionID, strings.ToUpper(item.CountryCode))
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
base = append(base, item)
}
return base
}