diff --git a/apps/backend/src/services/session/session-proof-constants.js b/apps/backend/src/services/session/session-proof-constants.js new file mode 100644 index 00000000..ea2e2785 --- /dev/null +++ b/apps/backend/src/services/session/session-proof-constants.js @@ -0,0 +1,9 @@ +export const BAIDU_BEIJING_TIME_URL = 'https://www.baidu.com/s?ie=utf-8&f=3&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E5%8C%97%E4%BA%AC%E6%97%B6%E9%97%B4&fenlei=256&rsv_pq=0xa7d529b30000a6eb&rsv_t=2d404COdLspTa8jnKQl%2FyCU%2BSCmRyb%2BvSqxxjk%2B7E1scokLYTuDbehaQ0Dvq&rqlang=en&rsv_enter=1&rsv_dl=ih_0&rsv_sug3=1&rsv_sug1=1&rsv_sug7=001&rsv_sug2=1&rsv_btype=i&rsp=0&rsv_sug9=es_2_1&rsv_sug=1' +export const BEIJING_TIME_PROOF_VIEWPORT = { width: 1440, height: 860 } +export const BEIJING_TIME_PROOF_CLIP = { + x: 0, + y: 0, + width: BEIJING_TIME_PROOF_VIEWPORT.width, + height: 620, +} +export const DEFAULT_REDEEM_PROOF_MODE = 'full' diff --git a/apps/backend/src/services/session/session-proof-mode.js b/apps/backend/src/services/session/session-proof-mode.js new file mode 100644 index 00000000..ffc12500 --- /dev/null +++ b/apps/backend/src/services/session/session-proof-mode.js @@ -0,0 +1,30 @@ +// @ts-nocheck + +import { runtimeConfig } from '../../config/runtime.js' + +import { DEFAULT_REDEEM_PROOF_MODE } from './session-proof-constants.js' + +export function resolveRedeemProofMode(rawMode = runtimeConfig.redeem.proofMode) { + const normalized = String(rawMode || '') + .trim() + .toLowerCase() + + if (normalized === 'basic' || normalized === 'off') { + return normalized + } + + return DEFAULT_REDEEM_PROOF_MODE +} + +export function shouldCaptureBeijingTimeProof(finalResult) { + if (finalResult?.classification?.success === true) { + return true + } + + if (finalResult?.classification?.success === false) { + return false + } + + const retCode = Number(finalResult?.redeem?.iRet ?? finalResult?.redeem?.ret) + return Number.isFinite(retCode) && retCode === 0 +} diff --git a/apps/backend/src/services/session/session-proof-paths.js b/apps/backend/src/services/session/session-proof-paths.js new file mode 100644 index 00000000..83876666 --- /dev/null +++ b/apps/backend/src/services/session/session-proof-paths.js @@ -0,0 +1,11 @@ +import path from 'node:path' + +export function buildArtifactPaths(outputDir) { + return { + redeemPageScreenshotPath: path.join(outputDir, 'redeem-page.png'), + beijingTimeScreenshotPath: path.join(outputDir, 'beijing-time.png'), + screenshotPath: path.join(outputDir, 'redeem-result.png'), + htmlPath: path.join(outputDir, 'page.html'), + resultPath: path.join(outputDir, 'result.json'), + } +} diff --git a/apps/backend/src/services/session/session-proof-result-writer.js b/apps/backend/src/services/session/session-proof-result-writer.js new file mode 100644 index 00000000..e259334a --- /dev/null +++ b/apps/backend/src/services/session/session-proof-result-writer.js @@ -0,0 +1,47 @@ +// @ts-nocheck + +import fs from 'node:fs/promises' + +import { BAIDU_BEIJING_TIME_URL } from './session-proof-constants.js' + +export async function writeRedeemResultFile( + resultPath, + { + proofMode, + sessionId, + activityUrl, + code, + finalResult, + attempts, + screenshotPath, + htmlPath = '', + redeemPageScreenshotPath = '', + beijingTimeScreenshotPath = '', + beijingTimePageUrl = BAIDU_BEIJING_TIME_URL, + beijingTimeError = '', + }, +) { + await fs.writeFile( + resultPath, + JSON.stringify( + { + proofMode, + sessionId, + activityUrl, + code, + area: finalResult?.role?.area || '', + final: finalResult, + attempts, + redeemPageScreenshotPath, + beijingTimeScreenshotPath, + beijingTimePageUrl, + beijingTimeError, + screenshotPath, + htmlPath, + }, + null, + 2, + ), + 'utf8', + ) +} diff --git a/apps/backend/src/services/session/session-proof.js b/apps/backend/src/services/session/session-proof.js index ee75f679..fdd68dad 100644 --- a/apps/backend/src/services/session/session-proof.js +++ b/apps/backend/src/services/session/session-proof.js @@ -1,19 +1,16 @@ // @ts-nocheck import fs from 'node:fs/promises' -import path from 'node:path' -import { runtimeConfig } from '../../config/runtime.js' - -const BAIDU_BEIJING_TIME_URL = 'https://www.baidu.com/s?ie=utf-8&f=3&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E5%8C%97%E4%BA%AC%E6%97%B6%E9%97%B4&fenlei=256&rsv_pq=0xa7d529b30000a6eb&rsv_t=2d404COdLspTa8jnKQl%2FyCU%2BSCmRyb%2BvSqxxjk%2B7E1scokLYTuDbehaQ0Dvq&rqlang=en&rsv_enter=1&rsv_dl=ih_0&rsv_sug3=1&rsv_sug1=1&rsv_sug7=001&rsv_sug2=1&rsv_btype=i&rsp=0&rsv_sug9=es_2_1&rsv_sug=1' -const BEIJING_TIME_PROOF_VIEWPORT = { width: 1440, height: 860 } -const BEIJING_TIME_PROOF_CLIP = { - x: 0, - y: 0, - width: BEIJING_TIME_PROOF_VIEWPORT.width, - height: 620, -} -const DEFAULT_REDEEM_PROOF_MODE = 'full' +import { + BAIDU_BEIJING_TIME_URL, + BEIJING_TIME_PROOF_CLIP, + BEIJING_TIME_PROOF_VIEWPORT, + DEFAULT_REDEEM_PROOF_MODE, +} from './session-proof-constants.js' +import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof-mode.js' +import { buildArtifactPaths } from './session-proof-paths.js' +import { writeRedeemResultFile } from './session-proof-result-writer.js' export async function saveRedeemArtifacts({ browserContext, @@ -145,82 +142,7 @@ export async function writeRedeemArtifactFiles({ } } -export function resolveRedeemProofMode(rawMode = runtimeConfig.redeem.proofMode) { - const normalized = String(rawMode || '') - .trim() - .toLowerCase() - - if (normalized === 'basic' || normalized === 'off') { - return normalized - } - - return DEFAULT_REDEEM_PROOF_MODE -} - -export function shouldCaptureBeijingTimeProof(finalResult) { - if (finalResult?.classification?.success === true) { - return true - } - - if (finalResult?.classification?.success === false) { - return false - } - - const retCode = Number(finalResult?.redeem?.iRet ?? finalResult?.redeem?.ret) - return Number.isFinite(retCode) && retCode === 0 -} - -function buildArtifactPaths(outputDir) { - return { - redeemPageScreenshotPath: path.join(outputDir, 'redeem-page.png'), - beijingTimeScreenshotPath: path.join(outputDir, 'beijing-time.png'), - screenshotPath: path.join(outputDir, 'redeem-result.png'), - htmlPath: path.join(outputDir, 'page.html'), - resultPath: path.join(outputDir, 'result.json'), - } -} - -async function writeRedeemResultFile( - resultPath, - { - proofMode, - sessionId, - activityUrl, - code, - finalResult, - attempts, - screenshotPath, - htmlPath = '', - redeemPageScreenshotPath = '', - beijingTimeScreenshotPath = '', - beijingTimePageUrl = BAIDU_BEIJING_TIME_URL, - beijingTimeError = '', - }, -) { - await fs.writeFile( - resultPath, - JSON.stringify( - { - proofMode, - sessionId, - activityUrl, - code, - area: finalResult?.role?.area || '', - final: finalResult, - attempts, - redeemPageScreenshotPath, - beijingTimeScreenshotPath, - beijingTimePageUrl, - beijingTimeError, - screenshotPath, - htmlPath, - }, - null, - 2, - ), - 'utf8', - ) -} +export { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } async function showResultDialog(page, message) { await page.evaluate((text) => { diff --git a/apps/backend/src/services/session/session-proof.test.js b/apps/backend/src/services/session/session-proof.test.js index 517f4e9d..7e3430aa 100644 --- a/apps/backend/src/services/session/session-proof.test.js +++ b/apps/backend/src/services/session/session-proof.test.js @@ -1,7 +1,17 @@ import test from 'node:test' import assert from 'node:assert/strict' +import path from 'node:path' -import { shouldCaptureBeijingTimeProof } from './session-proof.js' +import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof.js' +import { buildArtifactPaths } from './session-proof-paths.js' + +test('resolveRedeemProofMode normalizes configured modes and falls back to full', () => { + assert.equal(resolveRedeemProofMode('basic'), 'basic') + assert.equal(resolveRedeemProofMode(' OFF '), 'off') + assert.equal(resolveRedeemProofMode('full'), 'full') + assert.equal(resolveRedeemProofMode('unexpected'), 'full') + assert.equal(resolveRedeemProofMode(''), 'full') +}) test('shouldCaptureBeijingTimeProof only returns true for successful redeem results', () => { assert.equal( @@ -42,3 +52,13 @@ test('shouldCaptureBeijingTimeProof only returns true for successful redeem resu assert.equal(shouldCaptureBeijingTimeProof(null), false) }) + +test('buildArtifactPaths keeps redeem proof artifact filenames stable', () => { + const artifactPaths = buildArtifactPaths('/tmp/redeem-proof') + + assert.equal(artifactPaths.redeemPageScreenshotPath, path.join('/tmp/redeem-proof', 'redeem-page.png')) + assert.equal(artifactPaths.beijingTimeScreenshotPath, path.join('/tmp/redeem-proof', 'beijing-time.png')) + assert.equal(artifactPaths.screenshotPath, path.join('/tmp/redeem-proof', 'redeem-result.png')) + assert.equal(artifactPaths.htmlPath, path.join('/tmp/redeem-proof', 'page.html')) + assert.equal(artifactPaths.resultPath, path.join('/tmp/redeem-proof', 'result.json')) +}) diff --git a/前后端现状分析.md b/前后端现状分析.md new file mode 100644 index 00000000..08c7fb87 --- /dev/null +++ b/前后端现状分析.md @@ -0,0 +1,339 @@ +# 前后端现状分析 + +## 当前结论 + +整体架构方向没有跑偏,后端主干仍然是 `routes -> services -> repositories`,前端也已经逐步从“巨石页面”往“薄页面 + composable + 子组件”推进。 + +现在最需要继续优化的,不是全项目平均整理,而是两类高风险区域: + +1. 已经明显长成巨石、后续改动容易连锁的后端运行时模块 +2. 已经完成第一轮拆分、但还需要收口和补测试的前端后台配置模块 + +## 已完成 + +### 平台配置页前端拆分 + +这部分已经不再是最初判断里的“一个页面承载四套完整平台流”。 + +- `8e1a4be` 拆分前端后台管理 API 客户端 + - `apps/frontend/src/services/admin.ts` 已改成聚合入口 + - 后台 API 已按领域拆到 `services/admin/` 目录 + +- `dff7da7` 拆分平台配置页脚本逻辑 + - `AdminPlatformShopsView.vue` 的平台状态和行为已经拆到多个 composable + - 已有: + - `useAdminAgisoPlatform.ts` + - `useAdminKhhaoPlatform.ts` + - `useAdminKuaishouEticketPlatform.ts` + - `useAdminCloudtentaclesPlatform.ts` + +- `4650c3e` 抽离平台配置页共享样式 + - 共享样式已统一收口到 `apps/frontend/src/styles/admin-platform-shops.css` + +- `60c2d91` 拆分平台配置页 Agiso 与 khhao 区块 +- `254a031` 拆分平台配置页快手核销区块 +- `8f6a72f` 拆分平台配置页履约调试区块 + - 四个平台区块已经分别下沉为独立组件: + - `AdminPlatformAgisoSection.vue` + - `AdminPlatformKhhaoSection.vue` + - `AdminPlatformKuaishouEticketSection.vue` + - `AdminPlatformCloudtentaclesSection.vue` + +- `58f25fa` 抽离平台配置结果信息卡片 + - 新增 `AdminResultCard.vue` + - `khhao` / `快手核销` / `cloudtentacles` 的重复结果卡片已统一 + +### 现阶段对平台配置页的重新判断 + +`apps/frontend/src/views/admin/AdminPlatformShopsView.vue` 仍然不算小,但问题性质已经变了: + +- 现在主要问题不再是“大模板混杂” +- 现在的主要问题是: + - 父页面 `script setup` 仍然偏胖 + - 几个子组件之间还有少量可复用片段 + - 前端缺少测试防线 + +也就是说,这条线的第一阶段“拆分大页面”已经基本完成,后面不应该继续无限抽象,而应该适时收口。 + +## 已过时的判断 + +下面这些结论需要更新,不再按最初版本理解: + +### `AdminPlatformShopsView.vue` 是前端第一优先级 + +这个判断已经部分过时。 + +原因: + +- 该页面的四套平台模板已经完成拆分 +- 平台行为已经移动到 composable +- 剩余问题主要是“收口”和“测试”,不是继续大拆 + +新判断: + +- 这条线仍可继续小步优化 +- 但优先级已经低于后端的 `session.js`、`session-proof.js`、`admin-platform-config-service.js` + +### 继续深挖前端平台配置页 + +这个方向不应该再作为主战场。 + +剩余可做项有,但应该控制强度: + +- 可以继续抽 `section-title-row` 或 `meta-card` 这一层公共片段 +- 但不能为了统一而统一 +- 再往下更应该转向后端高风险链路 + +## 仍然优先拆分 / 优先优化 + +### 1. `apps/backend/src/services/session/session.js` + +后端第一优先级,判断不变。 + +它同时承担: + +- Playwright 浏览器生命周期 +- 会话内存状态 +- 二维码抓取 +- 登录态判断 +- 角色信息同步 +- 兑换执行 +- 截图产物 +- 自动关闭 + +而且仍然是 `@ts-nocheck`。这类文件每继续增长一次,后面改动成本都会上升。 + +建议目标: + +- `browser-runtime` +- `session-store` +- `session-presentation` +- `session-redeem-orchestrator` +- `session-persistence` + +### 2. `apps/backend/src/services/session/session-proof.js` + +这是下一步最合适的切入点。 + +当前职责混在一起: + +- 证明模式判定 +- 产物路径拼接 +- 结果页 DOM 注入 +- 北京时间来源抓取 +- 截图拼接 +- 结果 JSON 落盘 + +这类文件比 `session.js` 更小,适合作为后端拆分的第一刀,风险更低,能先把拆分节奏跑顺。 + +建议拆分方向: + +- `proof-mode` + - 处理 `resolveRedeemProofMode` + - 处理 `shouldCaptureBeijingTimeProof` + +- `proof-paths` + - 处理 `buildArtifactPaths` + +- `proof-result-writer` + - 处理 `writeRedeemResultFile` + +- `proof-beijing-time` + - 处理北京时间页面抓取与截图 + +- `proof-renderer` + - 处理结果弹层、合成图、截图表现逻辑 + +先保留 `session-proof.js` 作为门面导出层,第一阶段不要改公开接口。 + +### 3. `apps/backend/src/services/admin/admin-platform-config-service.js` + +这条线依然偏胖,而且和前端平台配置页是一整条竖线。 + +虽然前端页面已经拆开,但后端这条 service 仍然同时承载: + +- 平台配置读写 +- 登录测试 +- 订单查询 +- SKU 调试 +- 虚拟号调试 +- 履约绑定 + +建议后续按平台拆为: + +- `agiso-admin-service` +- `khhao-admin-service` +- `kuaishou-eticket-admin-service` +- `cloudtentacles-admin-service` + +同时把“配置读写”和“调试接口”分层处理。 + +### 4. `apps/backend/src/services/claim/claim-session-service.js` + +仍然是后续重点。 + +问题主要在: + +- 管理态 / 用户态创建逻辑平行复制 +- claim token、浏览器会话、任务同步、库存释放、自动发货收尾混在一起 + +建议方向: + +- `claim-context` +- `claim-session-lifecycle` +- `claim-task-sync` +- `claim-fulfillment-finalizer` + +### 5. `apps/backend/src/services/admin/admin-write-service.js` + +依然偏胖,但优先级略低于上面三项。 + +它更像“后台万能动作总线”,跨域过多: + +- 库存 +- claim +- webhook +- cloudtentacles +- 快手核销 +- 腾讯 session + +建议后续至少拆成: + +- `admin-inventory-write` +- `admin-task-actions` +- `admin-webhook-actions` +- `admin-manual-redeem-actions` + +### 6. `apps/backend/src/services/webhook/webhook-service.js` + +仍然适合改成流水线。 + +建议拆成: + +- `webhook-parse` +- `webhook-verify` +- `webhook-ignore-policy` +- `webhook-enrich` +- `webhook-process` +- `webhook-replay` + +### 7. `apps/backend/src/config/runtime.js` + +仍需要“配置声明化”优化。 + +目前问题不是功能错,而是长期可维护性差: + +- env override 越堆越长 +- 新平台配置接入成本越来越高 +- 难测、难查、难对照 + +建议后期改成: + +- 配置 schema +- env 映射表 +- 默认值与来源声明 + +## 质量防线现状 + +这个判断仍然成立,而且重要性上升了。 + +当前风险: + +- 后端高风险模块仍然存在 `@ts-nocheck` +- 前端后台配置页最近已经连续多次重构 +- 目前主要依靠 `typecheck + build` 防回归 + +这能挡住: + +- 类型问题 +- 组件引用问题 +- 构建问题 + +但挡不住: + +- 交互退化 +- 条件渲染错误 +- 任务状态流转异常 +- 外部平台调试链路断裂 + +建议至少补两层最小防线: + +1. 前端 + - 平台配置页基础渲染 smoke 测试 + - 关键按钮存在性 / 条件渲染测试 + +2. 后端 + - `session-proof.js` 纯函数单测 + - 快手 cloud / claim / task 状态流转的集成 smoke 测试 + +## 暂时不用急着拆 + +### 腾讯浏览器前端链路 + +- `TencentBrowserView.vue` +- `session-qq.js` +- 对应 `wx` 变体 + +这些区域当前模式反而比较健康,后续可以作为其它模块拆分时的参照。 + +## 下一步计划 + +### 已完成阶段 + +1. 拆分前端后台管理 API 客户端 +2. 拆分平台配置页脚本逻辑 +3. 抽离平台配置页共享样式 +4. 拆分 Agiso / khhao / 快手核销 / cloudtentacles 四个平台区块 +5. 抽离结果信息卡片 + +### 下一阶段 + +#### Phase 1:从 `session-proof.js` 开始 + +目标: + +- 先拆一个比 `session.js` 更小、更收敛的后端高风险文件 +- 建立后端拆分模板 +- 不改公开接口,不改产物格式,不改调用方 + +建议步骤: + +1. 抽纯函数 + - proof mode + - proof path + - result writer + +2. 抽副作用模块 + - 北京时间截图采集 + - 证明图合成 + +3. 保留门面文件 + - `session-proof.js` 暂时继续导出原接口 + +4. 每个小阶段都跑校验并单独提交 + +#### Phase 2:再处理 `session.js` + +前提: + +- `session-proof.js` 拆分完成 +- 对这条运行时链路已经熟悉 + +#### Phase 3:回到平台配置后端竖线 + +重点转向: + +- `admin-platform-config-service.js` +- 相关 route +- 相关调试接口 + +## 当前建议 + +前端平台配置页这条线可以暂时收口,不建议再继续大规模抽模板。 + +下一步最合适的是: + +1. 开始拆 `apps/backend/src/services/session/session-proof.js` +2. 同时补一层最小测试防线 + +如果继续执行,下一刀就从 `session-proof.js` 开始。