From 22ba319f7f6085cfc9f139fc376d4cff2b910345 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Thu, 5 Nov 2026 14:26:36 +0800 Subject: [PATCH] fix: align report date defaults with recent periods --- internal/biz/business/analytics.go | 18 +---- internal/biz/business/analytics_test.go | 10 +-- internal/biz/business/ledger.go | 9 +-- internal/biz/business/ledger_test.go | 4 +- internal/biz/business/ranking.go | 5 +- internal/biz/business/ranking_test.go | 3 + internal/biz/business/report_dates.go | 18 +++++ internal/biz/business/report_dates_test.go | 75 +++++++++++++++++++ internal/biz/business/server_analysis.go | 6 +- internal/biz/business/server_analysis_test.go | 2 +- web/src/view/sw/RankingAnalysis.vue | 5 +- web/src/view/sw/ServerAnalysis.vue | 14 +--- web/src/view/sw/components/LedgerDetails.vue | 14 +--- .../view/sw/components/ReportAnalytics.vue | 13 +--- web/src/view/sw/report-dates.js | 23 ++++++ web/src/view/sw/report-dates.test.js | 32 ++++++++ 16 files changed, 181 insertions(+), 70 deletions(-) create mode 100644 internal/biz/business/report_dates.go create mode 100644 internal/biz/business/report_dates_test.go create mode 100644 web/src/view/sw/report-dates.js create mode 100644 web/src/view/sw/report-dates.test.js diff --git a/internal/biz/business/analytics.go b/internal/biz/business/analytics.go index 1b30d84..ff56ca1 100644 --- a/internal/biz/business/analytics.go +++ b/internal/biz/business/analytics.go @@ -137,23 +137,11 @@ func (uc *AnalyticsUsecase) list(ctx context.Context, kind AnalyticsKind, filter filter.PromoteName = "" filter.PromoteType = nil } - if filter.EndDate == nil || kind == GameAnalytics && filter.StartDate == nil { - now := uc.now().In(portraitLocation) - day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, portraitLocation) - if kind == GameAnalytics && filter.StartDate == nil { - filter.StartDate = &day - } - if filter.EndDate == nil { - filter.EndDate = &day - } - } - if filter.StartDate != nil && filter.EndDate.Before(*filter.StartDate) { + filter.StartDate, filter.EndDate = reportDateDefaults(filter.StartDate, filter.EndDate, 30, uc.now()) + if filter.EndDate.Before(*filter.StartDate) { return nil, ErrAnalyticsDateOrder } - filter.Begin = "" - if filter.StartDate != nil { - filter.Begin = filter.StartDate.Format("2006-01-02") - } + filter.Begin = filter.StartDate.Format("2006-01-02") filter.End = filter.EndDate.Format("2006-01-02") out := &AnalyticsPage{Rows: []*AnalyticsRow{}, Begin: filter.Begin, End: filter.End, Available: true} scope := AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup} diff --git a/internal/biz/business/analytics_test.go b/internal/biz/business/analytics_test.go index ab915d5..8bd6b67 100644 --- a/internal/biz/business/analytics_test.go +++ b/internal/biz/business/analytics_test.go @@ -63,8 +63,8 @@ func TestGameAnalyticsSummaryUsesAllRowsAndCorrectDenominators(t *testing.T) { if len(page.Rows) != 1 || page.Total != 2 || page.Rows[0].ID != 2 || page.Summary.Counts.Register != 5 || page.Summary.Pay != "50.00" || page.Summary.PayRate != "30.00" || page.Summary.ARPU != "5.00" || page.Summary.ARPPU != "16.67" { t.Fatalf("游戏汇总、分页或分母错误: %+v", page) } - if page.Begin != "2026-11-04" || page.End != "2026-11-04" { - t.Fatal("后台缺省统计日期应为当天") + if page.Begin != "2026-10-06" || page.End != "2026-11-04" { + t.Fatal("后台缺省统计日期应为含今天的近30天") } } func TestChannelAnalyticsSummaryDependsOnViewer(t *testing.T) { @@ -82,7 +82,7 @@ func TestChannelAnalyticsSummaryDependsOnViewer(t *testing.T) { } } -func TestChannelAnalyticsAllPagesSummaryAndOpenDateRange(t *testing.T) { +func TestChannelAnalyticsAllPagesSummaryAndDefaultDateRange(t *testing.T) { uc, repo := analyticsFixture() repo.data.EffectiveCount = 3 for _, pageNum := range []int{1, 2, 8} { @@ -90,8 +90,8 @@ func TestChannelAnalyticsAllPagesSummaryAndOpenDateRange(t *testing.T) { if err != nil || page.Total != 2 || page.Summary.Counts.Register != 5 || page.Summary.Counts.Active != 10 || page.Summary.Counts.Pay != 3 || page.Summary.Counts.NewPay != 2 || page.Summary.Pay != "50.00" || page.Summary.PayRate != "30.00" || page.EffectiveCount != 3 { t.Fatalf("翻页不能改变完整合计: page=%d %+v %v", pageNum, page, err) } - if page.Begin != "" || page.End != "2026-11-04" || repo.filter.StartDate != nil { - t.Fatalf("渠道统计缺省应从最早数据统计至今天: %+v", repo.filter) + if page.Begin != "2026-10-06" || page.End != "2026-11-04" || repo.filter.StartDate == nil { + t.Fatalf("渠道统计缺省应为含今天的近30天: %+v", repo.filter) } } for _, sample := range []struct { diff --git a/internal/biz/business/ledger.go b/internal/biz/business/ledger.go index a1c3a21..1317659 100644 --- a/internal/biz/business/ledger.go +++ b/internal/biz/business/ledger.go @@ -119,18 +119,13 @@ func (uc *LedgerUsecase) prepare(ctx context.Context, filter LedgerFilter) (Ledg viewer := portraitUserScope(a) viewer.All = actor.AuthorityID == 888 scope := LedgerScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, Unrestricted: viewer.All && strings.TrimSpace(filter.BusierAccount) == ""} - begin, end := filter.StartDate, filter.EndDate - if begin == nil || end == nil { - now := uc.now().In(portraitLocation) - yesterday := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, portraitLocation) - first := yesterday.AddDate(0, 0, -6) - begin, end = &first, &yesterday - } + begin, end := reportDateDefaults(filter.StartDate, filter.EndDate, 30, uc.now()) if end.Before(*begin) { return scope, filter, ErrLedgerInput } filter.BeginUnix = begin.Unix() filter.EndUnix = end.AddDate(0, 0, 1).Unix() - 1 + filter.StartDate, filter.EndDate = begin, end return scope, filter, nil } func registrationSort(field string) string { diff --git a/internal/biz/business/ledger_test.go b/internal/biz/business/ledger_test.go index fbe6f06..ac55a35 100644 --- a/internal/biz/business/ledger_test.go +++ b/internal/biz/business/ledger_test.go @@ -46,8 +46,8 @@ func TestLedgerDateDefaultsAndBusinessScope(t *testing.T) { if _, err := uc.Registrations(accountContext(7, 901), LedgerFilter{SortField: "payAmount"}, LedgerListOptions{Limit: 10}); err != nil { t.Fatal(err) } - if time.Unix(r.filter.BeginUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2026-10-28 00:00:00" || time.Unix(r.filter.EndUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2026-11-03 23:59:59" { - t.Fatal("未传日期应取截至昨日的七天") + if time.Unix(r.filter.BeginUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2026-10-06 00:00:00" || time.Unix(r.filter.EndUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2026-11-04 23:59:59" { + t.Fatal("未传日期应取含今天的近30天") } if r.scope.Unrestricted || !r.scope.Viewer.IncludeSelf || r.filter.SortField != "totalPayAmount" { t.Fatal("商务范围或排序别名错误") diff --git a/internal/biz/business/ranking.go b/internal/biz/business/ranking.go index f910df1..ae993d0 100644 --- a/internal/biz/business/ranking.go +++ b/internal/biz/business/ranking.go @@ -70,10 +70,7 @@ func (uc *RankingUsecase) List(ctx context.Context, kind RankingKind, f Analytic if kind != ChannelRanking && kind != BusinessRanking && kind != GameRanking { return nil, ErrAnalyticsInput } - if f.StartDate == nil || f.EndDate == nil { - day := workbenchWindow(uc.now()).Today - f.StartDate, f.EndDate = &day, &day - } + f.StartDate, f.EndDate = reportDateDefaults(f.StartDate, f.EndDate, 7, uc.now()) if f.EndDate.Before(*f.StartDate) { return nil, ErrAnalyticsDateOrder } diff --git a/internal/biz/business/ranking_test.go b/internal/biz/business/ranking_test.go index 569a1ae..b7934a6 100644 --- a/internal/biz/business/ranking_test.go +++ b/internal/biz/business/ranking_test.go @@ -46,6 +46,9 @@ func TestRankingComparisonsAndBusinessCountSemantics(t *testing.T) { if err != nil || p.Active[0].Value != "5" || p.PayRate[0].Value != "40.00" || p.Register[0].Name != "二级商务 (L2)" || p.Register[0].SourceDB != "" { t.Fatal("商务需加总各渠道人数,不全局去重") } + if p.Begin != "2026-10-29" || p.End != "2026-11-04" || r.filter.PreviousBegin != "2026-10-22" || r.filter.PreviousEnd != "2026-10-28" { + t.Fatal("排行榜缺省为含今天的近7天,上期范围应等长") + } calls := r.calls p, err = uc.List(accountContext(9, 903), BusinessRanking, AnalyticsFilter{}) if err != nil || len(p.Register) != 0 || r.calls != calls { diff --git a/internal/biz/business/report_dates.go b/internal/biz/business/report_dates.go new file mode 100644 index 0000000..89ca6c5 --- /dev/null +++ b/internal/biz/business/report_dates.go @@ -0,0 +1,18 @@ +package business + +import "time" + +// reportDateDefaults fills missing bounds without discarding an explicit date. +// Reports include the end date, so a 30-day period starts 29 days earlier. +func reportDateDefaults(begin, end *time.Time, days int, now time.Time) (*time.Time, *time.Time) { + if end == nil { + local := now.In(portraitLocation) + day := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, portraitLocation) + end = &day + } + if begin == nil { + first := end.AddDate(0, 0, 1-days) + begin = &first + } + return begin, end +} diff --git a/internal/biz/business/report_dates_test.go b/internal/biz/business/report_dates_test.go new file mode 100644 index 0000000..40db353 --- /dev/null +++ b/internal/biz/business/report_dates_test.go @@ -0,0 +1,75 @@ +package business + +import ( + "testing" + "time" +) + +func TestAnalyticsDefaultDatesBoundListAndExportQueries(t *testing.T) { + date := func(value string) *time.Time { + parsed, err := time.ParseInLocation("2006-01-02", value, portraitLocation) + if err != nil { + t.Fatal(err) + } + return &parsed + } + for _, kind := range []AnalyticsKind{ChannelAnalytics, GameAnalytics} { + for _, sample := range []struct { + name, start, end, wantStart, wantEnd string + now time.Time + }{ + {"empty", "", "", "2026-10-06", "2026-11-04", time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation)}, + {"China day across UTC midnight", "", "", "2025-12-03", "2026-01-01", time.Date(2025, 12, 31, 17, 0, 0, 0, time.UTC)}, + {"only end across leap day", "", "2024-03-01", "2024-02-01", "2024-03-01", time.Now()}, + {"only start", "2026-01-01", "", "2026-01-01", "2026-11-04", time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation)}, + {"explicit historical year", "2023-01-01", "2023-12-31", "2023-01-01", "2023-12-31", time.Now()}, + } { + t.Run(string(kind)+"/"+sample.name, func(t *testing.T) { + uc, repo := analyticsFixture() + uc.now = func() time.Time { return sample.now } + filter := AnalyticsFilter{} + if sample.start != "" { + filter.StartDate = date(sample.start) + } + if sample.end != "" { + filter.EndDate = date(sample.end) + } + for _, export := range []bool{false, true} { + var page *AnalyticsPage + var err error + if export { + page, err = uc.ListAll(accountContext(7, 901), kind, filter) + } else { + page, err = uc.List(accountContext(7, 901), kind, filter, 1, 10) + } + if err != nil { + t.Fatal(err) + } + if page.Begin != sample.wantStart || page.End != sample.wantEnd || repo.filter.Begin != page.Begin || repo.filter.End != page.End || repo.filter.StartDate == nil || repo.filter.EndDate == nil { + t.Fatalf("export=%v expected %s..%s, got %+v", export, sample.wantStart, sample.wantEnd, repo.filter) + } + } + }) + } + } +} + +func TestLedgerPartialDateDoesNotDiscardHistoricalEnd(t *testing.T) { + uc, repo, _ := ledgerFixture() + end := time.Date(2024, 3, 1, 0, 0, 0, 0, portraitLocation) + for _, export := range []bool{false, true} { + filter := LedgerFilter{EndDate: &end} + var err error + if export { + _, err = uc.ExportSpends(accountContext(7, 901), filter) + } else { + _, err = uc.Registrations(accountContext(7, 901), filter, LedgerListOptions{Limit: 10}) + } + if err != nil { + t.Fatal(err) + } + if time.Unix(repo.filter.BeginUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2024-02-01 00:00:00" || time.Unix(repo.filter.EndUnix, 0).In(portraitLocation).Format("2006-01-02 15:04:05") != "2024-03-01 23:59:59" { + t.Fatalf("historical end date was discarded: %+v", repo.filter) + } + } +} diff --git a/internal/biz/business/server_analysis.go b/internal/biz/business/server_analysis.go index 50a075b..ad248fa 100644 --- a/internal/biz/business/server_analysis.go +++ b/internal/biz/business/server_analysis.go @@ -89,11 +89,7 @@ func (uc *ServerAnalysisUsecase) List(ctx context.Context, f ServerAnalysisFilte if err != nil { return nil, err } - if f.StartDate == nil || f.EndDate == nil { - now := uc.now().In(portraitLocation) - day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, portraitLocation) - f.StartDate, f.EndDate = &day, &day - } + f.StartDate, f.EndDate = reportDateDefaults(f.StartDate, f.EndDate, 30, uc.now()) if f.EndDate.Before(*f.StartDate) { return nil, ErrAnalyticsDateOrder } diff --git a/internal/biz/business/server_analysis_test.go b/internal/biz/business/server_analysis_test.go index 1c3847d..e259f92 100644 --- a/internal/biz/business/server_analysis_test.go +++ b/internal/biz/business/server_analysis_test.go @@ -38,7 +38,7 @@ func TestServerAnalysisSummaryZerosSortingAndLimits(t *testing.T) { uc.now = func() time.Time { return time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation) } f := ServerAnalysisFilter{AnalyticsFilter: AnalyticsFilter{GameNames: []string{"游戏"}}} p, err := uc.List(accountContext(7, 901), f, 1, 1) - if err != nil || p.Total != 2 || len(p.Rows) != 1 || p.Rows[0].ServerNum != "01" || p.Summary.Pay != "30.01" || p.Summary.Counts.Register != 5 || p.Summary.ARPU != "6.00" || p.Begin != "2026-11-04" { + if err != nil || p.Total != 2 || len(p.Rows) != 1 || p.Rows[0].ServerNum != "01" || p.Summary.Pay != "30.01" || p.Summary.Counts.Register != 5 || p.Summary.ARPU != "6.00" || p.Begin != "2026-10-06" || p.End != "2026-11-04" { t.Fatalf("汇总、四舍五入或分页错误: %+v %v", p, err) } if !r.scope.IncludeSub || !r.scope.IDCardDedup || r.scope.Viewer.All { diff --git a/web/src/view/sw/RankingAnalysis.vue b/web/src/view/sw/RankingAnalysis.vue index 1929b2d..1d8b808 100644 --- a/web/src/view/sw/RankingAnalysis.vue +++ b/web/src/view/sw/RankingAnalysis.vue @@ -25,12 +25,12 @@ import { computed, onMounted, reactive, ref } from 'vue' import { listRankings } from '@/api/ranking' import { channelCount, channelMoney } from './channel-display' import CompactRankingTable from './components/CompactRankingTable.vue' +import { normalizeReportDates } from './report-dates' defineOptions({ name: 'RankingAnalysis' }) const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }, dimensions = { channel: '渠道', business: '商务', game: '游戏' } const sources = ref(Object.keys(platforms)), dimension = ref('channel') const ranges = [{ value: 'today', label: '今日' }, { value: 'yesterday', label: '昨日' }, { value: '7d', label: '近7天' }, { value: '30d', label: '近30天' }] -const ymd = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}` -const dateRange = range => { const e = new Date(), b = new Date(); if (range === 'yesterday') { e.setDate(e.getDate() - 1); b.setDate(b.getDate() - 1) } else if (range === '7d') b.setDate(b.getDate() - 6); else if (range === '30d') b.setDate(b.getDate() - 29); return [ymd(b), ymd(e)] } +const dateRange = range => { const end = new Date(); if (range === 'yesterday') end.setDate(end.getDate() - 1); return normalizeReportDates(undefined, range === '7d' ? 7 : range === '30d' ? 30 : 1, end) } const memory = reactive(Object.fromEntries(Object.keys(dimensions).map(key => [key, { range: '7d', dates: dateRange('7d') }]))) const dates = computed(() => memory[dimension.value].dates) const result = ref(null), available = ref(true), loading = ref(false), error = ref('') @@ -49,6 +49,7 @@ const displayRows = panel => { } const metric = (panel, value) => value == null ? '—' : panel.money ? channelMoney(value) : panel.rate ? `${value}%` : channelCount(value) const load = async () => { + memory[dimension.value].dates = normalizeReportDates(dates.value, 7) const current = ++version; loading.value = true; result.value = null; error.value = ''; available.value = true try { // 旧排行榜空选平台表示全部,保留此语义;其他报表页面有各自的空选规则。 diff --git a/web/src/view/sw/ServerAnalysis.vue b/web/src/view/sw/ServerAnalysis.vue index bdab155..a5b3b13 100644 --- a/web/src/view/sw/ServerAnalysis.vue +++ b/web/src/view/sw/ServerAnalysis.vue @@ -36,19 +36,11 @@ import PlatformTag from './components/PlatformTag.vue' import MetricValue from './components/MetricValue.vue' import { metricTips, remoteSort, sortDescription } from './table-display' import { analysisGameSelection } from './analysis-game-selection' +import { formatReportDate as ymd, normalizeReportDates, reportDateShortcuts as shortcuts } from './report-dates' defineOptions({ name: 'ServerAnalysis' }) const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' } -const day = () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()) } -const ymd = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}` -const end = day(), begin = day(); begin.setDate(begin.getDate() - 29) -const dates = ref([ymd(begin), ymd(end)]) -const shortcuts = [ - { text: '今日', value: () => [day(), day()] }, - { text: '昨日', value: () => { const d = day(); d.setDate(d.getDate() - 1); return [d, new Date(d)] } }, - { text: '本周', value: () => { const e = day(), b = day(); b.setDate(b.getDate() - (b.getDay() || 7) + 1); return [b, e] } }, - { text: '本月', value: () => { const e = day(); return [new Date(e.getFullYear(), e.getMonth(), 1), e] } } -] +const dates = ref(normalizeReportDates()) const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), serverOptions = ref([]) const selectedGame = computed({ get: () => games.value[0] || '', set: value => { games.value = value ? [value] : [] } }) const query = reactive({ pageNum: 1, pageSize: 10, serverNum: '', sortField: 'totalPay', sortOrder: 'desc' }) @@ -71,6 +63,8 @@ const summaries = ({ columns: tableColumns }) => tableColumns.map((column, index const clear = () => { ++version; rows.value = []; total.value = 0; summary.value = null; result.value = null; loading.value = false; error.value = ''; available.value = true } const params = () => ({ ...query, sourceDb: sources.value.join(','), gameName: games.value.join(','), beginTime: dates.value[0], endTime: dates.value[1] }) const load = async () => { + dates.value = normalizeReportDates(dates.value) + if (dates.value[0] > dates.value[1]) { ElMessage.warning('统计开始时间不能晚于结束时间'); return } if (!sources.value.length || !games.value.length) { clear(); return } const current = ++version; loading.value = true; error.value = '' try { diff --git a/web/src/view/sw/components/LedgerDetails.vue b/web/src/view/sw/components/LedgerDetails.vue index e94c651..24edc78 100644 --- a/web/src/view/sw/components/LedgerDetails.vue +++ b/web/src/view/sw/components/LedgerDetails.vue @@ -54,6 +54,7 @@ import { createGameOptionsLoader } from '../game-options-loader' import { remoteSort, sortDescription } from '../table-display' import PlatformTag from './PlatformTag.vue' import MetricValue from './MetricValue.vue' +import { formatReportDate as ymd, normalizeReportDates, reportDateShortcuts as shortcuts } from '../report-dates' const fetchGameOptions = createGameOptionsLoader(listLedgerGames) const props = defineProps({ kind: { type: String, required: true } }) const spend = computed(() => props.kind === 'spend') @@ -63,16 +64,7 @@ const canExport = computed(() => Number(user.userInfo.authorityId) === 888 || !! const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' } const payWays = { 1: '绑币', 2: '平台币', 3: '支付宝', 4: '微信', 5: '金猪', 6: '苹果支付', 11: '盒子币', 12: '龙翔币', 14: '抖音小游戏' } const statuses = { 1: '成功', 2: '处理中', 0: '失败' } -const today = () => { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()) } -const ymd = date => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` -const end = today(), begin = today(); begin.setDate(begin.getDate() - 29) -const dates = ref([ymd(begin), ymd(end)]) -const shortcuts = [ - { text: '今日', value: () => [today(), today()] }, - { text: '昨日', value: () => { const d = today(); d.setDate(d.getDate() - 1); return [d, new Date(d)] } }, - { text: '本周', value: () => { const e = today(), b = today(); b.setDate(b.getDate() - (b.getDay() || 7) + 1); return [b, e] } }, - { text: '本月', value: () => { const e = today(); return [new Date(e.getFullYear(), e.getMonth(), 1), e] } } -] +const dates = ref(normalizeReportDates()) const sources = ref(Object.keys(platforms)), gameNames = ref([]), gameOptions = ref([]), playerID = ref('') const gamesLoading = ref(false), exporting = ref(false), loading = ref(false), available = ref(true) const error = ref('') @@ -82,6 +74,8 @@ const currentSort = computed(() => sortDescription(query, { id: 'ID', payTime: ' const sortChanged = event => { Object.assign(query, remoteSort(event, defaultSortField)); return search() } let listVersion = 0, gameVersion = 0 const buildQuery = () => { + dates.value = normalizeReportDates(dates.value) + if (dates.value[0] > dates.value[1]) { ElMessage.warning('开始日期不能晚于结束日期'); return null } const player = playerID.value.trim() if (player && !/^\d+$/.test(player)) { ElMessage.warning('玩家ID应为整数'); return null } const result = { ...query, sourceDb: sources.value.join(','), gameName: gameNames.value.join(','), userId: player || undefined, busierAccount: query.busierAccount.trim(), promoteAccount: query.promoteAccount.trim(), gamePlayerName: query.gamePlayerName.trim(), payOrderNumber: query.payOrderNumber.trim() } diff --git a/web/src/view/sw/components/ReportAnalytics.vue b/web/src/view/sw/components/ReportAnalytics.vue index de8dfc1..d7f5229 100644 --- a/web/src/view/sw/components/ReportAnalytics.vue +++ b/web/src/view/sw/components/ReportAnalytics.vue @@ -47,18 +47,12 @@ import MetricValue from './MetricValue.vue' import ChannelProfileDrawer from './ChannelProfileDrawer.vue' import { metricTips, remoteSort, sortDescription } from '../table-display' import { analysisGameSelection } from '../analysis-game-selection' +import { normalizeReportDates, reportDateShortcuts as shortcuts } from '../report-dates' const fetchGameOptions = createGameOptionsLoader(listLedgerGames) const props = defineProps({ kind: { type: String, required: true } }) const isChannel = computed(() => props.kind === 'channel') const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' } -const day = () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()) } -const ymd = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}` -const dates = ref(isChannel.value ? ['', ''] : [ymd(day()), ymd(day())]) -const shortcuts = [ - { text: '今日', value: () => [day(), day()] }, { text: '昨日', value: () => { const d = day(); d.setDate(d.getDate() - 1); return [d, new Date(d)] } }, - { text: '本周', value: () => { const e = day(), b = day(); b.setDate(b.getDate() - (b.getDay() || 7) + 1); return [b, e] } }, - { text: '本月', value: () => { const e = day(); return [new Date(e.getFullYear(), e.getMonth(), 1), e] } } -] +const dates = ref(normalizeReportDates()) const startShortcuts = shortcuts.map(item => ({ text: item.text, value: () => item.value()[0] })) const endShortcuts = shortcuts.map(item => ({ text: item.text, value: () => item.value()[1] })) const sources = ref(Object.keys(platforms)), games = ref([]), gameOptions = ref([]), gameLoading = ref(false) @@ -80,7 +74,8 @@ const profileOpen = ref(false), profileChannel = ref(null) const openChannel = row => { profileChannel.value = { id: row.promoteId, sourceDb: row.sourceDb, account: row.promoteName }; profileOpen.value = true } const clear = () => { rows.value = []; total.value = 0; summary.value = null; result.value = null; loadedQuery.value = null } const load = async () => { - if (dates.value[0] && dates.value[1] && dates.value[0] > dates.value[1]) { ElMessage.warning('统计开始时间不能晚于结束时间'); return } + dates.value = normalizeReportDates(dates.value) + if (dates.value[0] > dates.value[1]) { ElMessage.warning('统计开始时间不能晚于结束时间'); return } const current = ++version if (!sources.value.length) { clear(); loading.value = false; available.value = true; return } loading.value = true; error.value = '' diff --git a/web/src/view/sw/report-dates.js b/web/src/view/sw/report-dates.js new file mode 100644 index 0000000..b3c2b77 --- /dev/null +++ b/web/src/view/sw/report-dates.js @@ -0,0 +1,23 @@ +export const formatReportDate = date => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` + +const today = () => { + const now = new Date() + return new Date(now.getFullYear(), now.getMonth(), now.getDate()) +} + +// Match the API defaults and fill only missing bounds. Explicit historical +// periods remain selectable; clearing dates cannot trigger an unbounded query. +export function normalizeReportDates(dates, days = 30, now = new Date()) { + const end = dates?.[1] || formatReportDate(now) + const start = new Date(`${end}T00:00:00`) + start.setDate(start.getDate() - days + 1) + return [dates?.[0] || formatReportDate(start), end] +} + +export const reportDateShortcuts = [ + { text: '近30天', value: () => { const end = today(), start = today(); start.setDate(start.getDate() - 29); return [start, end] } }, + { text: '今日', value: () => [today(), today()] }, + { text: '昨日', value: () => { const day = today(); day.setDate(day.getDate() - 1); return [day, new Date(day)] } }, + { text: '本周', value: () => { const end = today(), start = today(); start.setDate(start.getDate() - (start.getDay() || 7) + 1); return [start, end] } }, + { text: '本月', value: () => { const end = today(); return [new Date(end.getFullYear(), end.getMonth(), 1), end] } } +] diff --git a/web/src/view/sw/report-dates.test.js b/web/src/view/sw/report-dates.test.js new file mode 100644 index 0000000..c8f685b --- /dev/null +++ b/web/src/view/sw/report-dates.test.js @@ -0,0 +1,32 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { normalizeReportDates, reportDateShortcuts, formatReportDate } from './report-dates.js' + +test('report defaults include today and span 30 calendar days across month, year and leap-day boundaries', () => { + for (const [today, start] of [['2026-11-05', '2026-10-07'], ['2026-01-05', '2025-12-07'], ['2024-03-01', '2024-02-01']]) { + const now = new Date(`${today}T13:45:00`) + for (const empty of [undefined, null, [], ['', ''], [null, null]]) { + assert.deepEqual(normalizeReportDates(empty, 30, now), [start, today]) + } + } +}) + +test('partial date inputs preserve the selected bound, and explicit history is never clamped', () => { + const now = new Date(2026, 10, 5, 13) + assert.deepEqual(normalizeReportDates(['', '2024-03-01'], 30, now), ['2024-02-01', '2024-03-01']) + assert.deepEqual(normalizeReportDates(['2026-01-01', ''], 30, now), ['2026-01-01', '2026-11-05']) + const history = ['2023-01-01', '2023-12-31'] + assert.deepEqual(normalizeReportDates(history, 30, now), history) + assert.deepEqual(history, ['2023-01-01', '2023-12-31']) +}) + +test('rankings keep their seven-day default and reversed ranges remain available for validation', () => { + const now = new Date(2026, 10, 5) + assert.deepEqual(normalizeReportDates(undefined, 7, now), ['2026-10-30', '2026-11-05']) + assert.deepEqual(normalizeReportDates(['2026-11-05', '2026-11-01'], 30, now), ['2026-11-05', '2026-11-01']) +}) + +test('the recent-30-days shortcut restores the default query interval', () => { + const [start, end] = reportDateShortcuts.find(item => item.text === '近30天').value() + assert.deepEqual([formatReportDate(start), formatReportDate(end)], normalizeReportDates(undefined, 30, end)) +})