Files
2026-09-20 16:31:24 +08:00

133 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 贴票台 · macOS 原生版
SwiftUI 原生报账材料工作台。无需浏览器、Java、Spring Boot、HTTP 服务或用户单独安装 Python。RapidOCR 模型、解释器及 Office 导出依赖随应用内的处理引擎打包,扫描和导出仅通过本地子进程的标准输入/输出交换数据,不监听网络端口。
## 开发环境
- Xcode 26.3macOS 15.7 或更高版本。
- 当前构建为 Apple Siliconarm64);Intel Mac 需要另行构建对应架构的处理引擎。
- 本机 Xcode`/Users/yuxiaoting/Downloads/Xcode.app`。无需修改系统默认开发工具目录。
- 原工程 `../fapiao` 不会被修改,也不是运行依赖。
## 在 Xcode 中运行
打开 `reimburse.xcodeproj`,选择 `reimburse` scheme 和 My Mac,运行即可。构建阶段先核对引擎源码、打包脚本、模板、依赖声明与已打包二进制的指纹;缺失或不一致时自动重新打包,并运行打包引擎的报销单导出回归测试。验证成功才将 `native-engine/dist/receipt-engine-helper.app` 放进应用的 `Contents/Helpers`,并为辅助进程添加沙盒继承权限。没有本地打包依赖时构建会明确失败,不会继续使用旧引擎。
当前开发目录已经准备了本地依赖和引擎。重新下载源码、切换架构或修改 Python 代码后,需先重建引擎:
```sh
python3.12 -m venv .build-tools
.build-tools/bin/python -m pip install -r native-engine/requirements.txt
bash native-engine/build-engine.sh
```
依赖安装只发生在开发打包时,最终用户不需要上述工具。`dist`、依赖环境及构建产物不提交到版本控制;发布流程需先生成引擎,再构建应用。
```sh
DEVELOPER_DIR=/Users/yuxiaoting/Downloads/Xcode.app/Contents/Developer \
xcodebuild -project reimburse.xcodeproj -scheme reimburse \
-configuration Debug -destination 'platform=macOS' \
-derivedDataPath .derived-data build
```
## 使用流程
1. 选择包含 `发票``付款截图``实物照片` 子目录的材料根目录,或从访达将一个材料根文件夹拖入“材料扫描”顶部文件夹卡片。拖入时卡片高亮;不接受单个文件、应用程序或多个文件夹,处理中不接受重复导入。
2. 材料复制到应用自己的本地工作区,原始文件不会修改。再次导入会先确认是否替换当前工作区;取消确认保留当前材料和核对结果。
3. 本地 OCR 提取信息,按旧项目规则自动匹配并分类。扫描显示真实处理进度。
4. 在“人工配对”两侧多选材料,选择分类后确认;识别失败的材料仍可人工配对。一对一、一对多、多对一及多对多的已选连线均持续显示,不依赖鼠标悬停。两侧独立滚动时连线跟随材料,屏幕外的已选材料以顶部或底部箭头和数量标记表示;搜索隐藏的选择仍保留并在列底部提示。
5. “已核对”以费用类型为固定表头并排分列,以紧凑缩略图、金额、日期和凭证份数展示核对组,各列共用纵向滚动区。不将未经确认的 OCR 商户候选或文件名作为卡片标题;详情按组号标识,商户识别内容仍可搜索。仅显示当前结果中有材料的类型,空列自动隐藏,有材料后自动出现,其余列自动分配可用宽度;搜索和筛选也遵循此规则,无结果时只显示空状态,窄窗口支持横向滚动。点击凭证进入详情,可放大原件、切换前后组、修改类型或撤销配对。可逐组勾选、通过类型表头全选本列当前结果,或使用底部“全选当前结果”,再点击“导出 PPT”或“报销单”。选择数量和发票金额在底部汇总。跨类型及搜索筛选保留勾选,取消一列的全选不影响其他列或筛选外的勾选。修改类型后材料移动至对应列。未勾选时不能导出这两类文件。其他导出菜单中的行程 Excel 仍使用全部已核对材料;列内排序不改变导出顺序。自动核对结果不代表已人工复核。
6. 工作区自动持久化到应用沙盒内的 Application Support/ReceiptDesk;清空只删除工作区副本,不删除原始材料。
## 自定义个人报销单模板
在工具栏点击“报销模板”,或点击材料扫描页、个人报销单弹窗中的“选择模板 / 查看、更换”。也可以直接从访达将一个 `.xlsx` 文件拖到模板卡片或模板窗口上方的虚线区域;拖入时高亮,处理中不接受重复导入。不接受文件夹、多个文件或网页链接,失败时原模板保持不变。
模板窗口左侧显示 Excel 原表,右侧显示识别到的工作表、明细范围和收款位置。“查看 / 更换”、新导入确认页与报销单预览共用同一套表格渲染,不再使用系统 Quick Look;切换填写工作表时同步显示对应工作表。模板预览为独立只读操作,不需要勾选费用,不计算或回写原模板公式,加载失败可重试。核对后点击“确认位置,使用此模板”。仅在识别不正确时打开“调整填写位置”。未确认的新模板不会覆盖正在使用的模板,可随时放弃更换。
模板只保存本机副本,重启后仍有效,不修改导入的原始文件。仅影响个人报销单,不影响 PPT、行程表及勾选范围;仍按所选费用类型合并金额、单据数,每类型一行。保留模板样式、合并区域、打印设置、其他工作表和未映射内容;明细超出确认范围时复制报销工作表分页。合计和其他公式在 Excel / WPS 打开后重新计算。
导入旧报销单时,务必将全部历史明细行包含在填充范围内,并指定收款字段及额外需要清空的单元格;未映射的位置会保留模板原文。映射的明细旧值会先清空;收款字段没有本机资料时也会清空,账号以文本填入。公司、项目等固定文字可预先在 Excel 中修改后再导入。
“清空重置”可选择“仅清空本次报销数据(保留模板)”或“报销数据和导入模板一起清空”。两者均不删除原始材料、原始 Excel 和钥匙串中的收款资料。模板独立保存于当前 macOS 用户的应用沙盒,不随项目管理登录账号切换。模板损坏或丢失时会阻止导出并提示重新导入,不会静默改用内置模板。
支持最多 20 MB 的未加密、无宏、无活动外部工作簿链接的 `.xlsx`;旧 `.xls` 需先另存为 `.xlsx`。不同格式需要确认填充位置,受保护的报销工作表不支持填充,包含 Excel 表对象的工作表不支持自动复制分页。内置模板仍保持原先导出方式。
### 导出前预览
在“已核对”勾选材料 → “报销单” → 编辑用途、签字及收款资料 → “预览报销单”。此处只显示已填写的报销单,不再提供原始模板切换。应用先按当前模板生成实际 Excel,再读取该文件的单元格内容与样式,以统一比例显示列宽、行高、合并区域、边框和文字;支持工作表选择、适合宽度、整页及手动缩放。右上角放大按钮打开可调整大小、可全屏的预览窗口,返回修改或关闭报销单时一并关闭。确认后保存的字节与预览源文件一致,不重新生成另一份报销单。取消保存仍留在预览页面。
跨单元格文字占用空白单元格时,按实际字宽隐藏被跨过的竖边框;保留其他边框,不擅自加线或改变模板合并关系。支持堆叠竖排、底部对齐及会计格式的右侧金额填充,合并区域使用外围边框。这些规则按实际工作表、单元格和样式读取,不绑定内置模板的具体地址。优先使用本机模板字体,缺失字体采用系统替代并明确提示;图片、图表、条件格式等高级效果会提示在办公软件核对,不承诺任意模板与 WPS 逐像素相同。预览限制不改变导出的模板样式。
预览会刷新受支持公式的缓存(求和、同表引用、基本四则运算、条件判断、今日日期及内置人民币大写公式),保留公式本身。复杂自定义公式不猜测结果:移除旧缓存,明确列出尚未计算的单元格,由 Excel / WPS 打开后重算。预览属于屏幕表格展示,不是分页打印校样;打印设置保留在 Excel 中。
修改所选数据、用途、收款资料或模板后需要重新预览。保存前核对输入及文件指纹,防止保存与预览不一致的内容;返回或关闭预览时清理临时文件。临时报销单仅存于应用本地沙盒,包含完整收款信息,不上传。
已核对页面的凭证详情中显示简要核对项,点击自动核对评分查看每项实际得分、未得分原因及核对建议。证据评分采用真正的 **100 分制**:单张发票与付款截图的权重为金额 40 分、商户 20 分、日期 10 分、单号 15 分、批次金额唯一性 15 分;满分之和为 100,实际得分之和就是百分比,各项未得分之和就是差额。例如仅金额一致且批次唯一时为 55/100 分,即 55%。日期相差不超过 3 天得 10 分,4–7 天得 5 分;商户达到 35% 相似度后按相似度 × 20 分四舍五入。其他组合规则的满分原本就是 100 分,仍保留;多笔付款固定 95 分会明确说明保留分。百分比不是金额匹配比例,也不是成功概率。人工配对不显示自动评分。
旧工作区启动时自动升级为新版评分,无需重扫,不改变配对、分类及导出内容。内部候选筛选、歧义排除沿用原规则及原 `score`,新版百分制证据评分单独存于 `explanation.rawScore``version: 2`),界面不再将内部筛选分数当作百分比。历史依据不足时不显示未经验证的百分比。
## 收款信息
在本地贴票工具栏点击“收款信息”,填写收款人、开户行(建议包含支行)、账号,以及可选的制单人。保存后重启仍可使用,也可在个人报销单弹窗内修改或删除。账号按文本保存,保留前导零和长账号;界面默认隐藏完整账号。
资料独立保存于当前 macOS 用户的本机钥匙串,不写入工作区 JSON,不上传、不云同步;导入新材料和清空工作区不会删除资料。它不随项目管理的登录账号切换,共用同一 macOS 用户时需自行核对收款人。
个人报销单每一页自动填写已保存资料;制单人未填写时使用收款人。没有保存资料时,相应单元格留空,不沿用模板的示例姓名、开户行及账号。PPT 和行程 Excel 不携带这些资料。导出的报销单包含完整收款账号,请妥善保管。删除本机资料不会修改已导出的文件。
## 功能对应
| 原项目 | 原生实现 |
|---|---|
| App.vue、三个页面 | ContentView、ScanPage、PairPage、MatchedPage |
| WorkspaceState / FileItem / MatchPair | Models.swift |
| 前端请求、状态操作 | WorkspaceStore.swift |
| OcrService / worker.py | native-engine/engine.py + 打包 RapidOCR |
| OcrExtractor / WorkspaceService | native-engine/domain.py |
| PptExportService | native-engine/exports.py / export_ppt |
| TravelExcelExportService | native-engine/exports.py / export_travel |
| PersonalExpenseExportService | native-engine/exports.py / export_expense |
保留一对一评分、多发票合计、多付款合计、同程多人归组、歧义判断、七类关键词分类、人工多对多、撤销、预览、金额统计和三类 Office 文件导出。
个人报销单只使用打开弹窗时勾选的材料,按费用类型合并明细。例如勾选 6 组交通,只生成一条“交通”,金额和发票数量累计;未勾选材料不计入。弹窗显示合并后的类型、组数、单据数和金额,可按类型填写用途。金额沿用原报销单口径(每张发票 OCR 金额的绝对值最大值),不改为付款金额;混合票据类型会一并列明。按首次出现的类型顺序输出,内置模板每页最多 13 个类型,自定义模板按确认的明细范围分页。
保存个人报销单前还会核对引擎返回的分类合并版本和明细行数;旧引擎或行数与预览不一致时停止保存,不覆盖用户选定的输出文件。打包引擎通过 `RECEIPT_ENGINE_BINARY` 运行 `test_exports.py``engine_process` 测试。最终应用的引擎带沙盒继承签名,不能直接从普通终端启动;应使用 Debug 应用的 `--verify-local-engine` 自检入口,从真实应用内验证两条汇总并检查生成的 `grouped-expense.xlsx`,而不能仅验证 Python 源码。
### 特意保留的业务口径
- 文件名不参与匹配;PDF 只识别、预览和导出首页。
- 一对一分数达到 90 时可通过歧义检查;其他候选需要至少领先 8 分。
- 一张发票对应多笔付款的唯一合计组合不另设商户/日期门槛。
- 页面发票汇总、OCR 首金额和个人报销单最大金额的不同计算口径沿用旧代码,未擅自合并。
- 订单截图与同金额支付凭证去重沿用旧规则,因此仍需人工检查不同订单同金额的情况。
- “按类型分类”PPT 沿用先日期、后类别排序,并非重新设计分类封面或类别分区。
- PPT 使用竖版 3:4(720×960 pt),纯白页面,不加封面或贴票类型标题。所有类别(包括交通、住宿)均预留实物照片位置,不自动填入实物照片。
- 一张发票配一张付款截图:发票上方整幅,付款截图左下,右下留可编辑、可删除的实物照片提示,不另加照片页。
- 一张发票配多张付款截图:首张发票页下方左右最多放两张付款,剩余付款按每页三列两行、最多六张续页,最后加一张手工照片页。
- 多张发票:先按每页两张上下排列;若剩一张,放在下一页上方,下方容纳前两张付款。剩余付款每页最多六张,最后加一张手工照片页。只有发票或只有付款的组也能完整导出并预留照片页。
- 不同核对组或不同审批申请不合页、不按比例猜测发票付款对应关系;原图等比缩放,不裁切、不重复、不漏图,PDF 仍只用首页。
- 个人报销单沿用原始 XLSX 模板,直接修改工作表 XML,保留样式、打印设置、公式和关联资源;每页 13 条明细,最多六个签字岗位。删除全部岗位时按旧规则回退默认岗位。
### 必须说明的环境差异
RapidOCR 主版本、模型及关键推理依赖与旧环境对齐,但 Windows x64 与 macOS arm64 的底层运行库不同。PDF 首页由本地 PDFium 栅格化,替代原来的 PDFBox。因此不能保证所有文件的 OCR 文本逐字或浮点结果完全一致,仍需用实际报账材料做回归验收。应用不会悄悄换用苹果 Vision 或云端 OCR。
## 验证
```sh
.build-tools/bin/python -m unittest discover -s native-engine/tests -v
.build-tools/bin/python native-engine/tests/smoke_engine.py \
--binary "$PWD/native-engine/dist/receipt-engine-helper.app/Contents/MacOS/receipt-engine-helper" \
--output "$PWD/.validation/packaged"
```
覆盖原铁路多人凭证案例、火车与航班提取、金额歧义、分类、去重、PPT 页面安排、个人报销单分页与公式保留、行程表等。端到端测试使用 `../fapiao/demo-materials` 作为开发测试数据,运行中的应用本身不依赖该目录。
Debug 构建还提供应用沙盒内自检:用 `--verify-local-engine` 参数启动应用,生成独立测试图片,经过真正的 Swift → 本地引擎链路验证 OCR 和三种导出。结果写入沙盒 `Application Support/ReceiptDesk/diagnostics/report.json`,不替换用户工作区;自检后该应用进程自动退出。Release 不包含此入口。
## 发布说明
当前是本机开发运行构建,并非已经完成 Developer ID 签名和苹果公证的公开安装包。正式分发时需用同一 Developer ID 对辅助引擎的所有嵌套 Mach-O 库、辅助程序和主应用按由内到外顺序签名,再进行公证。请同时随发布包保留第三方依赖许可声明。