Files
ARR-2.0-0918/arr_web/ARR_DOWNLOAD_HANDOFF.md

208 lines
18 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.
# 单日 ARR 自动下载卡片接入
2026-09-16。Web 卡片、任务队列、HTTP 入口及采集到处理的执行器框架已实现;当前 `arr_web.run` **尚未注入实际执行器**,
所以页面显示「自动下载服务暂未就绪」,按钮不可提交。不得把本文或合成测试解释为真实接口到 Finance 已接通。
2026-09-17:[独立本机XML重放入口](LOCAL_XML_REPLAY.md)已用用户准确XML跑通按钮、真实处理、隔离SQL和月报。
只在`arr_web.local_replay`装配中ready=true,来源明确为native_xml_replay;不是本文要求的OHIP正式来源适配,
不改变`arr_web.run`默认状态。可先用于按钮联调。
2026-09-17:[本机接口模拟入口](LOCAL_API_SIMULATION.md)8874也已完成152次HTTP采集、生成ARR处理XML、原XML字段核验及隔离入库/月报。`source_kind=local_api_simulation`,仍与Oracle实际来源验收区分;可沿用下面的提交/状态/下载契约联调。
## 日期与用户操作
- 新卡片位于 Upload ARR.XML 左侧。单个日期选择器的选值是本次下载的唯一日期来源。
- 表单初始建议值为曼谷日历昨天,用户可以改选;这仅是 Web 表单预填。
- 2026-09-18:日期选择与当前任务分离,进行中可预选下一报表日期,轮询不覆盖已编辑日期。
预选不提交、取消或改变原任务;运行中仍禁止新提交,待确认/中断重试继续沿用原编号/日期,按钮注明原日期。
- POST 必须显式带上有效 `report_date`,缺失、日期区间、多余字段、时间戳、无效日历日期一律拒绝。
下载执行器不重新计算昨日,也不按执行/重试当天限制输入日期。
- Web 调用下游时传两个相等的 Python `date`:`from_date=selected_day`、`to_date=selected_day`。
下游搜索的 arrivalStartDate/arrivalEndDate 和有效价的 detailDate 都必须使用这一天。
- 这与“上游系统提供明确 From/To,采集程序校验并冻结”的边界一致。本卡片是手动选择日期的上游入口;
其他系统的双日期调用协议仍由接口任务对接,不在本 Web API 中接受任意日期范围。
## HTTP 契约
所有入口要求现有登录会话,POST 还要求 `X-ARR-CSRF`。响应继续使用标准 `{ok, api_version, data}`。
| 方法与路径 | 请求 / 返回 |
|---|---|
| GET `/api/arr-downloads` | `ready`、`default_date`、`business_time_zone`、`latest_task`;读取无采集副作用 |
| POST `/api/arr-downloads` | 严格 JSON `{report_date:"2026-09-15", request_id:"32位小写十六进制"}`;202 为已登记任务,不是 Finance 成功 |
| GET `/api/arr-downloads/{request_id}` | 获取本次任务;未知编号404 |
| POST `/api/arr-downloads/{request_id}/retry` | 空 JSON `{}`;继续原编号/日期,不能带新日期 |
任务仅暴露 `request_id/report_date/from_date/to_date/status/job_id/can_retry/created_at/updated_at`。
状态有 queued、downloading、processing、needs_review、succeeded、failed、interrupted。
浏览器网络异常单独显示“重新查询原任务”,不把本地网络错误登记成业务失败。
用户请求编号先存入按登录名区分的 localStorage,再发起 POST;服务端 latest_task 提供另一路恢复入口。
首次连接失败或服务未就绪时,可见页面每10秒通过GET重新检查;切回页面或网络恢复时立即检查,隐藏页面暂停。
恢复检查等待会话初始化完成,同一配置请求不重叠,只恢复一次本地任务编号,保留用户已经选择/正在编辑的日期。
这些检查不会自动POST新任务或重试;中断任务仍需用户明确点击,沿用原请求编号和日期。
## 执行器与待完成的来源适配
[CapturedARRExecutor](arr_download_executor.py) 已实现 [ARRDownloadExecutor](arr_downloads.py) 的同步 `execute`。
其顺序如下,实际来源适配与独立映射验证仍需接口任务交付:
```python
def execute(self, *, request_id, from_date, to_date, report_stage):
# 1. Validate equal explicit dates and bind request_id to a stable capture batch.
# 2. Acquire/archive the complete accepted source, including date-specific prices.
# 3. Apply the accepted, versioned source adapter and independent mapping checks.
report_stage("processing")
# 4. Prepare/replay the SAME frozen package using the existing validation/ingestion path.
# 5. Return an authoritative acknowledged result, never just capture completeness.
return DownloadOutcome("succeeded", job_id=acknowledged_job_id)
```
上面的说明不是源适配实现。必须完成整日采集、字段/范围/顺序验收后才能连接真实数据。
`request_id` 必须固定关联 capture batch、交接文件组和 Finance job;未知提交结果重试必须核对原事务,
不能调用每次分配新 UUID 的人工 `ProgrammaticUploadCoordinator.submit`。
执行器需要显式注入:已验收的 `adapter.adapt(VerifiedArchive) -> bytes`、独立的
`mapping_validator.validate(VerifiedArchive, payload) -> None`、共同版本化的 `adapter_contract`、酒店 ID、
返回匹配版本 Reader 的 `reader_factory`,以及现有处理器策略、对象存储和入库服务。
映射验证发现任何不符必须抛异常;返回 False/其他值也会拒绝。它不能仅重跑同一适配逻辑或固定返回通过。
构造函数检查必需依赖存在,不替代来源验收;仓库当前没有正式适配器或映射验证器,也没有启用参数。
2026-09-16 接入复核:接口任务新建的 `source_facts.build_source_facts` 和
`validate_source_facts.verify_source_facts` 是字段证据 JSON 的提取/校验入口。前者明确不生成 XML,后者的
`facts_verified=true` 只证明字段证据与原始响应一致;`source_mapping_verified/report_equivalence_verified/finance_ready`
仍为 false。它们尚未实现上方 `adapt(archive) -> XML bytes` 和 `validate(archive, XML bytes) -> None` 协议,
不能直接注入执行器,也不能据此改变页面 readiness。实际还需完成报表范围、字段选择、显示和顺序的适配并独立校验。
这项技术交付缺口与已关闭的日常 XML/Resv.-GEN 用户确认无关,不再向用户重复询问基准文件。
执行器将选中日期转成三个相等的 ISO 日期,默认传给v2 `capture_day_job.run_batch`;
批次编号固定为 `web-<request_id>`。整个请求绑定酒店、日期、采集契约、适配/验证版本和处理规则,
任一内容变化会拒绝旧任务重试。不得通过换一个来源或规则版本“修复”结果未知的原任务。
2026-09-17新增含姓名的v3选择:构造时显式传`capture_version="v3", max_profiles=N`,N必须是1–10000的整数,
不可缺省或传布尔值。v2不接受姓名查询上限;其旧请求格式和默认行为保持兼容。v3调用
`capture_named_day_job.run_batch`,工厂依次返回`Reader`、`RateInfoReader`、`ProfileSummaryReader`,最后一个Reader
使用同样的N。v3归档经完整性重放后,才交给适配器和独立验证器;两个组件必须明确支持此版本。
请求身份含采集版本和姓名上限;改变版本/上限不能复用旧请求,数字与布尔值或浮点数也不互相冒充。
采集范围还可显式传`page_size`、`max_pages`、`max_records`,分别限定每页条数、最多页数、最多预订数;
三个值须为正整数,上限分别100、100、10000,v3还要求`max_profiles <= max_records`。
构造时先校验,再允许创建任务或加载凭据;默认值沿用已有v2行为。已约定的测试范围可配置为
`page_size=100, max_pages=2, max_records=139, max_profiles=112`,这只是配置示例,不启动新采集。
范围写入持久化请求身份;超限停止于搜索阶段,旧请求重试不能改变范围。Reader工厂仍须使用相同姓名上限。
已冻结后的未知提交结果恢复仍直接复用原交付包,不再查询姓名或重新采集。
新`prepare_arr_source`仅输出逐笔候选及缺口,虽然可在503期间离线运行,也不是已验收来源适配器/验证器。
构造依赖检查会拒绝它;不得改变页面readiness。此扩展只有合成来源和本地隔离处理测试,正式来源与运行接线仍待验收。
2026-09-17交付检查补充:执行器每次交付都向`processing_handoff.deliver`传完整`expected_binding`与
`expected_policy`。持有交付锁后,先核对实际冻结包的批次、酒店、日期、采集摘要、适配契约和处理规则,
匹配后才可上传或登记数据库任务。改动`prepared.json`里的采集摘要也不能绕过这一检查。
采集`job.json`及交接身份文件均按规范化JSON摘要比较;浮点数不能冒充原来的整数,字段顺序变化则允许。
`root` 必须是仓库外的私有持久目录,其父目录需已存在;执行器创建0700目录、0600文件:
- `requests/<request_id>/request.json`:在第一次采集前保存请求身份。
- `captures/web-<request_id>/`:保留采集尝试及原始响应;失败采集可沿用同批次重新取得整日。
- `handoffs/<job_id>/`:由现有交接库保存并验证源 XML、处理产物和不可变交付文件组。
- `requests/<request_id>/prepared.json`:在第一次外部对象写入/Finance交付前保存文件组摘要。
存在已准备文件组的恢复点时,重试直接验证并交付原文件组,不调用酒店、适配器或处理器;
若在保存恢复点前中断,会重读已完成采集并重新适配,但 `prepare` 必须复用同一文件组,改变内容即拒绝。
恢复点不是成功凭证,每次仍需 `deliver` 核对权威提交结果;摘要不符则停止。采集失败尚未形成处理任务时
可返回明确失败并允许同批次重试;未知状态、映射拒绝和交付异常保留为中断,不宣称 Finance 失败。
- `succeeded`:完整结果已验证并获得权威 Finance 提交回执,必须返回处理 job_id。
- `needs_review`:已登记原有人工价格复核,必须返回对应 job_id;尚无最终 Finance/月报结果。
- `failed`:权威终止失败,可有 job_id;只有确实可沿用相同身份安全重试时才能设 retryable=True。
- 异常/非法回执:Web 记录 interrupted,保留身份;不保存异常正文、不宣称提交失败。
- 默认最多10次执行;取消/失败原因应通过现有业务任务证据或部署诊断核实,不把原始响应返回前端。
- Web 查询需要复核的任务时继续读取现有权威任务状态,复核通过后显示 succeeded。
### 冻结处理回执转换
`arr_web.arr_download_handoff.outcome_from_handoff` 已提供严格的回执转换。未来执行器调用
`processing_handoff.deliver` 并获得本次权威响应后,将该响应与预期 `job_id`、所选 Python `date`
及 `prepare` 返回的 `manifest_sha256` 一同传入。不要用本地缓存 `receipt.json` 代替交付核对。
| 交付回执 `ingestion_status` | Web 任务结果 |
|---|---|
| `committed` | `succeeded`;必须有正整数 Finance 版本编号且业务日期等于所选日期 |
| `recorded_review` | `needs_review`;没有 Finance 版本,进入现有人工价格复核 |
| `recorded_failure` | `failed`;确定的业务失败,不自动重试;失败审计回执的业务日期可为null |
回执版本、处理任务、交付编号、文件组摘要、日期或提交状态不符均抛异常,由 Web 保留为 `interrupted`;
未知状态不会默认成功。转换函数本身不提供采集/来源适配,不证明 API 字段等价性,也没有使运行环境就绪。
合成联调的测试执行器仅存在于测试文件中;正式执行器仍须完成上方来源验收及身份绑定要求。
2026-09-17失败回执修正:真实PostgreSQL会为`recorded_failure`保存`version_status=rejected`的审计行,
所以`daily_version_id`可以是正整数,也可兼容为null;`version_no`必须为null。这不是有效日报版本,
不能据此改成成功或未知状态。`recorded_review`的两个版本字段仍必须都是null。
Web将权威失败显示为`failed`、保留`job_id`且不提供下载重试;提交后丢回执时暂为`interrupted`,
沿原冻结交付核对后收敛到同一失败,不重新采集/处理。数据库可保留一条`arr.processing_failed`审计事件;
不得产生`arr.daily_version_committed`事件、激活日报或触发月报。
### 首轮真实沙箱联调的结果边界
已有OHIPSB02固定139条采集的115条可取费率代码均不在本酒店现有白名单内,24条缺少有效费率代码,
见[原始覆盖检查](../.project-docs/50-evidence/topics/2026-09-16-arr-input-gap-triage.md)。
这批数据不能作为成功日报/月报的正例;不改白名单或删行来使其通过。
须先完成来源适配与独立映射验收,之后才能检查现有处理器给出的确定业务失败;
不能用预期业务失败替代来源验收,也不能把来源/网络失败当作确定处理失败。
成功处理链已有本机隔离模拟覆盖,真实正例留待有适当来源数据后验收。
处理器对白名单行要求公司名称、确认号、房号和姓名等必填值,公司参与价格匹配,备注首条参与Group Code。
`BLOCK_CODE`、`PRODUCTS`、`ROOM_CATEGORY_LABEL`允许来源确实为空;允许空不等于允许将未取得/未核实值填空。
依照[现有字段契约](../arr-opera-daily-ingest/references/field-contracts.md)保留其来源和值,不新增业务规则。
在受控运行组合中创建 `PersistentARRDownloads(private_jobs_root, accepted_executor)`,通过
`PortalApplication(..., arr_downloads=coordinator)` 注入,并负责关闭协调器和相关依赖。
不要仅添加环境变量就把未验收执行器标记为可用。
### 受控启动装配(2026-09-17)
`arr_web.arr_download_runtime.CapturedARRSource`封装显式来源依赖;
`arr_web.run.main(argv, arr_source=source)`已支持将其注入标准Web启动流程。
默认`main(argv)`和现有容器命令仍不配置来源,没有新增CLI/环境变量开关,也没有加载酒店凭据。
这一步只完成程序装配,不签发业务验收;真实适配器和独立映射验证器仍待接口任务交付。
已验收来源的Python启动包装器可以使用以下组合(变量须由受控包装器提供,不是当前可运行的真实来源示例):
```python
source = CapturedARRSource(
root=private_state_directory, hotel_id=hotel_id, adapter_contract=accepted_contract,
adapter=accepted_adapter, mapping_validator=independent_validator,
reader_factory=reader_factory,
capture_version="v2", page_size=100, max_pages=100, max_records=10000,
)
main(["--enable-processing"], arr_source=source)
```
- 须同时启用并成功装配处理依赖。下载与人工上传共享同一处理策略、处理器、对象存储、入库仓库及独立校验服务。
不重复创建一套可能不同的处理规则。构造来源不会调用Reader或酒店接口;已持久化的queued任务会在启动后恢复调度。
- `root`是仓库外的私有0700目录,其父目录须存在。`source.json`在队列启动前固定服务/应用命名空间、酒店、
采集协议及上限、适配版本和处理规则。任何变化拒绝复用旧根目录,尚未开始采集的queued任务也不能被改绑。
无此绑定文件的既有queue/acquisition目录会被拒绝;不要直接指向8873/8874实例或借删除绑定绕过检查。
- `compose_arr_downloads(source, processing_runtime)`可供其他受控组合复用;只拥有返回的队列,处理依赖仍由调用方拥有。
reader_factory若持有客户端/凭据上下文,应由包装器保持到`main`返回后再关闭。
- Web构造/服务异常都会关闭已取得的资源;队列先停止调度,并通过`close(wait=True)`等待正在执行的任务结束,
再关闭共享存储。标准主线程的显式来源启动还将SIGTERM接入这个退出过程,并恢复先前的信号处理器。
强制终止仍依赖既有中断/冻结恢复机制,不能保证正在执行的网络请求立即返回。
- 原文件重放及本机模拟的关闭路径也等待队列,再停止月报worker和自有数据库。当前运行的录制实例未重启。
- 月报仍由原有独立worker消费outbox;本装配不新增定时下载、月报按钮或来源切换UI。
## 持久化与运行边界
- 单机私有 SQLite 队列;目录0700、数据库0600;完整事务写入请求后才交给单个后台线程执行。
- 进程独占文件锁避免同一队列被两个进程调度。不是跨机器队列,必须使用同一可靠的本地持久卷。
- 同日期有 queued/downloading/processing/interrupted 任务时,新点击会返回已有任务。
- 同编号改日期返回409;同编号完成后重复提交返回旧结果;显式新编号可重新获取已完成日期的数据。
- 重启时未开始的 queued 可执行;下载/处理中的任务变为 interrupted,由用户继续原任务核对结果。
- `.close()`保留最多2秒的兼容等待,正在执行的调用继续持有锁到返回;资源拥有者使用`.close(wait=True)`
等待执行器结束后再关闭依赖。标准显式来源装配和本机重放/模拟已采用完整等待。
- 原始数据、适配后的源、处理文件及金融幂等性属于真实执行器/现有交接库的责任,不能从本队列推定已验收。
## 已验证 / 未验证
新单元测试覆盖单日与校验、并发点击、原任务重试、跨进程锁、重启恢复、身份冲突、鉴权/CSRF和人工复核状态。
合成浏览器检查覆盖中英泰/五种窗口宽度、失去响应与未登记请求恢复、复核入口和未就绪运行状态。
没有调用酒店接口、生产数据库或 OSS,没有发布/重启正式服务;真实来源验收及运行注入仍待完成。