2026-10-20-manual-ops-import.md 9.1 KB

店铺运营数据上传 · 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 让用户上传商家后台/人工统计的 店铺运营数据.xlsx,原件入库、解析成结构化行,并在新增「店铺运营」页按指标矩阵/卡片查看。

Architecture: 上传走 POST /api/ops/upload(base64)→ ops-import.js 识别并清洗 → ops-store.js 统一接口 → 本地 SQLite(node:sqlite,data/ops.db)。云端 PG 驱动留同接口的壳,本期不装依赖。前端 public/ops.js 独立视图,复用现有 .view / .btn / 表格样式。

Tech Stack: Node 22.23(内置 zlib / node:sqlite)、原生前端 JS、零新增依赖。

Global Constraints

  • Node ≥ 22.5(node:sqlite 要求);低于此版本只有「店铺运营」不可用,其余功能照常
  • 本期不新增 npm 依赖;pg 只在 PG 驱动里懒加载,未安装时给出明确错误
  • 数据文件:data/ops.db(data/ 已在 .gitignore)
  • 所有新接口前缀 /api/ops/,沿用 originGuard / tokenGuard
  • 界面文案中文,样式复用现有 .page-header / .btn / .card / table
  • 每完成一个任务提交一次,提交信息用 feat: ... / test: ...

文件结构

文件 责任
server/services/xlsx.js ZIP+XML → { sheets: [{ name, rows }] },纯函数
ops-import.js 解析器注册表;manual-ops 识别/解析/清洗;输出行与警告
server/stores/ops-sqlite.js SQLite 实现:建表、事务、upsert、查询、原件读写
server/stores/ops-pg.js PG 实现壳(懒加载 pg),本期不启用
ops-store.js 读 config.storage、选驱动、暴露统一接口与降级状态
server/routes/ops.js 7 个端点 + 降级响应
public/ops.js 页面:工具条、矩阵、卡片、上传弹窗、导入记录
scripts/test-ops-import.js 解析器单测 + 端到端接口测试(临时库)
scripts/db-init.js / db-export.js / db-import.js 建表与数据搬迁
scripts/fixtures/ops-manual-sample.xlsx 脱敏样例(36 店 × 29 天)

Task 1: xlsx 读取器

Files: Create server/services/xlsx.js; Test scripts/test-ops-import.js(先建最小用例)

Interfaces:

  • Produces: readXlsx(buffer) → { sheets: [{ name, rows: (string|number|null)[][] }] },rows[0] 为第一行;失败抛 Error 且 err.code = 'XLSX_PARSE_FAILED'

  • [ ] 写失败用例:readXlsx(Buffer.from('not a zip')) 应抛 XLSX_PARSE_FAILED

  • [ ] 跑测试确认失败

  • [ ] 实现:定位 ZIP EOCD(0x06054b50)→ 中央目录 → 取 xl/workbook.xml、第一个 sheet、xl/sharedStrings.xml → zlib.inflateRawSync(stored 直接切片)→ 正则取 <row r="N">、<c r="A1" t="s|inlineStr|str|b"><v>、<is><t> → 共享字符串按索引取 → 按列字母换算列号补空 → 返回二维数组

  • [ ] 用真实文件 c = readXlsx(fs.readFileSync('<Desktop>/店铺运营数据.xlsx')) 断言 rows.length === 43、rows[0].length === 122、rows[1][3] === '千里香馄饨(康诺餐厅店)'

  • [ ] 提交 feat: xlsx 读取器(零依赖)

Task 2: 人工统计表解析器

Files: Create ops-import.js; Create scripts/fixtures/ops-manual-sample.xlsx(用 python openpyxl 脱敏生成:店名 样例店 NN、学校保留 3 个、数值按原形态含 10..6%/12.3/未开放/空);Test 扩展 scripts/test-ops-import.js

Interfaces:

  • Produces: detect(cells) → 'manual-ops' | null;importManualOps(cells, { year }) → { rows, shops, warnings, dates }
  • rows[i] = { date:'YYYYMMDD', shopKey, shopId, shopName, school, status, orderCnt, revenue, exposure, clickRate, raw:{...}, quality:[...] }
  • warnings[i] = { row, column, raw, reason }

  • [ ] 写用例:表头识别(9.9订单量 等)→ 'manual-ops';9.9+2026 → 20260909;10..6% → 0.106 + malformed-percent;12.3 → 0.123 + percent-scaled;未开放 → null + non-numeric;空 → null + missing;占位行(无店名)不产生行;无 ID 店 shopKey = school|shopName + no-shop-id;总行数 = 店数 × 29

  • [ ] 跑测试确认失败

  • [ ] 实现 detect(表头含「订单量/营收/曝光量」≥2 且含「转换率」)、parseHeader(按名定位,容忍空格)、parseDate(月.日 + year,跨年 +1)、clean(六步清洗)、buildRows(跳过空店名/全空行)

  • [ ] 跑测试确认通过

  • [ ] 提交 feat: 人工统计表解析器

