Commit 19280667 authored by wenwen.tang's avatar wenwen.tang 😕

docs(memory): 记录提交分 3 层、草稿落盘、14 张残留清理与部署验证

parent 9b014b66
...@@ -72,4 +72,169 @@ test_round5 54 · test_round6 39 · test_masters 19 = **303 项全绿**。 ...@@ -72,4 +72,169 @@ test_round5 54 · test_round6 39 · test_masters 19 = **303 项全绿**。
- 静态资源含列显示修复(`colpick-menu[hidden]` ×1、「收起列显示」×3) - 静态资源含列显示修复(`colpick-menu[hidden]` ×1、「收起列显示」×3)
## 待办 ## 待办
- 9 月电费批次错计(多计 10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记精锐)仍待处置 - 9 月电费批次错计(多计 10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记精锐)仍待处置
\ No newline at end of file
## 第十五轮:核对中可直接改数据(「保存修改」)
巴特菲:「核对时也要可以编辑,一并操作了。不用回归测试,先直接改」
### 设计要点
核对是**账期级**(一个账期 N 张批次),而 `reingest` 是**单批次 PUT 且一律重置核对**。
解法 = **按 `_bid` 分组逐张保存** + 后端新增 `keep_verify=True`:
只让**改动过的行**退回待核对,没改的行照旧算已核,不用整张账期重新核。
后端 `rentos/service.py`:
- `_FP_FIELDS = (meter, prev, cur, price, factor, company, owner)`、`_NUM_FP` 数值归一
- `_detail_fp(d)` 行内容指纹(字符串 "100" 与 100 视为相同)
- `_PLACEHOLDER = ("(空置)",)` + `_same_detail(old_fp, new)`:**任一侧为空/占位符的字段一律跳过**
—— 否则前端只提交 unit/类目/抄表数时,服务端补全的 company/price 会被误判成改动,
**把整张账期的核对无辜清空**(这个坑真踩到过,第一次冒烟就红了)
- `_kept_verify(old_batch, new_details)`:行键 → **指纹多重集**(不能只存一条!)
逐行消费匹配,`all_matched` 才保留 `done`
- ⚠️ **`meter` 必须在指纹里,且要逐行判定不能只看行键集合**:
行键是「单元|类目」,把「302室|空调」改成「照明」会与原有照明行**撞同一个行键**,
按集合判会把「两行变一行」误当成没变 → 确认状态错误地继续有效
- `reingest(..., keep_verify=False)`,不传时行为完全不变(老调用方零影响)
前端 `web/app.js`:
- `renderElecRows()` 去掉 verify 的 `disabled`;核对模式也能加行(`#e-vf-add`)、删行(带确认)
- 新增 `#e-vf-save`「保存修改(N)」,`saveVerifyEdits()` 按 `_bid` 分组逐张 PUT
- 脏数据基线:`ELEC_CMP_FIELDS` + `ESNAP`/`takeVerifySnapshot()`/`verifyDirty()`/`markVerifyDirty()`
- ⚠️ **基线键必须用稳定的 `_rid`**,不能用数组下标 —— 删中间行会让后面所有行的基线错位
- ⚠️ **`markVerifyDirty()` 只改文字、绝不重渲染模式条**(重渲染会让输入框失焦);
按键事件里也不能调 `renderEModeBar()`,改用 300ms 节流 + 只更新 `#e-vf-dirty` / `#e-vf-save`
- 顺手修:`tr.children[4]/[10]` 改为 `querySelector('[data-col="qty"]/[data-col="amt"]')`
(项目规矩:表格按 data-col 定位,不按第 N 列)
### 验证
- 新增单测 `TestKeepVerify` 9 项 + 沙箱冒烟 `tools/smoke_verify_edit.py`(11 断言)
- **单测 312 项全绿**(303 + 9)、冒烟全通过、真实 `data/batches` 7→7 零污染
- 前端 `node --check` 通过
### 踩坑记录(三次失败的教训)
1. `_detail_fp` 三元里字段名硬编码成 `d.get("prev")` → 指纹全返回 prev 的值,
**改了 cur 也检测不到**。写映射表时务必 `d.get(f)`
2. 提交字段不全 → 勾选被全清(见上,`_same_detail` 宽容规则)
3. 类目合并撞行键 → 确认错误地保留(见上,逐行判定)
---
## 核对口径再改 + 录入临时保存(13:09 起 · 当天第二个需求)
巴特菲原话:
> 「核对时直接修改数据,不影响核对状态,点击确认核对直接执行数据保存,去掉保存修改按钮。
> 录入本期电费时增加临时保存按钮,将数据落盘存储。下次可直接编辑。」
### 一、核对口径反转:从「改过就退回」改成「改数据不影响核对状态」
上午那版是「点保存修改 → 只让改动过的行退回待核对」。现在**去掉「保存修改」按钮**,
改成:表里随便改,**改数据不动核对状态**;点「确认核对」时**先存数据、再写核对**。
前端 `confirmVerify()` 两步走(这也顺手修掉一个隐藏 bug):
1. `saveVerifyData()` —— 按 `_bid` 分组逐张 `PUT /api/batches/{id}`。
**有改动才发请求**:`editSummary().any === false` 时一个包都不发,不白白重出单据。
2. `POST /api/periods/verify {period, rows, done:true}` —— 把整期勾选写成「已核对」。
于是「改过的行要不要重新核」由**用户勾没勾**决定,服务端不猜。
**删掉的东西**(旧口径整套):
- `service._kept_verify` / `_detail_fp` / `_same_detail` / `_FP_FIELDS` / `_NUM_FP`
- `reingest(..., keep_verify=)` 参数 + `server.py` 透传 + 文档行
- `web/app.js` 的 `saveVerifyEdits()` 与 `#e-vf-save` 按钮
- `test_round6.TestKeepVerify`(9 项)
> 留着会与新口径**直接矛盾**,不如删干净。`reingest` 恢复为「核对一律重置」。
⚠️ **顺手发现一个真 bug**:`saveVerifyEdits()` 里调了 `await enterVerify(EPERIOD)`,
但 **`enterVerify` 这个函数从来不存在**(只有 `enterPeriodVerify`)——一调用就 ReferenceError。
因为 UI 回归只测了 `confirmVerify` 这条路径,这个函数从没被真正跑到过。删掉它正好。
⚠️ **PUT 是全量覆盖**:所以「按批次筛要不要发」只能在**发不发**这一层做,
每张要发的批次必须带上**它的全部行**(不能只发改过的那几行,否则其余行会被覆盖没了)。
⚠️ **退出核对必须警告**:数据只在「确认核对」时落盘,改完直接「退出核对」改动就没了。
`cancelElecMode()` 现在检测到 `editSummary().any` 会明确说「改过的还没保存,退出就丢了」。
前端辅助函数:`dirtyRows()`(返回行对象)/ `editSummary()`(`{n, structural, any}`)/
`dirtyTip(ed)` / `doneLabel(ed)`;`verifyDirty()` 保留为兼容旧名。
`markVerifyDirty()` 改成刷新 `#e-vf-done` 文案 + `#e-vf-dirty` 提示(依旧只改文字不重渲染)。
`structural` 用 `ELEC.length !== ESNAP.size` 判增删行(基线一行一条,键是 `_rid`)。
### 二、新增:录入「临时保存」草稿(落盘,下次直接编辑)
- `config.DRAFT_DIR = data/drafts/`(已加进 `ensure_dirs`)
- `store.draft_path/save_draft/load_draft/delete_draft`;账号名净化 `[^0-9A-Za-z_\-] → _`
- 接口:`GET /api/drafts/elec`(读,无则 `draft: null`)、`PUT`(存)、`DELETE`(清)
- `_write_cap`:PUT/DELETE 记 `CAP_ELEC` → **访客(guest)也能临时保存**
- 草稿按**账号**隔离:`_draft_key(user) = "elec__" + username`
- 前端:`saveElecDraft()` / `clearElecDraft()` / `restoreElecDraft()` / `elecBootFill()`
- `applyNav` 里电费页由 `autoFillFromPrev()` 改为 `elecBootFill()`:
**有草稿恢复草稿(并 `PREV_DONE=true`),没草稿才自动带入上一期** —— 否则两者互相覆盖
- 恢复后 `#uploadResult` 显示「已恢复 X 临时保存的录入:N 行」+「丢弃这份草稿」按钮
- 生成通知书成功后 `clearElecDraft()`;「清空」按钮也一并清草稿
- `#btn-e-draft` 只在 `EMODE === 'new'` 显示(`#eDraftWrap` 在重编模式隐藏)
### ⚠️ 最大的坑:沙箱白名单漏了 `DRAFT_DIR`
`ui_verify_runner` 与自带沙箱的 `ui_verify7/8/9/10/11`、`check_scheduler` 都是
**枚举式改写 config 路径常量**。新增 `DRAFT_DIR` 时若不同步补进这些列表,
草稿就会写到**真实** `data/drafts/` —— 和上次 17 个测试批次泄漏到真实 data/ 是**同一类**坑。
已给 7 个脚本全部补上(各 +1 行,`git diff --stat` 已核对无行尾变动)。
### 验证(按规矩:没跑 UI 回归,等巴特菲发话)
- **单测 308 项全绿**:test_rules 45 / test_rent 53 / test_directory 33 / test_accounts 60 /
test_round5 54 / **test_round6 44**(删 9 项 keep_verify + 加 5 项 `TestElecDraft`)/ test_masters 19
- 沙箱冒烟 `tools/smoke_verify_edit.py` **重写**为 16 断言,全过;真实 `data/` 17→17 零污染
- A 段:PUT 重编后核对清空 → 紧接账期确认 → **改动落盘(303 本期=150)+ 仍 3/3 ok**
- A5:只勾 2 行 → `ok=True` `partial=True` 2/3(部分核对可确认)
- B 段:草稿存/读/按账号隔离(guest 读不到 admin 的、guest 清不影响 admin)/删后为 null
- `node --check web/app.js`、4 个 .py `ast.parse` 均通过
- **本机预览已重启**(旧进程 PID 65840 已 kill 再用新代码起,safe 模式无定时器):
`GET /api/drafts/elec` → `{"ok":true,"draft":null}`;PUT/DELETE 实测通
(⚠️ **改完后端必须重启**:静态资源是每次请求读盘所以前端立即生效,
但 Python 进程不重启就没有新接口,临时保存会 404)
### 第四轮:账期口径答疑 + 再次清理测试残留(14 张)
**巴特菲问「电费账期里的批次是根据什么来的」**,答(口径已记进 MEMORY.md):
批次 = 一次「生成通知书」;账期 `period_label` 是批次上的文本字段,不是实体;
`list_periods()` 按它归组 = 页面「账期汇总」。来源三处:录入页「所属期间」(默认今天年月 /
带入上期时 +1 月)、Excel 标题或抄表日期、后端兜底用通知日期年月。
⚠️ 自由文本无归一化,「2026-11」与「2026年11月」会分成两个账期。
**顺带查出真实 `data/batches/` 又被写了 14 张测试残留**(账期 `2026年11月`、302/303室仅照明、
公司「科博(上海)仓储有限公司」、成对出现在 10:20/12:49/12:52/12:53/12:54/12:55/13:15/13:17):
- **硬证据**:批次 `documents[].abs_path` = `D:\AI\rentos\output\elec\{id}\...`(真实仓库路径)。
若走 runner 沙箱,`OUTPUT_DIR` 会被改写成 `tmp/output/elec` → 证明写入方是**未改写 config 的真实服务**
- **受控实验**:`RENTOS_VERIFY_PORT=8931 runner tools/smoke_verify_edit.py` → 真实 data **17→17 零增量**,
冒烟 16 项全过(沙箱批次 `20261014-133456` 已自动删除)→ **runner 隔离本身有效**
- 结论:泄漏发生在**某几轮没走 runner / 早期版冒烟脚本**上,机制现已收口(1~6 号脚本硬闸门 + runner 覆盖
`BATCH_DIR`/`OUTPUT_DIR`/`DRAFT_DIR`)
- **处置**:14 张批次 JSON 移入 `data/batches/_trash/20261014/`,对应 `output/elec/{id}/` 同步移入
`output/elec/_trash/20261014/`,附 README。清理后顶层只剩 3 张真实批次:
`20261009-115501`(电费 2026年11月)、`20261009-175756`(房租 2026年10月)、`20261013-134828`(电费 2026年9月补单)
- 台账未被污染(`git status data/tables/rent_parking.json` 为空)
### 第五轮:提交 + 部署上线(13:44 起)
**提交分 3 层**(`7709e42..9b014b6`,推送 origin/main 成功):
1. `9f0dd9e` feat(elec) 后端:DRAFT_DIR + store 草稿读写 + `/api/drafts/elec` + 删 keep_verify 那套 + `.gitignore` 加 `data/drafts/`
2. `fff9b2a` feat(web) 前端:核对态可编辑 + `confirmVerify` 两步存盘 + 稳定 `_rid` 基线 + 临时保存按钮
3. `9b014b6` test:沙箱补 `DRAFT_DIR`、test_round6 换草稿用例、新增 `tools/smoke_verify_edit.py`
**提交前处置**:本机测试草稿 `data/drafts/elec__admin.json`(302/303室 4 行、账期 2026年11月)
移入 `data/_trash/drafts-20261014/`,避免下次打开录入页自动灌入测试数据;`data/drafts/` 已进 `.gitignore`
(运行时状态,同 `data/batches/`)。台账文件未出现在改动里(未被污染)。
**单测**:7 个脚本全 OK(rules/round6/directory/rent/round5/accounts/masters)。未跑 UI 回归(巴特菲规矩)。
**部署**:`RENTOS_SSH_PASSWORD=... venv-python tools/deploy_git.py sync` rc=0;
容器 Up、容器内/宿主机 HTTP 200、台账 85×31、名册 33 公司 + 4 出租方、
wecom-cli 1.3.4 authorized(Bot 唐文文)、scheduler enabled。
线上 6b 自检:字体 `/app/assets/fonts/simhei.ttf`、AJ 有值 64 行。
**线上接口验证**:`/api/drafts/elec`、`/api/periods`、`/api/batches?kind=elec` 均返回 **401**(未登录),
证明路由已挂上(404 才是没生效);容器内 `DRAFT_DIR=/app/data/drafts` 与新代码一致。
无列模型变更 → **无需跑数据迁移**。
### 待办
- 9 月电费批次错计(10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记精锐)仍未处置
- `ui_verify11` 里「保存修改」按钮的断言需同步改写(本轮删了该按钮,用户未点头跑回归)
...@@ -14,7 +14,17 @@ ...@@ -14,7 +14,17 @@
- **AD~AI 房产/房产税六列已删**(`RETIRED_TAX_COLS`,守护 `test_retired_cols_not_used_by_billing`) - **AD~AI 房产/房产税六列已删**(`RETIRED_TAX_COLS`,守护 `test_retired_cols_not_used_by_billing`)
- 默认锁定只读;改单元格/增行/删行必须 `confirm=true`(否则 428);备份 `data/tables/.backup/` - 默认锁定只读;改单元格/增行/删行必须 `confirm=true`(否则 428);备份 `data/tables/.backup/`
- **电费批次** `data/batches/{id}.json`:只留原始数据(details/totals/documents/发送状态),按账期月份留档; - **电费批次** `data/batches/{id}.json`:只留原始数据(details/totals/documents/发送状态),按账期月份留档;
测试残留移入 `data/batches/_trash/`(store 只 listdir 顶层 `*.json`,子目录隐身、可恢复) 测试残留移入 `data/batches/_trash/YYYYMMDD/`(store 只 listdir 顶层 `*.json`,子目录隐身、可恢复)
- 已有回收站:`_trash/20261013/`(17 张)、`_trash/20261014/`(14 张,output 同批移入 `output/elec/_trash/`)
- **账期口径**(2026-10-14 确认):**批次 = 点一次「生成通知书」的产物**(号 `YYYYMMDD-HHMMSS` ·
`make_batch_id` 纯 ASCII 时间戳);**账期 `period_label` 只是批次上的一个文本字段,不是独立实体**,
`list_periods()` 按它归组即为页面上的「账期汇总」(组内按 `created_at` 倒序)
- 来源:① 录入页「所属期间」(默认今天年月;点「带入上一期」时 `nextPeriod()` +1 月)—— 主要
② Excel 解析 `elec_parser._extract_period`(先正则抓 A1 标题「YYYY年M月」,再退抄表日期年月)
③ 后端兜底 `ingest_rows`:未传时用**通知日期**的年月
- ⚠️ **自由文本、无格式归一化**:匹配是 `strip()` 后精确比较 → 「2026-11」与「2026年11月」会分成两组;
空值归「(未标账期)」
- 同账期多张批次是正常的(分批录 / 按公司分批 / 补录),故台账按账期汇总并默认折叠
## 免租(`rentos/rentfree.py`,2026-10-13 重写为两种类型) ## 免租(`rentos/rentfree.py`,2026-10-13 重写为两种类型)
- `首N月免租`:窗口 `[I, I+L月)`,含起始不含结束;0.5 月 = 15 天 - `首N月免租`:窗口 `[I, I+L月)`,含起始不含结束;0.5 月 = 15 天
...@@ -55,6 +65,29 @@ ...@@ -55,6 +65,29 @@
- ⚠️ **会 `innerHTML` 重渲染的绑定函数里不要挂 document 级监听**:每渲染一次堆一个, - ⚠️ **会 `innerHTML` 重渲染的绑定函数里不要挂 document 级监听**:每渲染一次堆一个,
旧闭包持有已移除的节点 → 用一次性模块标志(`VCOLS_DOC_BOUND`)或事件委托 旧闭包持有已移除的节点 → 用一次性模块标志(`VCOLS_DOC_BOUND`)或事件委托
## 电费「核对中改数据」(2026-10-14 · 当日晚改口径)
核对表**可直接编辑**,**改数据不影响核对状态**;点「确认核对」才把改动**一并存盘**。
前端 `confirmVerify()` 两步:①`saveVerifyData()` 按 `_bid` 分组逐张 `PUT /api/batches/{id}`
(有改动才发请求)→ ②`POST /api/periods/verify {rows, done:true}` 把勾选整期写成「已核对」。
于是「改过的行要不要重新核」由**用户勾了没勾**决定,服务端不猜。
- **已删** `keep_verify` / `_kept_verify` / `_detail_fp` / `_same_detail`(旧「改过的行退回待核对」
口径被上面两步取代,留着会与新口径矛盾);`reingest` 恢复为**一律重置核对**
- 前端脏数据基线:`ELEC_CMP_FIELDS` + `ESNAP`/`takeVerifySnapshot()`/`dirtyRows()`/`editSummary()`
- ⚠️ 基线键必须用稳定 `_rid`,**不能用数组下标**(删中间行会让基线整体错位)
- ⚠️ 改动提示**只改文字、绝不重渲染模式条**(`markVerifyDirty` 300ms 节流),否则输入框失焦
- ⚠️ `PUT` 是**全量覆盖**:每组必须带上该批次的**全部行**,只是「哪些批次要发」按有无改动筛
- ⚠️ 退出核对若有未保存改动要**明确警告会丢失**(数据只在确认时落盘)
## 电费录入「临时保存」草稿(2026-10-14)
录入页「临时保存」把当前明细整份落盘 `data/drafts/elec__{账号}.json`(**按账号隔离**),
下次进电费页自动恢复、直接接着编辑;生成通知书 / 清空 / 手动丢弃时删除草稿。
- 接口 `GET|PUT|DELETE /api/drafts/elec`;`_write_cap` 里 PUT/DELETE 记 `CAP_ELEC`(访客可用)
- `store.draft_path/save_draft/load_draft/delete_draft`;账号名净化 `[^0-9A-Za-z_\-] → _`
- 前端 `saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`;`applyNav` 里电费页改调
`elecBootFill()` —— **有草稿就不「带入上期」**(否则互相覆盖)
- ⚠️ **沙箱白名单必须加 `DRAFT_DIR`**:`ui_verify_runner` 与自带沙箱的 `7/8/9/10/11`、
`check_scheduler` 都逐一补过 —— 漏了就会把草稿写进**真实** `data/drafts/`(与批次泄漏同类坑)
## 房租车位费出单 ## 房租车位费出单
链路:`rent_parser.py` → `rent_render.py`(→ `notice_pdf`)→ `service.ingest_rent` → 批次 链路:`rent_parser.py` → `rent_render.py`(→ `notice_pdf`)→ `service.ingest_rent` → 批次
- 计费:期间起 = **AC**,止 = 起 + N 个自然月 − 1 天;`房屋租赁费 = P × N`、`物业费 = O × N`; - 计费:期间起 = **AC**,止 = 起 + N 个自然月 − 1 天;`房屋租赁费 = P × N`、`物业费 = O × N`;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment