351 lines
12 KiB
Go
351 lines
12 KiB
Go
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 ObjectId;admin 授权表只能存 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
|
||
}
|