修复了问题

This commit is contained in:
zhx 2026-07-03 14:54:21 +08:00
parent 0cd30e222d
commit d8c07b07ac
2 changed files with 149 additions and 35 deletions

View File

@ -109,17 +109,13 @@ func (s *MongoRechargeBillSource) ListRechargeBills(ctx context.Context, query l
if s == nil || s.collection == nil {
return nil, 0, fmt.Errorf("legacy bill source is not configured")
}
filter, matchable, err := s.billFilter(ctx, query)
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
if err != nil {
return nil, 0, err
}
if !matchable {
return []rechargeBillDTO{}, 0, nil
}
total, err := s.collection.CountDocuments(ctx, filter)
if err != nil {
return nil, 0, fmt.Errorf("count legacy recharge bills for %s: %w", s.config.AppCode, err)
}
page := query.Page
if page < 1 {
page = 1
@ -128,6 +124,17 @@ func (s *MongoRechargeBillSource) ListRechargeBills(ctx context.Context, query l
if pageSize < 1 {
pageSize = 10
}
if len(regionCodes) == 0 {
return s.listBillsByFilter(ctx, filter, page, pageSize)
}
return s.listBillsByRegionPipeline(ctx, filter, regionCodes, page, pageSize)
}
func (s *MongoRechargeBillSource) listBillsByFilter(ctx context.Context, filter bson.M, page int, pageSize int) ([]rechargeBillDTO, int64, error) {
total, err := s.collection.CountDocuments(ctx, filter)
if err != nil {
return nil, 0, fmt.Errorf("count legacy recharge bills for %s: %w", s.config.AppCode, err)
}
cursor, err := s.collection.Find(ctx, filter, options.Find().
SetSort(bson.D{{Key: "createTime", Value: -1}, {Key: "_id", Value: -1}}).
SetSkip(int64(page-1)*int64(pageSize)).
@ -136,26 +143,77 @@ func (s *MongoRechargeBillSource) ListRechargeBills(ctx context.Context, query l
return nil, 0, fmt.Errorf("query legacy recharge bills for %s: %w", s.config.AppCode, err)
}
defer cursor.Close(ctx)
items, err := s.decodeBillCursor(ctx, cursor, pageSize)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
// listBillsByRegionPipeline 处理区域筛选likei 账单的 countryCode 长期为空,区域归属只能按付款用户
// user_run_profile.countryCode 反查,因此列表和计数都要走 $lookup 聚合。
func (s *MongoRechargeBillSource) listBillsByRegionPipeline(ctx context.Context, filter bson.M, regionCodes []string, page int, pageSize int) ([]rechargeBillDTO, int64, error) {
base := append(mongo.Pipeline{bson.D{{Key: "$match", Value: filter}}}, legacyRegionLookupStages(regionCodes)...)
countCursor, err := s.collection.Aggregate(ctx, append(clonePipeline(base), bson.D{{Key: "$count", Value: "n"}}))
if err != nil {
return nil, 0, fmt.Errorf("count legacy recharge bills for %s: %w", s.config.AppCode, err)
}
defer countCursor.Close(ctx)
var total int64
if countCursor.Next(ctx) {
var row struct {
N int64 `bson:"n"`
}
if err := countCursor.Decode(&row); err != nil {
return nil, 0, fmt.Errorf("decode legacy recharge bill count for %s: %w", s.config.AppCode, err)
}
total = row.N
}
if err := countCursor.Err(); err != nil {
return nil, 0, fmt.Errorf("iterate legacy recharge bill count for %s: %w", s.config.AppCode, err)
}
if total == 0 {
return []rechargeBillDTO{}, 0, nil
}
pagePipeline := append(clonePipeline(base),
bson.D{{Key: "$sort", Value: bson.D{{Key: "createTime", Value: -1}, {Key: "_id", Value: -1}}}},
bson.D{{Key: "$skip", Value: int64(page-1) * int64(pageSize)}},
bson.D{{Key: "$limit", Value: int64(pageSize)}},
)
cursor, err := s.collection.Aggregate(ctx, pagePipeline)
if err != nil {
return nil, 0, fmt.Errorf("query legacy recharge bills for %s: %w", s.config.AppCode, err)
}
defer cursor.Close(ctx)
items, err := s.decodeBillCursor(ctx, cursor, pageSize)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
func (s *MongoRechargeBillSource) decodeBillCursor(ctx context.Context, cursor *mongo.Cursor, pageSize int) ([]rechargeBillDTO, error) {
items := make([]rechargeBillDTO, 0, pageSize)
for cursor.Next(ctx) {
var document legacyPurchaseDocument
if err := cursor.Decode(&document); err != nil {
return nil, 0, fmt.Errorf("decode legacy recharge bill for %s: %w", s.config.AppCode, err)
return nil, fmt.Errorf("decode legacy recharge bill for %s: %w", s.config.AppCode, err)
}
items = append(items, legacyRechargeBillDTO(s.config, document))
}
if err := cursor.Err(); err != nil {
return nil, 0, fmt.Errorf("iterate legacy recharge bills for %s: %w", s.config.AppCode, err)
return nil, fmt.Errorf("iterate legacy recharge bills for %s: %w", s.config.AppCode, err)
}
return items, total, nil
return items, nil
}
func (s *MongoRechargeBillSource) SummarizeRechargeBills(ctx context.Context, query legacyRechargeBillQuery) (rechargeBillSummaryDTO, error) {
if s == nil || s.collection == nil {
return rechargeBillSummaryDTO{}, fmt.Errorf("legacy bill source is not configured")
}
filter, matchable, err := s.billFilter(ctx, query)
filter, regionCodes, matchable, err := s.billFilter(ctx, query)
if err != nil {
return rechargeBillSummaryDTO{}, err
}
@ -174,19 +232,18 @@ func (s *MongoRechargeBillSource) SummarizeRechargeBills(ctx context.Context, qu
bson.M{"$convert": bson.M{"input": "$$this.content", "to": "long", "onError": 0, "onNull": 0}},
}},
}}
cursor, err := s.collection.Aggregate(ctx, mongo.Pipeline{
{{Key: "$match", Value: filter}},
{{Key: "$group", Value: bson.M{
"_id": bson.M{"$cond": bson.A{
bson.M{"$eq": bson.A{"$factory.factoryCode", "GOOGLE"}},
"google",
"third_party",
}},
"billCount": bson.M{"$sum": 1},
"usd": bson.M{"$sum": bson.M{"$ifNull": bson.A{"$amountUsd", 0}}},
"coin": bson.M{"$sum": coinExpr},
}}},
})
pipeline := append(mongo.Pipeline{bson.D{{Key: "$match", Value: filter}}}, legacyRegionLookupStages(regionCodes)...)
pipeline = append(pipeline, bson.D{{Key: "$group", Value: bson.M{
"_id": bson.M{"$cond": bson.A{
bson.M{"$eq": bson.A{"$factory.factoryCode", "GOOGLE"}},
"google",
"third_party",
}},
"billCount": bson.M{"$sum": 1},
"usd": bson.M{"$sum": bson.M{"$ifNull": bson.A{"$amountUsd", 0}}},
"coin": bson.M{"$sum": coinExpr},
}}})
cursor, err := s.collection.Aggregate(ctx, pipeline)
if err != nil {
return rechargeBillSummaryDTO{}, fmt.Errorf("summarize legacy recharge bills for %s: %w", s.config.AppCode, err)
}
@ -228,7 +285,8 @@ func (s *MongoRechargeBillSource) SummarizeRechargeBills(ctx context.Context, qu
}
// billFilter 构造与 hyapp 账单列表同口径的 Mongo 过滤条件;返回 matchable=false 表示筛选注定无结果。
func (s *MongoRechargeBillSource) billFilter(ctx context.Context, query legacyRechargeBillQuery) (bson.M, bool, error) {
// regionCodes 非空时调用方必须叠加 legacyRegionLookupStageslikei 账单不带国家码,区域归属按付款用户资料判定。
func (s *MongoRechargeBillSource) billFilter(ctx context.Context, query legacyRechargeBillQuery) (bson.M, []string, bool, error) {
filter := bson.M{
"sysOrigin": s.config.SysOrigin,
"status": "SUCCESS",
@ -255,14 +313,45 @@ func (s *MongoRechargeBillSource) billFilter(ctx context.Context, query legacyRe
if query.RegionID > 0 {
codes, handled, err := s.regionCountryCodes(ctx, query.RegionID)
if err != nil {
return nil, false, err
return nil, nil, false, err
}
if !handled || len(codes) == 0 {
return nil, false, nil
return nil, nil, false, nil
}
filter["countryCode"] = bson.M{"$in": codes}
return filter, codes, true, nil
}
return filter, true, nil
return filter, nil, true, nil
}
const legacyUserProfileCollection = "user_run_profile"
// legacyRegionLookupStages 按付款用户的 user_run_profile.countryCode 过滤区域codes 为空时不追加任何阶段。
func legacyRegionLookupStages(codes []string) mongo.Pipeline {
if len(codes) == 0 {
return mongo.Pipeline{}
}
return mongo.Pipeline{
bson.D{{Key: "$lookup", Value: bson.M{
"from": legacyUserProfileCollection,
"localField": "acceptUserId",
"foreignField": "_id",
"as": "legacyBillUser",
}}},
bson.D{{Key: "$addFields", Value: bson.M{
"legacyBillCountry": bson.M{"$toUpper": bson.M{"$ifNull": bson.A{
bson.M{"$first": "$legacyBillUser.countryCode"},
"",
}}},
}}},
bson.D{{Key: "$match", Value: bson.M{"legacyBillCountry": bson.M{"$in": codes}}}},
}
}
// clonePipeline 复制聚合管道,避免 append 复用底层数组导致 count/分页两条管道互相踩踏。
func clonePipeline(pipeline mongo.Pipeline) mongo.Pipeline {
cloned := make(mongo.Pipeline, len(pipeline))
copy(cloned, pipeline)
return cloned
}
func (s *MongoRechargeBillSource) regionCountryCodes(ctx context.Context, regionID int64) ([]string, bool, error) {

View File

@ -124,7 +124,7 @@ func TestLegacyBillFilterBuildsQuery(t *testing.T) {
config: yumiBillSourceConfig(),
regionResolvers: []legacyRegionCountryResolver{staticLegacyRegionResolver{codes: []string{"tr", "SA"}, handled: true}},
}
filter, matchable, err := source.billFilter(context.Background(), legacyRechargeBillQuery{
filter, regionCodes, matchable, err := source.billFilter(context.Background(), legacyRechargeBillQuery{
Keyword: "GPA.1",
RegionID: 9_000_000_000_000_000_001,
StartAtMS: 1_000,
@ -140,13 +140,12 @@ func TestLegacyBillFilterBuildsQuery(t *testing.T) {
if !ok || !timeRange["$gte"].(time.Time).Equal(time.UnixMilli(1_000)) || !timeRange["$lt"].(time.Time).Equal(time.UnixMilli(2_000)) {
t.Fatalf("unexpected time range: %v", filter["createTime"])
}
countries, ok := filter["countryCode"].(bson.M)
if !ok {
t.Fatalf("country filter missing: %v", filter)
// likei 账单不带国家码;区域条件必须以国家码清单返回,由聚合阶段按付款用户资料匹配。
if len(regionCodes) != 2 || regionCodes[0] != "TR" || regionCodes[1] != "SA" {
t.Fatalf("unexpected region codes: %v", regionCodes)
}
codes, ok := countries["$in"].([]string)
if !ok || len(codes) != 2 || codes[0] != "TR" || codes[1] != "SA" {
t.Fatalf("unexpected country codes: %v", countries["$in"])
if _, ok := filter["countryCode"]; ok {
t.Fatalf("bill countryCode filter should not be used: %v", filter)
}
if _, ok := filter["$or"]; !ok {
t.Fatalf("keyword filter missing: %v", filter)
@ -155,7 +154,7 @@ func TestLegacyBillFilterBuildsQuery(t *testing.T) {
func TestLegacyBillFilterUnresolvedRegionMatchesNothing(t *testing.T) {
source := &MongoRechargeBillSource{config: yumiBillSourceConfig()}
_, matchable, err := source.billFilter(context.Background(), legacyRechargeBillQuery{RegionID: 123})
_, _, matchable, err := source.billFilter(context.Background(), legacyRechargeBillQuery{RegionID: 123})
if err != nil {
t.Fatalf("billFilter err=%v", err)
}
@ -163,3 +162,29 @@ func TestLegacyBillFilterUnresolvedRegionMatchesNothing(t *testing.T) {
t.Fatalf("unresolved region should not be matchable")
}
}
func TestLegacyRegionLookupStages(t *testing.T) {
if stages := legacyRegionLookupStages(nil); len(stages) != 0 {
t.Fatalf("empty codes should not add stages: %v", stages)
}
stages := legacyRegionLookupStages([]string{"TR", "SA"})
if len(stages) != 3 {
t.Fatalf("expected lookup/addFields/match stages, got %d", len(stages))
}
lookup, ok := stages[0][0].Value.(bson.M)
if !ok || lookup["from"] != legacyUserProfileCollection || lookup["localField"] != "acceptUserId" || lookup["foreignField"] != "_id" {
t.Fatalf("unexpected lookup stage: %v", stages[0])
}
match, ok := stages[2][0].Value.(bson.M)
if !ok {
t.Fatalf("unexpected match stage: %v", stages[2])
}
condition, ok := match["legacyBillCountry"].(bson.M)
if !ok {
t.Fatalf("match stage missing country condition: %v", match)
}
codes, ok := condition["$in"].([]string)
if !ok || len(codes) != 2 || codes[0] != "TR" || codes[1] != "SA" {
t.Fatalf("unexpected region codes in match: %v", condition["$in"])
}
}