fix: align report date defaults with recent periods
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("商务范围或排序别名错误")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
// 旧排行榜空选平台表示全部,保留此语义;其他报表页面有各自的空选规则。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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] } }
|
||||
]
|
||||
@@ -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))
|
||||
})
|
||||
Reference in New Issue
Block a user