feat: 团体奖牌 / 个人积分两个榜单的专属明细导出
用户答复「允许细化」——给另外两个榜单各做一份逐项来源导出(此前它们的「明细」共用 单位积分明细、个人榜连明细列都没有)。 - `GET /api/export/medals.xlsx` → `团体奖牌明细.xlsx`:总表 单位/大项/小项/组别/名次/奖牌/运动员-队 + 每个有奖牌的单位一张分表(末尾小计 金X/银Y/铜Z);无奖牌的单位不出分表。 去重与牌色判定**完全交给 domain**(每组调 domain.BuildMedalStandings),导出侧只做 「同一报名项归一组」,不复制任何计奖规则——双计通道只记一枚奖牌因此自动成立。 - `GET /api/export/athletes.xlsx` → `个人积分总明细.xlsx`:总表 单位/运动员/大项/小项/组别/ 名次/成绩/得分/来源 + 每个有得分的单位一张分表(按单位分,不按人分)。 得分取**记到个人的原始分**(与个人积分榜同一口径,不看 count_to_team、不乘折算系数)。 - 管理端三个榜单的卡片头各一个导出入口(导出总明细 / 导出奖牌明细 / 导出个人明细), 展开单位时明细卡头是「导出该单位明细」;导出页仍是 5 项,两个新导出只在积分榜页触发。 护栏(跨端点一致性,都有实测断言): - 奖牌明细逐单位与「团体奖牌榜」对齐,双计项目只出一行; - 个人明细合计与「个人积分榜」一致,且把折算系数改成 0.5 后个人明细一分不变、单位总分变 5。 测试:服务端 12 包全绿(358 个测试函数);前端 vue-tsc 通过、190 例全过;Console 18 文件。 docs/ 与 devdocs/ 同步(FAQ 到 Q72,含上一轮"个人积分榜不能导出"的修正)。
This commit is contained in:
1 parent
782a38f82f
commit
8e707e1e9b
15 files changed
+1546
-45
No files matched your search
@@ -144,3 +144,79 @@ test('连不上服务端时降级为占位文件,网络错误留在 mockReason
|
||||
assert.equal(mockReason.value, '无法连接服务端')
|
||||
assert.equal(lastRequest.url, '/api/export/teams.xlsx')
|
||||
})
|
||||
|
||||
test('奖牌明细导出走 GET /api/export/medals.xlsx,文件名是「团体奖牌明细.xlsx」', async () => {
|
||||
respondWith(() => xlsxResponse())
|
||||
|
||||
const result = await resultsExportApi.download({ kind: 'medalAll' })
|
||||
|
||||
assert.equal(lastRequest.url, '/api/export/medals.xlsx')
|
||||
assert.equal(lastRequest.method, 'GET')
|
||||
assert.equal(result.data.filename, '团体奖牌明细.xlsx')
|
||||
assert.equal(result.mock, false, '正常响应不应被当成示例数据')
|
||||
})
|
||||
|
||||
test('个人明细导出走 GET /api/export/athletes.xlsx,文件名是「个人积分总明细.xlsx」', async () => {
|
||||
respondWith(() => xlsxResponse())
|
||||
|
||||
const result = await resultsExportApi.download({ kind: 'athleteAll' })
|
||||
|
||||
assert.equal(lastRequest.url, '/api/export/athletes.xlsx')
|
||||
assert.equal(lastRequest.method, 'GET')
|
||||
assert.equal(result.data.filename, '个人积分总明细.xlsx')
|
||||
})
|
||||
|
||||
// 整榜导出不需要 teamId;一旦误落进 team 分支就会变成 /export/teams/undefined。
|
||||
test('medalAll / athleteAll 不被 team 分支截胡,也不要求 teamId', async () => {
|
||||
respondWith(() => xlsxResponse())
|
||||
await resultsExportApi.download({ kind: 'medalAll' })
|
||||
assert.equal(lastRequest.url, '/api/export/medals.xlsx')
|
||||
|
||||
respondWith(() => xlsxResponse())
|
||||
await resultsExportApi.download({ kind: 'athleteAll' })
|
||||
assert.equal(lastRequest.url, '/api/export/athletes.xlsx')
|
||||
})
|
||||
|
||||
test('奖牌明细失败时服务端原文原样抛出,不吞成笼统的「导出失败」', async () => {
|
||||
respondWith(() =>
|
||||
jsonResponse({ ok: false, error: { code: 'INTERNAL', message: '奖牌明细生成失败:event.db 已锁定' } }, 500),
|
||||
)
|
||||
|
||||
await assert.rejects(
|
||||
() => resultsExportApi.download({ kind: 'medalAll' }),
|
||||
(error: unknown) => {
|
||||
assert.ok(error instanceof ApiError)
|
||||
assert.equal(error.status, 500)
|
||||
assert.equal(error.message, '奖牌明细生成失败:event.db 已锁定')
|
||||
return true
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('个人明细失败时 403 原文原样抛出', async () => {
|
||||
respondWith(() =>
|
||||
jsonResponse({ ok: false, error: { code: 'FORBIDDEN', message: '需要裁判长权限' } }, 403),
|
||||
)
|
||||
|
||||
await assert.rejects(
|
||||
() => resultsExportApi.download({ kind: 'athleteAll' }),
|
||||
(error: unknown) => {
|
||||
assert.ok(error instanceof ApiError)
|
||||
assert.equal(error.status, 403)
|
||||
assert.equal(error.message, '需要裁判长权限')
|
||||
return true
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// 占位文件名也要跟着 kind 走,否则连不上时会存成一个叫「单位积分总明细(示例数据).txt」的奖牌文件。
|
||||
test('连不上时新 kind 的占位文件名也正确', async () => {
|
||||
respondWith(() => {
|
||||
throw new TypeError('fetch failed')
|
||||
})
|
||||
|
||||
const result = await resultsExportApi.download({ kind: 'medalAll' })
|
||||
|
||||
assert.equal(result.mock, true)
|
||||
assert.equal(result.data.filename, '团体奖牌明细(示例数据).txt')
|
||||
})
|
||||
Reference in new issue
Block a user