Commit 066f0d99 authored by wenwen.tang's avatar wenwen.tang 😕

docs(memory): 用户交互流程图交付;MEMORY.md 二次精简(16 章节口径未丢)

parent 2dfe5556
......@@ -154,6 +154,16 @@
- `output/合同-台账核对报告.md`(可入库)
- `output/合同-台账核对明细.csv`(35 行 × 17 列,output/*.csv 被忽略)
## 用户交互流程图(16:10)
- 需求:生成用户交互流程图,展示业务流程
- 产出 `output/rentos-用户交互流程图.html`:**自包含无外链**,含 4 张 SVG ——
① 全局交互地图(登录 → 概览 → 三条业务线)② 电费链路(录入→生成→逐行核对→确认存盘→发送→流水)
③ 房租车位费链路(台账/合同 → 账期预览 → 出单 → 收款发送)④ 自动排期(AJ − 提前天数 → 自动出单发送 → AC/AJ 推进)
外加角色权限对照表 + 贯穿性交互约定表;带 `@media print` 可直接打印
- 图按**真实** `PAGES` 5 项 + `admin.html` 按钮清单绘制(不凭印象);会话内另用内联图逐张展示
- ⚠️ 配色用固定浅色底 + 深字(不依赖主题变量),保证明暗主题下都清晰
- 顺带:MEMORY.md 二次精简(18.9KB → 17.7KB / 11143 字符),16 个章节口径未丢
## 待办
- 上面 17 个单元的修正要不要动手(涉及台账写操作,需 `confirm=true`)
- 4 张本机残留批次是否移入 `_trash/`
......
# 租金通知系统 · 项目长期记忆
# RentOS 租金通知系统 · 长期记忆
## 目标 & 命名
三份 Excel → 固定模板 + 数据填充 → **PDF 收费通知书**,自动发企微 + Web 维护。
- 文件名 = `{单元列表}-{费用类型}-收费通知书.pdf`;单元按 `B.unit_sort_key` 排序、`、` 连接;**不含公司名**
- 费用类型词序固定:房租 → 物业费 → 车位费
三份 Excel → 固定模板填充 → **PDF 收费通知书**(reportlab 程序化绘制),发企微 + Web 维护。
文件名 `{单元列表}-{费用类型}-收费通知书.pdf`:单元按 `B.unit_sort_key` 排序、`、` 连接、**不含公司名**;
费用类型词序固定 房租 → 物业费 → 车位费。
## 数据模型
- **唯一台账** `rent_parking`(85 行 × **32 列**)
- `BILL_COLS`:owner=A unit=C tenant=F begin=I end=J rentfree=L **free_type=AK** cycle=N fee=O rent=P
last_paid=AB bill_start=AC next_bill=AJ;另 **AL「合同原件」**(type=`files`,排最右)
- **AK「免租类型」**(显示在 L 之后):`首N月免租` / `每年8-9月免租`,空=无免租
- **唯一台账** `rent_parking`(85 行 × **32 列**)。关键列:
owner=A unit=C tenant=F begin=I end=J rentfree=L **free_type=AK** cycle=N fee=O rent=P;
last_paid=AB bill_start=AC next_bill=AJ;**AL「合同原件」**(type=`files`,最右)
- AJ **只用于定时排期**(推送日 = AJ − 提前天数);出单口径看 AC;出单后 AC/AJ 一起推进
- **AD~AI 房产/房产税六列已删**(`RETIRED_TAX_COLS`,有守护测试);列定义**以代码为准**
(`load_table` 用 spec 覆盖存档列)→ **加列不需要数据迁移**
- 默认锁定只读;改单元格/增行/删行必须 `confirm=true`(否则 428);备份 `data/tables/.backup/`
- **批次** `data/batches/{id}.json`(= 点一次「生成通知书」的产物,id=`YYYYMMDD-HHMMSS`);
测试残留移入 `data/batches/_trash/YYYYMMDD/`(store 只 listdir 顶层 `*.json`,子目录隐身可恢复)
- **账期口径**:`period_label` 只是批次上的**文本字段**,`list_periods()` 按它归组 = 页面「账期汇总」
- 来源:① 录入页「所属期间」(默认今天年月;带入上期时 +1 月)② `elec_parser._extract_period`
③ 后端兜底用**通知日期**年月
- ⚠️ **自由文本、无归一化**:`strip()` 后精确比较 →「2026-11」与「2026年11月」会分成两组
- 同账期多张批次正常(分批录 / 按公司分批 / 补录)→ 台账按账期汇总并默认折叠
- ⚠️ 列定义**以代码为准**(`load_table` 用 spec 覆盖存档列)→ **加列不需要数据迁移**
- 默认只读;改格/增行/删行须 `confirm=true`(否则 428);备份 `data/tables/.backup/`
- **批次** `data/batches/{id}.json`(点一次「生成通知书」的产物,id=`YYYYMMDD-HHMMSS`);
测试残留移 `data/batches/_trash/YYYYMMDD/`(store 只 listdir 顶层 `*.json`,子目录隐身可恢复)
- **账期**:`period_label` 只是批次上的**文本字段**,`list_periods()` 按它归组 = 页面「账期汇总」。
来源 ① 录入页「所属期间」(默认今天年月,带入上期 +1 月)② `elec_parser._extract_period`
③ 后端兜底用通知日期年月。⚠️ **自由文本无归一化**:`strip()` 精确比较 →「2026-11」≠「2026年11月」
- 同账期多张批次正常(分批/按公司/补录)→ 台账按账期汇总并默认折叠
- 8F/11F 付款周期统一 **3 个月**、**不收物业费**(O 列留空)
## 免租(`rentos/rentfree.py`)
- `首N月免租`:窗口 `[I, I+L月)`,含起始不含结束;0.5 月 = 15 天
- `每年8-9月免租`:合约期 `[I, J]` 内每个自然年 `[max(该年08-01,I), min(该年10-01, J+1天))`(跨年裁剪)
- `首N月免租`:`[I, I+L月)`,含起始不含结束;0.5 月 = 15 天
- `每年8-9月免租`:合约期 `[I, J]` 内每年 `[max(该年08-01,I), min(该年10-01, J+1天))`(跨年裁剪)
- `hit()` 按时点;`overlaps()` 按账单期间交集(「每年8-9月」必需)
- ⚠️ `windows()` 返回键是 **`begin`/`end`**(非 from/to),另有 `type`/`months`/`year`/`key`
- ⚠️ `windows()` 返回键是 **`begin`/`end`**(非 from/to);AK「免租类型」空 = 无免租
## 名册(`data/config.json`,唯一入口 `rentos/directory.py`)
- `companies` 租客(名称即唯一键,`kind`=company|person);`owners` 出租方(**键=简称**)
- 当前 4 家:精锐 37 / 科博 24 / 瑞格嘉德 16 / 国隆 8
- `companies` 租客(名称即唯一键,`kind`=company|person);`owners` 出租方(键=简称)
—— 当前 4 家:精锐 37 / 科博 24 / 瑞格嘉德 16 / 国隆 8
- `None`/缺失 = 未维护过 → 回落 `DEFAULT_OWNERS`;**写过(哪怕 `{}`)= 权威**
- 改名**联动台账**(F/A 列);删除有引用 → 409,`force=true` 才真删
- 归属三级优先:**行内 `owner` > 台账 A 列 > 名单兜底(仅当名单恰好一家)**
- ⚠️ 写入必须 `store.set_config()`(顶层整体替换);`patch_config` 浅合并**删不掉字典键**
## 业务口径
- 电费:`用量 = 本期 − 上期`;`空调 = 用量 × 电价 × 12.25`、**`照明` = 用量 × 电价**
- 只填上期不填本期 → `pending` 不出单;**车位不抄电表**(`P.is_park`,候选/带入/入库三处都要挡)
- 8F/11F **付款周期统一 3 个月**、**不收物业费**(O 列留空)
- 两张账单(`plans.py`):`elec` 触发日 = 缴费期限 − 提前天数;`rent_parking` = **AJ − 提前天数**,
- 电费:`用量 = 本期 − 上期`;`空调 = 用量 × 电价 × 12.25`、`照明 = 用量 × 电价`;
只填上期不填本期 → `pending` 不出单;**车位不抄电表**(`P.is_park`,候选/带入/入库三处都要挡)
- `plans.py` 两张账单:`elec` 触发日 = 缴费期限 − 提前天数;`rent_parking` = **AJ − 提前天数**,
按行判定(`rent_state()`),到点自动出单+发送(`service.auto_issue_rent()`)
## 电费核对(行粒度)
- 行键 = `单元|类目`(如 `302室|照明`);`verify = {units:[], rows:[], done, at, by, scope}`
- `verify_summary()["ok"] = done`(**确认即可,允许部分核对**);`partial` = done 且有未勾行
- 行键 = `单元|类目`;`verify = {units, rows, done, at, by, scope}`
- `ok = done`(**确认即可,允许部分核对**);`partial` = done 且有未勾行
- **自动撤销**:确认时把勾选快照存 `scope`;之后勾选少于 scope → 撤销确认
- 发送闸门:未确认 → 409;部分核对发送前前端 `partialVerifyWarn()` 二次提醒
- 发送闸门:未确认 → 409;部分核对发送前 `partialVerifyWarn()` 二次提醒
- 账期级:一行算已核对 ⇔ **含它的每张批次都勾了它**;`POST /api/periods/verify` 一次写全账期
- 核对表**可直接编辑**,**改数据不影响核对状态**;`confirmVerify()` 两步:
① `saveVerifyData()` 按 `_bid` 分组逐张 `PUT /api/batches/{id}`(有改动才发)
② `POST /api/periods/verify {rows, done:true}` → 要不要重核**由用户勾选决定**,服务端不猜
- `confirmVerify()` 两步:① `saveVerifyData()` 按 `_bid` 分组逐张 `PUT /api/batches/{id}`(有改动才发)
② `POST /api/periods/verify {rows, done:true}` —— 要不要重核**由用户勾选决定**,服务端不猜
- 已删 `keep_verify` 那套;`reingest` = **一律重置核对**
- 脏基线:`ELEC_CMP_FIELDS` + `ESNAP`/`takeVerifySnapshot()`/`dirtyRows()`/`editSummary()`
- ⚠️ 基线键用稳定 `_rid`,**不能用数组下标**;⚠️ 改动提示只改文字、绝不重渲染模式条(否则输入框失焦)
- ⚠️ 脏基线(`ELEC_CMP_FIELDS`/`ESNAP`/`takeVerifySnapshot`/`dirtyRows`/`editSummary`)用稳定 `_rid`,
**不能用数组下标**;⚠️ 改动提示只改文字、**绝不重渲染模式条**(否则输入框失焦)
- ⚠️ `PUT` 是**全量覆盖**:每组必须带该批次**全部行**;退出核对有未保存改动要**明确警告会丢**
- 前端:核对模式按行勾选 + 列显示菜单(`localStorage['rentos_vcols']`);**重新编辑/核对入口只在批次详情**
- 列显示菜单:开关状态存模块级 `VCOLS_OPEN`;收起 = 再点按钮 / `×` / 点外部 / Esc;空间不足自动 `.up`
- ⚠️ **HTML `hidden` 不是 `!important`**:类里写了 `display:flex/grid` 就完全失效 → 必须补 `[hidden]{display:none}`
- 核对表**可直接编辑**,**改数据不影响核对状态**;**重新编辑/核对入口只在批次详情**
- ⚠️ **HTML `hidden` 不是 `!important`**:类里写了 `display:flex/grid` 就失效 → 必须补 `[hidden]{display:none}`
- ⚠️ **会 `innerHTML` 重渲染的绑定函数里别挂 document 级监听**(用一次性模块标志或事件委托)
- 列显示菜单:`localStorage['rentos_vcols']`,开关存模块级 `VCOLS_OPEN`;
收起 = 再点按钮 / `×` / 点外部 / Esc;空间不足自动 `.up`
## 电费录入「临时保存」草稿
- 「临时保存」把明细整份落盘 `data/drafts/elec__{账号}.json`(**按账号隔离**);下次进页**自动恢复**;
生成通知书 / 清空 / 手动丢弃时删除。
- 接口 `GET|PUT|DELETE /api/drafts/elec`(`_write_cap` 记 `CAP_ELEC`,访客可用)
- 前端 `saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`;`applyNav` 改调 `elecBootFill()`
- 明细整份落盘 `data/drafts/elec__{账号}.json`(**按账号隔离**);下次进页**自动恢复**;
生成通知书 / 清空 / 手动丢弃时删除;接口 `GET|PUT|DELETE /api/drafts/elec`(`CAP_ELEC`,访客可用)
- 前端 `saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill`;`applyNav` 调 `elecBootFill()`
—— **有草稿就不「带入上期」**
## 合同原件(台账 AL 列,2026-10-15)
一格可挂多份(租赁合同 + 补充协议 + 车位合同)。
- 单元格值 = 紧凑 JSON `[{"id","name"}]`;本体在 `data/contracts/{id}.{ext}` + `index.json`
- `rentos/contracts.py`:save/find/path_of/list_all/remove/parse/dump/describe;
白名单 pdf/doc/docx/xls/xlsx/png/jpg/gif/webp/bmp,单份 **≤ 40MB**;id=uuid4 hex,`clean_id` 挡路径穿越
## 合同原件(台账 AL 列)
一格可挂多份(租赁合同 + 补充协议 + 车位合同)。单元格值 = 紧凑 JSON `[{"id","name"}]`;
本体 `data/contracts/{id}.{ext}` + `index.json`。
- `rentos/contracts.py`:save/find/path_of/list_all/remove/parse/dump/describe;白名单
pdf/doc/docx/xls/xlsx/png/jpg/gif/webp/bmp,单份 **≤ 40MB**;id=uuid4 hex,`clean_id` 挡路径穿越
- ⚠️ 40MB 不是随手定的:扫描件能到 21.8MB,而 40MB base64 后 53MB 仍 < server `MAX_BODY=64MB`
- 接口:`POST|GET /api/contracts` · `GET /api/contracts/{id}`(内联预览;`?dl=1`;支持 `?token=`)·
`DELETE /api/contracts/{id}` —— **增删仅管理员**(访客可查阅、不能增删)
- 前端:`td.files` 按钮(`📎 N 份` / 编辑态 `+ 合同`)→ `openFilesDlg()`;
增删走 `stageCell(rowId,'AL','files',…)`,**与「保存 N 处改动」同一口径**
- 接口 `POST|GET /api/contracts` · `GET /api/contracts/{id}`(内联预览;`?dl=1`;支持 `?token=`)·
`DELETE` —— **增删仅管理员**(访客可查阅、不能增删)
- 前端 `td.files` 按钮(`📎 N 份` / 编辑态 `+ 合同`)→ `openFilesDlg()`;增删走
`stageCell(rowId,'AL','files',…)`(**与「保存 N 处改动」同一口径**)
- ⚠️ **从某行移除只解关联、不删文件**(多行可共享同一份)
- ⚠️ `renderGrid` 渲染 `files` 列**必须读 `TB_PEND` 暂存值**,否则上传后格子仍显示旧按钮
- 导出 xlsx 该列输出**文件名文本**(`tables.files_text`),不是 JSON
- 导入:`tools/import_contracts.py`(默认干跑,`--apply` 写入,`--replace` 覆盖);
解析文件名**开头**的单元号串(`1101、1115` / `124.155号` / `132&227车位` / `501-502室` / `601A&702`)
- ⚠️ **同一份文件只落盘一次**(按文件名缓存 meta),多单元挂同一个 id —— 否则 29 份膨胀成 35 份
- **线上已导入**:29 份 → **35 个单元**,落盘 **29 份 / 92.3MB**、无失效关联;
6 组共用:307/603 · 601A/702 · 132/227车位 · 124/155车位 · 1101/1115 · 501/502
- 导入 `tools/import_contracts.py`(默认干跑,`--apply` 写入,`--replace` 覆盖):解析文件名**开头**
的单元号串(`1101、1115` / `124.155号` / `132&227车位` / `501-502室` / `601A&702`);
⚠️ **同一份文件只落盘一次**(按文件名缓存 meta),多单元挂同一 id —— 否则 29 份膨胀成 35 份
- **线上已导入**:29 份 → **35 个单元**,落盘 **29 份 / 92.3MB**、无失效关联;6 组共用:
307/603 · 601A/702 · 132/227车位 · 124/155车位 · 1101/1115 · 501/502
- 线上跑法:文件放 `/opt/rentos/uploads/项目合约`(容器内 `/app/uploads/...`)→
`docker exec rentos python /app/tools/import_contracts.py --dir /app/uploads/项目合约 --apply`
- 源副本留服务器 `/opt/rentos/contracts-src/项目合约`(93MB),`uploads/` 已清空
## 合同 × 台账核对(2026-10-15 下半场)
- **`tools/sync_pull.py`** —— 数据**回拉**通道(`deploy_git.py` 只管推代码)。
`--dry` 列差异 → 备份 `data/_snap/pull-<ts>/` → SFTP 递归下载;默认跳 `attachments`、
**永不回拉 `sessions.json`**;⚠️ **只增/改,不删本地独有文件** → 本地残留需自己清。
- **`tools/check_contracts_vs_table.py`** —— 文件名级机械比对(复用 `import_contracts` 的单元解析)
- ⚠️ **合同文件名不可信**!已知 4 处与正文不符:119车位(租客/期限都写错)、
1102(漏「安装」)、307&603、601A&702(多主体)。**判断一律以正文为准**
- 读扫描件:`pypdfium2` 渲染成图人工判读。房屋合同的甲乙方在**第 2 页**,
「第三条 期限与交付」在**第 3~4 页**(因模板而异,别写死页号)
- 读 `.doc`:`raw.decode("utf-16-le","ignore")` + 中文串抽取即可读到正文
- **核对结论(29 份 → 35 单元,18 对 / 17 有出入)**:
- 影响出单:903室月租台账 36300/合同 16500;904室月租空/合同 19800;
501室期空、502室标「(空置)」(合同 501+502 一体租给明境 2025-06-16~2028-06-15、月租合计 35648);
126/128车位合同期整体早一年(应 2026-03-19~/2026-03-01~);154车位台账空置(合同 徐相天);
601B室租客名带**「(空置)」后缀**会被出单逻辑跳过;315室期错(合同 2026-11-01~2028-10-31,JR202609315);
305室物业费台账 1512 / 合同 1312
- 待确认:132/227车位合同承租方=**上海伯凌建筑设计**而台账=上海爱种草(且已按爱种草出单);
`docker exec rentos python /app/tools/import_contracts.py --dir /app/uploads/项目合约 --apply`;
源副本留 `/opt/rentos/contracts-src/项目合约`(93MB),`uploads/` 已清空
## 合同 × 台账核对
- **`tools/sync_pull.py`** = 数据**回拉**通道(`deploy_git.py` 只管推代码):`--dry` 列差异 →
备份 `data/_snap/pull-<ts>/` → SFTP 递归下载;默认跳 `attachments`、**永不回拉 `sessions.json`**;
⚠️ **只增/改,不删本地独有文件** → 本地残留需自己清
- **`tools/check_contracts_vs_table.py`** = 文件名级机械比对(复用 `import_contracts` 的单元解析)
- ⚠️ **合同文件名不可信**!已知 4 处与正文不符:119车位(租客/期限都写错)、1102(漏「安装」)、
307&603、601A&702(多主体)。**判断一律以正文为准**
- 读扫描件:`pypdfium2` 渲染成图人工判读;房屋合同甲乙方多在**第 2 页**、「第三条 期限与交付」在
**第 3~4 页**(因模板而异,别写死页号)。读 `.doc`:`raw.decode("utf-16-le","ignore")` + 中文串抽取
- **结论(29 份 → 35 单元,18 对 / 17 有出入)**:明细见 `output/合同-台账核对报告.md`(入库)。
影响出单 9 条:903室月租 36300/合同 16500;904室月租空/合同 19800;501室期空、502室标「(空置)」
(合同 501+502 一体租给明境 2025-06-16~2028-06-15、合计 35648);126/128车位合同期整体早一年
(应 2026-03-19~/2026-03-01~);154车位台账空置(合同 徐相天);601B室租客名带**「(空置)」后缀**
会被出单逻辑跳过;315室期错(应 2026-11-01~2028-10-31);305室物业费 1512/合同 1312
- 待确认:132/227车位合同承租方=**上海伯凌建筑设计**而台账=爱种草(已按爱种草出单);
119/124/155/191/227 车位合同甲方均为**精锐**,台账出租方是 瑞格嘉德/科博;
903/904 面积都填 271.17(均值,应 247.24/295.10);306/310「租赁期(月)」12/19 应为 24
- **当前不出单的 9 个单元**(未出现在任何房租批次):401 · 501 · 502 · 903 · 904 · 126车位 ·
128车位 · 154车位 · 601B室,月租合计约 **¥98,262/月**
- 产出:`output/合同-台账核对报告.md`(入库)+ `output/合同-台账核对明细.csv`(output/*.csv 被忽略)
- **当前不出单的 9 个单元**:401 · 501 · 502 · 903 · 904 · 126车位 · 128车位 · 154车位 · 601B室,
月租合计约 **¥98,262/月**
## 房租车位费出单
链路:`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`;
单元号含「车位」→ 车位租赁费;N 缺值回落 `DEFAULT_MONTHS = 3`
- **不可出单三条件**:空置(F 空或含「空置」)/ 每月租金 ≤ 0 / AC 空(各带 `reasons`)
- **按免租数据跳过**:`rent_preview` 对每行与每份单据标 `rentfree`
- 房间费与车位费**合并一张**(键 `(出租方, 租客, 期间)`);批次按 `kind` 隔离
- **不可出单三条件**:空置(F 空或含「空置」)/ 每月租金 ≤ 0 / AC 空
- `rent_preview` 对每行与每份单据标 `rentfree` 供跳过;房间费与车位费**合并一张**
(键 `(出租方, 租客, 期间)`);批次按 `kind` 隔离
- 推进/回滚走 `tables.update_cells`;`rollback_rent_advance` **只回滚当前值仍等于推进后的行**
- **账期区间筛选**:`classify_period(from, to)` 命中 = 账期起始日落在区间内(旧 `?on=` 是 `to` 别名)
- 接口:`GET /api/batches?kind=` · `GET /api/rent/preview?from=&to=&all=1` · `POST /api/rent/batches` ·
- 接口 `GET /api/batches?kind=` · `GET /api/rent/preview?from=&to=&all=1` · `POST /api/rent/batches` ·
`DELETE /api/batches/{id}` `{rollback:true}` · `POST /api/batches/{id}/receipt`(**只房租批次**)·
`GET /api/batches/{id}/export`
- ⚠️ `GET /api/batches/{id}` 的 `receipt` 在**响应顶层**
`GET /api/batches/{id}/export` ⚠️ `GET /api/batches/{id}` 的 `receipt` 在**响应顶层**
## 台账编辑(批量暂存)
- 改格**不逐格弹确认**:`stageCell()` 暂存 + 黄底高亮(`TB_PEND`),顶条显示「N 处改动待保存」
- 「保存全部」→ 一次 `confirmBox` → 单次 `PUT /api/tables/{key}/cells`
(`{changes:[{row_id,col,value}], confirm:true, who}`)→ `pushHist({t:'batch'})` 可**整批撤销**
- `navTo()` / `beforeunload` 拦截未保存改动
- ⚠️ `renderGrid()` 末尾必须同时调 `paintPending()` **和** `renderPendBar()`
- 确认框是 `.modal-mask.show`(**不是 `.open`**)
- ⚠️ `renderGrid()` 末尾必须同时调 `paintPending()` **和** `renderPendBar()`;
确认框是 `.modal-mask.show`(**不是 `.open`**)
## 系统骨架 / 渲染
- 启动 `python -m rentos.server --port 8899`;`--no-scheduler` 只开 Web;admin / 1989@rent05;
访客 **guest / guest@2026**(`role=viewer` + `perms=['elec']`)
## 骨架 / 权限 / 渲染
- 启动 `python -m rentos.server --port 8899`(`--no-scheduler` 只开 Web);
admin / 1989@rent05;访客 **guest / guest@2026**(`role=viewer` + `perms=['elec']`)
- **权限模型**:`role`(admin|viewer)+ `perms` 能力清单(目前只有 `elec`)
- 管理员隐含 `{'*'}`;`viewer` 按 `perms` **逐接口**放行,`perms=[]` 即纯只读
- `auth.caps()/can()`;`server._write_cap(method,path)` 解析所需能力,`_guard_write()` 判定
- `auth.caps()/can()`;`server._write_cap(method,path)` + `_guard_write()` 判定
- **批次类接口按 `kind` 复核**:房租车位费的删/重出/发送/收款**仍只有管理员**
- 前端:`body.cap-elec` 才显示 `[data-cap="elec"]`;`data-ro` = 仅管理员;**主账号不可降级**
- 导航 5 项:概览 / 电费通知单 / 房租车位费 / 公司管理 / 设置;hash 路由在 `app.js` 的
`PAGES` + `applyNav()`;**新增页面必须同时登记 PAGES 与 `#nav`**
- **唯一渲染层** `rentos/notice_pdf.py`:reportlab + platypus 程序化绘制(**不用 win32com / LibreOffice**);
- **唯一渲染层** `rentos/notice_pdf.py`:reportlab + platypus(**不用 win32com / LibreOffice**);
字体 `simhei.ttf` 子集嵌入;A4、`MARGIN = 1.9cm`
## 环境
- Python `…/python/versions/3.13.12/python.exe`(openpyxl / reportlab / pillow;**无 pypdfium2**)
- venv `…/envs/default/Scripts/python.exe`:**Playwright + reportlab + pypdfium2 + PyPDF2** →
**单测 / UI 回归 / 合同判读都用它**
- venv `…/envs/default/Scripts/python.exe`:**Playwright + reportlab + pypdfium2 + PyPDF2**
→ **单测 / UI 回归 / 合同判读都用它**(3.13.12 无 pypdfium2、无 paramiko)
- Bash 先 `export PATH=…/PortableGit/versions/1.2.0/usr/bin:…/bin:$PATH`
- `.bat` 必须 **cp936 + CRLF**;分支用 `goto` 标签;echo 不放半角括号/`!`
- **改完后端代码必须 kill 所有在跑实例再重启**(旧进程会按旧逻辑写同一 `data/`)
- ⚠️ 本工具链会把命令里的 `\n` 字面化 → 写多行脚本用 heredoc + `textwrap.dedent`,别用 `"\n".join`
- ⚠️ 别用 `io.TextIOWrapper(sys.stdout.buffer,…)` 二次包 stdout(旧包装器 GC 时关掉 buffer →
「I/O operation on closed file」);用 `sys.stdout.reconfigure(encoding="utf-8")`
- ⚠️ 本工具链会把命令里的 `\n` 字面化 → 多行脚本用 heredoc + `textwrap.dedent`
- ⚠️ 别用 `io.TextIOWrapper(sys.stdout.buffer,…)` 二次包 stdout;用 `sys.stdout.reconfigure`
## 启动 / 部署(192.168.68.121 · CentOS 7)
- `start.bat [safe] [端口]` / `stop.bat`;`notify.enabled=True` 时双击 `start.bat` **会开定时发送**
→ **会话内一律 `start.bat safe`**;会话里它内层 `start` 会被拦 → 直接后台起
`python -m rentos.server --port 8899 --no-scheduler`
## 部署(192.168.68.121 · CentOS 7)
- 云端 `http://192.168.68.121:8899/`,容器 `rentos`,root / 12345678
- ⚠️ **线上 admin 密码已不是 1989@rent05**(10-14 之后被改过,`auth.verify` 实测 False)→
部署自检已改成登录失败时提示并跳过 HTTP 抽查,不再抛栈
- **主用 Git 同步**(⚠️ **必须用 venv python**,3.13.12 无 paramiko):
`RENTOS_SSH_PASSWORD=12345678 …python tools/deploy_git.py sync`
- push 绕代理:`env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY git push`
- ⚠️ **线上 admin 密码已不是 1989@rent05**(`auth.verify` 实测 False)→ 部署自检改为
登录失败时提示并跳过 HTTP 抽查,不再抛栈
- **主用 Git 同步**(⚠️ **必须用 venv python**):`RENTOS_SSH_PASSWORD=12345678 …python
tools/deploy_git.py sync`;push 绕代理 `env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY git push`
- 代码落 `/opt/rentos/repo`,与数据同级独立 → `git reset --hard` **不碰线上数据**
- ⚠️ **sync 只更代码、不动数据** → 改列模型/加列的迁移**部署后要在容器里手动再跑**
(`docker exec rentos python /app/tools/xxx.py`,服务器**无 Python 3**)
- 构建必须在**仓库根**:`docker build -f deploy/Dockerfile -t rentos:latest .`,零联网;
- 构建在**仓库根**:`docker build -f deploy/Dockerfile -t rentos:latest .`,零联网;
**切勿 `docker build … | tail`**(吞掉失败会拿旧镜像重启)
- `docker -v` 的 SELinux 选项是**冒号 `:z`**(写成 `,z` 的症状:页面 200 但台账 0 行)
- 云端已能直发企微:`/usr/local/bin/wecom-cli` + 凭据 `/opt/rentos/wecom-config/`(700)
- ⚠️ **线上是唯一生产方,本机只作预览**;**本机与云端不要同时开定时器**
- 远端 `https://git.thiztech.com/accounting/rentos.git`(Gitea),分支 `main`;
`.gitignore` 排除 `data/sessions.json`/`batches`/`drafts`/`contracts`/`attachments`/`_snap`/`_trash`/
`output/*`(**`!output/*.md` 保留验收报告**)/`项目合约/`;`.gitattributes`:`* text=auto eol=lf`、
- `start.bat [safe] [端口]` / `stop.bat`;`notify.enabled=True` 时双击 `start.bat` **会开定时发送**
→ 会话内一律 `start.bat safe`
- 远端 `https://git.thiztech.com/accounting/rentos.git`(Gitea)分支 `main`;`.gitignore` 排除
`data/sessions.json`/`batches`/`drafts`/`contracts`/`attachments`/`_snap`/`_trash`/`output/*`
(**`!output/*.md` 保留验收报告**)/`项目合约/`;`.gitattributes`:`* text=auto eol=lf`、
`*.bat -text`、**Dockerfile 必须 LF**
## 验证脚本(tools/)
......@@ -187,19 +174,18 @@
`test_accounts` 60 · `test_round6` 44 · `test_masters` 19 · `test_contracts` 21
(`test_scheduler.py` 需活跃 8899 服务,无服务时失败属预期)
- UI 回归:**自带沙箱** `ui_verify7~12.py`;**需连服务** `ui_verify`·`2`~`6.py`
→ 台账+电费录入 / 设置+锁定+批量暂存 / 带入+照明+图片 / 公司管理 / 房租出单全链 /
收款开票+导出 / 访客能力 / 账期区间 / 批量编辑 / 行粒度核对+账期汇总 / **合同原件(ui_verify12)**
(台账+电费录入 / 设置+批量暂存 / 带入+图片 / 公司管理 / 房租出单全链 / 收款开票+导出 /
访客能力 / 账期区间 / 批量编辑 / 行粒度核对 / **合同原件**)
- API 冒烟(沙箱):`smoke_verify_edit.py` · `smoke_verify_contracts.py`(合同 24 项)
- ⚠️ `ui_verify7` 的只读样本是脚本自建 `ro_verify`(guest 已是「电费操作员」,不能当纯只读样本)
- **`tools/ui_verify_runner.py <脚本…>`**:每个脚本起**全新沙箱**(复制 data/ + 改写 config 常量 +
另起端口 + 注入 `RENTOS_BASE`);⚠️ **1~6 号有硬闸门**(未设 `RENTOS_BASE`/`RENTOS_ALLOW_REAL=1` 直接退出)
- ⚠️ **沙箱常量清单要同步补**(`BATCH_DIR`/`OUTPUT_DIR`/`DRAFT_DIR`/`CONTRACT_DIR`)——
共 7 处:`ui_verify_runner` + `ui_verify7/8/9/10/11` + `check_scheduler`;漏了就会写进**真实** `data/`
共 7 处:`ui_verify_runner` + `ui_verify7/8/9/10/11` + `check_scheduler`;漏了会写进**真实** `data/`
(2026-10-13 直连 8899 曾误写 17 个测试批次)
- 迁移:`migrate_rentfree_type.py` · `import_park_notes.py` · `migrate_next_bill.py`
- 端到端 `e2e_round5.py`(PDF 出单 → 收款开票 → 导出 xlsx → 排期 → 删除回滚)
- **断言不要写死行数/列数/日期**,**按列 id / 表头文字定位**,不按第 N 列
(新增列使 DOM 位置后移,`ui_verify3` 的 `tds[14]` 曾误读「付款周期」)
- 其他:迁移 `migrate_rentfree_type/import_park_notes/migrate_next_bill.py`;端到端 `e2e_round5.py`
- **断言不要写死行数/列数/日期**,**按列 id / 表头文字定位**(新增列使 DOM 位置后移,
`ui_verify3` 的 `tds[14]` 曾误读「付款周期」)
## 工作规矩(巴特菲)
- **UI 回归不要每次小改都跑**,等他发话;小改只跑单元测试
......@@ -211,6 +197,7 @@
## 待巴特菲拍板
多办公地是否带公司名、车位是否算「办公地」、同名单据加期间、电费 0 用量是否出单、
4F/5F 与爱种草渠道口径、通知日期/缴费期限口径、是否做收款核销;
**合同核对发现的 17 处台账出入要不要动手修**;本机 4 张残留测试批次是否移 `_trash/`。
**合同核对发现的 17 处台账出入要不要动手修**;本机 4 张残留测试批次是否移 `_trash/`
(`20261014-134503`、`20261014-134504`、`20261015-134717`、`20261015-135238`)。
**未处置**:9 月电费批次多计 10 行 ¥167,947.41 / 少计 601B ¥299.67 / 出租方错记「精锐」;
`ui_verify11` 里「保存修改」按钮的断言待改写。
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