Task 3: 存储层(SQLite + 统一接口)

Files: Create server/stores/ops-sqlite.js、ops-store.js、server/stores/ops-pg.js;Modify server/context.js(storage 默认值)

Interfaces:

  • createStore(config, { rootDir }) → { kind, init(), health(), withTransaction(fn), insertUpload(tx, upload), upsertRows(tx, rows), upsertShops(tx, shops), findUploadBySha(sha), listUploads(), getUpload(id), getContent(id), updateReport(id, report), queryRows({from,to,schools,shops,metrics}), summary() }
  • upload = { id, originalName, sha256, sizeBytes, uploadedAt, year, parserKind, parserVersion, report, content:Buffer }
  • init() 幂等;health() → { ok, kind, detail }

  • [ ] 写用例(临时库):init() 两次不报错;事务内写 upload+rows 后 queryRows 可读;ON CONFLICT 覆盖同 date|shop_key;事务抛错后无残留

  • [ ] 实现 ops-sqlite.js:new DatabaseSync(file),PRAGMA journal_mode=WAL、busy_timeout、foreign_keys=ON;DDL 见设计文档 §5.3;withTransaction 用 BEGIN/COMMIT/ROLLBACK

  • [ ] ops-store.js:读 config.storage(默认 sqlite),懒加载驱动,缓存 init 的 promise;kind/health 暴露给路由;失败不抛到进程

  • [ ] ops-pg.js:同接口,require('pg') 失败时 health() = { ok:false, detail:'pg-not-installed' }

  • [ ] 跑测试通过;提交 feat: ops 存储层(SQLite 驱动 + PG 壳)

Task 4: HTTP 端点

Files: Create server/routes/ops.js;Modify server/routes/index.js

Interfaces:

  • POST /api/ops/upload {fileName, contentBase64, year, force} → §7.1
  • GET /api/ops/summary、/api/ops/rows、/api/ops/uploads、/api/ops/uploads/:id/file、POST /api/ops/uploads/:id/reparse、GET /api/ops/export

  • [ ] 写用例:空态 summary 200;上传 200 且 content sha256 一致;重复 409;force 覆盖;查询过滤;下载字节一致;非法 id 400;reparse 行数不变

  • [ ] 实现:readBody 取 base64 → Buffer.from(b64,'base64');校验大小/类型/sha256;opsImport → store.withTransaction;sendJson 统一错误码(400/409/413/503/500)

  • [ ] 降级:store.health().ok === false 时 ops 接口 503,summary 返回 {enabled:false, reason, detail}

  • [ ] 注册到 server/routes/index.js(ops 放在 reports 之后、static 之前)

  • [ ] 跑测试通过;提交 feat: /api/ops 端点

Task 5: 前端页面

Files: Create public/ops.js;Modify public/index.html(菜单 + viewOps)、public/style.css(矩阵/卡片少量样式)、public/app.js(views 映射 + shanOpsInit 钩子)

Interfaces:

  • window.shanOpsInit() 首次激活时拉 summary,渲染工具条与空态
  • 指标键固定 order_cnt|revenue|exposure|click_rate,默认全选

  • [ ] 菜单:data-view="ops",放在「数据采集」后

  • [ ] 页面骨架:page-header + 工具条(上传、学校、日期、指标、视图切换、导出)+ 内容区 + 上传弹窗 + 记录抽屉

  • [ ] 矩阵渲染:按学校分组、指标分区块、首列 sticky、quality 角标 ⚠(title 带原文)

  • [ ] 卡片渲染:区间合计 + SVG 迷你折线

  • [ ] 上传:<input type="file" accept=".xlsx"> → FileReader base64 → POST → 报告;409 提示;503 提示

  • [ ] 导出:拼查询串打开 /api/ops/export?...

  • [ ] 手测:上传真实文件,切指标/学校/日期,两种视图,导出 CSV

  • [ ] 提交 feat: 店铺运营页面

Task 6: 工具脚本、smoke、文档

Files: Create scripts/db-init.js、scripts/db-export.js、scripts/db-import.js;Modify scripts/smoke.js、package.json(test:ops、db:*)、README.md

  • db-init.js:createStore(config).init() 后打印驱动与表
  • db-export.js:SQLite → JSON(uploads 元数据 + rows + shops,不含 content 或含 base64 可选)
  • db-import.js:JSON → 目标驱动
  • smoke 增加三个只读断言(summary/uploads/rows 均 200)
  • README 新增章节:功能说明、Node 版本要求、数据文件位置、备份、切 PG 步骤
  • npm run test:ops 全绿;npm run smoke 全绿
  • 提交 feat: 店铺运营数据导入(脚本/冒烟/文档)

验收

  • npm run test:ops:解析器 + 端到端用例全过(临时库,不依赖网络)
  • npm run smoke:原有断言 + 新增 3 条全过
  • 手工:上传 店铺运营数据.xlsx → 36 店 × 29 天 → 切换指标/学校 → 卡片与矩阵 → 导出 CSV → 重传 409 → 重启服务数据仍在