From 14604861eaeace1b475bf5bc4b08c688a75f44ef Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 14 Jul 2026 11:07:06 +0800 Subject: [PATCH] fix(admin): correct Google paid sync count --- .../legacy_google_paid_repository.go | 20 +++++++---- .../legacy_google_paid_repository_test.go | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 server/admin/internal/repository/legacy_google_paid_repository_test.go diff --git a/server/admin/internal/repository/legacy_google_paid_repository.go b/server/admin/internal/repository/legacy_google_paid_repository.go index 325250d1..0d6fd417 100644 --- a/server/admin/internal/repository/legacy_google_paid_repository.go +++ b/server/admin/internal/repository/legacy_google_paid_repository.go @@ -37,9 +37,12 @@ type LegacyGooglePaidStats struct { EstNetUSDMinor int64 } -// LegacyGooglePaidStatsByBillTime 按账单创建时间段聚合已同步实付:税/费/净收按实付占比折算到账单美金口径。 +// LegacyGooglePaidStatsByBillTime 按账单创建时间段聚合实付同步覆盖度。 +// synced_at_ms 是 Orders API 请求已成功落库的事实;Google 作为 Merchant of Record 时可能不返回 +// developerRevenueInBuyerCurrency,此时 paid_net_micro=0 只表示缺少费用明细,不能把已同步订单重新算成未同步。 +// 税/费/净收仍只对实付和净收都有效的订单按比例折算,避免用缺失字段伪造费用数据。 func (s *Store) LegacyGooglePaidStatsByBillTime(appCode string, startAtMS int64, endAtMS int64) (LegacyGooglePaidStats, error) { - where := "app_code = ? AND paid_amount_micro > 0 AND paid_net_micro > 0" + where := "app_code = ?" args := []any{strings.ToLower(strings.TrimSpace(appCode))} if startAtMS > 0 { where += " AND bill_created_at_ms >= ?" @@ -51,11 +54,14 @@ func (s *Store) LegacyGooglePaidStatsByBillTime(appCode string, startAtMS int64, } var stats LegacyGooglePaidStats row := s.db.Raw(` - SELECT COUNT(*), - COALESCE(SUM(bill_usd_minor), 0), - COALESCE(SUM(ROUND(bill_usd_minor * GREATEST(paid_amount_micro - paid_net_micro - paid_tax_micro, 0) / paid_amount_micro)), 0), - COALESCE(SUM(ROUND(bill_usd_minor * paid_tax_micro / paid_amount_micro)), 0), - COALESCE(SUM(ROUND(bill_usd_minor * paid_net_micro / paid_amount_micro)), 0) + SELECT COALESCE(SUM(CASE WHEN synced_at_ms > 0 THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN paid_amount_micro > 0 AND paid_net_micro > 0 THEN bill_usd_minor ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN paid_amount_micro > 0 AND paid_net_micro > 0 + THEN ROUND(bill_usd_minor * GREATEST(paid_amount_micro - paid_net_micro - paid_tax_micro, 0) / paid_amount_micro) ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN paid_amount_micro > 0 AND paid_net_micro > 0 + THEN ROUND(bill_usd_minor * paid_tax_micro / paid_amount_micro) ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN paid_amount_micro > 0 AND paid_net_micro > 0 + THEN ROUND(bill_usd_minor * paid_net_micro / paid_amount_micro) ELSE 0 END), 0) FROM admin_legacy_google_paid_details WHERE `+where, args...).Row() if err := row.Scan(&stats.SyncedCount, &stats.CoveredUSDMinor, &stats.EstFeeUSDMinor, &stats.EstTaxUSDMinor, &stats.EstNetUSDMinor); err != nil { diff --git a/server/admin/internal/repository/legacy_google_paid_repository_test.go b/server/admin/internal/repository/legacy_google_paid_repository_test.go new file mode 100644 index 00000000..ccf29984 --- /dev/null +++ b/server/admin/internal/repository/legacy_google_paid_repository_test.go @@ -0,0 +1,33 @@ +package repository + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestLegacyGooglePaidStatsCountsSyncedRowsWithoutNetRevenue(t *testing.T) { + store, mock, closeStore := newRepositorySQLMock(t) + defer closeStore() + + // 108 笔 Orders API 结果都已落库,但只有 2 笔带开发者净收。同步覆盖度必须仍为 108, + // 金额估算则继续只覆盖净收有效的 2 笔,这正是 Google Merchant of Record 订单的线上边界。 + mock.ExpectQuery(`(?s)SELECT.*SUM\(CASE WHEN synced_at_ms > 0 THEN 1 ELSE 0 END\).*CASE WHEN paid_amount_micro > 0 AND paid_net_micro > 0.*WHERE app_code = \? AND bill_created_at_ms >= \? AND bill_created_at_ms < \?`). + WithArgs("yumi", int64(1783872000000), int64(1783958400000)). + WillReturnRows(sqlmock.NewRows([]string{"synced_count", "covered_usd_minor", "fee_usd_minor", "tax_usd_minor", "net_usd_minor"}). + AddRow(108, 4200, 630, 210, 3360)) + + stats, err := store.LegacyGooglePaidStatsByBillTime(" YUMI ", 1783872000000, 1783958400000) + if err != nil { + t.Fatalf("load legacy google paid stats failed: %v", err) + } + if stats.SyncedCount != 108 { + t.Fatalf("synced count must use synced_at_ms instead of net revenue: %+v", stats) + } + if stats.CoveredUSDMinor != 4200 || stats.EstFeeUSDMinor != 630 || stats.EstTaxUSDMinor != 210 || stats.EstNetUSDMinor != 3360 { + t.Fatalf("settlement estimates mismatch: %+v", stats) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations mismatch: %v", err) + } +}