Compare commits
No commits in common. "3ccda7d6c23915dd1c39b1abd0104a69c7169f53" and "0fa29ec9bda61717982c63a4366cc12381765493" have entirely different histories.
3ccda7d6c2
...
0fa29ec9bd
|
|
@ -23,7 +23,6 @@ node_modules/
|
|||
# Local runtime / agent
|
||||
.runtime/
|
||||
rpa_contract/screenshots/
|
||||
rpa_rebate/screenshots/
|
||||
rpa-chrome-profile/
|
||||
**/chrome-profile/
|
||||
.tmp/
|
||||
|
|
@ -31,7 +30,6 @@ rpa-chrome-profile/
|
|||
.grokignore
|
||||
.claude/
|
||||
.superpowers/
|
||||
.worktrees/
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
42
AGENTS.md
42
AGENTS.md
|
|
@ -1,6 +1,6 @@
|
|||
# AutoRpa
|
||||
|
||||
洁达 RPA。当前主线是**订单中心入站**(`rpa_oms`)、**选品班次**(`rpa_selection/`)、**加购班次**(`rpa_cart/`)、**合同班次**(`rpa_contract/`)和**返利班次**(`rpa_rebate/`):收件落 Oracle,请求里不跑选品、不录合同;选品另起进程捞 `deal_flag=0`;加购另起进程领表 2 成功行;合同另起进程领表 3;返利另起进程领表 4 起草行 / 表 3 只录协议行。根上不再有 `选品流程/`、`state_center/`、`rpa_yisaiqi/`。
|
||||
洁达 RPA。当前主线是**订单中心入站**(`rpa_oms`)、**选品班次**(`rpa_selection/`)和**加购班次**(`rpa_cart/`):收件落 Oracle,请求里不跑选品;选品作业另起进程捞 `deal_flag=0`;加购另起进程领表 2 成功行。合同页面在 `rpa_contract/`;返利 `rpa_rebate/` 仍是占位。根上不再有 `选品流程/`、`state_center/`、`rpa_yisaiqi/`。
|
||||
|
||||
智力用在**定落点、想清楚对错**;速度和 token 靠下面这张地图定点打开,禁止用扫仓代替思考。改对应了,反工和消耗都会少。
|
||||
|
||||
|
|
@ -22,12 +22,10 @@
|
|||
| 接口1 Oracle 落表 | `rpa_oms/app/inbound/oracle_repo.py` | `rpa_oms/tests/test_oracle_repo.py::点名` |
|
||||
| 接口1 入口 / 鉴权 | `rpa_oms/app/main.py`、`auth.py` | 同上;真库见 `rpa_oms/README.md` Windows 段 |
|
||||
| 接口2 取数组装 | `rpa_oms/app/outbound/cart_settlement.py` | `rpa_oms/tests/test_cart_settlement_outbound.py::test_assembles_unpushed_rows_as_document_envelope` |
|
||||
| 接口4 取数组装 | `rpa_oms/app/outbound/contract_agreement.py` | `rpa_oms/tests/test_contract_agreement_outbound.py::test_assembles_unpushed_rows_as_document_envelope` |
|
||||
| 接口3 收件契约 | `rpa_oms/app/inbound/purchase_contract.py` | `rpa_oms/tests/test_purchase_contract_receive.py::点名` |
|
||||
| 接口3 Oracle 落表 | `rpa_oms/app/inbound/oracle_contract_repo.py` | `rpa_oms/tests/test_oracle_contract_repo.py::点名` |
|
||||
| 选品规则 / 拆量 | `rpa_selection/rules.py`、`pipeline.py` | `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` |
|
||||
| 选品班次落表 | `rpa_selection/job.py`、`persist.py`、`repo.py` | `rpa_selection/tests/test_job.py::test_job_searches_barcode_not_approval_no` |
|
||||
| 合同班合单落表 | `rpa_contract/job.py`、`group.py`、`persist.py`、`repo.py` | `rpa_contract/tests/test_job.py::test_job_merges_same_supplier_and_writes_table4` |
|
||||
| 合同页面步骤 | `rpa_contract/contract_flow_steps.py` 里对应函数 | `ACCEPT SUMMARY` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt` |
|
||||
| 合同入参 / 客商草稿 | `rpa_contract/contract_draft.py` | `rpa_contract` 点名用例 |
|
||||
| 节点入口 | `rpa_contract/contract_create_node.py`、`main.py` | 读到正式合同号 |
|
||||
|
|
@ -35,12 +33,11 @@
|
|||
| 文字验收 / 截图策略 | `rpa_contract/accept.py` | 只读 `ACCEPT.txt`,禁止读 png |
|
||||
| 加购班次 | `rpa_cart/cart_job.py`、`cart_repo.py`、`cart_match.py` | `rpa_cart/tests/test_cart_job.py::test_job_adds_split_rows_without_insert` |
|
||||
| 支付回传 | `rpa_cart/job.py`、`settle.py` | 1 号药城支付按 Excel 落表,不对加购行 |
|
||||
| 返利班查号落表 | `rpa_rebate/job.py`、`persist.py`、`repo.py` | `rpa_rebate/tests/test_job.py::test_job_finds_contract_and_writes_agreement` |
|
||||
| 返利班查号页面 | `rpa_rebate/rebate_flow_steps.py`、`rebate_finder.py` | `ACCEPT SUMMARY REBATE_FIND` 或 `rpa_rebate/tests/test_finder.py::test_find_flow_writes_accept_for_missing_number` |
|
||||
| 返利班 | `rpa_rebate/README.md` | 占位,未实现 |
|
||||
| 四个口地图 | `rpa_oms/README.md`、根 `README.md` | — |
|
||||
| 节点业务规则(点名一节) | `docs/RPA订单中心接口开发口径_20260930.md` | 禁止通读 `docs/` |
|
||||
|
||||
禁止打开:`node_modules/`、`chrome-profile/`、任何 png。Chrome 用户目录只在仓库外 `D:\work\rpa-chrome-profile`。Oracle 密码放本机 `rpa_oms/.env`(选品空缺时复用);商城账号放 `rpa_selection/.env`;返利中心账号放 `rpa_rebate/.env`。都已 gitignore,不要写进仓库、README、示例 JSON,不要 `git add` / commit / push。
|
||||
禁止打开:`node_modules/`、`chrome-profile/`、任何 png。Chrome 用户目录只在仓库外 `D:\work\rpa-chrome-profile`。Oracle 密码放本机 `rpa_oms/.env`(选品空缺时复用);商城账号放 `rpa_selection/.env`。都已 gitignore,不要写进仓库、README、示例 JSON,不要 `git add` / commit / push。
|
||||
|
||||
## 接口1 落点(订单中心)
|
||||
|
||||
|
|
@ -67,7 +64,7 @@
|
|||
|
||||
验收(内存、不连库):`cd rpa_oms` → `uv run pytest tests/test_sales_goods_receive.py::点名 -q`。真库:`GET /health` 后把 `examples/sales_goods_batch.json` POST 两次(第一次两条 `ACCEPTED`,第二次两条 `DUPLICATE`)。接口 3 点名 `tests/test_purchase_contract_receive.py`;真库 POST `examples/purchase_contract_batch.json` 两次。接口 2 点名 `tests/test_cart_settlement_outbound.py::test_assembles_unpushed_rows_as_document_envelope`。
|
||||
|
||||
修接口1不要顺手改 `rpa_selection/`、`rpa_cart/`、`rpa_contract/`、`rpa_rebate/`。接口 2、4 只组装不推送。
|
||||
修接口1不要顺手改 `rpa_selection/`、`rpa_cart/`、`rpa_contract/`、`rpa_rebate/`。接口 4 未开放;接口 2 只组装不推送。
|
||||
|
||||
## 选品落点
|
||||
|
||||
|
|
@ -105,7 +102,7 @@
|
|||
|
||||
## CONTRACT_CREATE 落点
|
||||
|
||||
班次作业在 `rpa_contract/`,不在接口 3 请求里跑。领表 3 `process_status=0`(跳过协议重录行),按 `supplierId` 合单,调页面流,写表 4。`run_contract_create_flow` 串页面;修一步只打开对应函数。
|
||||
页面脚本在 `rpa_contract/`。`run_contract_create_flow` 串起来;修一步只打开对应函数。从表 3 合单、写表 4 尚未接。
|
||||
|
||||
| 步 | 函数 | 要点 |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -113,9 +110,8 @@
|
|||
| S03 登录 | `login_contract_system` | `test002` / `1` |
|
||||
| S04–S05 起草 | `enter_contract_create`、`select_contract_template` | 直达 `FORM_PAGE_URL`,不要点 SPA 菜单 |
|
||||
| S06 签约方 | `fill_contract_signatory_info` | 客商 `DW0100046361` / `重庆亿昊药业有限公司` |
|
||||
| 尽调 | `check_customer_diligence` | 正文含「未尽调」则失败,不提交 |
|
||||
| S07 基础 | `fill_contract_basic_info` | 标题 `_expected_title` |
|
||||
| S08 金额日期 | `fill_contract_amount_and_dates` | 签订日/开始日用接口 3 三日期,缺则回退 `order_date` |
|
||||
| S08 金额日期 | `fill_contract_amount_and_dates` | 签约日=订单日 |
|
||||
| S09 货品 | `fill_contract_goods_lines` | 弹窗按操作码搜 |
|
||||
| S10 付款 | `fill_contract_payment_info` | |
|
||||
| S11 PDF | `upload_contract_document_if_needed` | 仅 YYC 传合并 PDF |
|
||||
|
|
@ -124,29 +120,13 @@
|
|||
|
||||
成功路径默认不截图(`RPA_SCREENSHOT=always` 才全截)。页面等待用 `wait_for_text` / `wait_for_element`,不要固定长睡。
|
||||
|
||||
## 返利班落点
|
||||
|
||||
班次作业在 `rpa_rebate/`,不在接口 3 请求里跑。领表 4 `action_code=1 AND contract_status=1`(合同班起草、尚未回成功),以及表 3 `contract_entry_status=0` 且已有 `contractNo` 的只录协议行。按合同号查返利中心;搜到再回 `contractStatus=2`、录协议写 `actionCode=2`。搜不到不重录合同,留给下一班。查号走页面;录协议仍用替身。接口 4 组包在 oms,path 空,不发 HTTP。
|
||||
|
||||
| 步 | 函数 | 要点 |
|
||||
| --- | --- | --- |
|
||||
| 领取 | `claim_pending` | 表 4 盖 `查号中`;表 3 `process_status` 0→3。崩溃停戳,改回 `合同班` / `0` 再跑 |
|
||||
| 查号 | `RebateFinder` / `search_contract` | 打开 `REBATE_URL` → 登录 → 采购协议 → 按合同号搜;表里出现该号才 `True`,输入框回显不算 |
|
||||
| 落表 | `build_found` + `save_group` | 起草行 `contract_status=2`;每 `oms_id` 插 `action_code=2`、`agreement_status=1`;`push_status=0` |
|
||||
| 搜不到 | `build_not_found` | 戳改回 `合同班`;表 3 改回 `0`;不插协议行 |
|
||||
|
||||
验收:`cd rpa_rebate` → `uv run pytest tests/test_job.py::test_job_finds_contract_and_writes_agreement -q`。页面:`uv run python run_find.py`,只读 `ACCEPT SUMMARY REBATE_FIND` 或 `rpa_rebate/screenshots/REBATE_FIND/ACCEPT.txt`。接口 4:`cd rpa_oms` → `uv run pytest tests/test_contract_agreement_outbound.py::test_assembles_unpushed_rows_as_document_envelope -q`。
|
||||
|
||||
录协议、映射表默认值、送审下一刀。不推接口 4,不改 `push_status`,不碰选品 / 加购 / 合同页面。
|
||||
|
||||
## 返利中心及之后
|
||||
|
||||
查号页面已接 `REBATE_FIND`。录协议班次骨架仍是替身。三把钥匙:`contractSystemNo` → `rebateOrderNo` → `receiptNoticeNo`。开页面时按合同同款:一个节点一份 `*_flow_steps.py`,验收只看 ACCEPT / 点名用例。
|
||||
`rpa_rebate/` 仍是占位。合同审批通过后才进这一段。三把钥匙:`contractSystemNo` → `rebateOrderNo` → `receiptNoticeNo`。开做时按合同同款:一个节点一份 `*_flow_steps.py`,验收只看 ACCEPT / 点名用例。
|
||||
|
||||
| 节点 | 谁做 | 要回传 | 页面要做什么 |
|
||||
| 节点 | 谁做 | 要回传 | 页面要做什么(尚未拆函数) |
|
||||
| --- | --- | --- | --- |
|
||||
| `REBATE_FIND` | RPA | 搜到 / 搜不到 | 已拆:登录 → 采购协议 → 按合同号搜 |
|
||||
| `REBATE_HEADER` | RPA | `rebateOrderNo` | 尚未拆:填必填 → 保存拿总单号 |
|
||||
| `REBATE_HEADER` | RPA | `rebateOrderNo` | 登录返利中心 → 按合同编号查总单 → 填必填 → 保存拿总单号 |
|
||||
| `REBATE_IMPORT` | RPA | 成功/失败行 | 下模板 → 按字段填 Excel → 上传 → 读导入结果 |
|
||||
| `REBATE_SUBMIT` | RPA | `submitted` | 点提交送审 |
|
||||
| `M5_CREATE_ORDER` | RPA 桌面端 | `m5OrderNo` | 录单后**停、不保存**。`M5_QC_MISSING` 必须识别准(直接转人工、不重试) |
|
||||
|
|
@ -162,10 +142,6 @@
|
|||
- 接口3:点名 `rpa_oms/tests/test_purchase_contract_receive.py` 或 `test_oracle_contract_repo.py`。
|
||||
- 选品:点名 `rpa_selection/tests/test_pipeline.py::test_split_across_malls_is_success` 或 `test_rules.py::test_margin_exactly_4_percent_passes`。
|
||||
- 加购:点名 `rpa_cart/tests/test_cart_job.py::test_job_adds_split_rows_without_insert` 或 `test_job_marks_out_of_stock`。
|
||||
- 合同班:点名 `rpa_contract/tests/test_job.py::test_job_merges_same_supplier_and_writes_table4` 或 `test_job_marks_diligence_fail`。
|
||||
- 返利班:点名 `rpa_rebate/tests/test_job.py::test_job_finds_contract_and_writes_agreement` 或 `test_job_leaves_not_found_for_next_shift`。
|
||||
- 返利查号页:只读控制台 `ACCEPT SUMMARY REBATE_FIND` 或 `rpa_rebate/screenshots/REBATE_FIND/ACCEPT.txt`。
|
||||
- 接口4:点名 `rpa_oms/tests/test_contract_agreement_outbound.py::test_assembles_unpushed_rows_as_document_envelope`。
|
||||
- 合同页:只读控制台 `ACCEPT` 或 `rpa_contract/screenshots/` 下 `ACCEPT.txt`。
|
||||
- 单测只跑点名用例。禁止跑整份带页面等待的套件当回归。
|
||||
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -11,7 +11,7 @@
|
|||
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | `POST /api/oms/v1/sales-goods` |
|
||||
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | 取 `push_status=0` 组装报文;路径仍空,不发 HTTP |
|
||||
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | `POST /api/oms/v1/purchase-contracts` |
|
||||
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 取 `push_status=0` 组装报文;路径仍空,不发 HTTP |
|
||||
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 路径空,先造数 |
|
||||
|
||||
作业互不串联:选品读表 1,合同/返利读表 3。HTTP 收件不同步跑 RPA。
|
||||
|
||||
|
|
@ -19,18 +19,18 @@
|
|||
|
||||
| 目录 | 身份 |
|
||||
| --- | --- |
|
||||
| `rpa_oms/` | 四个 HTTP 口,一个 FastAPI。接口 1、3 收件;接口 2、4 组装报文 |
|
||||
| `rpa_oms/` | 四个 HTTP 口,一个 FastAPI。接口 1、3 收件;接口 2 组装报文 |
|
||||
| `rpa_selection/` | 选品班。读表 1 `deal_flag=0`,双平台选品,写表 2 |
|
||||
| `rpa_cart/` | 加购(未做)+ 1 号药城支付落表。表 2 → 口 2 仍未推 |
|
||||
| `rpa_contract/` | 合同班。表 3 合单录入 → 表 4;接口 4 组包在 oms,path 空 |
|
||||
| `rpa_rebate/` | 返利班。表 4 查号 / 表 3 只录协议 → 表 4;查号走页面,录协议仍用替身 |
|
||||
| `rpa_contract/` | 合同班。页面录入在本包;表 3 合单、接口 4 尚未接 |
|
||||
| `rpa_rebate/` | 返利班(占位)。查号、录协议 → 表 4 |
|
||||
|
||||
| 包 | 读 | 写 | 口 |
|
||||
| --- | --- | --- | --- |
|
||||
| `rpa_selection` | 表 1 `deal_flag=0` | 表 2 `operate_type=1`,表 1 处理标志 | 不直接调口;口 2 由 `rpa_cart` 推 |
|
||||
| `rpa_cart` | 表 2 | 表 2 `push_status`;支付行 | 口 2 |
|
||||
| `rpa_contract` | 表 3 `process_status=0` | 表 4 `actionCode=1` | 口 3 收件在 oms;口 4 回写 |
|
||||
| `rpa_rebate` | 表 4 起草行 / 表 3 只录协议行 | 表 4 `contractStatus=2`、`actionCode=2` | 口 4(组包在 oms,path 空) |
|
||||
| `rpa_rebate` | 表 3 / 表 4 待查号与待录协议 | 表 4 `contractStatus=2`、`actionCode=2` | 口 4 |
|
||||
|
||||
合同怎么录、合单和回写口径见 `docs/合同系统录入流程.md`。页面控件以 `rpa_contract/contract_flow_steps.py` 为准。
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,212 +0,0 @@
|
|||
> **历史版本,已合并替代。** 当前请使用[福建洁达采购 RPA 概要设计与详细设计 V3.0](/Users/yuuuu/Documents/AutoRpa/docs/superpowers/specs/2026-09-13-jieda-rpa-integrated-design.md)。合同与返利一次接收、共用接口头明细,以新稿为准。下文保留历史内容,不作为当前实施依据。
|
||||
|
||||
# 福建洁达 RPA 四流程设计稿
|
||||
|
||||
版本:V1.9 · 2026-09-12
|
||||
配套:[技术实现说明 V2.3](/Users/yuuuu/Documents/AutoRpa/docs/superpowers/specs/2026-09-12-jieda-four-flow-impl.md)。本次为文档修订,应用代码及数据库尚未改动。
|
||||
|
||||
## 1. 范围和运行方式
|
||||
|
||||
本期做四条流程:选品加购、平台订单导出推送、合同录入、返利录入。业务可以点击各自脚本执行,也可以设置不同时间、不同频率自动执行。一条脚本处理完本批数据就退出。
|
||||
|
||||
FastAPI 接收订单中心推送的数据,校验必填字段,缺项当次返回失败及具体字段;通过后保存到 RPA 表并返回成功。RPA 按各自排期读取未处理数据,不在接收请求中同步执行四条流程。
|
||||
|
||||
业务范围以本次确认内容为准;字段依据[合同返利字段映射](/Users/yuuuu/Documents/AutoRpa/资料/合同返利系统字段映射_20260911.xlsx),选品依据[采购条件](/Users/yuuuu/Documents/AutoRpa/资料/采购平台选品条件优先项_20260902.txt),合同和返利操作参考 `资料/合同系统录入流程.md`。手工单暂不做。
|
||||
|
||||
## 2. 四条流程
|
||||
|
||||
### 2.1 选品加购 `PROCURE`
|
||||
|
||||
```text
|
||||
读取订单中心已传入的未处理销售明细
|
||||
→ 取一条批准文号去平台查询
|
||||
→ 满足采购条件:加购物车,记录已加购
|
||||
→ 不满足:记录已跳过及原因
|
||||
→ 下一条 → 本批处理完结束
|
||||
```
|
||||
|
||||
本次业务已确认,相同批准文号只有一个规格。选品按批准文号查询,不增加规格、厂家、包装和单位换算匹配。
|
||||
|
||||
每条记录已加购、已跳过或执行失败。再次执行读取未处理及需要重试的数据。人工付款在平台由业务处理,第一条脚本不等付款。
|
||||
|
||||
### 2.2 平台订单导出推送 `ORDER_PUSH`
|
||||
|
||||
```text
|
||||
从对应平台导出订单 → 保存到该平台对应的共享文件夹
|
||||
→ 按订单中心接口格式组装数据 → 推送订单中心
|
||||
→ 记录接收结果 → 本批结束
|
||||
```
|
||||
|
||||
1 药城、药帮忙分别配置共享目录及导出字段映射。保留原订单文件、来源订单号和推送结果。对码在订单中心内部完成,本方不增加对码表、候选确认或对码轮询。
|
||||
|
||||
### 2.3 合同录入 `CONTRACT`
|
||||
|
||||
```text
|
||||
读取已接收成功、未处理的合同数据
|
||||
→ 打开合同系统,选择客户,填写合同头和导入明细
|
||||
→ 汇总金额并保存合同 → 记录合同号和保存结果
|
||||
→ 调用订单中心链路接口更新对应节点 → 完成
|
||||
```
|
||||
|
||||
必填字段在接收时检查,成功落表后直接供 RPA 录入。合同系统保存成功后才更新订单中心节点;更新失败保留“合同已保存、链路更新失败”,再次处理只补接口调用。
|
||||
|
||||
按原业务流程读取尽调回显;未尽调由采购员处理,本条停止自动录入,继续下一条。沿用业务要求的通知方式,不新增审批步骤。
|
||||
|
||||
### 2.4 返利录入 `REBATE`
|
||||
|
||||
```text
|
||||
读取已接收的返利数据 → 按合同号查询采购协议
|
||||
→ 填写返利总单 → 导入细单 → 提交
|
||||
→ 记录成功或失败 → 下一条
|
||||
```
|
||||
|
||||
字段按第 4 节填写。提交失败按原流程交业务处理。
|
||||
|
||||
## 3. 选品条件
|
||||
|
||||
前四项全部满足后,按仓库名单顺序选择。
|
||||
|
||||
| 条件 | 业务要求 |
|
||||
| --- | --- |
|
||||
| 自营仓 | 业务勾选的自营仓 |
|
||||
| 盈利 | 对比销售价格,有 5% 以上盈利 |
|
||||
| 有效期 | 大于等于 12 个月 |
|
||||
| 库存 | 满足需求采购数量 |
|
||||
| 仓库顺序 | 按以下名单优先选择 |
|
||||
|
||||
广东壹号药业有限公司、昆山亿方药业有限公司、湖北亿昊药业有限公司、天津壹号药业有限公司、重庆亿昊药业有限公司、重庆壹号医药有限公司、湖南壹号药业股份有限公司、辽宁亿昊药业有限公司、河南壹号医药有限公司、甘肃壹号云医药有限责任公司企业店、陕西壹号药业有限公司。
|
||||
|
||||
## 4. 合同及返利字段
|
||||
|
||||
以下字段和行号对应 Excel 的两个业务工作表。订单中心提供的字段在接收时校验;固定默认值由程序填写,系统生成及页面回显字段不要求上游必传。
|
||||
|
||||
### 4.1 合同头
|
||||
|
||||
| 目标字段 | 取值规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 客户名称 | 订单中心 | 12 |
|
||||
| 客户编码 | 目标系统自动生成 | 11 |
|
||||
| 尽调情况 | 读取回显;未尽调停止本单自动录入并转采购员处理 | 13 |
|
||||
| 结算类型 | 收支双向 | 15 |
|
||||
| 合同版本类型 | 预付:我司定制;账期:非格式合同 | 17 |
|
||||
| 合同金额(单位元) | 预估本次合作金额 | 24 |
|
||||
| 金额(元) | 明细汇总;与导入后页面汇总核对并回填 | 25 |
|
||||
| 合同签订日期、合同开始日期、合同结束日期 | 订单中心分别提供,三个独立字段 | 26–28 |
|
||||
| 是否重大合同 | 否 | 29 |
|
||||
| 是否预付款 | 预付:是;账期:否 | 30 |
|
||||
| 付款方式 | 电汇 | 32 |
|
||||
| 付款期限 | 预付:1;账期:订单中心账期天数,缺失不默认 30 | 33 |
|
||||
| 合同具体分类 | 国内药品类、器械类品种 | 37 |
|
||||
| 运输方式 | 公路运输 | 39 |
|
||||
|
||||
### 4.2 合同明细
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 货品编码 | 订单中心 | 43 |
|
||||
| 货品名称 | 订单中心标准货品名称 | 44 |
|
||||
| 注册证号 | 订单中心 | 46 |
|
||||
| 规格、单位 | 按订单中心标准字段填报 | 47–48 |
|
||||
| 数量、单价 | 按订单中心最终采购数量、单价填报 | 49–50 |
|
||||
| 付款方式 | 电汇 | 52 |
|
||||
| 付款条件 | 订单中心;模板无此字段时不填 | 53 |
|
||||
| 付款期限 | 同合同头规则 | 54 |
|
||||
|
||||
### 4.3 返利总单
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 返利合约 | 否 | 5 |
|
||||
| 供应商编码 | 订单中心 | 6 |
|
||||
| 返利单位编码 | 订单中心 | 7 |
|
||||
| 计划员 | 订单中心,按业务给定的计划员/采购员对应关系填写 | 8 |
|
||||
| 采购结算方式 | 文案“否”;与细单代码 3 分别映射 | 9 |
|
||||
| 承付模式 | 下月 N 号结 | 10 |
|
||||
| 协议开始日期、协议结束日期 | 当天 | 11–12 |
|
||||
|
||||
### 4.4 返利细单
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 货品编码、数量、含税协议单价 | 订单中心返利源数据 | 15–17 |
|
||||
| 税率、关税税率 | 均为 0.13 | 18–19 |
|
||||
| 承付模式、承付参数 | 11、25 | 20–21 |
|
||||
| 细单开始日期 | 当天 | 22 |
|
||||
| 细单结束日期 | 本年度最后一天 | 23 |
|
||||
| 采购结算方式 | 3 | 24 |
|
||||
| 特定补偿预估值、进货补偿预估值 | 均为 0 | 25–26 |
|
||||
| 细单备注 | 订单中心订单号 | 33 |
|
||||
| 货源细单ID | 订单中心货源细单号;与订单号分列 | 34 |
|
||||
|
||||
合同明细导入列按 Excel 的 Sheet3。表中标注无需填写的字段不填;系统自动生成字段由系统处理。
|
||||
|
||||
## 5. 接口和存储
|
||||
|
||||
### 5.1 接口方向
|
||||
|
||||
| 方向 | 数据 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| 订单中心 → 本方 | 销售总单及明细 | 校验后落表,供选品使用 |
|
||||
| 订单中心 → 本方 | 合同头及明细 | 校验后落表,供合同录入使用 |
|
||||
| 订单中心 → 本方 | 返利头及明细、计划员对应信息 | 校验后落表,供返利使用 |
|
||||
| 本方 → 订单中心 | 平台导出的订单 | 按正式接口格式推送,保存接收结果 |
|
||||
| 本方 → 订单中心 | 合同保存结果及对应链路节点 | 保存成功后调用更新接口,保存返回结果 |
|
||||
|
||||
接收成功表示数据校验通过并已保存。缺必填返回字段错误,订单中心补齐后重传;数据库保存失败返回失败。相同来源单据重复传入时,按双方约定的来源标识识别,避免重复执行。
|
||||
|
||||
### 5.2 需要保存的数据
|
||||
|
||||
表名为建议,开发时先核对已有 RPA Oracle 表,能复用的直接复用。正式列名和类型按接口报文及 Excel 确定。
|
||||
|
||||
| 逻辑表/现有记录 | 保存内容 |
|
||||
| --- | --- |
|
||||
| `RPA_JD_SALES_HEAD / LINE` | 来源销售单号、明细标识、批准文号、采购数量、销售价格及上游提供的商品信息 |
|
||||
| `RPA_JD_CONTRACT_HEAD / LINE` | 第 4 节合同字段、来源标识、订单中心链路关联 |
|
||||
| `RPA_JD_REBATE_HEAD / LINE` | 第 4 节返利字段、合同/协议关联及计划员对应信息 |
|
||||
| 现有 `job / job_node` | 流程类型、来源数据关联、处理状态、已加购/已跳过结果、合同号、失败原因及执行次数 |
|
||||
| 现有 `job_artifact / job_event` | 平台订单文件路径、平台、来源订单号、发送报文和接收结果;合同链路请求和返回结果 |
|
||||
|
||||
一个业务头关联它自己的明细,订单号和明细标识分别保存。原平台订单文件保存在共享目录。选品和订单推送不另建一套主数据匹配表。
|
||||
|
||||
业务数据放 RPA Oracle,执行状态复用 MySQL 状态中心。脚本通过状态中心 API 取数和回报,由后端统一读写数据库。
|
||||
|
||||
## 6. 对当前代码的改动
|
||||
|
||||
| 位置 | 本次需要改什么 |
|
||||
| --- | --- |
|
||||
| `state_center/app/main.py` | 增加销售/合同/返利接收及取数接口;必填在接收时校验;按流程返回待处理数据 |
|
||||
| `state_center/app/db.py` 及数据库脚本 | 接入 RPA 业务表;job 增加流程类型和来源关联,原供应商+日期键改为适合各流程的来源标识 |
|
||||
| `state_center/app/web_state_machine.py` | 按流程定义节点及完成条件 |
|
||||
| `rpa_yisaiqi/agent.py`、`runtime/shell.py`、`callback_client.py` | 支持四个独立脚本入口,取数、逐条处理、回报及本批退出 |
|
||||
| 选品与平台导出模块 | 接入平台查询/加购、订单导出、共享目录保存、报文组装及推送 |
|
||||
| `contract_draft.py`、`contract_create_node.py`、`contract_flow_steps.py` | 按第 4 节字段录入,保存成功后调用链路更新接口;原页面操作可复用部分继续使用 |
|
||||
| 返利执行模块 | 按第 4 节字段导入和提交;真实页面流程需补齐 |
|
||||
| 前端列表/详情 | 按四条流程展示状态、文件、结果及失败原因,复用现有页面 |
|
||||
|
||||
当前实现仍是一条固定主链;平台真实查询/导出能力需核对现有模块,不能把 mock 当作已完成。整体改动集中在数据接入、流程拆分和接口适配,合同页面操作有可复用基础。
|
||||
|
||||
具体执行节点和开发顺序见[技术实现说明](/Users/yuuuu/Documents/AutoRpa/docs/superpowers/specs/2026-09-12-jieda-four-flow-impl.md)。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 接收数据缺必填当次失败;补齐后可重传,成功数据能被对应脚本读取。
|
||||
2. 选品合格加购、不合格跳过,处理完本批结束,不等待付款。
|
||||
3. 平台订单保存到对应共享文件夹,按接口格式推送并记录结果。
|
||||
4. 合同字段按 Excel 填写;保存成功后更新订单中心节点,更新失败不重复建合同。
|
||||
5. 返利按字段映射填写并记录提交结果。
|
||||
6. 四条脚本按各自时间和频率执行,失败可定位到具体记录,已完成数据不重复处理。
|
||||
|
||||
## 8. 待补充信息和可能出现的问题
|
||||
|
||||
以下是联调需要补齐的信息及实现注意点,不增加业务操作步骤。
|
||||
|
||||
| 项目 | 需要处理的内容 |
|
||||
| --- | --- |
|
||||
| 正式接口和文件 | 提供订单中心接收/链路更新格式、节点代码,平台导出样例、共享路径和四条排期 |
|
||||
| 5% 盈利 | 原材料未给出计算公式,需明确按什么基数计算 |
|
||||
| 合同/返利模板 | 确认实际导入模板、报文中每份合同及返利明细的范围;附件及审批要求以实际系统和业务确认为准,不先设计额外流程 |
|
||||
| 重复运行 | 同一脚本运行中再次点击不重复启动;重复传单或重新导出同一订单不重复办理 |
|
||||
| 接口超时 | 记录本次请求及错误,结果不明先核对;合同已保存时仅补链路更新 |
|
||||
| 平台/共享目录故障 | 查询失败和无合格商品分开记录;文件保存失败时记录错误,不继续推送 |
|
||||
| 当前合同代码 | 缺单价默认 1.00、同编码行定位和前 30 行扫描等行为需修正;接收缺价应直接返回错误 |
|
||||
| 旧流程影响 | 新四流程按 flow 区分,保留旧在途单的原节点和处理方式,避免改全局节点影响其他流程 |
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
> **历史版本,已合并替代。** 当前请使用[福建洁达采购 RPA 概要设计与详细设计 V3.0](/Users/yuuuu/Documents/AutoRpa/docs/superpowers/specs/2026-09-13-jieda-rpa-integrated-design.md)。合同与返利一次接收、共用接口头明细,以新稿为准。下文保留历史内容,不作为当前实施依据。
|
||||
|
||||
# 福建洁达四流程技术实现说明
|
||||
|
||||
版本:V2.3 · 2026-09-12
|
||||
配套:[设计稿 V1.9](/Users/yuuuu/Documents/AutoRpa/docs/superpowers/specs/2026-09-12-jieda-four-flow-design-codex.md)。本文为待实施说明,代码和数据库尚未修改。
|
||||
|
||||
## 1. 在现有结构上改
|
||||
|
||||
FastAPI 接收并校验订单中心数据,RPA Oracle 保存业务头明细,Oracle 继续管理 job、节点、日志和结果。四条脚本分别由业务点击或定时任务启动,不在一个 HTTP 请求里同步执行四条流程。
|
||||
|
||||
现有 `job` 增加流程类型和来源数据关联;按各流程配置节点。优先复用 `job_node / job_event / job_artifact`,不另建运行中心、对码中心或多套状态表。
|
||||
|
||||
## 2. 接收和取数
|
||||
|
||||
建议本方接收入口为 sales、contract-data、rebate-data,正式路径开发时统一命名。每个入口执行:
|
||||
|
||||
```text
|
||||
检查对应必填字段 → 不通过:返回失败和字段位置
|
||||
→ 通过:同事务保存业务头明细 → 返回成功
|
||||
```
|
||||
|
||||
字段来源和默认值按[设计稿第 4 节](/Users/yuuuu/Documents/AutoRpa/docs/superpowers/specs/2026-09-12-jieda-four-flow-design-codex.md)。系统生成和回显字段不作为上游必填。失败由订单中心修正重传,不建立等待补字段的业务作业。
|
||||
|
||||
取数接口按 flow 返回成功接收且未处理的数据。保存来源单号及明细标识,用于识别重复传入。状态中心登记或复用对应 job;领取时将其标记处理中,避免重复执行。脚本逐条回报,处理完本批退出。
|
||||
|
||||
## 3. 四条脚本
|
||||
|
||||
| flow | job 对象 | 执行节点 | 结束结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| PROCURE | 一条销售明细 | `PROCURE_ITEM_RUN` | 已加购或已跳过;执行失败记原因 |
|
||||
| ORDER_PUSH | 一次平台订单导出批次 | `ORDER_EXPORT_PUSH` | 文件已保存,应推送订单已接收;失败保留具体订单和原因 |
|
||||
| CONTRACT | 一份上游合同数据及其明细 | `CONTRACT_CREATE → CONTRACT_CHAIN_UPDATE` | 合同已保存,订单中心节点已更新 |
|
||||
| REBATE | 一份返利数据及其明细 | `REBATE_HEADER → REBATE_IMPORT → REBATE_SUBMIT` | 提交成功或失败 |
|
||||
|
||||
**选品脚本:** 按批准文号查询,依次判断自营仓、盈利、效期、库存和仓库顺序;合格加购物车,不合格记录跳过,继续下一条。同批准文号按一个规格处理,不增加属性匹配。复用节点结果记录已加购/已跳过,不等待人工付款。
|
||||
|
||||
**订单脚本:** 登录平台导出订单,保存到平台对应共享目录,按映射生成订单中心报文并推送。原文件、来源订单号、发送内容和回执保存在现有文件/日志记录中。再次处理依据已接收订单记录跳过成功项。
|
||||
|
||||
**合同脚本:** 读取成功接收的合同头明细,复用页面操作填写并保存。确认保存成功后回报合同号,进入链路更新节点。该节点通过订单中心接口适配器调用更新接口并回报结果。两步在同一条合同流程中执行,失败后恢复当前步骤;不增加独立后台调度任务。
|
||||
|
||||
**返利脚本:** 按合同号查询协议,填写总单、导入明细并提交,回报实际结果。原业务要求的未尽调合同及返利失败交业务处理,继续其他记录。
|
||||
|
||||
## 4. 数据如何保存
|
||||
|
||||
| 内容 | 保存位置 |
|
||||
| --- | --- |
|
||||
| 销售头明细 | RPA Oracle 销售表,字段按订单中心数据及选品条件 |
|
||||
| 合同头明细及链路关联 | RPA Oracle 合同表,字段按 Excel |
|
||||
| 返利头明细及计划员对应信息 | RPA Oracle 返利表,字段按 Excel |
|
||||
| 流程、处理状态、合同号、失败原因 | 现有 Oracle job/job_node |
|
||||
| 文件路径及推送/链路调用记录 | 现有 job_artifact/job_event |
|
||||
| 平台原始订单文件 | 对应平台共享文件夹 |
|
||||
|
||||
表名及列定义见设计稿第 5 节;开发前对照现场已有表和正式报文确定。头明细按来源关联保存,不用供应商+日期代替所有业务唯一标识,不自行合并合同。
|
||||
|
||||
处理状态复用现有待处理、处理中、成功、失败等状态。选品正常跳过属于处理完成,业务结果记录为“已跳过”。合同的“保存成功”和“链路更新成功”分别记录,更新失败不回退到重新建合同。
|
||||
|
||||
## 5. 代码改动
|
||||
|
||||
| 文件/模块 | 改动 |
|
||||
| --- | --- |
|
||||
| `state_center/app/main.py` | 接收校验、取数及结果接口;按 flow 创建和查询作业 |
|
||||
| `state_center/app/db.py`、数据库脚本 | RPA 业务表访问、头明细事务、job 的 flow 和来源字段;成功数据重复传入不重复建单 |
|
||||
| `state_center/app/web_state_machine.py` | 四条节点配置;旧作业继续用旧配置 |
|
||||
| `state_center/app/config.py` | 数据库、订单中心接口、平台共享路径和字段映射配置 |
|
||||
| 订单中心接口适配器 | 两项调用:平台订单推送、合同保存后的链路节点更新;路径及参数按对方协议 |
|
||||
| `rpa_yisaiqi/agent.py`、`runtime/shell.py`、`callback_client.py` | 四种脚本入口、取数循环、实际执行次数、结果回报、失败步骤恢复 |
|
||||
| 选品/平台导出模块 | 登录查询、采购筛选、加购、导出、共享文件保存及报文组装 |
|
||||
| `contract_draft.py`、`contract_create_node.py`、`contract_flow_steps.py` | 按 Excel 字段录入;确认保存结果,衔接链路接口;修正错误单价默认值及明细定位 |
|
||||
| 返利模块 | 补真实页面填写、导入、提交操作 |
|
||||
| 前端列表/详情 | 展示四种流程及已有状态,增加文件、接收结果和链路更新结果 |
|
||||
|
||||
现有固定主链、供应商+日期建单规则不能直接套用到全部新流程。商城实际登录/查询/导出模块需核对是否已有可用代码;现有 mock 代理和返利 runner 不能算真实执行已完成。
|
||||
|
||||
## 6. 开发顺序
|
||||
|
||||
1. 按正式报文和 Excel 确定字段,核对已有表,完成接收校验和落表。
|
||||
2. 增加 flow、取数、结果记录及四个脚本入口,复用现有状态中心。
|
||||
3. 完成平台导出、共享归档和订单推送。
|
||||
4. 完成合同字段适配,接上保存后的链路更新接口。
|
||||
5. 完成逐条选品加购及返利页面操作。
|
||||
6. 在实际执行机配置四条排期,按设计稿第 7 节联调验收。
|
||||
|
||||
## 7. 待确认及可能的问题
|
||||
|
||||
- 订单中心正式字段、链路节点代码、平台导出样例、共享路径及排期需提供;5% 盈利公式需明确。
|
||||
- 重复点击、重复传单、重复导出可能重复办理;使用现有状态和来源标识防重,同一流程不重入。
|
||||
- 保存或推送成功后回报失败,不能直接重做;先查看已有文件、回执或目标合同结果,再从失败步骤继续。
|
||||
- 合同链路更新失败只补接口调用;不额外增加后台维护任务。
|
||||
- 当前合同缺价填 1.00、同编码行定位及前 30 行扫描行为需要处理;具体风险见设计稿末尾。
|
||||
- 新四流程改动按 flow 区分,保留旧在途单节点,避免影响旧 OCR、对码和 M5 流程。
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
# 福建洁达:接口数据入 RPA 库(小改)
|
||||
|
||||
> 日期:2026-09-12
|
||||
> 范围更正:流程仍是 RPA,状态机不动。改的是图上那些「从系统/表来」的数据——FastAPI 调接口,落到对方 RPA Oracle,带状态,再给 RPA 用。
|
||||
> 上一版把四步理解成换主链、砍 M5,范围过大,作废。
|
||||
|
||||
---
|
||||
|
||||
## 1. 改什么、不改什么
|
||||
|
||||
不改:
|
||||
|
||||
- 艺赛旗照旧点 1 药城 / 药帮忙 / 合同系统 / 返利中心
|
||||
- 状态中心 MySQL 状态机、`start/complete/fail`、待办、幂等
|
||||
- 人工闸门:选品复核付款、未尽调、返利失败
|
||||
|
||||
改:
|
||||
|
||||
- 原先靠导 Excel / 页面上抄的**主数据、对照表、总单**,改为接口拉取
|
||||
- FastAPI 按四步拉数,写入 `RPADBTEST`(`192.168.12.244/rpa`,用户 `rpainit`)
|
||||
- 每张业务表带处理状态,失败可重拉、可跳过、可给 RPA 重跑,不把半截 JSON 丢给机器人
|
||||
|
||||
```
|
||||
对方接口 → FastAPI(四步同步) → Oracle 暂存表(带 STATUS)
|
||||
↓
|
||||
RPA 读 READY 行,点页面
|
||||
↓
|
||||
回写 SUCCESS/FAILED
|
||||
↓
|
||||
状态中心节点 complete/fail
|
||||
```
|
||||
|
||||
两套状态分工:
|
||||
|
||||
| 库 | 记什么 | 不记什么 |
|
||||
| --- | --- | --- |
|
||||
| Oracle `RPADBTEST` | 这批数据到了没有、RPA 用过没有 | 整单流程走到哪一步 |
|
||||
| MySQL `rpa_center` | job / 节点 / 待办 | 接口原始报文(必要时只存镜像) |
|
||||
|
||||
不要在 Oracle 再做一套 13 节点状态机。
|
||||
|
||||
---
|
||||
|
||||
## 2. 四步对应图上哪些数据
|
||||
|
||||
图上没有写「接口」二字。按「这格是取数/对照表,不是点按钮」切开。点按钮的仍归 RPA。
|
||||
|
||||
| 步 | FastAPI | 图上取数 | RPA 随后用它做什么 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `POST /api/jieda/sales/sync` | 订单中心「销售总单」 | 按行里的批准文号去 1 药城 / 药帮忙搜品、筛选、加购 |
|
||||
| 2 | `POST /api/jieda/match/sync` | 手工单 DocAgent 结果;提取字段;对码商家信息;回写订单中心对码表的入参 | 合同里填「客户名称」;对码表有数才能往下 |
|
||||
| 3 | `POST /api/jieda/contract-refs/sync` | 客户是否已尽调;《客户类型》预付/账期;合同明细导入用的货品行 | 选格式/非格式、填模板、导明细、回填合同金额 |
|
||||
| 4 | `POST /api/jieda/rebate-refs/sync` | 采购总单;计划员↔采购员对照;返利细单模板行 | 按合同号查总单、填必填项、导细单 |
|
||||
|
||||
第 1 步失败:RPA 不要上平台乱搜。
|
||||
第 3 步尽调=否:RPA 不填合同,走现有人工待办(邮件通知采购员)。
|
||||
第 4 步对照表缺失:RPA 不猜计划员,节点 fail。
|
||||
|
||||
---
|
||||
|
||||
## 3. Oracle 表(建议新建,前缀 `RPA_JD_`)
|
||||
|
||||
连接(只放环境变量,不进仓库):
|
||||
|
||||
```
|
||||
RPA_ORACLE_HOST=192.168.12.244
|
||||
RPA_ORACLE_PORT=1521
|
||||
RPA_ORACLE_SERVICE=rpa
|
||||
RPA_ORACLE_USER=rpainit
|
||||
RPA_ORACLE_PASSWORD=...
|
||||
```
|
||||
|
||||
本机目前 TCP `192.168.12.244:1521` 不通,建表要在能进这台库的机器上执行。先 `SELECT table_name FROM user_tables`,有现成表就映射,不要重复造。
|
||||
|
||||
### 3.1 每张业务表共用状态列
|
||||
|
||||
```
|
||||
JOB_ID VARCHAR2(64) -- 对齐状态中心 job_id
|
||||
BATCH_ID VARCHAR2(64) -- 一次 sync
|
||||
STATUS VARCHAR2(16) -- NEW / READY / RUNNING / SUCCESS / FAILED / SKIPPED
|
||||
ERR_CODE VARCHAR2(64)
|
||||
ERR_MSG VARCHAR2(500)
|
||||
ATTEMPT_NO NUMBER(4)
|
||||
RAW_JSON CLOB -- 接口原文,排障用
|
||||
SYNCED_AT DATE
|
||||
UPDATED_AT DATE
|
||||
IDEMPOTENCY_KEY VARCHAR2(128)
|
||||
```
|
||||
|
||||
行状态只表示**这条数据**:
|
||||
|
||||
- FastAPI 写入成功 → `READY`
|
||||
- 接口失败、校验失败 → `FAILED`(RPA 不抢)
|
||||
- RPA 开始用 → `RUNNING`
|
||||
- 页面做完 → `SUCCESS`
|
||||
- 业务跳过(如未尽调)→ `SKIPPED`
|
||||
|
||||
同一 `JOB_ID + IDEMPOTENCY_KEY` 再 sync:已 `SUCCESS` 的不覆盖;`FAILED` 允许重拉。
|
||||
|
||||
### 3.2 四步表
|
||||
|
||||
**第 1 步 销售总单**
|
||||
|
||||
- `RPA_JD_SALES_HEAD`:销售总单号、订单日期、客户/商家、金额、来源
|
||||
- `RPA_JD_SALES_LINE`:批准文号、品名、规格、厂家、数量、单价、平台商品编码
|
||||
|
||||
**第 2 步 对码 / 手工单**
|
||||
|
||||
- `RPA_JD_MATCH_PARTY`:对码商家信息(合同「客户名称 / 客户编码」从这里取)
|
||||
- `RPA_JD_MATCH_LINE`:对码后货品 ID/编码、是否命中;也承接 DocAgent 抽出的手工单行
|
||||
- 回写订单中心对码表:另调对方写入接口,本地留 `PUSH_STATUS`
|
||||
|
||||
**第 3 步 合同辅助**
|
||||
|
||||
- `RPA_JD_CUSTOMER_REF`:客户编码、是否已尽调、客户类型(预付/账期)、应用哪套合同模板
|
||||
- `RPA_JD_CONTRACT_LINE`:按合同明细导入模板整理好的货品行(RPA 只负责上传,不在页面上拼 Excel)
|
||||
|
||||
**第 4 步 返利辅助**
|
||||
|
||||
- `RPA_JD_PLANNER_MAP`:计划员 ↔ 采购员(可全量同步,不必每单)
|
||||
- `RPA_JD_REBATE_HEAD`:采购总单上要填进返利总单的字段
|
||||
- `RPA_JD_REBATE_LINE`:细单导入行
|
||||
|
||||
列级 DDL 等对方接口字段确定后再锁。现在只锁表职责和状态机,避免先猜 40 个列名再推翻。
|
||||
|
||||
---
|
||||
|
||||
## 4. FastAPI(状态中心里加一层,不新起服务)
|
||||
|
||||
四个同步入口,鉴权仍用 `X-Agent-Token`。
|
||||
|
||||
```
|
||||
POST /api/jieda/sales/sync
|
||||
POST /api/jieda/match/sync
|
||||
POST /api/jieda/contract-refs/sync
|
||||
POST /api/jieda/rebate-refs/sync
|
||||
```
|
||||
|
||||
入参最小集:`job_id`、`order_date`、可选 `idempotencyKey`。
|
||||
出参:`ok`、`batch_id`、`ready_count`、`failed_count`、`status`。
|
||||
|
||||
每个 sync:
|
||||
|
||||
1. 调对方接口(没有正式文档前走 fixture,契约用 Pydantic 写死)
|
||||
2. 校验必填(第 1 步无批准文号直接 `FAILED`)
|
||||
3. upsert 到对应 Oracle 表,`STATUS=READY`
|
||||
4. 写状态中心事件;该节点仍由 RPA `start/complete` 推进,**sync 成功不等于节点成功**
|
||||
|
||||
查询给 RPA:
|
||||
|
||||
```
|
||||
GET /api/jieda/{job_id}/sales
|
||||
GET /api/jieda/{job_id}/contract-refs
|
||||
...
|
||||
```
|
||||
|
||||
RPA 也可以直连 Oracle 读 `READY` 行。推荐先走 FastAPI 读,避免脚本里写死账号;直连作为艺赛旗侧备选。
|
||||
|
||||
RPA 回写状态:
|
||||
|
||||
```
|
||||
POST /api/jieda/{job_id}/rows/{table}/{id}/status
|
||||
body: { "status": "RUNNING|SUCCESS|FAILED", "errMsg": "..." }
|
||||
```
|
||||
|
||||
内部更新 Oracle 行状态。节点级成功仍走原来的 `/nodes/{node}/complete`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 和现有节点怎么接(不大改)
|
||||
|
||||
不改 `NODE_ORDER`。四步 sync 挂在现有节点**开始前**:
|
||||
|
||||
| 现有节点 | 开始前多做的 sync | RPA 行为变化 |
|
||||
| --- | --- | --- |
|
||||
| `ORDER_FETCH` | `sales/sync` | 不再以导 Excel 为唯一数据源;以 Oracle 总单/明细为准去搜品 |
|
||||
| `OCR_EXTRACT` / 对码前 | `match/sync` | 线下单走 DocAgent 结果表;商家名称从表读 |
|
||||
| `CONTRACT_CREATE` | `contract-refs/sync` | 尽调、客户类型、明细行从表读;未尽调不点填报 |
|
||||
| `REBATE_HEADER` / `REBATE_IMPORT` | `rebate-refs/sync` | 总单必填项、计划员、细单行从表读 |
|
||||
|
||||
`PRODUCT_MATCH`、`CONTRACT_APPROVE_WAIT`、`M5_*` 先不动。新图没画 M5,不等于本期删 M5。
|
||||
|
||||
艺赛旗每个泳道开头加一个「取数」代码块:调对应 `sync`(或确认 scheduler 已 sync)→ 读 `READY` → 再点页面。失败把行打成 `FAILED` 并调节点 `fail`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 代码改动面(小)
|
||||
|
||||
| 位置 | 做什么 |
|
||||
| --- | --- |
|
||||
| `state_center/app/config.py` | 增加 `RPA_ORACLE_*`,密码只走环境变量 |
|
||||
| `state_center/app/rpa_oracle.py` 新建 | 连接、upsert、按 job 查询、改行状态 |
|
||||
| `state_center/app/jieda_sync.py` 新建 | 四个 sync 的编排 + Pydantic |
|
||||
| `state_center/app/adapters/` 新建 | 订单中心 / 尽调 / 客户类型 / 计划员 的 HTTP client,先 fixture |
|
||||
| `state_center/app/main.py` | 挂 4 个 sync + 查询 + 行状态 |
|
||||
| `state_center/sql/rpa_jd_oracle.sql` 新建 | 上面那些表的 DDL |
|
||||
| `rpa_yisaiqi` | 合同/选品/返利流程开头读 FastAPI 或 Oracle,不改状态机顺序 |
|
||||
| 测试 | sync 幂等、失败不覆盖 SUCCESS、无批准文号 → FAILED、未尽调 → SKIPPED |
|
||||
|
||||
密码和 TNS 不进 git。`connection.local.json` 已有先例。
|
||||
|
||||
---
|
||||
|
||||
## 7. 落地顺序
|
||||
|
||||
1. 能连 `192.168.12.244` 的机器上看 `rpainit` 已有表,避免重名。
|
||||
2. 建第 1 步两张表 + `sales/sync` fixture,RPA 能把批准文号读出来。
|
||||
3. 第 3 步尽调 / 客户类型(合同填报刚需)。
|
||||
4. 第 4 步计划员对照 + 采购总单。
|
||||
5. 第 2 步对码回写和 DocAgent,等对方对码接口。
|
||||
|
||||
没有接口文档时,每步用一份 JSON fixture 冒充对方响应,表结构和 FastAPI 先定。
|
||||
|
||||
---
|
||||
|
||||
## 8. 还缺的输入
|
||||
|
||||
- 四个接口的 URL、鉴权、字段(尤其销售总单、尽调、客户类型、计划员对照)
|
||||
- `rpainit` 是否已有同类暂存表
|
||||
- RPA 读数:走我们 FastAPI,还是艺赛旗直连 Oracle
|
||||
|
||||
这三项不齐也能先建表和 fixture;接真接口时只换 adapter。
|
||||
|
|
@ -1,683 +0,0 @@
|
|||
# 福建洁达采购 RPA 详细设计
|
||||
|
||||
| 文档属性 | 内容 |
|
||||
| --- | --- |
|
||||
| 版本 | V3.1 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 配套概要设计 | [《福建洁达采购 RPA 概要设计》](./2026-09-13-jieda-rpa-overview-design.md) |
|
||||
| 状态 | 设计评审稿;未实施代码或建表 |
|
||||
| 适用范围 | 接口接收、四条流程执行、状态记录、运维与异常恢复 |
|
||||
|
||||
本文件承接概要设计,规定可供开发、测试和运维执行的表、字段、状态、接口和处理规则。外部正式字段名、节点代码和现场 Oracle 表结构以第 9 章未决事项为准。
|
||||
|
||||
**本期范围收敛:**只实现订单接收、平台订单推送、选品、合同、返利四条流程及基本状态记录。版本冲突、内容哈希、执行令牌、文件证据表、复杂事件分类和自动化核验接口不作为本期新增必做项;现场已有能力可以复用,确需新增时另行确认。
|
||||
|
||||
## 4. 详细设计:数据模型
|
||||
|
||||
### 4.1 实体关系和存储位置
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
IF[接口接收记录<br/>RPA_JD_IF_REQ]
|
||||
SH[销售接口头<br/>RPA_JD_SALES_H]
|
||||
SL[销售接口明细<br/>RPA_JD_SALES_L]
|
||||
OH[合同返利共用头<br/>RPA_JD_ORDER_H]
|
||||
OL[合同返利共用明细<br/>RPA_JD_ORDER_L]
|
||||
J[流程作业<br/>JOB]
|
||||
N[作业节点<br/>JOB_NODE]
|
||||
E[操作事件<br/>JOB_EVENT]
|
||||
A[文件证据<br/>JOB_ARTIFACT]
|
||||
P[订单推送台账<br/>JD_ORDER_PUSH]
|
||||
IF --> SH
|
||||
IF --> OH
|
||||
SH --> SL
|
||||
OH --> OL
|
||||
SL --> J
|
||||
OH --> J
|
||||
J --> N
|
||||
J --> E
|
||||
J --> A
|
||||
J --> P
|
||||
```
|
||||
|
||||
*图中括号内为数据库表名,前面的中文为业务含义。*
|
||||
|
||||
本节所有实体均存放在同一套 **Oracle 数据库**,不再区分接口库、状态库或 MySQL。实体按用途分为三组:
|
||||
|
||||
| 分组 | 表 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| 接口输入表 | `RPA_JD_IF_REQ`、`RPA_JD_SALES_H/L`、`RPA_JD_ORDER_H/L` | 保存订单中心传入的原始报文、销售数据及合同/返利共用数据 |
|
||||
| 流程业务表 | `JOB`、`JOB_NODE`、`JOB_EVENT`、`JOB_ARTIFACT` | 保存四条流程的作业、节点执行、操作记录和文件证据 |
|
||||
| 订单推送表 | `JD_ORDER_PUSH` | 保存平台订单导出后推送订单中心的请求、回执和推送状态 |
|
||||
|
||||
接口输入数据只保存一份。合同和返利通过相同的 `INPUT_ID`、`INPUT_VERSION` 关联;两条流程分别在 `JOB` 中记录自己的状态,不复制两套合同/返利输入表。已有表能承载上述用途时优先复用,只有确实缺少的表或字段才在 Oracle 中新增。图中的关联由主键、外键或业务关联字段实现,具体以现场 Oracle 表结构核对结果为准。
|
||||
|
||||
接口表内的合同/返利状态不各写一份。运维总览按 INPUT_ID 汇总两份 job 与其节点,展示三个独立标志:合同保存、链路更新、返利办理。读写来源见第 5 章。
|
||||
|
||||
### 4.2 接收记录 `RPA_JD_IF_REQ`
|
||||
|
||||
一行是一回接收尝试;错误请求也能查到,不能依赖先有 job 才记录。
|
||||
|
||||
| 字段 | 建议类型 | 含义与约束 |
|
||||
| --- | --- | --- |
|
||||
| REQUEST_LOG_ID | VARCHAR2(64) PK | 本方生成,返回给上游和运维定位 |
|
||||
| TRACE_ID | VARCHAR2(64) | 本次请求关联应用日志 |
|
||||
| SOURCE_SYSTEM / MESSAGE_ID | VARCHAR2(32) / VARCHAR2(128) | 调用方及其请求标识;MESSAGE_ID 格式由对方提供 |
|
||||
| DATA_TYPE | VARCHAR2(24) | SALES 或 ORDER_COMMON;没有独立 REBATE 接收类型 |
|
||||
| BODY_HASH | VARCHAR2(64) | 规范化业务报文哈希,识别同请求内容是否一致 |
|
||||
| RECEIVE_STATUS | VARCHAR2(16) | ACCEPTED、REJECTED、DUPLICATE、CONFLICT |
|
||||
| ORIGINAL_REQ_ID | VARCHAR2(64),可空 | 重复请求关联首次成功记录 |
|
||||
| RAW_BODY / RESPONSE_BODY | CLOB | 实际业务报文和返回内容,不记录鉴权口令 |
|
||||
| ERROR_CODE / ERROR_DETAIL | VARCHAR2(64) / CLOB | 字段路径、明细标识、错误说明 |
|
||||
| RECEIVED_AT / RESPONDED_AT | TIMESTAMP | 收到及生成返回结果的时间 |
|
||||
| REGISTER_STATUS | VARCHAR2(8),可空 | 成功记录的作业登记标志:PENDING、DONE、FAILED;非成功记录为空 |
|
||||
| REGISTER_ERROR / REGISTERED_AT | VARCHAR2(500) / TIMESTAMP | 作业登记失败原因及最终登记时间 |
|
||||
|
||||
RECEIVE_STATUS 表示接收事实,REGISTER_STATUS 仅表示是否已登记作业,均不代表合同或返利完成。成功接收同事务保存接口头明细并置 REGISTER_STATUS=PENDING;脚本取数前补登记,完成后更新 DONE。登记失败在下一次脚本取数重试,无需上游重传。
|
||||
|
||||
约束:对 ACCEPTED 记录建立 `(SOURCE_SYSTEM, MESSAGE_ID)` 条件唯一索引;Oracle 可用 CASE 表达式索引实现。失败/重复尝试保留独立行,不占用成功消息键。索引还包括 `(DATA_TYPE, REGISTER_STATUS, RECEIVED_AT)`、`TRACE_ID`、`(SOURCE_SYSTEM, MESSAGE_ID)`。
|
||||
|
||||
### 4.3 接口头公共字段
|
||||
|
||||
销售头和共用订单头均使用以下控制列。
|
||||
|
||||
| 字段 | 建议类型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| INPUT_ID | VARCHAR2(64) PK | 本方输入身份,跨流程统一关联 |
|
||||
| REQUEST_LOG_ID | VARCHAR2(64) FK | 首次成功接收记录 |
|
||||
| SOURCE_SYSTEM / SOURCE_DOC_ID | VARCHAR2(32) / VARCHAR2(128) | 上游稳定业务标识;与展示单号分开 |
|
||||
| SOURCE_DOC_NO | VARCHAR2(128) | 业务查询展示用单号 |
|
||||
| INPUT_VERSION | VARCHAR2(64) | 上游版本;未提供时首版用固定 1,变更走第 6.2 节冲突规则 |
|
||||
| DATA_HASH | VARCHAR2(64) | 本对象完整头明细内容哈希 |
|
||||
| DATA_STATUS | VARCHAR2(16) | READY:成功接收的完整输入;本期不另设待补字段状态 |
|
||||
| CREATED_AT | TIMESTAMP | 成功保存时间 |
|
||||
|
||||
头表唯一键为 `(SOURCE_SYSTEM, SOURCE_DOC_ID, INPUT_VERSION)`。明细以 `(INPUT_ID, SOURCE_LINE_ID)` 唯一,关联本头,不以货品编码唯一。成功输入不由 RPA 改写,处理结果记录在 Oracle 业务表中。
|
||||
|
||||
### 4.4 销售接口头明细 `RPA_JD_SALES_H / L`
|
||||
|
||||
| 表 | 业务字段 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| SALES_H | 第 4.3 节公共字段、BUSINESS_DATE | 一份销售总单 |
|
||||
| SALES_L | LINE_ID、INPUT_ID、SOURCE_LINE_ID、APPROVAL_NO、PURCHASE_QTY、SALES_PRICE | 逐条查询所需批准文号、需求采购数量、销售价格 |
|
||||
| SALES_L 可选描述 | GOODS_NAME、SPEC、MANUFACTURER、UNIT | 按上游原值保留供查看,不作为新增属性匹配门槛 |
|
||||
|
||||
LINE_ID 建议 VARCHAR2(64),数量 NUMBER(18,4),价格 NUMBER(18,6),标识/文号 VARCHAR2(128),描述 VARCHAR2(256)。最终长度和精度按真实接口及目标系统核对。
|
||||
|
||||
### 4.5 合同/返利共用接口头明细 `RPA_JD_ORDER_H / L`
|
||||
|
||||
一份头代表上游明确给定的一组合同及对应返利数据,范围由报文的 SOURCE_DOC_ID 和明细集合确定。首版不按供应商+日期自行合单。若实际存在一组数据对应多份合同/返利,需要上游给出组标识,见第 12 章。
|
||||
|
||||
| 位置 | 建议字段 | 含义/使用流程 |
|
||||
| --- | --- | --- |
|
||||
| ORDER_H | 第 4.3 节公共字段 | 合同和返利共用输入标识 |
|
||||
| ORDER_H | CUSTOMER_NAME、CUSTOMER_TYPE、CREDIT_DAYS | 合同客户名称、预付/账期类型、账期天数 |
|
||||
| ORDER_H | CONTRACT_SIGN_DATE、CONTRACT_START_DATE、CONTRACT_END_DATE | 三个独立合同日期 |
|
||||
| ORDER_H | SUPPLIER_CODE、REBATE_UNIT_CODE、PLANNER、PURCHASER | 返利供应商、返利单位、计划员及采购员对应信息 |
|
||||
| ORDER_H | CHAIN_REF | 订单中心链路/源业务关联;精确结构按正式接口 |
|
||||
| ORDER_L | LINE_ID、INPUT_ID、SOURCE_LINE_ID、ORDER_NO、SOURCE_DETAIL_ID | 本方行 ID、来源行、订单号、货源细单号;订单号与货源细单号分列 |
|
||||
| ORDER_L | GOODS_CODE、GOODS_NAME、REG_CERT_NO、SPEC、UNIT | 标准货品编码/名称、注册证号、规格、单位 |
|
||||
| ORDER_L | PURCHASE_QTY、PURCHASE_UNIT_PRICE | 采购数量和单价;用于合同明细 |
|
||||
| ORDER_L | REBATE_QTY、REBATE_TAX_PRICE | 返利数量和含税协议单价;用于返利明细 |
|
||||
| ORDER_L | PAYMENT_CONDITION,可空 | 实际合同模板需要时从同一报文提供 |
|
||||
|
||||
同一次报文中数量如只有一个来源字段且供两流程共用,映射层同时引用它;不因此要求上游重复传两个数量。采购单价与含税协议单价按 Excel 的不同含义分别映射,未经接口确认不互相代替。
|
||||
|
||||
建议数量 NUMBER(18,4)、价格 NUMBER(18,6)、金额 NUMBER(18,2)、日期 DATE;名称 VARCHAR2(256)、编码 VARCHAR2(128)、类型 VARCHAR2(16)、账期 NUMBER(6)、CHAIN_REF CLOB 或按正式关联键拆列。固定默认值不强迫订单中心传入,在节点执行输入中生成并记录所用模板/映射版本。
|
||||
|
||||
**此处没有 `RPA_JD_CONTRACT_HEAD / RPA_JD_REBATE_HEAD` 两套输入表。两流程取数的 INPUT_ID 必须相同。**
|
||||
|
||||
### 4.6 业务作业与节点记录
|
||||
|
||||
| 表/字段 | 现有或新增 | 详细用途 |
|
||||
| --- | --- | --- |
|
||||
| job.flow_code / flow_version | 新增 VARCHAR(24) / VARCHAR(16) | 旧作业归原主链,新作业区分四条流程 |
|
||||
| job.input_id / input_version | 新增 VARCHAR(64) | 指向 Oracle 成功输入;ORDER_PUSH 指向本方导出批次 ID |
|
||||
| job.source_line_id | 新增 VARCHAR(128),头级作业用空串 | PROCURE 对应一条销售明细;合同/返利对应头 |
|
||||
| job.business_key | 复用 | 改为上述来源关联的展示/定位键;防重以组合唯一约束为准 |
|
||||
| job.status / current_node_code | 复用 | 当前执行状态和恢复节点 |
|
||||
| job.contract_system_no / rebate_order_no | 复用 | 合同 job 保存合同号,返利 job 保存返利总单/协议编号 |
|
||||
| job.next_check_at | 复用 | 可重试失败或外部等待的下次处理时间 |
|
||||
| job.execution_token / execution_host | 本期不新增 | 如现有状态中心已有执行占用机制则继续复用,否则按单实例运行约束处理 |
|
||||
| job_node.attempt_no、status、output_json | 复用 | 每次尝试独立记录;输入引用、业务结果和外部编号 |
|
||||
| job_node.started_at / ended_at / duration_ms | 复用 | 操作开始结束及耗时 |
|
||||
| job_node.error_code / error_summary | 复用 | 可筛选错误分类和具体原因 |
|
||||
| job_node.executor_run_id / callback_idempotency_key | 复用 | 一次脚本运行 ID、稳定回报键,重发回报不重复推进 |
|
||||
|
||||
本期按 `(flow_code, input_id, source_line_id)` 识别同一流程作业;已有系统若必须保留版本字段,可作为可选扩展,不因规则升级自动创建第二份作业。
|
||||
|
||||
共用数据成功接收后登记两份 job:CONTRACT 初始 PENDING,REBATE 初始 WAITING_EXTERNAL,原因为 WAIT_CONTRACT。同一 Oracle 输入即便登记调用重复,也只建立这一对作业。一次接收多组业务对象则逐头建立,不共享一个总完成标志。
|
||||
|
||||
旧列兼容迁移:保留 job_id、business_key、source_type、current_node_code、status、created_by 非空;supplier_name 和 order_date 改为允许空值,新来源数据有值时填入。旧入口继续校验其原必填,不给新作业补造供应商或日期。business_key 由新唯一键各项组合生成可读/哈希值;created_by 记录业务启动人或计划任务账号。
|
||||
|
||||
| flow | source_type | supplier_name / order_date | 首节点 |
|
||||
| --- | --- | --- | --- |
|
||||
| PROCURE | 本方定义 ORDER_CENTER | 供应商为空;日期取销售业务日(若提供) | PROCURE_ITEM_RUN |
|
||||
| ORDER_PUSH | 明确的平台配置代码 | 文件批次可含多供应商,供应商为空;日期不强制单日 | ORDER_EXPORT_FILE |
|
||||
| CONTRACT / REBATE | ORDER_CENTER | 接口明确提供的展示供应商及业务日期;合同三个日期仍取接口独立字段 | CONTRACT_CREATE / REBATE_HEADER |
|
||||
|
||||
source_type 上述取值是本方分类建议;药帮忙使用单独配置代码,不映射成旧 XYY。若任一新增标识超出现有列长度,迁移时扩列。旧 job 赋旧 flow/version 及唯一 input_id,新组合唯一键对全部历史行先回填再建立。
|
||||
|
||||
### 4.7 逐订单推送记录 `JD_ORDER_PUSH`(按现有表复用,缺少时再新增)
|
||||
|
||||
文件/操作日志不能替代按订单查询的处理标志,因此该表只保存推送控制与结果,不复制完整平台业务表。
|
||||
|
||||
| 字段 | 建议类型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| id | BIGINT PK | 推送记录 ID |
|
||||
| platform / account_key / source_order_id | VARCHAR(32) / VARCHAR(64) / VARCHAR(128) | 来源订单唯一键,不按文件名去重 |
|
||||
| source_order_no / job_id / artifact_id | VARCHAR(128) / VARCHAR(64) / BIGINT | 展示单号、首次导出 job、原始文件关联 |
|
||||
| content_hash / request_key | VARCHAR(64) / VARCHAR(128) | 订单内容识别和本次固定请求标识 |
|
||||
| push_status | VARCHAR(16) | PENDING、SENDING、ACCEPTED、REJECTED、UNKNOWN |
|
||||
| request_body / response_body | JSON | 本次实际结构化发送内容、返回内容或所属批次引用 |
|
||||
| receipt_no / error_code / error_message | VARCHAR(128) / VARCHAR(64) / VARCHAR(500) | 回执及失败原因 |
|
||||
| attempt_count / next_retry_at | INT / DATETIME | 发送次数及重试时间 |
|
||||
| created_at / last_sent_at / accepted_at | DATETIME | 关键时点 |
|
||||
|
||||
唯一键 `(platform, account_key, source_order_id)`;相同订单相同内容复用结果,内容变化不能覆盖旧成功记录,记录冲突交运维核对。每次发送详细历史写 job_event,最近状态保存在此表。多个文件含同一订单时,在各导出 job 的日志中记录对应 artifact_id 与台账 id。
|
||||
|
||||
接口若支持批量发送,固定请求键绑定整份实际请求体;同键重发内容不变。逐单成功的回执更新对应台账;只发失败子集时生成新请求键并保留与原批次关联。没有逐单回执时按对方整批接收语义记录,不凭本方猜部分成功。
|
||||
|
||||
## 5. 详细设计:标志字段与取数条件
|
||||
|
||||
### 5.1 状态来源
|
||||
|
||||
| 业务标志 | 保存/计算位置 | 何时改变 |
|
||||
| --- | --- | --- |
|
||||
| 接收是否成功 | IF_REQ.RECEIVE_STATUS | 当前接收请求完成时 |
|
||||
| 作业是否登记 | IF_REQ.REGISTER_STATUS | 在 Oracle 中幂等建立本次所需 job 后 |
|
||||
| 选品处理结果 | PROCURE job_node.output_json.result | 加购或正常跳过回报时:ADDED / SKIPPED |
|
||||
| 合同保存状态 | CONTRACT_CREATE 节点状态及 result | 开始、保存成功、失败或结果未知回报 |
|
||||
| 链路更新状态 | CONTRACT_CHAIN_UPDATE 节点状态及 result | 合同保存后调用订单中心,单独记录结果 |
|
||||
| 返利状态 | REBATE job.status / current_node_code | 等合同、执行、失败、提交成功 |
|
||||
| 平台订单接收状态 | JD_ORDER_PUSH.push_status | 每次发送和回执记录 |
|
||||
|
||||
运维接口返回 `contract_save_status / chain_update_status / rebate_status` 三列,是以上权威记录的汇总字段,**不再写回接口表形成第二套状态**。当前节点按 job 指针及该节点最新 attempt 取值,历史成功节点保留,不用整个合同 job.status 代替保存状态。
|
||||
|
||||
### 5.2 状态转换
|
||||
|
||||
| job 状态 | 含义 | 后续动作 |
|
||||
| --- | --- | --- |
|
||||
| PENDING | 已登记,等待执行 | 对应脚本领取 |
|
||||
| RUNNING | 已被领取且正在执行 | 接收匹配 execution_token 的回报 |
|
||||
| SUCCESS | 本流程应做步骤完成 | 常规取数不再领取 |
|
||||
| FAILED_RETRYABLE | 明确失败,可重试当前步骤 | 到 next_check_at 后或运维重试,新增 attempt |
|
||||
| WAITING_EXTERNAL | 前置尚未完成或结果待核对 | 按 wait_reason 处理,不能一律自动重做 |
|
||||
| WAITING_USER / FAILED_MANUAL | 业务或运维需介入 | 在原待办上处理、核对并记录动作 |
|
||||
| CANCELLED | 明确停止办理 | 保留原数据和历史 |
|
||||
|
||||
wait_reason 建议保存在 job 新增 VARCHAR(32) 字段:WAIT_CONTRACT、PROTOCOL_NOT_READY、RESULT_UNKNOWN。前两种下次检查条件;RESULT_UNKNOWN 需要核对外部结果,确认失败才可重试。
|
||||
|
||||
节点复用 PENDING/RUNNING/SUCCESS/FAILED,未知结果用节点 `WAITING_EXTERNAL` 并在 output_json 记 `result=UNKNOWN`;需要新增相应前后端状态支持。未尽调转人工保留待办,不能把“停止自动办理”显示为“合同已保存”。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PENDING
|
||||
PENDING --> RUNNING: 领取
|
||||
RUNNING --> SUCCESS: 完成
|
||||
RUNNING --> FAILED_RETRYABLE: 明确可重试失败
|
||||
RUNNING --> WAITING_EXTERNAL: 前置等待或结果未知
|
||||
RUNNING --> WAITING_USER: 业务处理
|
||||
FAILED_RETRYABLE --> PENDING: 重试当前步骤
|
||||
WAITING_EXTERNAL --> PENDING: 前置满足或核对未成功
|
||||
WAITING_EXTERNAL --> SUCCESS: 核对本流程已完成
|
||||
WAITING_USER --> PENDING: 处理后恢复未完成步骤
|
||||
PENDING --> CANCELLED: 停止办理
|
||||
```
|
||||
|
||||
*中文对照:`PENDING`=待执行,`RUNNING`=执行中,`SUCCESS`=成功,`FAILED_RETRYABLE`=可重试失败,`WAITING_EXTERNAL`=等待外部结果,`WAITING_USER`=等待人工处理,`CANCELLED`=已取消。*
|
||||
|
||||
上图为通用 job 状态。合同保存结果核对成功时若链路尚未更新,实际转移到链路节点 PENDING,不能直接标整个合同 SUCCESS。
|
||||
|
||||
### 5.3 领取规则
|
||||
|
||||
| flow | 读取范围及条件 | 成功/失败记录 |
|
||||
| --- | --- | --- |
|
||||
| PROCURE | SALES 数据 READY;分别按 1 号药城、药帮忙的平台配置领取;行 job=PENDING,或到期可重试 | 两个平台使用同一套采购条件;合格 ADDED、不合格 SKIPPED 均结束行 job;错误单独失败 |
|
||||
| ORDER_PUSH | 对应平台导出任务及历史未完成台账;ACCEPTED 不重发 | 归档文件及每单 push_status |
|
||||
| CONTRACT | ORDER_COMMON 数据 READY;合同 job 待执行,按 current_node 恢复 | CREATE 成功保存合同号;CHAIN 失败只重试 CHAIN |
|
||||
| REBATE | 同 INPUT_ID/版本的 CREATE 节点确认 SUCCESS,合同号非空;返利待执行或到期重试 | 从共用接口表读取返利字段;读取合同 job 合同号;提交后 SUCCESS |
|
||||
|
||||
新录入与重试均覆盖历史未完成记录,不只筛选当天。创建返利 job 时可处于 WAIT_CONTRACT,合同失败不改它的接口输入;合同成功后,返利下一次扫描将其转 PENDING。
|
||||
|
||||
领取使用现有状态中心的领取机制;同一条记录同一时刻只允许一个脚本处理。脚本中断后由运维确认并恢复,不自动启动第二个执行器。
|
||||
|
||||
### 5.4 节点定义与订单导出生命周期
|
||||
|
||||
| flow | 节点链 | 正常结束条件 |
|
||||
| --- | --- | --- |
|
||||
| PROCURE | PROCURE_ITEM_RUN | 本行 ADDED 或 SKIPPED |
|
||||
| ORDER_PUSH | ORDER_EXPORT_FILE → ORDER_CENTER_PUSH | 文件完整归档后,解析及推送该批应发送订单,均确认接收 |
|
||||
| CONTRACT | CONTRACT_CREATE → CONTRACT_CHAIN_UPDATE | 保存及链路更新分别成功;未尽调为当前节点人工待办,不另建供数流程 |
|
||||
| REBATE | REBATE_HEADER → REBATE_IMPORT → REBATE_SUBMIT | 正常提交成功;首次领取前检查合同保存条件 |
|
||||
|
||||
ORDER_PUSH 脚本先领取历史未完成批次;再按当前排期需要产生 export_batch_id 并登记新 job(input_id=export_batch_id,input_version=1,source_line_id=空串)。新 job 从 ORDER_EXPORT_FILE 领取执行。导出失败保留该 job;已保存文件和路径写 job_artifact,node_code=ORDER_EXPORT_FILE,恢复时先查已有归档文件。
|
||||
|
||||
归档成功后推进 ORDER_CENTER_PUSH:解析文件、按源订单建立/复用 JD_ORDER_PUSH、保存固定请求并推送。本批所有源订单到台账 ID 的关联写入该 job 的节点 output_json(大批量可用 artifact 保存清单),因此同一台账复用于多个文件时仍可判断每个批次是否完成。首次 job_id/artifact_id 不覆盖,后续文件关联由各批次记录持有。
|
||||
|
||||
源订单均为 ACCEPTED,或本轮确无可推送订单时,批次 job SUCCESS;零数据结果记 NO_DATA 并显示零条,不创建空订单。部分失败保留当前推送节点,已接收项跳过;同订单不同内容记冲突,阻止该批盲目完成,交运维核对。脚本退出与该批次 job 成功是两件事。
|
||||
|
||||
## 6. 详细设计:接口与交互时序
|
||||
|
||||
### 6.1.1 平台订单推送中的对码原始数据
|
||||
|
||||
平台订单推送给订单中心时,RPA 不传本方判断出的货品编码作为对码结论,而是传订单中心完成判断所需的原始订单信息。每条订单明细至少需要保留并按正式接口字段映射传递:
|
||||
|
||||
| 数据组 | 建议字段 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 来源标识 | 平台代码、平台账号、源订单号、父订单号(如有)、源订单明细号、导出批次号 | 订单中心识别来源、拆单/明细及幂等关系 |
|
||||
| 商品原始信息 | 平台商品编码(如有)、平台品名、批准文号、通用名/商品名称、规格、生产厂家、单位 | 订单中心进行货品对码;平台没有的字段传空并按接口规则处理 |
|
||||
| 采购信息 | 实际采购数量、采购单价、订单金额、订单日期、订单状态 | 形成标准采购订单并供后续合同数据使用 |
|
||||
| 商家及追溯 | 平台商家/供应商原始标识、仓库、文件名、文件哈希、原始行号 | 追溯和订单关联;文件信息作为来源追踪,不替代业务订单键 |
|
||||
|
||||
其中,批准文号、平台品名、通用名/商品名称、规格、生产厂家、单位属于“给订单中心判断用的原始值”,不代表 RPA 已经完成对码。平台订单接口的正式必填字段、字段名称、枚举和缺项规则由订单中心确认;不能只传一个批准文号、数量和金额就宣称对码数据完整。
|
||||
|
||||
订单中心处理后至少应能按源订单号/源明细号返回或在合同共用接口数据中提供:标准货品编码、标准货品名称、注册证号、规格、单位,以及合同/返利所需的采购数量、单价、货源细单号等字段。返回方式可以是订单接收后的后续推送或双方约定的查询接口,但必须保持与源订单明细的关联。本方只使用该标准结果填合同和返利,不在本地补做对码。
|
||||
|
||||
### 6.1 接口清单
|
||||
|
||||
| 接口/方向 | 状态 | 主要契约 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/jieda/inbound/sales` | 本方拟新增 | 完整销售对象,接收校验后落表 |
|
||||
| `POST /api/jieda/inbound/order-data` | 本方拟新增 | **一次传入合同及返利共同需要的数据**,返回接收记录 ID 和 INPUT_ID |
|
||||
| `POST /api/flows/{flow_code}/jobs/claim` | 复用或按现有方式实现 | 按流程领取待处理作业并返回输入关联 |
|
||||
| `POST /api/flows/ORDER_PUSH/jobs` | 本方拟新增 | 导出前按 export_batch_id 幂等登记平台/账号/范围及 job,导出失败也有作业可查 |
|
||||
| `GET /api/jobs/{job_id}/input` | 本方拟新增 | 对应接口头明细;返利额外返回关联合同 job 及合同号 |
|
||||
| `POST /api/jobs/{job_id}/nodes/{node}/{action}` | 复用 start/complete/fail | 保存结果、编号和错误原因 |
|
||||
| `POST /api/jobs/{job_id}/push-records` | 按现有回报方式实现 | 保存订单推送结果;不强制新增独立接口 |
|
||||
| `POST /api/jobs/{job_id}/retry` | 复用并修正 | 只恢复当前失败步骤,记录操作者及原因 |
|
||||
| 平台订单接收 API(本方→订单中心) | 外部契约待提供 | 来源订单、对码原始字段、数量金额及实际接收回执 |
|
||||
| 链路节点更新 API(本方→订单中心) | 外部契约待提供 | 来源链路关联、对应节点、已保存合同标识及业务结果 |
|
||||
|
||||
订单推送由 ORDER_PUSH 脚本、链路更新由 CONTRACT 脚本通过统一客户端调用。调用前通过状态中心记录固定请求,调用后保存回执。状态中心不同时后台发送同一请求。外部正式路径、业务枚举和鉴权由订单中心提供。
|
||||
|
||||
### 6.2 请求、校验及重复处理
|
||||
|
||||
接收报文在本方适配层统一表示为 `message_id + objects[]`。每个对象有 source_doc_id、input_version(可选)、header、lines。ORDER_COMMON 的 header/lines 包含第 4.5、7 章共同字段,不是分别推 contract 和 rebate。
|
||||
|
||||
| 返回情况 | 本方建议 HTTP | 业务返回及持久化 |
|
||||
| --- | --- | --- |
|
||||
| 缺字段/格式不合法/空明细 | 422 | REJECTED,errors 含对象标识、行标识及字段路径;无业务头明细 |
|
||||
| 全部校验通过且保存成功 | 200 | ACCEPTED、request_log_id、input_ids;表示接收成功 |
|
||||
| 已接收同消息同内容 | 200 | DUPLICATE,返回原 input_ids,另记接收尝试 |
|
||||
| 已接收同标识不同内容 | 409 | CONFLICT,旧成功输入及作业不变 |
|
||||
| 保存失败 | 503 | STORE_FAILED+trace_id;不返回接收成功 |
|
||||
| 鉴权失败 | 401/403 | 拒绝,不建立业务作业 |
|
||||
|
||||
首版整次请求校验通过后,同一个 Oracle 事务保存成功接收记录及全部对象头明细。任一必填缺失则整次失败,返回全部可识别字段错误;上游可按完整单据分批发送。缺的是返利字段也当次返回,因为这一份数据承担两个流程。
|
||||
|
||||
客户类型、三个合同日期、合同明细字段,以及供应商/返利单位/计划员/订单号/货源细单号/含税协议单价在本次共用数据中按第 7 章校验。合同号是本方执行结果,接收时允许不存在。固定值、系统生成字段、页面尽调回显不要求上游提供。
|
||||
|
||||
相同业务对象换 message_id 再传,由头表来源+版本唯一键识别:内容相同引用原 INPUT_ID,内容不同冲突。没有源版本号时,哈希只判断内容同异,不自动判定“哪条更新”。已成功接收的同 SOURCE_DOC_ID 改内容或改版本,本期返回 INPUT_CONFLICT,不自动创建第二组合同/返利 job;这是按业务标识检查,不能仅依赖版本唯一索引。运维和订单中心确认改单处理后再另行安排,原作业和结果保持可查。此前校验失败的修正重传可正常接收。
|
||||
|
||||
接收失败日志存 IF_REQ;Oracle 本身不可用时只能写应用错误日志和 trace_id,不能宣称数据库必有失败记录。响应在事务提交后产生;若响应丢失,上游重传可取得原接收结果。
|
||||
|
||||
### 6.3 时序图:订单中心接收共用数据
|
||||
|
||||
订单中心推送完成后只表示本方已接收并保存数据,合同、返利由各自排期执行。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant OC as 订单中心
|
||||
participant API as 接口服务
|
||||
participant ORA as Oracle
|
||||
OC->>API: 推送合同返利共用数据
|
||||
API->>API: 校验必填字段
|
||||
alt 校验失败
|
||||
API->>ORA: 保存失败记录
|
||||
API-->>OC: 失败及字段位置
|
||||
else 校验通过
|
||||
API->>ORA: 保存接口数据
|
||||
ORA-->>API: 返回 INPUT_ID
|
||||
API-->>OC: 返回接收成功
|
||||
end
|
||||
```
|
||||
|
||||
### 6.4 时序图:平台订单导出并推送
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R as 订单脚本
|
||||
participant P as 采购平台
|
||||
participant F as 共享文件夹
|
||||
participant OC as 订单中心
|
||||
participant ORA as Oracle
|
||||
R->>P: 导出订单文件
|
||||
P-->>R: 返回订单文件
|
||||
R->>F: 保存平台订单文件
|
||||
R->>ORA: 保存文件和订单记录
|
||||
R->>OC: 推送订单及对码原始字段
|
||||
alt 接收成功
|
||||
OC-->>R: 返回接收结果
|
||||
R->>ORA: 保存推送成功
|
||||
else 保存失败或接口失败
|
||||
R->>ORA: 保存失败原因
|
||||
end
|
||||
```
|
||||
订单中心接收平台订单和对码原始字段,订单中心内部完成对码,并在后续共用数据中提供标准货品结果。
|
||||
|
||||
### 6.5 时序图:合同排期处理
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as 合同脚本
|
||||
participant ORA as Oracle
|
||||
participant CT as 合同系统
|
||||
participant OC as 订单中心
|
||||
C->>ORA: 按合同排期领取待处理数据
|
||||
ORA-->>C: 返回共用数据
|
||||
C->>CT: 录入并保存合同
|
||||
CT-->>C: 返回合同号
|
||||
C->>ORA: 保存合同号和合同结果
|
||||
C->>OC: 更新订单链路
|
||||
OC-->>C: 返回链路结果
|
||||
C->>ORA: 保存链路结果
|
||||
```
|
||||
合同保存和链路更新分别记录。链路失败时保留合同号,后续只重试链路更新,不重新创建合同。
|
||||
|
||||
### 6.6 时序图:返利排期处理
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R as 返利脚本
|
||||
participant ORA as Oracle
|
||||
participant RB as 返利中心
|
||||
R->>ORA: 按返利排期领取待处理数据
|
||||
ORA->>ORA: 检查同 INPUT_ID 的合同结果
|
||||
alt 合同未保存成功
|
||||
ORA-->>R: 暂不处理,保留等待状态
|
||||
else 合同已保存且合同号存在
|
||||
ORA-->>R: 返回共用数据和合同号
|
||||
R->>RB: 查询协议并提交返利
|
||||
RB-->>R: 返回办理结果
|
||||
R->>ORA: 保存返利结果
|
||||
end
|
||||
```
|
||||
|
||||
四张图之间的关系是:第一张完成数据接收,第二张完成平台订单推送,第三张和第四张由各自排期独立运行。合同和返利通过同一个 `INPUT_ID` 关联。
|
||||
|
||||
## 7. 详细设计:业务字段映射
|
||||
|
||||
本章保留 Excel 的实际字段和行号。合同/返利列来源都指向第 4.5 节同一份接口输入;按各自模板生成执行数据。生成的模板、默认日期及映射版本记录到节点 output_json/文件证据,不再复制成独立合同或返利输入表。
|
||||
|
||||
### 7.1 合同头
|
||||
|
||||
| 目标字段 | 取值规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 客户名称 | 订单中心 | 12 |
|
||||
| 客户编码 | 目标系统自动生成 | 11 |
|
||||
| 尽调情况 | 读取回显;未尽调停止本单自动录入并转采购员处理 | 13 |
|
||||
| 结算类型 | 收支双向 | 15 |
|
||||
| 合同版本类型 | 预付:我司定制;账期:非格式合同 | 17 |
|
||||
| 合同金额(单位元) | 预估本次合作金额 | 24 |
|
||||
| 金额(元) | 明细汇总;与导入后页面汇总核对并回填 | 25 |
|
||||
| 合同签订日期、合同开始日期、合同结束日期 | 订单中心分别提供,三个独立字段 | 26–28 |
|
||||
| 是否重大合同 | 否 | 29 |
|
||||
| 是否预付款 | 预付:是;账期:否 | 30 |
|
||||
| 付款方式 | 电汇 | 32 |
|
||||
| 付款期限 | 预付:1;账期:订单中心账期天数,缺失不默认 30 | 33 |
|
||||
| 合同具体分类 | 国内药品类、器械类品种 | 37 |
|
||||
| 运输方式 | 公路运输 | 39 |
|
||||
|
||||
### 7.2 合同明细
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 货品编码 | 订单中心 | 43 |
|
||||
| 货品名称 | 订单中心标准货品名称 | 44 |
|
||||
| 注册证号 | 订单中心 | 46 |
|
||||
| 规格、单位 | 按订单中心标准字段填报 | 47–48 |
|
||||
| 数量、单价 | 按订单中心最终采购数量、单价填报 | 49–50 |
|
||||
| 付款方式 | 电汇 | 52 |
|
||||
| 付款条件 | 订单中心;模板无此字段时不填 | 53 |
|
||||
| 付款期限 | 同合同头规则 | 54 |
|
||||
|
||||
### 7.3 返利总单
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 返利合约 | 否 | 5 |
|
||||
| 供应商编码 | 订单中心 | 6 |
|
||||
| 返利单位编码 | 订单中心 | 7 |
|
||||
| 计划员 | 订单中心,按业务给定的计划员/采购员对应关系填写 | 8 |
|
||||
| 采购结算方式 | 文案“否”;与细单代码 3 分别映射 | 9 |
|
||||
| 承付模式 | 下月 N 号结 | 10 |
|
||||
| 协议开始日期、协议结束日期 | 当天 | 11–12 |
|
||||
|
||||
### 7.4 返利细单
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 货品编码、数量、含税协议单价 | 本次共用接口数据的货品编码、数量及含税协议单价 | 15–17 |
|
||||
| 税率、关税税率 | 均为 0.13 | 18–19 |
|
||||
| 承付模式、承付参数 | 11、25 | 20–21 |
|
||||
| 细单开始日期 | 当天 | 22 |
|
||||
| 细单结束日期 | 本年度最后一天 | 23 |
|
||||
| 采购结算方式 | 3 | 24 |
|
||||
| 特定补偿预估值、进货补偿预估值 | 均为 0 | 25–26 |
|
||||
| 细单备注 | 订单中心订单号 | 33 |
|
||||
| 货源细单ID | 订单中心货源细单号;与订单号分列 | 34 |
|
||||
|
||||
合同明细导入列按 Excel 的 Sheet3。表中标注无需填写的字段不填;系统自动生成字段由系统处理。
|
||||
|
||||
|
||||
### 7.5 映射与运行日期规则
|
||||
|
||||
- 合同与返利的数量来自同一次报文;实际只提供一列数量时共用该列。含税协议单价与采购单价分别保留来源映射。
|
||||
- 首次执行该流程时生成所用默认日期及模板记录。重试已生成的相同步骤使用原输入,避免跨日重试更改已导入单据;新返利作业按返利首次执行当天生成“当天/年底”。这是实现规则,不增加业务确认步骤。
|
||||
- 合同客户编码为系统生成字段。现有 RPA 入口要求的 customerCode 实际用作客商搜索标识,须与系统自动生成的客户编码分清;按新页面取数方式适配,不要求上游为自动生成字段填占位符。
|
||||
- 映射表标注无需填写的字段不填。尽调读取页面回显;未尽调按原流程转采购员并通知。接口数据保留,人工完成后核对实际合同号并登记保存成功,进入链路更新步骤;返利按第 5.3 节条件领取。
|
||||
|
||||
## 8. 详细设计:操作记录与一致性
|
||||
|
||||
### 8.1 最少需要记录的操作信息
|
||||
|
||||
| 场景 | 保存位置 | 必须能查到的信息 |
|
||||
| --- | --- | --- |
|
||||
| 每次接口接收 | IF_REQ | 请求时间、来源消息、INPUT_ID 关联、原报文、返回结果、字段错误、trace_id |
|
||||
| 脚本执行一步 | job_node | flow、job、INPUT_ID/版本、节点、attempt、run_id、执行机、开始结束、结果、错误 |
|
||||
| 平台订单发送一次 | JD_ORDER_PUSH+job_event | 原文件、源订单、固定请求键、实际请求、HTTP/业务返回、耗时、成功/拒绝范围 |
|
||||
| 合同保存 | job_node+job_artifact | 实际合同标识、保存确认、目标页面证据、所用输入和模板 |
|
||||
| 链路调用一次 | job_event+链路 job_node | 链路关联、节点代码、合同号、请求键、报文、回执、错误 |
|
||||
| 运维操作一次 | job_event / user_task | 操作人、时间、动作、原因、前后状态、证据和操作结果 |
|
||||
| 文件 | job_artifact | 原始订单、导入模板、截图或日志文件路径,文件类型及哈希 |
|
||||
|
||||
job_event 建议事件类型包括 INPUT_BOUND、NODE_STARTED、NODE_SUCCESS、NODE_FAILED、API_REQUEST、API_RESPONSE、RETRY_REQUESTED、RESULT_VERIFIED、MANUAL_HANDOFF、JOB_FINISHED。detail_json 保存结构化信息。节点列表用 job_node.error_code/error_summary 筛选及展示;事件详情从 detail_json.errorCode/errorMessage 读取错误,不假定 job_event 存在独立错误列。详细错误与报文在详情展开。
|
||||
|
||||
接收失败没有 job 时由 IF_REQ 独立展示;所有能定位到作业的动作都写 job_event。脚本运行 ID 使用现有 executor_run_id;无数据或启动失败的本轮摘要写应用日志,通过 run_id 查询,暂不新增运行历史表。
|
||||
|
||||
### 8.2 事务及恢复顺序
|
||||
|
||||
| 写入过程 | 事务范围 | 恢复办法 |
|
||||
| --- | --- | --- |
|
||||
| 接收成功 | Oracle:成功请求记录+所有接口头明细 | 事务失败整体返回失败;响应丢失可同消息重传 |
|
||||
| 登记作业 | Oracle:同输入的合同和返利 job 一起幂等建立 | 成功后回写 IF_REQ.REGISTER_STATUS;回写失败下次重复登记返回同 job,再补 DONE |
|
||||
| 节点成功 | Oracle:该 attempt 成功+业务编号+操作日志/证据索引+推进当前节点 | 回报丢失重发同完成键;同成功结果返回原状态,不再次推进 |
|
||||
| 外部接口调用 | 先 Oracle 保存请求,事务提交后调用,返回后独立事务记结果 | 超时保留 UNKNOWN,不把网络调用放在数据库事务中 |
|
||||
| 人工恢复 | Oracle:核查结果、状态变更和操作日志 | 同一操作请求 ID 重复提交返回原结果 |
|
||||
|
||||
接口输入和业务执行结果均在 Oracle 中保存,登记、领取、结果回报使用同一数据库事务边界;外部接口调用仍在事务提交后执行。接口表登记失败时可再次幂等登记,组合唯一约束保证不多建合同/返利作业。
|
||||
|
||||
### 8.3 文件和配置
|
||||
|
||||
1 药城与药帮忙分别配置共享根目录和字段映射;现场路径由配置提供。下载完成后发布文件,文件名按批次区分,保留原件。后台返回文件列表,业务/运维通过现有证据入口打开或下载,不要求共享盘在所有机器使用同一盘符。
|
||||
|
||||
订单中心连接、凭据、超时、链路节点映射、四条排期、重试间隔及模板配置集中管理。真实环境缺必要配置返回明确错误,不自动使用 fixture。日期、数量和金额按第 4、7 章数据类型保存,日志不保存 token 或数据库密码。
|
||||
|
||||
## 9. 详细设计:正常路径与异常处理
|
||||
|
||||
### 9.1 正常路径示例
|
||||
|
||||
本例标识仅用于说明字段关系,不代表正式接口报文。
|
||||
|
||||
| 时间/动作 | 接口记录 | 合同保存 | 链路更新 | 返利 | 记录位置 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 接收 M001 | ACCEPTED,INPUT_ID=I001,REGISTER=PENDING | 未登记 | 未登记 | 未登记 | IF_REQ+ORDER_H/L |
|
||||
| 任一相关脚本启动登记 | REGISTER=DONE | PENDING | 未开始 | WAIT_CONTRACT | 两个 job 都绑定 I001/1 |
|
||||
| 合同脚本处理 | 原输入不变 | RUNNING | 未开始 | WAIT_CONTRACT | CREATE 第 1 次尝试 |
|
||||
| 合同保存成功 HT001 | 原输入不变 | SUCCESS | PENDING | 可领取 | 合同号及保存证据 |
|
||||
| 更新链路成功 | 原输入不变 | SUCCESS | SUCCESS | 可领取 | 链路请求、回执;合同 job 完成 |
|
||||
| 返利下次运行 | 原输入不变 | SUCCESS | SUCCESS | RUNNING | 读取 I001 同一头明细+HT001 |
|
||||
| 返利提交完成 FL001 | 原输入不变 | SUCCESS | SUCCESS | SUCCESS | 返利编号、模板及提交结果 |
|
||||
|
||||
### 9.2 异常矩阵
|
||||
|
||||
| 异常代码(建议) | 识别条件 | 状态和保留内容 | 处理及恢复位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| INPUT_REQUIRED | 本次共用数据缺合同或返利上游必填 | IF_REQ=REJECTED,字段路径/行号,无业务输入 | 上游补齐后重传;RPA 无需处理 |
|
||||
| INPUT_CONFLICT | 成功来源/版本再传不同内容 | CONFLICT,关联旧请求、保存新原文 | 核对来源及版本;不覆盖已使用数据 |
|
||||
| STORE_FAILED | 接口表事务失败 | 返回 503;应用日志 trace_id | 修复数据库后重传 |
|
||||
| JOB_REGISTER_FAILED | Oracle 已接收,作业登记失败 | REGISTER=FAILED、原因;输入 READY | 修复数据库或服务后,下一次脚本取数补登记 |
|
||||
| PLATFORM_QUERY_FAILED | 页面打不开或查询报错 | 行 job 明确失败,截图/错误 | 修复后重试该行;不记 SKIPPED |
|
||||
| NO_ELIGIBLE_PRODUCT | 查询成功但不满足采购条件 | job SUCCESS,result=SKIPPED、原因 | 正常结束,常规轮询不重复处理 |
|
||||
| FILE_SAVE_FAILED | 共享目录不可写/文件未完整 | ORDER_PUSH 失败、已知路径 | 修复目录后从导出/归档失败处继续 |
|
||||
| OC_ORDER_REJECTED | 订单中心明确业务拒收 | push=REJECTED,拒绝原因及范围 | 修正接口映射/源数据后处理失败单,成功单保留 |
|
||||
| EXTERNAL_RESULT_UNKNOWN | 加购、保存、推送或提交后无确定结果 | WAITING_EXTERNAL/UNKNOWN,请求或页面证据 | 先核对目标结果;确认未成功才重试 |
|
||||
| CONTRACT_DUE_DILIGENCE | 合同页面显示未尽调 | WAITING_USER、采购员待办和页面记录 | 按原业务人工处理,核验保存结果后继续链路 |
|
||||
| CHAIN_UPDATE_FAILED | 合同已保存,链路明确失败 | CREATE 成功;CHAIN 失败、回执 | 下次仅重试 CHAIN;合同号不清空 |
|
||||
| PROTOCOL_NOT_READY | 已有合同号,但返利协议不可查/暂不可操作 | 返利 WAITING_EXTERNAL,原因及检查时间 | 到期再查协议;不重建合同,不等第二次供数 |
|
||||
| REBATE_SUBMIT_FAILED | 返利系统明确提交失败 | 失败步骤、目标协议/总单、已导入信息 | 按原业务交人工处理;保留前面成功步骤 |
|
||||
| EXECUTOR_INTERRUPTED | 作业仍 RUNNING,进程已退出 | 执行机/run_id、最近节点、证据 | 运维确认无运行进程并核对结果后,恢复当前步骤 |
|
||||
|
||||
所有正常失败恢复保留原 INPUT_ID、job 和历史 attempt。通用“重试”只增加当前失败节点尝试,不把 job 从第一个节点重新执行。已完成外部写入不能仅凭“失败”按钮反向当作没有发生。
|
||||
|
||||
## 10. 运维设计:如何查看与处理
|
||||
|
||||
### 10.1 查询入口及页面内容
|
||||
|
||||
复用现有列表、节点详情、时间线、待办和证据组件,新增接收列表和“同一输入的合同/返利总览”,不用另建运维平台。
|
||||
|
||||
| 页面 | 筛选条件 | 列表/详情内容 |
|
||||
| --- | --- | --- |
|
||||
| 接口接收列表 | 时间、来源单号、MESSAGE_ID、TRACE_ID、接收状态、登记状态 | 请求/返回原文、缺字段路径、INPUT_ID、重复来源、登记失败原因 |
|
||||
| 合同返利总览 | INPUT_ID、来源单号、合同号、返利号、三个业务状态、时间 | 同一行显示合同保存、链路更新、返利状态及最近错误;点击进入各 job |
|
||||
| 作业详情 | flow、job_id、状态、执行机、run_id | 来源输入、当前节点、每次尝试、耗时、错误、回报、外部编号 |
|
||||
| 订单推送列表 | 平台/账号、订单号、导出批次、push_status | 原文件、发送内容、回执、次数、结果未知/拒收原因 |
|
||||
| 操作和证据 | job/INPUT_ID、操作者、动作时间 | 谁点击重试/人工处理,前后状态、原因、截图、模板及日志 |
|
||||
|
||||
“合同返利总览”由后端以 Oracle INPUT_ID 为基底,关联两份 job/节点结果;不存在 job 时显示未登记。分页/筛选由后端实现,不要求浏览器一次拉全表。
|
||||
|
||||
### 10.2 典型查看路径
|
||||
|
||||
| 运维问题 | 如何查 | 结论/下一步 |
|
||||
| --- | --- | --- |
|
||||
| 订单中心说推过,RPA 没处理 | 先查 MESSAGE_ID/来源单号的接收记录 | REJECTED 看缺字段;ACCEPTED+登记失败看数据库错误;DONE 看对应 job |
|
||||
| 合同存在,为什么流程显示失败 | 总览看合同保存和链路更新两列 | 保存成功/链路失败只补链路调用,不重建 |
|
||||
| 返利为什么一直没开始 | 看同 INPUT_ID 的合同保存结果及返利 wait_reason | 合同未保存则正常等待;保存成功但协议不可查则按目标状态处理 |
|
||||
| 同一批有的成功有的失败 | 订单推送按源订单筛选,选品按行 job 查看 | 重试失败项;已接收/已加购项不重复处理 |
|
||||
| 数据被改过或重复推过 | 接收尝试记录+DATA_HASH+操作日志 | 明确首次输入、重复/冲突报文及处理人 |
|
||||
|
||||
### 10.3 运维动作及权限
|
||||
|
||||
| 动作 | 允许条件 | 实际改变 | 必须留痕 |
|
||||
| --- | --- | --- | --- |
|
||||
| 重试当前步骤 | FAILED_RETRYABLE,且外部结果已明确 | 当前节点新增 attempt,job 转 PENDING | 操作人、原因、旧状态、新 attempt |
|
||||
| 重新登记作业 | 已 ACCEPTED 且 REGISTER≠DONE | 幂等补建缺失 job,完成登记标志 | 接收记录、结果、操作者 |
|
||||
| 核对结果:已成功 | UNKNOWN 或人工完成,提供实际业务编号/回执证据 | 确认对应节点成功并推进;合同保存后仍执行链路 | 目标编号、证据、核对人和时间 |
|
||||
| 核对结果:未成功 | 确认目标未写入,原执行进程已停止 | 转可重试当前步骤 | 核对依据、操作者、原因 |
|
||||
| 转业务处理 | 业务错误或系统要求人工 | 创建/复用现有 user_task,保存责任人 | 原因、接收人、操作记录 |
|
||||
| 停止办理 | 有明确业务指令 | CANCELLED,保留已产生结果 | 原因及外部已发生操作,不自动撤销外部单据 |
|
||||
|
||||
只读运维可看列表及证据;恢复动作限定 OPS,业务待办按采购员等现有角色处理。恢复入口必须核对实际操作者身份,不能只相信请求体里填写的 operator。沿用现有鉴权方案补动作权限,不另加业务审批流。
|
||||
|
||||
不能用“统一确认并继续”替代所有核对动作,不能直接改接口原始字段或数据库状态。金额、货品等源数据错误由订单中心修正重传;已被使用的输入按冲突规则处理。
|
||||
|
||||
### 10.4 查询 API 及日志定位
|
||||
|
||||
| API(建议) | 用途 |
|
||||
| --- | --- |
|
||||
| `GET /api/jieda/receipts`、`/receipts/{request_log_id}` | 无 job 的失败接收也能查 |
|
||||
| `GET /api/jieda/orders`、`/orders/{input_id}` | 同一输入的合同/链路/返利总览及头明细 |
|
||||
| `GET /api/jieda/order-pushes` | 按源订单查推送及文件 |
|
||||
| 现有 `GET /api/jobs/{job_id}`、`/timeline`、`/evidence-package` | 节点、历史尝试和证据 |
|
||||
| `GET /api/jobs/{job_id}/events`(新增) | 操作日志和出站请求/返回记录 |
|
||||
| `POST /api/jieda/receipts/{id}/register`(新增) | 运维手动补登记,与脚本取数复用同一逻辑 |
|
||||
| `POST /api/jobs/{job_id}/verify-result`(新增) | 按第 10.3 节核对 UNKNOWN/人工结果,记录依据后恢复 |
|
||||
|
||||
应用日志统一携带 `trace_id、input_id、job_id、flow_code、node_code、attempt_no、run_id` 中当前已知项。接收阶段没有 job_id 时不编造;执行阶段没有 HTTP trace 时以 job/run 定位。列表展示最近错误,详情保留全部历史,不只覆盖最后一次失败。
|
||||
|
||||
索引建议:接口记录按时间/状态/来源,业务头按源单号,job 按 `(flow_code,status,next_check_at)` 和 `(input_id,input_version)`,节点按现有 `(job_id,node_code,attempt_no)`,事件按 `(job_id,created_at)`,推送按来源唯一键及 `(push_status,next_retry_at)`。
|
||||
|
||||
## 11. 实施影响与验收
|
||||
|
||||
### 11.1 现有代码到新设计的映射
|
||||
|
||||
| 现有位置 | 已核对现状 | 实施改动 |
|
||||
| --- | --- | --- |
|
||||
| [main.py 建单](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:499) | 供应商+日期键,固定首节点,只复用未完成作业 | 新接收/登记/领取按 flow 和 INPUT_ID;已完成作业也防重 |
|
||||
| [schema.sql](/Users/yuuuu/Documents/AutoRpa/state_center/sql/schema.sql:11) | job 无 flow/input 关联;已有节点、日志及证据表 | 第 4 章 Oracle 接口表及推送台账;job 扩展字段/索引,supplier_name/order_date 允许空;job_event 错误保存在现有 detail_json |
|
||||
| [db.py](/Users/yuuuu/Documents/AutoRpa/state_center/app/db.py:22) | 当前数据库访问实现 | 按 Oracle 驱动和事务方式调整登记、领取、结果及推进 |
|
||||
| [main.py 完成回报](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:712) | 保存节点、编号和推进分步执行;节点定义全局固定 | 同事务回报,校验 execution_token/attempt;按 flow 定义完成规则 |
|
||||
| [main.py 重试](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:812) | 已保留 attempt 历史、重试当前节点 | 保留机制,支持共用输入关联及 UNKNOWN 核对 |
|
||||
| [main.py 人工提交](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:1346) | 通用确认可直接推进;RETRY_CURRENT_NODE 分支当前只记录事件 | 按第 10.3 节校验结果和动作,实现真实重试及核查恢复 |
|
||||
| [agent.py](/Users/yuuuu/Documents/AutoRpa/rpa_yisaiqi/agent.py:1) | 通用代理主要调用 mock;存在节点硬编码与默认 attempt=1 | 四条独立入口、真实 runner、领取结果中的 attempt/token |
|
||||
| [contract_create_node.py](/Users/yuuuu/Documents/AutoRpa/rpa_yisaiqi/contract_create_node.py:241) | 已校验空明细及正数量,仍要求旧 customerCode | 适配共用接口数据和客商定位;保留有效输入检查 |
|
||||
| [contract_flow_steps.py](/Users/yuuuu/Documents/AutoRpa/rpa_yisaiqi/contract_flow_steps.py:788) | 缺价补 1.00,同码首行及前 30 行定位 | 移除缺价补值;按来源明细/目标行定位并核对实际导入结果 |
|
||||
| [Web 节点详情](/Users/yuuuu/Documents/AutoRpa/state_center_web/src/components/JobNodeDetailList.tsx:1) | 已展示时间、尝试次数、错误及证据 | 复用组件,补三个独立状态、输入关联、接收列表及恢复动作 |
|
||||
|
||||
当前 Oracle 配置主要服务货品主数据读取;新接口表写入连接和权限需现场配置。当前代码不等于已具备四条真实平台执行能力,平台查询/导出及返利页面模块需要核对或实现。
|
||||
|
||||
### 11.2 实施顺序
|
||||
|
||||
1. 按共同输入契约核对第 4、7 章字段及合同/返利范围;确认现场接口表复用方式。
|
||||
2. 完成接收校验、Oracle 同事务保存及接收查询,验证失败重传和重复消息。
|
||||
3. 增加 flow/input 关联、作业登记、独立状态、领取及结果事务,跑通“同一输入两个 job”。
|
||||
4. 接平台订单导出、共享文件、台账及推送回执。
|
||||
5. 接合同页面保存和链路更新;返利读取同 INPUT_ID 与合同号完成提交。
|
||||
6. 接逐条选品;补齐异常及运维动作,配置四条排期后联调。
|
||||
|
||||
### 11.3 验收用例
|
||||
|
||||
| 编号 | 场景 | 预期结果 |
|
||||
| --- | --- | --- |
|
||||
| AT-01 | 一次完整 ORDER_COMMON 推送 | 一份头明细;登记两个相同 INPUT_ID/版本的合同、返利 job |
|
||||
| AT-02 | 仅返利必填缺失 | 当次返回字段错误,无业务输入;补齐后可重传 |
|
||||
| AT-03 | 相同请求重发、相同对象换消息 ID 重发 | 返回原输入,不新增执行作业 |
|
||||
| AT-04 | 同已成功来源/版本传不同内容 | 冲突可查,旧数据和外部结果不被覆盖 |
|
||||
| AT-05 | 接收已提交,作业登记失败 | 接收记录显示登记失败;后续幂等补登记,不要求上游再推 |
|
||||
| AT-06 | 返利脚本先于合同脚本启动 | WAIT_CONTRACT;无额外供数请求、无返利提交 |
|
||||
| AT-07 | 合同保存成功,链路更新失败 | 保存 SUCCESS、链路 FAILED;只恢复链路,返利可按保存条件领取 |
|
||||
| AT-08 | 合同完成后返利按另一排期启动 | 读取同一接口数据和本方合同号,字段来源可追溯 |
|
||||
| AT-09 | 平台订单重复导出到不同文件 | 原件保留,已接收同源订单不重复入单 |
|
||||
| AT-10 | 推送部分成功/超时 | 按实际回执记录逐单结果;UNKNOWN 不直接按失败重发 |
|
||||
| AT-11 | 选品合格/不合格/查询失败 | 分别 ADDED、SKIPPED、FAILED;本批结束,不等付款 |
|
||||
| AT-12 | 脚本中断和重复点击 | 不重复领取;已有结果可查,确认停止并核对后恢复当前步骤 |
|
||||
| AT-13 | 合同已保存但回报丢失 | 重发完成键或核对目标结果,不再次建合同 |
|
||||
| AT-14 | 未尽调或返利提交失败 | 保留输入及原业务待办,其他记录继续;人工结果核对可留痕 |
|
||||
| AT-15 | 46 个 Excel RPA 字段 | 共用接口来源、固定值及日期按映射填报,缺价不补 1.00 |
|
||||
| AT-16 | 运维按 MESSAGE_ID、INPUT_ID、合同号查询 | 能串起接收、两流程、尝试、文件、请求和人工动作 |
|
||||
| AT-17 | 未授权恢复、旧 token 回报 | 拒绝写入;旧成功结果不被覆盖 |
|
||||
| AT-18 | 新旧流程并存 | 旧在途单保留原节点;新流程不误入对码/OCR/M5 |
|
||||
|
||||
文档检查与业务验收分开:当前只完成设计;以上需要在测试数据库、实际共享目录及外部测试系统执行,mock 成功不代表真实业务通过。
|
||||
|
||||
## 12. 未决事项、风险及对其他模块的影响
|
||||
|
||||
### 12.1 联调前需要确认
|
||||
|
||||
| 项目 | 已明确 | 剩余信息 |
|
||||
| --- | --- | --- |
|
||||
| 合同返利供数 | 一次推送共用 | 正式头明细、业务范围标识、消息 ID/版本;一组数据是否有多合同/多返利范围 |
|
||||
| 业务字段 | Excel 来源及默认值 | 实际 JSON 列名;采购/返利数量是否同字段,价格口径及精度 |
|
||||
| 订单出站接口及对码字段 | 本方归档后推送原始订单字段 | 确认批准文号、品名、通用名、规格、厂家、单位等字段是否必填,以及标准货品结果的返回/后续供数方式 |
|
||||
| 链路更新 | 合同保存成功后更新对应节点 | 节点代码、链路关联、合同标识字段及成功返回码 |
|
||||
| 返利可操作性 | 用同一份输入加已保存合同号 | 目标协议生成时间、是否依赖审批或链路更新;未确认前不新增业务审批门槛 |
|
||||
| 平台和共享目录 | 两平台分别导出归档 | 真实账号、订单状态/范围、文件样例、共享路径及四条排期 |
|
||||
| 盈利 | 5% 以上盈利 | 计算基数及边界;不直接套用旧方案 0.95 公式 |
|
||||
| 现场库和模板 | 接口输入与处理记录分开 | 现有 Oracle 表、权限、正式模板和确有要求的附件 |
|
||||
|
||||
### 12.2 可能问题与控制位置
|
||||
|
||||
| 问题 | 影响 | 对应设计 |
|
||||
| --- | --- | --- |
|
||||
| 共用一个 processed 标志 | 合同处理后返利漏取 | 第 5 章分离合同保存、链路、返利标志 |
|
||||
| 两份接口输入被分别修改 | 合同/返利货品数量不一致 | 同 INPUT_ID/版本只保留一份原始头明细 |
|
||||
| 成功接收后没有 job | 数据落表但长期未执行 | REGISTER_STATUS 和取数前补登记;运维可查 |
|
||||
| 接口/页面结果未知被当失败 | 重复订单、合同或返利 | UNKNOWN 核对后恢复,不无条件重做 |
|
||||
| 把链路失败当合同未保存 | 再建一份合同 | 保存节点结果与链路节点独立 |
|
||||
| 当前合同缺价补值或明细定位错误 | 错金额、同码行覆盖或漏行 | 接收校验及第 11.1 节代码修正;单规格规则不消除多订单行的问题 |
|
||||
| 原人工确认直接推进 | 未核实外部结果就完成作业 | 第 10.3 节按动作和证据校验 |
|
||||
| 新旧流程改全局状态机 | 旧对码、OCR、M5 在途单无法继续 | flow/version 分开,旧单保持原链;切换前确定同一来源由哪套办理 |
|
||||
| 两库或共享目录不可用 | 接收失败、原文不可查、文件不可达 | 独立错误可见;执行记录保留,不伪装成空数据或成功 |
|
||||
|
||||
本稿的表与字段是详细设计建议,尚未核对现场字典。对外契约尚未提供的部分保留为明确缺口;不以增加业务操作、复制数据或自动猜规则来填补。
|
||||
|
|
@ -1,781 +0,0 @@
|
|||
# 福建洁达采购 RPA 概要设计与详细设计
|
||||
|
||||
| 文档属性 | 内容 |
|
||||
| --- | --- |
|
||||
| 版本 | V3.0 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 状态 | 设计评审稿;未实施代码或建表 |
|
||||
| 适用系统 | 当前 AutoRpa 状态中心、RPA 执行程序及福建洁达采购业务 |
|
||||
| 本版核心关系 | 订单中心一次推送合同/返利共用数据;合同先处理,返利按自己的排期读取同一份数据 |
|
||||
| 文档范围 | 概要设计、表与状态详细设计、接口及时序、异常恢复、运维查询、实施与验收 |
|
||||
| 替代关系 | 整合原 Codex 设计稿 V1.9 和技术实现说明 V2.3;旧方案仅供历史参考 |
|
||||
|
||||
**阅读顺序:** 业务评审看第 1–3 章;开发看第 4–9 章;运维看第 10 章;实施验收及未决问题见第 11–12 章。
|
||||
|
||||
## 1. 设计依据与范围
|
||||
|
||||
### 1.1 已确认业务规则
|
||||
|
||||
| 编号 | 规则 |
|
||||
| --- | --- |
|
||||
| BR-01 | 四条脚本分别运行,自动时间和频率不同,也支持业务点击;一次 HTTP 接收不等待四条流程全部执行 |
|
||||
| BR-02 | 选品读取未处理销售明细,按批准文号查询;合格加购物车,不合格跳过,处理完本批结束;不等待人工付款 |
|
||||
| BR-03 | 本业务相同批准文号只有一个规格,不增加规格、厂家、包装或单位换算匹配 |
|
||||
| BR-04 | 平台订单导出到各平台对应共享文件夹,按订单中心接口格式组装并推送;对码由订单中心内部处理 |
|
||||
| BR-05 | 订单中心一次传来的完整业务数据供合同、返利共用,不拆成两次独立供数,不重复保存两份相同头明细 |
|
||||
| BR-06 | 接收时检查上游应传的必填字段,缺项当次返回失败和字段位置;通过并落表后返回成功 |
|
||||
| BR-07 | 合同保存成功后调用订单中心链路接口更新对应节点;返利读取同一份接口数据,并使用本方已取得的合同号 |
|
||||
| BR-08 | 手工单暂不做;合同/返利字段、固定值及原业务人工分支按提供材料执行 |
|
||||
|
||||
### 1.2 参考材料
|
||||
|
||||
| 材料 | 用途 |
|
||||
| --- | --- |
|
||||
| [合同系统录入流程](/Users/yuuuu/Documents/AutoRpa/资料/合同系统录入流程.md) | 合单、尽调、提交、回写;页面步骤指向 rpa_yisaiqi |
|
||||
| [选品条件 20260902](/Users/yuuuu/Documents/AutoRpa/资料/采购平台选品条件优先项_20260902.txt) | 自营仓、盈利、效期、库存和仓库顺序 |
|
||||
| [合同返利字段映射 20260911](/Users/yuuuu/Documents/AutoRpa/资料/合同返利系统字段映射_20260911.xlsx) | 两个工作表的 46 个 RPA 字段及 Sheet3 导入列 |
|
||||
| [仓库一期技术设计](/Users/yuuuu/Documents/AutoRpa/资料/采购主链RPA一期设计文档.md) | 参考概要/详细设计、实体、接口、状态和审计的组织方式;不沿用其旧业务范围 |
|
||||
| [现有数据库结构](/Users/yuuuu/Documents/AutoRpa/state_center/sql/schema.sql) | 判断 job、节点、待办、日志及证据记录可复用部分 |
|
||||
|
||||
本次会话的业务确认优先于旧文档。下文表名、内部代码、API 路径及列类型为本方建议,正式外部字段名、节点代码和现场 Oracle 结构仍需按第 12 章核对。
|
||||
|
||||
## 2. 概要设计:架构与分工
|
||||
|
||||
### 2.1 总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
OC[订单中心] -->|销售数据 / 合同返利共用数据| API[FastAPI 接收校验]
|
||||
API -->|校验成功同事务落表| IFDB[(Oracle 接口头明细及接收记录)]
|
||||
S[四条独立排期 / 点击脚本] --> RPA[RPA 执行程序]
|
||||
RPA <-->|领取数据 / 回报结果| SC[状态中心]
|
||||
SC -->|读取同一份输入| IFDB
|
||||
SC <--> BDB[(Oracle 作业、节点、调用记录)]
|
||||
RPA -->|查品加购 / 导出订单| PL[采购平台]
|
||||
PL --> F[对应共享文件夹]
|
||||
F -->|组装报文并推送| OC
|
||||
RPA -->|录入保存| CT[合同系统]
|
||||
RPA -->|保存后调用链路接口| OC
|
||||
RPA -->|同一接口数据加合同号| RB[返利中心]
|
||||
UI[运维列表 / 详情 / 日志] --> SC
|
||||
```
|
||||
|
||||
接口数据与业务处理记录分开:**Oracle 接口表回答“订单中心传了什么”,Oracle 业务记录回答“本方做到哪一步、结果是什么、谁操作过”。** RPA 通过状态中心读取接口数据,数据库连接集中在后端。
|
||||
|
||||
### 2.2 接口表与业务表的选择
|
||||
|
||||
| 方式 | 特点 | 本期选择 |
|
||||
| --- | --- | --- |
|
||||
| 仅一张接口表加一个 processed 标志 | 简单,但合同处理后可能被整体标记完成,返利漏取;也无法表达多次失败尝试 | 不采用 |
|
||||
| 接口表再完整复制成合同表、返利表 | 三份字段容易不一致,需要额外同步 | 不采用 |
|
||||
| 接口头明细保留一份输入,业务作业/节点保存独立状态与结果 | 合同、返利通过相同 INPUT_ID 关联;历史操作复用现有日志 | **采用** |
|
||||
|
||||
现有 `job / job_node` 就是本期业务处理表,无需再复制合同、返利输入。为无 job 的接收失败补接收记录;为跨导出文件识别同一订单,补逐订单推送台账。这两项有明确查询及防重用途。
|
||||
|
||||
### 2.3 模块职责及运行方式
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| FastAPI | 校验、保存接收数据、取数、原子领取、保存回报、查询及运维动作 |
|
||||
| Oracle 接口库 | 接收尝试、销售头明细、合同/返利共用头明细;成功输入保留原值 |
|
||||
| Oracle 状态中心 | 各流程 job、节点尝试、业务编号、订单推送状态、操作日志、证据索引 |
|
||||
| 四条 RPA 脚本 | 本批逐条执行;平台订单和合同链路接口由对应脚本通过适配器调用并回报 |
|
||||
| 现有 Web | 接收查询、共用数据处理总览、节点尝试、文件和运维动作 |
|
||||
|
||||
四条脚本分别配置时间、频率及本轮条数上限,单执行机每条脚本设单实例启动控制。FastAPI 常驻。脚本启动时调用后端登记未建立的 job,再领取本流程数据;订单导出先生成并登记 export_batch_id,恢复失败批次沿用该 ID;本批结束释放本机运行锁。没有数据时记录本轮零条并退出,不新增第五条业务或后台维护流程。
|
||||
|
||||
## 3. 概要设计:业务流程与完成边界
|
||||
|
||||
| 流程 | 输入 | 正常处理 | 完成含义 |
|
||||
| --- | --- | --- | --- |
|
||||
| PROCURE | 销售接口表的一条明细 | 按批准文号查询,按采购条件加购或跳过 | 该行处理完;不代表付款成交 |
|
||||
| ORDER_PUSH | 对应平台导出的订单文件 | 保存共享目录、解析、组装、推送、记录回执 | 应推送订单确认接收;不等待内部对码 |
|
||||
| CONTRACT | 共用订单接口头明细 | 录入并保存合同,保存合同号,更新订单中心链路 | 保存及链路更新分别记录;链路失败不重建合同 |
|
||||
| REBATE | **同一共用订单接口头明细**+本方合同保存结果 | 按合同号查协议、填总单、导细单、提交 | 返利提交成功;不要求订单中心再次推数据 |
|
||||
|
||||
### 3.1 合同与返利的关联
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
I[一次推送:INPUT_ID = I001] --> C[合同 job:INPUT_ID = I001]
|
||||
I --> R[返利 job:INPUT_ID = I001]
|
||||
C --> CS[合同保存成功:合同号 HT001]
|
||||
CS --> CH[合同链路更新]
|
||||
CS -->|提供合同号| R
|
||||
R --> RS[返利提交结果]
|
||||
```
|
||||
|
||||
合同、返利是两个独立作业,绑定同一 INPUT_ID 和 INPUT_VERSION。合同保存号只从对应合同 job 取得,不能按同品种或同一天猜配。
|
||||
|
||||
返利默认在“合同已确认保存成功且合同号存在”后可领取;链路回传结果另列显示,**不擅自把链路成功增加为返利前置条件**。若目标返利协议暂不可查,记录等待原因,下次返利运行再查询。是否必须审批通过由目标系统及接口方确认,不新增审批动作。
|
||||
|
||||
### 3.2 选品规则
|
||||
|
||||
自营仓、5% 以上盈利、有效期大于等于 12 个月、库存满足采购数量四项都通过后,按下列仓库顺序选择。不合格记跳过,平台技术错误记失败。
|
||||
|
||||
广东壹号药业有限公司、昆山亿方药业有限公司、湖北亿昊药业有限公司、天津壹号药业有限公司、重庆亿昊药业有限公司、重庆壹号医药有限公司、湖南壹号药业股份有限公司、辽宁亿昊药业有限公司、河南壹号医药有限公司、甘肃壹号云医药有限责任公司企业店、陕西壹号药业有限公司。
|
||||
|
||||
## 4. 详细设计:数据模型
|
||||
|
||||
### 4.1 实体关系和存储位置
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
RPA_JD_IF_REQ ||--o{ RPA_JD_SALES_H : accepts
|
||||
RPA_JD_IF_REQ ||--o{ RPA_JD_ORDER_H : accepts
|
||||
RPA_JD_SALES_H ||--|{ RPA_JD_SALES_L : contains
|
||||
RPA_JD_ORDER_H ||--|{ RPA_JD_ORDER_L : contains
|
||||
RPA_JD_SALES_L ||--o{ JOB : procure
|
||||
RPA_JD_ORDER_H ||--o{ JOB : contract_and_rebate
|
||||
JOB ||--o{ JOB_NODE : attempts
|
||||
JOB ||--o{ JOB_EVENT : operations
|
||||
JOB ||--o{ JOB_ARTIFACT : files
|
||||
JOB ||--o{ JD_ORDER_PUSH : exports
|
||||
```
|
||||
|
||||
前五张为 Oracle 逻辑表:一张接收记录、销售头明细、共用订单头明细;已有同用途表优先映射复用。其余为 Oracle,`JD_ORDER_PUSH` 拟新增,其余复用。图中的同库关系是应用关联,不是跨数据库外键。
|
||||
|
||||
接口表内的合同/返利状态不各写一份。运维总览按 INPUT_ID 汇总两份 job 与其节点,展示三个独立标志:合同保存、链路更新、返利办理。读写来源见第 5 章。
|
||||
|
||||
### 4.2 接收记录 `RPA_JD_IF_REQ`
|
||||
|
||||
一行是一回接收尝试;错误请求也能查到,不能依赖先有 job 才记录。
|
||||
|
||||
| 字段 | 建议类型 | 含义与约束 |
|
||||
| --- | --- | --- |
|
||||
| REQUEST_LOG_ID | VARCHAR2(64) PK | 本方生成,返回给上游和运维定位 |
|
||||
| TRACE_ID | VARCHAR2(64) | 本次请求关联应用日志 |
|
||||
| SOURCE_SYSTEM / MESSAGE_ID | VARCHAR2(32) / VARCHAR2(128) | 调用方及其请求标识;MESSAGE_ID 格式由对方提供 |
|
||||
| DATA_TYPE | VARCHAR2(24) | SALES 或 ORDER_COMMON;没有独立 REBATE 接收类型 |
|
||||
| BODY_HASH | VARCHAR2(64) | 规范化业务报文哈希,识别同请求内容是否一致 |
|
||||
| RECEIVE_STATUS | VARCHAR2(16) | ACCEPTED、REJECTED、DUPLICATE、CONFLICT |
|
||||
| ORIGINAL_REQ_ID | VARCHAR2(64),可空 | 重复请求关联首次成功记录 |
|
||||
| RAW_BODY / RESPONSE_BODY | CLOB | 实际业务报文和返回内容,不记录鉴权口令 |
|
||||
| ERROR_CODE / ERROR_DETAIL | VARCHAR2(64) / CLOB | 字段路径、明细标识、错误说明 |
|
||||
| RECEIVED_AT / RESPONDED_AT | TIMESTAMP | 收到及生成返回结果的时间 |
|
||||
| REGISTER_STATUS | VARCHAR2(8),可空 | 成功记录的作业登记标志:PENDING、DONE、FAILED;非成功记录为空 |
|
||||
| REGISTER_ERROR / REGISTERED_AT | VARCHAR2(500) / TIMESTAMP | Oracle 登记失败原因及最终登记时间 |
|
||||
|
||||
RECEIVE_STATUS 表示接收事实,REGISTER_STATUS 仅表示是否已登记作业,均不代表合同或返利完成。成功接收同事务保存接口头明细并置 REGISTER_STATUS=PENDING;脚本取数前补登记,完成后更新 DONE。登记失败在下一次脚本取数重试,无需上游重传。
|
||||
|
||||
约束:对 ACCEPTED 记录建立 `(SOURCE_SYSTEM, MESSAGE_ID)` 条件唯一索引;Oracle 可用 CASE 表达式索引实现。失败/重复尝试保留独立行,不占用成功消息键。索引还包括 `(DATA_TYPE, REGISTER_STATUS, RECEIVED_AT)`、`TRACE_ID`、`(SOURCE_SYSTEM, MESSAGE_ID)`。
|
||||
|
||||
### 4.3 接口头公共字段
|
||||
|
||||
销售头和共用订单头均使用以下控制列。
|
||||
|
||||
| 字段 | 建议类型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| INPUT_ID | VARCHAR2(64) PK | 本方输入身份,跨流程统一关联 |
|
||||
| REQUEST_LOG_ID | VARCHAR2(64) FK | 首次成功接收记录 |
|
||||
| SOURCE_SYSTEM / SOURCE_DOC_ID | VARCHAR2(32) / VARCHAR2(128) | 上游稳定业务标识;与展示单号分开 |
|
||||
| SOURCE_DOC_NO | VARCHAR2(128) | 业务查询展示用单号 |
|
||||
| INPUT_VERSION | VARCHAR2(64) | 上游版本;未提供时首版用固定 1,变更走第 6.2 节冲突规则 |
|
||||
| DATA_HASH | VARCHAR2(64) | 本对象完整头明细内容哈希 |
|
||||
| DATA_STATUS | VARCHAR2(16) | READY:成功接收的完整输入;本期不另设待补字段状态 |
|
||||
| CREATED_AT | TIMESTAMP | 成功保存时间 |
|
||||
|
||||
头表唯一键为 `(SOURCE_SYSTEM, SOURCE_DOC_ID, INPUT_VERSION)`。明细以 `(INPUT_ID, SOURCE_LINE_ID)` 唯一,关联本头,不以货品编码唯一。成功输入不由 RPA 改写,处理结果另存 Oracle。
|
||||
|
||||
### 4.4 销售接口头明细 `RPA_JD_SALES_H / L`
|
||||
|
||||
| 表 | 业务字段 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| SALES_H | 第 4.3 节公共字段、BUSINESS_DATE | 一份销售总单 |
|
||||
| SALES_L | LINE_ID、INPUT_ID、SOURCE_LINE_ID、APPROVAL_NO、PURCHASE_QTY、SALES_PRICE | 逐条查询所需批准文号、需求采购数量、销售价格 |
|
||||
| SALES_L 可选描述 | GOODS_NAME、SPEC、MANUFACTURER、UNIT | 按上游原值保留供查看,不作为新增属性匹配门槛 |
|
||||
|
||||
LINE_ID 建议 VARCHAR2(64),数量 NUMBER(18,4),价格 NUMBER(18,6),标识/文号 VARCHAR2(128),描述 VARCHAR2(256)。最终长度和精度按真实接口及目标系统核对。
|
||||
|
||||
### 4.5 合同/返利共用接口头明细 `RPA_JD_ORDER_H / L`
|
||||
|
||||
一份头代表上游明确给定的一组合同及对应返利数据,范围由报文的 SOURCE_DOC_ID 和明细集合确定。首版不按供应商+日期自行合单。若实际存在一组数据对应多份合同/返利,需要上游给出组标识,见第 12 章。
|
||||
|
||||
| 位置 | 建议字段 | 含义/使用流程 |
|
||||
| --- | --- | --- |
|
||||
| ORDER_H | 第 4.3 节公共字段 | 合同和返利共用输入标识 |
|
||||
| ORDER_H | CUSTOMER_NAME、CUSTOMER_TYPE、CREDIT_DAYS | 合同客户名称、预付/账期类型、账期天数 |
|
||||
| ORDER_H | CONTRACT_SIGN_DATE、CONTRACT_START_DATE、CONTRACT_END_DATE | 三个独立合同日期 |
|
||||
| ORDER_H | SUPPLIER_CODE、REBATE_UNIT_CODE、PLANNER、PURCHASER | 返利供应商、返利单位、计划员及采购员对应信息 |
|
||||
| ORDER_H | CHAIN_REF | 订单中心链路/源业务关联;精确结构按正式接口 |
|
||||
| ORDER_L | LINE_ID、INPUT_ID、SOURCE_LINE_ID、ORDER_NO、SOURCE_DETAIL_ID | 本方行 ID、来源行、订单号、货源细单号;订单号与货源细单号分列 |
|
||||
| ORDER_L | GOODS_CODE、GOODS_NAME、REG_CERT_NO、SPEC、UNIT | 标准货品编码/名称、注册证号、规格、单位 |
|
||||
| ORDER_L | PURCHASE_QTY、PURCHASE_UNIT_PRICE | 采购数量和单价;用于合同明细 |
|
||||
| ORDER_L | REBATE_QTY、REBATE_TAX_PRICE | 返利数量和含税协议单价;用于返利明细 |
|
||||
| ORDER_L | PAYMENT_CONDITION,可空 | 实际合同模板需要时从同一报文提供 |
|
||||
|
||||
同一次报文中数量如只有一个来源字段且供两流程共用,映射层同时引用它;不因此要求上游重复传两个数量。采购单价与含税协议单价按 Excel 的不同含义分别映射,未经接口确认不互相代替。
|
||||
|
||||
建议数量 NUMBER(18,4)、价格 NUMBER(18,6)、金额 NUMBER(18,2)、日期 DATE;名称 VARCHAR2(256)、编码 VARCHAR2(128)、类型 VARCHAR2(16)、账期 NUMBER(6)、CHAIN_REF CLOB 或按正式关联键拆列。固定默认值不强迫订单中心传入,在节点执行输入中生成并记录所用模板/映射版本。
|
||||
|
||||
**此处没有 `RPA_JD_CONTRACT_HEAD / RPA_JD_REBATE_HEAD` 两套输入表。两流程取数的 INPUT_ID 必须相同。**
|
||||
|
||||
### 4.6 业务作业与节点记录
|
||||
|
||||
| 表/字段 | 现有或新增 | 详细用途 |
|
||||
| --- | --- | --- |
|
||||
| job.flow_code / flow_version | 新增 VARCHAR(24) / VARCHAR(16) | 旧作业归原主链,新作业区分四条流程 |
|
||||
| job.input_id / input_version | 新增 VARCHAR(64) | 指向 Oracle 成功输入;ORDER_PUSH 指向本方导出批次 ID |
|
||||
| job.source_line_id | 新增 VARCHAR(128),头级作业用空串 | PROCURE 对应一条销售明细;合同/返利对应头 |
|
||||
| job.business_key | 复用 | 改为上述来源关联的展示/定位键;防重以组合唯一约束为准 |
|
||||
| job.status / current_node_code | 复用 | 当前执行状态和恢复节点 |
|
||||
| job.contract_system_no / rebate_order_no | 复用 | 合同 job 保存合同号,返利 job 保存返利总单/协议编号 |
|
||||
| job.next_check_at | 复用 | 可重试失败或外部等待的下次处理时间 |
|
||||
| job.execution_token / execution_host | 新增 VARCHAR(64) / VARCHAR(128) | 当前领取身份,拒绝旧执行回报;记录实际执行机 |
|
||||
| job_node.attempt_no、status、output_json | 复用 | 每次尝试独立记录;输入引用、业务结果和外部编号 |
|
||||
| job_node.started_at / ended_at / duration_ms | 复用 | 操作开始结束及耗时 |
|
||||
| job_node.error_code / error_summary | 复用 | 可筛选错误分类和具体原因 |
|
||||
| job_node.executor_run_id / callback_idempotency_key | 复用 | 一次脚本运行 ID、稳定回报键,重发回报不重复推进 |
|
||||
|
||||
新 flow 的组合唯一约束:`(flow_code, input_id, input_version, source_line_id)`;已成功作业也受约束。flow_version 是实现规则版本,不因升级自动为旧输入创建第二份作业。旧 job 回填其自身唯一 input_id,保留原入口规则。
|
||||
|
||||
共用数据成功接收后登记两份 job:CONTRACT 初始 PENDING,REBATE 初始 WAITING_EXTERNAL,原因为 WAIT_CONTRACT。同一 Oracle 输入即便登记调用重复,也只建立这一对作业。一次接收多组业务对象则逐头建立,不共享一个总完成标志。
|
||||
|
||||
旧列兼容迁移:保留 job_id、business_key、source_type、current_node_code、status、created_by 非空;supplier_name 和 order_date 改为允许空值,新来源数据有值时填入。旧入口继续校验其原必填,不给新作业补造供应商或日期。business_key 由新唯一键各项组合生成可读/哈希值;created_by 记录业务启动人或计划任务账号。
|
||||
|
||||
| flow | source_type | supplier_name / order_date | 首节点 |
|
||||
| --- | --- | --- | --- |
|
||||
| PROCURE | 本方定义 ORDER_CENTER | 供应商为空;日期取销售业务日(若提供) | PROCURE_ITEM_RUN |
|
||||
| ORDER_PUSH | 明确的平台配置代码 | 文件批次可含多供应商,供应商为空;日期不强制单日 | ORDER_EXPORT_FILE |
|
||||
| CONTRACT / REBATE | ORDER_CENTER | 接口明确提供的展示供应商及业务日期;合同三个日期仍取接口独立字段 | CONTRACT_CREATE / REBATE_HEADER |
|
||||
|
||||
source_type 上述取值是本方分类建议;药帮忙使用单独配置代码,不映射成旧 XYY。若任一新增标识超出现有列长度,迁移时扩列。旧 job 赋旧 flow/version 及唯一 input_id,新组合唯一键对全部历史行先回填再建立。
|
||||
|
||||
### 4.7 逐订单推送台账 `JD_ORDER_PUSH`(Oracle 拟新增)
|
||||
|
||||
文件/操作日志不能替代按订单查询的处理标志,因此该表只保存推送控制与结果,不复制完整平台业务表。
|
||||
|
||||
| 字段 | 建议类型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| id | BIGINT PK | 推送记录 ID |
|
||||
| platform / account_key / source_order_id | VARCHAR(32) / VARCHAR(64) / VARCHAR(128) | 来源订单唯一键,不按文件名去重 |
|
||||
| source_order_no / job_id / artifact_id | VARCHAR(128) / VARCHAR(64) / BIGINT | 展示单号、首次导出 job、原始文件关联 |
|
||||
| content_hash / request_key | VARCHAR(64) / VARCHAR(128) | 订单内容识别和本次固定请求标识 |
|
||||
| push_status | VARCHAR(16) | PENDING、SENDING、ACCEPTED、REJECTED、UNKNOWN |
|
||||
| request_body / response_body | JSON | 本次实际结构化发送内容、返回内容或所属批次引用 |
|
||||
| receipt_no / error_code / error_message | VARCHAR(128) / VARCHAR(64) / VARCHAR(500) | 回执及失败原因 |
|
||||
| attempt_count / next_retry_at | INT / DATETIME | 发送次数及重试时间 |
|
||||
| created_at / last_sent_at / accepted_at | DATETIME | 关键时点 |
|
||||
|
||||
唯一键 `(platform, account_key, source_order_id)`;相同订单相同内容复用结果,内容变化不能覆盖旧成功记录,记录冲突交运维核对。每次发送详细历史写 job_event,最近状态保存在此表。多个文件含同一订单时,在各导出 job 的日志中记录对应 artifact_id 与台账 id。
|
||||
|
||||
接口若支持批量发送,固定请求键绑定整份实际请求体;同键重发内容不变。逐单成功的回执更新对应台账;只发失败子集时生成新请求键并保留与原批次关联。没有逐单回执时按对方整批接收语义记录,不凭本方猜部分成功。
|
||||
|
||||
## 5. 详细设计:标志字段与取数条件
|
||||
|
||||
### 5.1 状态来源
|
||||
|
||||
| 业务标志 | 保存/计算位置 | 何时改变 |
|
||||
| --- | --- | --- |
|
||||
| 接收是否成功 | IF_REQ.RECEIVE_STATUS | 当前接收请求完成时 |
|
||||
| 作业是否登记 | IF_REQ.REGISTER_STATUS | 在 Oracle 幂等建立本次所需 job 后 |
|
||||
| 选品处理结果 | PROCURE job_node.output_json.result | 加购或正常跳过回报时:ADDED / SKIPPED |
|
||||
| 合同保存状态 | CONTRACT_CREATE 节点状态及 result | 开始、保存成功、失败或结果未知回报 |
|
||||
| 链路更新状态 | CONTRACT_CHAIN_UPDATE 节点状态及 result | 合同保存后调用订单中心,单独记录结果 |
|
||||
| 返利状态 | REBATE job.status / current_node_code | 等合同、执行、失败、提交成功 |
|
||||
| 平台订单接收状态 | JD_ORDER_PUSH.push_status | 每次发送和回执记录 |
|
||||
|
||||
运维接口返回 `contract_save_status / chain_update_status / rebate_status` 三列,是以上权威记录的汇总字段,**不再写回接口表形成第二套状态**。当前节点按 job 指针及该节点最新 attempt 取值,历史成功节点保留,不用整个合同 job.status 代替保存状态。
|
||||
|
||||
### 5.2 状态转换
|
||||
|
||||
| job 状态 | 含义 | 后续动作 |
|
||||
| --- | --- | --- |
|
||||
| PENDING | 已登记,等待执行 | 对应脚本领取 |
|
||||
| RUNNING | 已被领取且正在执行 | 接收匹配 execution_token 的回报 |
|
||||
| SUCCESS | 本流程应做步骤完成 | 常规取数不再领取 |
|
||||
| FAILED_RETRYABLE | 明确失败,可重试当前步骤 | 到 next_check_at 后或运维重试,新增 attempt |
|
||||
| WAITING_EXTERNAL | 前置尚未完成或结果待核对 | 按 wait_reason 处理,不能一律自动重做 |
|
||||
| WAITING_USER / FAILED_MANUAL | 业务或运维需介入 | 在原待办上处理、核对并记录动作 |
|
||||
| CANCELLED | 明确停止办理 | 保留原数据和历史 |
|
||||
|
||||
wait_reason 建议保存在 job 新增 VARCHAR(32) 字段:WAIT_CONTRACT、PROTOCOL_NOT_READY、RESULT_UNKNOWN。前两种下次检查条件;RESULT_UNKNOWN 需要核对外部结果,确认失败才可重试。
|
||||
|
||||
节点复用 PENDING/RUNNING/SUCCESS/FAILED,未知结果用节点 `WAITING_EXTERNAL` 并在 output_json 记 `result=UNKNOWN`;需要新增相应前后端状态支持。未尽调转人工保留待办,不能把“停止自动办理”显示为“合同已保存”。
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> PENDING
|
||||
PENDING --> RUNNING: 领取
|
||||
RUNNING --> SUCCESS: 完成
|
||||
RUNNING --> FAILED_RETRYABLE: 明确可重试失败
|
||||
RUNNING --> WAITING_EXTERNAL: 前置等待或结果未知
|
||||
RUNNING --> WAITING_USER: 业务处理
|
||||
FAILED_RETRYABLE --> PENDING: 重试当前步骤
|
||||
WAITING_EXTERNAL --> PENDING: 前置满足或核对未成功
|
||||
WAITING_EXTERNAL --> SUCCESS: 核对本流程已完成
|
||||
WAITING_USER --> PENDING: 处理后恢复未完成步骤
|
||||
PENDING --> CANCELLED: 停止办理
|
||||
```
|
||||
|
||||
上图为通用 job 状态。合同保存结果核对成功时若链路尚未更新,实际转移到链路节点 PENDING,不能直接标整个合同 SUCCESS。
|
||||
|
||||
### 5.3 领取规则
|
||||
|
||||
| flow | 读取范围及条件 | 成功/失败记录 |
|
||||
| --- | --- | --- |
|
||||
| PROCURE | SALES 数据 READY;行 job=PENDING,或到期可重试 | 合格 ADDED、不合格 SKIPPED 均结束行 job;错误单独失败 |
|
||||
| ORDER_PUSH | 对应平台导出任务及历史未完成台账;ACCEPTED 不重发 | 归档文件及每单 push_status |
|
||||
| CONTRACT | ORDER_COMMON 数据 READY;合同 job 待执行,按 current_node 恢复 | CREATE 成功保存合同号;CHAIN 失败只重试 CHAIN |
|
||||
| REBATE | 同 INPUT_ID/版本的 CREATE 节点确认 SUCCESS,合同号非空;返利待执行或到期重试 | 从共用接口表读取返利字段;读取合同 job 合同号;提交后 SUCCESS |
|
||||
|
||||
新录入与重试均覆盖历史未完成记录,不只筛选当天。创建返利 job 时可处于 WAIT_CONTRACT,合同失败不改它的接口输入;合同成功后,返利下一次扫描将其转 PENDING。
|
||||
|
||||
领取使用 Oracle 短事务条件更新或行锁,同时检查状态并写 execution_token;不能先查询再无条件改状态。脚本中断保留 RUNNING,运维确认原进程已停止后释放并恢复;本期不以超时自动启动第二个执行器。
|
||||
|
||||
### 5.4 节点定义与订单导出生命周期
|
||||
|
||||
| flow | 节点链 | 正常结束条件 |
|
||||
| --- | --- | --- |
|
||||
| PROCURE | PROCURE_ITEM_RUN | 本行 ADDED 或 SKIPPED |
|
||||
| ORDER_PUSH | ORDER_EXPORT_FILE → ORDER_CENTER_PUSH | 文件完整归档后,解析及推送该批应发送订单,均确认接收 |
|
||||
| CONTRACT | CONTRACT_CREATE → CONTRACT_CHAIN_UPDATE | 保存及链路更新分别成功;未尽调为当前节点人工待办,不另建供数流程 |
|
||||
| REBATE | REBATE_HEADER → REBATE_IMPORT → REBATE_SUBMIT | 正常提交成功;首次领取前检查合同保存条件 |
|
||||
|
||||
ORDER_PUSH 脚本先领取历史未完成批次;再按当前排期需要产生 export_batch_id 并登记新 job(input_id=export_batch_id,input_version=1,source_line_id=空串)。新 job 从 ORDER_EXPORT_FILE 领取执行。导出失败保留该 job;已保存文件和路径写 job_artifact,node_code=ORDER_EXPORT_FILE,恢复时先查已有归档文件。
|
||||
|
||||
归档成功后推进 ORDER_CENTER_PUSH:解析文件、按源订单建立/复用 JD_ORDER_PUSH、保存固定请求并推送。本批所有源订单到台账 ID 的关联写入该 job 的节点 output_json(大批量可用 artifact 保存清单),因此同一台账复用于多个文件时仍可判断每个批次是否完成。首次 job_id/artifact_id 不覆盖,后续文件关联由各批次记录持有。
|
||||
|
||||
源订单均为 ACCEPTED,或本轮确无可推送订单时,批次 job SUCCESS;零数据结果记 NO_DATA 并显示零条,不创建空订单。部分失败保留当前推送节点,已接收项跳过;同订单不同内容记冲突,阻止该批盲目完成,交运维核对。脚本退出与该批次 job 成功是两件事。
|
||||
|
||||
## 6. 详细设计:接口与交互时序
|
||||
|
||||
### 6.1 接口清单
|
||||
|
||||
| 接口/方向 | 状态 | 主要契约 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/jieda/inbound/sales` | 本方拟新增 | 完整销售对象,接收校验后落表 |
|
||||
| `POST /api/jieda/inbound/order-data` | 本方拟新增 | **一次传入合同及返利共同需要的数据**,返回接收记录 ID 和 INPUT_ID |
|
||||
| `POST /api/flows/{flow_code}/jobs/claim` | 本方拟新增 | 指定本轮 run_id,补登记后领取;返回 job、节点、attempt、执行令牌及输入关联 |
|
||||
| `POST /api/flows/ORDER_PUSH/jobs` | 本方拟新增 | 导出前按 export_batch_id 幂等登记平台/账号/范围及 job,导出失败也有作业可查 |
|
||||
| `GET /api/jobs/{job_id}/input` | 本方拟新增 | 对应接口头明细;返利额外返回关联合同 job 及合同号 |
|
||||
| `POST /api/jobs/{job_id}/nodes/{node}/{action}` | 复用 start/complete/fail 并扩展 | 结果、编号、错误、证据、attempt 和执行令牌;未知结果单独表达 |
|
||||
| `POST /api/jobs/{job_id}/push-records` | 本方拟新增 | 按源订单登记固定请求、逐单结果;校验所属 ORDER_PUSH 作业 |
|
||||
| `POST /api/jobs/{job_id}/retry` | 复用并修正 | 只恢复当前失败步骤,记录操作者及原因 |
|
||||
| 平台订单接收 API(本方→订单中心) | 外部契约待提供 | 来源订单、商品数量金额及实际接收回执 |
|
||||
| 链路节点更新 API(本方→订单中心) | 外部契约待提供 | 来源链路关联、对应节点、已保存合同标识及业务结果 |
|
||||
|
||||
订单推送由 ORDER_PUSH 脚本、链路更新由 CONTRACT 脚本通过统一客户端调用。调用前通过状态中心记录固定请求,调用后保存回执。状态中心不同时后台发送同一请求。外部正式路径、业务枚举和鉴权由订单中心提供。
|
||||
|
||||
### 6.2 请求、校验及重复处理
|
||||
|
||||
接收报文在本方适配层统一表示为 `message_id + objects[]`。每个对象有 source_doc_id、input_version(可选)、header、lines。ORDER_COMMON 的 header/lines 包含第 4.5、7 章共同字段,不是分别推 contract 和 rebate。
|
||||
|
||||
| 返回情况 | 本方建议 HTTP | 业务返回及持久化 |
|
||||
| --- | --- | --- |
|
||||
| 缺字段/格式不合法/空明细 | 422 | REJECTED,errors 含对象标识、行标识及字段路径;无业务头明细 |
|
||||
| 全部校验通过且保存成功 | 200 | ACCEPTED、request_log_id、input_ids;表示接收成功 |
|
||||
| 已接收同消息同内容 | 200 | DUPLICATE,返回原 input_ids,另记接收尝试 |
|
||||
| 已接收同标识不同内容 | 409 | CONFLICT,旧成功输入及作业不变 |
|
||||
| 保存失败 | 503 | STORE_FAILED+trace_id;不返回接收成功 |
|
||||
| 鉴权失败 | 401/403 | 拒绝,不建立业务作业 |
|
||||
|
||||
首版整次请求校验通过后,同一个 Oracle 事务保存成功接收记录及全部对象头明细。任一必填缺失则整次失败,返回全部可识别字段错误;上游可按完整单据分批发送。缺的是返利字段也当次返回,因为这一份数据承担两个流程。
|
||||
|
||||
客户类型、三个合同日期、合同明细字段,以及供应商/返利单位/计划员/订单号/货源细单号/含税协议单价在本次共用数据中按第 7 章校验。合同号是本方执行结果,接收时允许不存在。固定值、系统生成字段、页面尽调回显不要求上游提供。
|
||||
|
||||
相同业务对象换 message_id 再传,由头表来源+版本唯一键识别:内容相同引用原 INPUT_ID,内容不同冲突。没有源版本号时,哈希只判断内容同异,不自动判定“哪条更新”。已成功接收的同 SOURCE_DOC_ID 改内容或改版本,本期返回 INPUT_CONFLICT,不自动创建第二组合同/返利 job;这是按业务标识检查,不能仅依赖版本唯一索引。运维和订单中心确认改单处理后再另行安排,原作业和结果保持可查。此前校验失败的修正重传可正常接收。
|
||||
|
||||
接收失败日志存 IF_REQ;Oracle 本身不可用时只能写应用错误日志和 trace_id,不能宣称数据库必有失败记录。响应在事务提交后产生;若响应丢失,上游重传可取得原接收结果。
|
||||
|
||||
### 6.3 时序图:一次接收,两流程共用
|
||||
|
||||
**接收及合同处理。** 请求内校验并落表;合同排期触发后读取这份数据。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant OC as 订单中心
|
||||
participant API as 状态中心
|
||||
participant IF as 共用接口表
|
||||
participant C as 合同脚本
|
||||
participant CT as 合同系统
|
||||
OC->>API: 一次推送合同返利共用数据
|
||||
API->>API: 校验两流程上游必填
|
||||
alt 校验失败
|
||||
API->>IF: 记REJECTED,不建业务头明细
|
||||
API-->>OC: 失败及字段位置
|
||||
else 校验通过
|
||||
API->>IF: 同事务保存接收记录和头明细
|
||||
IF-->>API: 提交成功 INPUT_ID
|
||||
API-->>OC: 接收成功 INPUT_ID
|
||||
C->>API: 按合同排期领取
|
||||
API->>API: 幂等登记同输入的合同/返利job
|
||||
API->>IF: 按INPUT_ID读取输入并记登记完成
|
||||
API-->>C: 合同job及共用数据
|
||||
C->>CT: 录入并保存
|
||||
CT-->>C: 保存成功及合同号
|
||||
C->>API: 回报保存结果,推进至链路节点
|
||||
C->>OC: 更新对应链路节点
|
||||
OC-->>C: 更新回执
|
||||
C->>API: 保存链路结果
|
||||
end
|
||||
```
|
||||
|
||||
**返利在另一排期继续。** 返利不再向订单中心获取另一份数据,按相同 INPUT_ID 读取接口表,并取得本方关联合同结果。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R as 返利脚本
|
||||
participant API as 状态中心
|
||||
participant DB as 业务记录
|
||||
participant IF as 同一共用接口表
|
||||
participant RB as 返利中心
|
||||
R->>API: 按返利排期领取
|
||||
API->>DB: 检查同INPUT_ID的合同保存结果
|
||||
alt 合同未保存
|
||||
API->>DB: 返利保持WAIT_CONTRACT
|
||||
API-->>R: 本条暂不领取,继续其他数据
|
||||
else 合同保存成功且合同号存在
|
||||
API->>IF: 读取相同INPUT_ID和版本
|
||||
IF-->>API: 原头明细中的返利字段
|
||||
API-->>R: 返利数据加本方合同号
|
||||
R->>RB: 查协议、填总单、导明细并提交
|
||||
RB-->>R: 提交结果
|
||||
R->>API: 回报返利结果
|
||||
API->>DB: 保存节点、编号和操作日志
|
||||
end
|
||||
```
|
||||
|
||||
两图通过同一 INPUT_ID 衔接,不要求两脚本连续运行。返利先启动时可先完成作业登记,但保持 WAIT_CONTRACT。
|
||||
|
||||
### 6.4 时序图:平台订单归档推送
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant R as 订单脚本
|
||||
participant P as 平台
|
||||
participant F as 共享文件夹
|
||||
participant API as 状态中心
|
||||
participant OC as 订单中心
|
||||
R->>P: 按约定范围导出订单
|
||||
P-->>R: 完整文件
|
||||
R->>F: 保存到对应平台目录
|
||||
alt 保存失败
|
||||
R->>API: 回报文件错误,保留失败步骤
|
||||
else 保存成功
|
||||
R->>API: 登记文件、源订单及固定请求
|
||||
API-->>R: 未接收订单和请求键
|
||||
R->>OC: 按正式格式推送
|
||||
alt 接收确认
|
||||
OC-->>R: 业务接收回执
|
||||
R->>API: 更新逐订单台账,记录成功/拒绝项
|
||||
else 超时或连接中断
|
||||
R->>API: 记录UNKNOWN及实际请求
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
共享目录保存后再组装推送。文件名冲突保留新批次路径,不覆盖原件;导出失败恢复导出,已归档的文件恢复时直接使用原件。
|
||||
|
||||
### 6.5 时序图:合同保存成功、链路更新失败
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as 合同脚本
|
||||
participant CT as 合同系统
|
||||
participant API as 状态中心
|
||||
participant DB as Oracle
|
||||
participant OC as 订单中心
|
||||
C->>CT: 保存合同
|
||||
CT-->>C: 成功 HT001
|
||||
C->>API: CREATE complete,合同号和证据
|
||||
API->>DB: 保存CREATE成功,当前节点改CHAIN_UPDATE
|
||||
C->>API: 记录链路固定请求
|
||||
C->>OC: 链路更新
|
||||
OC-->>C: 明确失败或无响应
|
||||
C->>API: 记录FAILED或UNKNOWN及原因
|
||||
API->>DB: 保留合同号和失败位置
|
||||
Note over C,DB: 脚本继续下一单;运维可见保存成功、链路异常
|
||||
C->>API: 下次仅领取当前链路节点
|
||||
Note over C,OC: UNKNOWN先核对;明确可重试才再次调用
|
||||
C->>OC: 使用约定幂等请求更新
|
||||
OC-->>C: 更新成功
|
||||
C->>API: CHAIN complete及回执
|
||||
API->>DB: 完成合同job;原CREATE记录不变
|
||||
```
|
||||
|
||||
## 7. 详细设计:业务字段映射
|
||||
|
||||
本章保留 Excel 的实际字段和行号。合同/返利列来源都指向第 4.5 节同一份接口输入;按各自模板生成执行数据。生成的模板、默认日期及映射版本记录到节点 output_json/文件证据,不再复制成独立合同或返利输入表。
|
||||
|
||||
### 7.1 合同头
|
||||
|
||||
| 目标字段 | 取值规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 客户名称 | 订单中心 | 12 |
|
||||
| 客户编码 | 目标系统自动生成 | 11 |
|
||||
| 尽调情况 | 读取回显;未尽调停止本单自动录入并转采购员处理 | 13 |
|
||||
| 结算类型 | 收支双向 | 15 |
|
||||
| 合同版本类型 | 预付:我司定制;账期:非格式合同 | 17 |
|
||||
| 合同金额(单位元) | 预估本次合作金额 | 24 |
|
||||
| 金额(元) | 明细汇总;与导入后页面汇总核对并回填 | 25 |
|
||||
| 合同签订日期、合同开始日期、合同结束日期 | 订单中心分别提供,三个独立字段 | 26–28 |
|
||||
| 是否重大合同 | 否 | 29 |
|
||||
| 是否预付款 | 预付:是;账期:否 | 30 |
|
||||
| 付款方式 | 电汇 | 32 |
|
||||
| 付款期限 | 预付:1;账期:订单中心账期天数,缺失不默认 30 | 33 |
|
||||
| 合同具体分类 | 国内药品类、器械类品种 | 37 |
|
||||
| 运输方式 | 公路运输 | 39 |
|
||||
|
||||
### 7.2 合同明细
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 货品编码 | 订单中心 | 43 |
|
||||
| 货品名称 | 订单中心标准货品名称 | 44 |
|
||||
| 注册证号 | 订单中心 | 46 |
|
||||
| 规格、单位 | 按订单中心标准字段填报 | 47–48 |
|
||||
| 数量、单价 | 按订单中心最终采购数量、单价填报 | 49–50 |
|
||||
| 付款方式 | 电汇 | 52 |
|
||||
| 付款条件 | 订单中心;模板无此字段时不填 | 53 |
|
||||
| 付款期限 | 同合同头规则 | 54 |
|
||||
|
||||
### 7.3 返利总单
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 返利合约 | 否 | 5 |
|
||||
| 供应商编码 | 订单中心 | 6 |
|
||||
| 返利单位编码 | 订单中心 | 7 |
|
||||
| 计划员 | 订单中心,按业务给定的计划员/采购员对应关系填写 | 8 |
|
||||
| 采购结算方式 | 文案“否”;与细单代码 3 分别映射 | 9 |
|
||||
| 承付模式 | 下月 N 号结 | 10 |
|
||||
| 协议开始日期、协议结束日期 | 当天 | 11–12 |
|
||||
|
||||
### 7.4 返利细单
|
||||
|
||||
| 目标字段 | 来源/规则 | 行号 |
|
||||
| --- | --- | --- |
|
||||
| 货品编码、数量、含税协议单价 | 本次共用接口数据的货品编码、数量及含税协议单价 | 15–17 |
|
||||
| 税率、关税税率 | 均为 0.13 | 18–19 |
|
||||
| 承付模式、承付参数 | 11、25 | 20–21 |
|
||||
| 细单开始日期 | 当天 | 22 |
|
||||
| 细单结束日期 | 本年度最后一天 | 23 |
|
||||
| 采购结算方式 | 3 | 24 |
|
||||
| 特定补偿预估值、进货补偿预估值 | 均为 0 | 25–26 |
|
||||
| 细单备注 | 订单中心订单号 | 33 |
|
||||
| 货源细单ID | 订单中心货源细单号;与订单号分列 | 34 |
|
||||
|
||||
合同明细导入列按 Excel 的 Sheet3。表中标注无需填写的字段不填;系统自动生成字段由系统处理。
|
||||
|
||||
|
||||
### 7.5 映射与运行日期规则
|
||||
|
||||
- 合同与返利的数量来自同一次报文;实际只提供一列数量时共用该列。含税协议单价与采购单价分别保留来源映射。
|
||||
- 首次执行该流程时生成所用默认日期及模板记录。重试已生成的相同步骤使用原输入,避免跨日重试更改已导入单据;新返利作业按返利首次执行当天生成“当天/年底”。这是实现规则,不增加业务确认步骤。
|
||||
- 合同客户编码为系统生成字段。现有 RPA 入口要求的 customerCode 实际用作客商搜索标识,须与系统自动生成的客户编码分清;按新页面取数方式适配,不要求上游为自动生成字段填占位符。
|
||||
- 映射表标注无需填写的字段不填。尽调读取页面回显;未尽调按原流程转采购员并通知。接口数据保留,人工完成后核对实际合同号并登记保存成功,进入链路更新步骤;返利按第 5.3 节条件领取。
|
||||
|
||||
## 8. 详细设计:操作记录与一致性
|
||||
|
||||
### 8.1 最少需要记录的操作信息
|
||||
|
||||
| 场景 | 保存位置 | 必须能查到的信息 |
|
||||
| --- | --- | --- |
|
||||
| 每次接口接收 | IF_REQ | 请求时间、来源消息、INPUT_ID 关联、原报文、返回结果、字段错误、trace_id |
|
||||
| 脚本执行一步 | job_node | flow、job、INPUT_ID/版本、节点、attempt、run_id、执行机、开始结束、结果、错误 |
|
||||
| 平台订单发送一次 | JD_ORDER_PUSH+job_event | 原文件、源订单、固定请求键、实际请求、HTTP/业务返回、耗时、成功/拒绝范围 |
|
||||
| 合同保存 | job_node+job_artifact | 实际合同标识、保存确认、目标页面证据、所用输入和模板 |
|
||||
| 链路调用一次 | job_event+链路 job_node | 链路关联、节点代码、合同号、请求键、报文、回执、错误 |
|
||||
| 运维操作一次 | job_event / user_task | 操作人、时间、动作、原因、前后状态、证据和操作结果 |
|
||||
| 文件 | job_artifact | 原始订单、导入模板、截图或日志文件路径,文件类型及哈希 |
|
||||
|
||||
job_event 建议事件类型包括 INPUT_BOUND、NODE_STARTED、NODE_SUCCESS、NODE_FAILED、API_REQUEST、API_RESPONSE、RETRY_REQUESTED、RESULT_VERIFIED、MANUAL_HANDOFF、JOB_FINISHED。detail_json 保存结构化信息。节点列表用 job_node.error_code/error_summary 筛选及展示;事件详情从 detail_json.errorCode/errorMessage 读取错误,不假定 job_event 存在独立错误列。详细错误与报文在详情展开。
|
||||
|
||||
接收失败没有 job 时由 IF_REQ 独立展示;所有能定位到作业的动作都写 job_event。脚本运行 ID 使用现有 executor_run_id;无数据或启动失败的本轮摘要写应用日志,通过 run_id 查询,暂不新增运行历史表。
|
||||
|
||||
### 8.2 事务及恢复顺序
|
||||
|
||||
| 写入过程 | 事务范围 | 恢复办法 |
|
||||
| --- | --- | --- |
|
||||
| 接收成功 | Oracle:成功请求记录+所有接口头明细 | 事务失败整体返回失败;响应丢失可同消息重传 |
|
||||
| 登记作业 | Oracle:同输入的合同和返利 job 一起幂等建立 | 成功后回写 IF_REQ.REGISTER_STATUS;回写失败下次重复登记返回同 job,再补 DONE |
|
||||
| 节点成功 | Oracle:该 attempt 成功+业务编号+操作日志/证据索引+推进当前节点 | 回报丢失重发同完成键;同成功结果返回原状态,不再次推进 |
|
||||
| 外部接口调用 | 先 Oracle 保存请求,事务提交后调用,返回后独立事务记结果 | 超时保留 UNKNOWN,不把网络调用放在数据库事务中 |
|
||||
| 人工恢复 | Oracle:核查结果、状态变更和操作日志 | 同一操作请求 ID 重复提交返回原结果 |
|
||||
|
||||
Oracle 接口输入和 Oracle 执行结果不要求同库事务,也不把执行状态反复写回 Oracle。接口表登记标志失败最多导致再次尝试登记,组合唯一约束保证不多建合同/返利作业。运维总览查两库时若 Oracle 不可用,仍可展示 Oracle 已保存的执行结果,并标记“接口原文暂不可查”,不能误显示数据不存在。
|
||||
|
||||
### 8.3 文件和配置
|
||||
|
||||
1 药城与药帮忙分别配置共享根目录和字段映射;现场路径由配置提供。下载完成后发布文件,文件名按批次区分,保留原件。后台返回文件列表,业务/运维通过现有证据入口打开或下载,不要求共享盘在所有机器使用同一盘符。
|
||||
|
||||
订单中心连接、凭据、超时、链路节点映射、四条排期、重试间隔及模板配置集中管理。真实环境缺必要配置返回明确错误,不自动使用 fixture。日期、数量和金额按第 4、7 章数据类型保存,日志不保存 token 或数据库密码。
|
||||
|
||||
## 9. 详细设计:正常路径与异常处理
|
||||
|
||||
### 9.1 正常路径示例
|
||||
|
||||
本例标识仅用于说明字段关系,不代表正式接口报文。
|
||||
|
||||
| 时间/动作 | 接口记录 | 合同保存 | 链路更新 | 返利 | 记录位置 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 接收 M001 | ACCEPTED,INPUT_ID=I001,REGISTER=PENDING | 未登记 | 未登记 | 未登记 | IF_REQ+ORDER_H/L |
|
||||
| 任一相关脚本启动登记 | REGISTER=DONE | PENDING | 未开始 | WAIT_CONTRACT | 两个 job 都绑定 I001/1 |
|
||||
| 合同脚本处理 | 原输入不变 | RUNNING | 未开始 | WAIT_CONTRACT | CREATE 第 1 次尝试 |
|
||||
| 合同保存成功 HT001 | 原输入不变 | SUCCESS | PENDING | 可领取 | 合同号及保存证据 |
|
||||
| 更新链路成功 | 原输入不变 | SUCCESS | SUCCESS | 可领取 | 链路请求、回执;合同 job 完成 |
|
||||
| 返利下次运行 | 原输入不变 | SUCCESS | SUCCESS | RUNNING | 读取 I001 同一头明细+HT001 |
|
||||
| 返利提交完成 FL001 | 原输入不变 | SUCCESS | SUCCESS | SUCCESS | 返利编号、模板及提交结果 |
|
||||
|
||||
### 9.2 异常矩阵
|
||||
|
||||
| 异常代码(建议) | 识别条件 | 状态和保留内容 | 处理及恢复位置 |
|
||||
| --- | --- | --- | --- |
|
||||
| INPUT_REQUIRED | 本次共用数据缺合同或返利上游必填 | IF_REQ=REJECTED,字段路径/行号,无业务输入 | 上游补齐后重传;RPA 无需处理 |
|
||||
| INPUT_CONFLICT | 成功来源/版本再传不同内容 | CONFLICT,关联旧请求、保存新原文 | 核对来源及版本;不覆盖已使用数据 |
|
||||
| STORE_FAILED | 接口表事务失败 | 返回 503;应用日志 trace_id | 修复数据库后重传 |
|
||||
| JOB_REGISTER_FAILED | Oracle 已接收,Oracle 登记失败 | REGISTER=FAILED、原因;输入 READY | 恢复 Oracle,下一次脚本取数补登记 |
|
||||
| PLATFORM_QUERY_FAILED | 页面打不开或查询报错 | 行 job 明确失败,截图/错误 | 修复后重试该行;不记 SKIPPED |
|
||||
| NO_ELIGIBLE_PRODUCT | 查询成功但不满足采购条件 | job SUCCESS,result=SKIPPED、原因 | 正常结束,常规轮询不重复处理 |
|
||||
| FILE_SAVE_FAILED | 共享目录不可写/文件未完整 | ORDER_PUSH 失败、已知路径 | 修复目录后从导出/归档失败处继续 |
|
||||
| OC_ORDER_REJECTED | 订单中心明确业务拒收 | push=REJECTED,拒绝原因及范围 | 修正接口映射/源数据后处理失败单,成功单保留 |
|
||||
| EXTERNAL_RESULT_UNKNOWN | 加购、保存、推送或提交后无确定结果 | WAITING_EXTERNAL/UNKNOWN,请求或页面证据 | 先核对目标结果;确认未成功才重试 |
|
||||
| CONTRACT_DUE_DILIGENCE | 合同页面显示未尽调 | WAITING_USER、采购员待办和页面记录 | 按原业务人工处理,核验保存结果后继续链路 |
|
||||
| CHAIN_UPDATE_FAILED | 合同已保存,链路明确失败 | CREATE 成功;CHAIN 失败、回执 | 下次仅重试 CHAIN;合同号不清空 |
|
||||
| PROTOCOL_NOT_READY | 已有合同号,但返利协议不可查/暂不可操作 | 返利 WAITING_EXTERNAL,原因及检查时间 | 到期再查协议;不重建合同,不等第二次供数 |
|
||||
| REBATE_SUBMIT_FAILED | 返利系统明确提交失败 | 失败步骤、目标协议/总单、已导入信息 | 按原业务交人工处理;保留前面成功步骤 |
|
||||
| EXECUTOR_INTERRUPTED | 作业仍 RUNNING,进程已退出 | 执行机/run_id、最近节点、证据 | 运维确认无运行进程并核对结果后,恢复当前步骤 |
|
||||
|
||||
所有正常失败恢复保留原 INPUT_ID、job 和历史 attempt。通用“重试”只增加当前失败节点尝试,不把 job 从第一个节点重新执行。已完成外部写入不能仅凭“失败”按钮反向当作没有发生。
|
||||
|
||||
## 10. 运维设计:如何查看与处理
|
||||
|
||||
### 10.1 查询入口及页面内容
|
||||
|
||||
复用现有列表、节点详情、时间线、待办和证据组件,新增接收列表和“同一输入的合同/返利总览”,不用另建运维平台。
|
||||
|
||||
| 页面 | 筛选条件 | 列表/详情内容 |
|
||||
| --- | --- | --- |
|
||||
| 接口接收列表 | 时间、来源单号、MESSAGE_ID、TRACE_ID、接收状态、登记状态 | 请求/返回原文、缺字段路径、INPUT_ID、重复来源、登记失败原因 |
|
||||
| 合同返利总览 | INPUT_ID、来源单号、合同号、返利号、三个业务状态、时间 | 同一行显示合同保存、链路更新、返利状态及最近错误;点击进入各 job |
|
||||
| 作业详情 | flow、job_id、状态、执行机、run_id | 来源输入、当前节点、每次尝试、耗时、错误、回报、外部编号 |
|
||||
| 订单推送列表 | 平台/账号、订单号、导出批次、push_status | 原文件、发送内容、回执、次数、结果未知/拒收原因 |
|
||||
| 操作和证据 | job/INPUT_ID、操作者、动作时间 | 谁点击重试/人工处理,前后状态、原因、截图、模板及日志 |
|
||||
|
||||
“合同返利总览”由后端以 Oracle INPUT_ID 为基底,合并两份 Oracle job/节点结果;不存在 job 时显示未登记。分页/筛选由后端实现,不要求浏览器一次拉全表。只有接口原文不可查时保留错误提示,已存在的执行结果仍显示。
|
||||
|
||||
### 10.2 典型查看路径
|
||||
|
||||
| 运维问题 | 如何查 | 结论/下一步 |
|
||||
| --- | --- | --- |
|
||||
| 订单中心说推过,RPA 没处理 | 先查 MESSAGE_ID/来源单号的接收记录 | REJECTED 看缺字段;ACCEPTED+登记失败看数据库错误;DONE 看对应 job |
|
||||
| 合同存在,为什么流程显示失败 | 总览看合同保存和链路更新两列 | 保存成功/链路失败只补链路调用,不重建 |
|
||||
| 返利为什么一直没开始 | 看同 INPUT_ID 的合同保存结果及返利 wait_reason | 合同未保存则正常等待;保存成功但协议不可查则按目标状态处理 |
|
||||
| 同一批有的成功有的失败 | 订单推送按源订单筛选,选品按行 job 查看 | 重试失败项;已接收/已加购项不重复处理 |
|
||||
| 数据被改过或重复推过 | 接收尝试记录+DATA_HASH+操作日志 | 明确首次输入、重复/冲突报文及处理人 |
|
||||
|
||||
### 10.3 运维动作及权限
|
||||
|
||||
| 动作 | 允许条件 | 实际改变 | 必须留痕 |
|
||||
| --- | --- | --- | --- |
|
||||
| 重试当前步骤 | FAILED_RETRYABLE,且外部结果已明确 | 当前节点新增 attempt,job 转 PENDING | 操作人、原因、旧状态、新 attempt |
|
||||
| 重新登记作业 | 已 ACCEPTED 且 REGISTER≠DONE | 幂等补建缺失 job,完成登记标志 | 接收记录、结果、操作者 |
|
||||
| 核对结果:已成功 | UNKNOWN 或人工完成,提供实际业务编号/回执证据 | 确认对应节点成功并推进;合同保存后仍执行链路 | 目标编号、证据、核对人和时间 |
|
||||
| 核对结果:未成功 | 确认目标未写入,原执行进程已停止 | 转可重试当前步骤 | 核对依据、操作者、原因 |
|
||||
| 转业务处理 | 业务错误或系统要求人工 | 创建/复用现有 user_task,保存责任人 | 原因、接收人、操作记录 |
|
||||
| 停止办理 | 有明确业务指令 | CANCELLED,保留已产生结果 | 原因及外部已发生操作,不自动撤销外部单据 |
|
||||
|
||||
只读运维可看列表及证据;恢复动作限定 OPS,业务待办按采购员等现有角色处理。恢复入口必须核对实际操作者身份,不能只相信请求体里填写的 operator。沿用现有鉴权方案补动作权限,不另加业务审批流。
|
||||
|
||||
不能用“统一确认并继续”替代所有核对动作,不能直接改接口原始字段或数据库状态。金额、货品等源数据错误由订单中心修正重传;已被使用的输入按冲突规则处理。
|
||||
|
||||
### 10.4 查询 API 及日志定位
|
||||
|
||||
| API(建议) | 用途 |
|
||||
| --- | --- |
|
||||
| `GET /api/jieda/receipts`、`/receipts/{request_log_id}` | 无 job 的失败接收也能查 |
|
||||
| `GET /api/jieda/orders`、`/orders/{input_id}` | 同一输入的合同/链路/返利总览及头明细 |
|
||||
| `GET /api/jieda/order-pushes` | 按源订单查推送及文件 |
|
||||
| 现有 `GET /api/jobs/{job_id}`、`/timeline`、`/evidence-package` | 节点、历史尝试和证据 |
|
||||
| `GET /api/jobs/{job_id}/events`(新增) | 操作日志和出站请求/返回记录 |
|
||||
| `POST /api/jieda/receipts/{id}/register`(新增) | 运维手动补登记,与脚本取数复用同一逻辑 |
|
||||
| `POST /api/jobs/{job_id}/verify-result`(新增) | 按第 10.3 节核对 UNKNOWN/人工结果,记录依据后恢复 |
|
||||
|
||||
应用日志统一携带 `trace_id、input_id、job_id、flow_code、node_code、attempt_no、run_id` 中当前已知项。接收阶段没有 job_id 时不编造;执行阶段没有 HTTP trace 时以 job/run 定位。列表展示最近错误,详情保留全部历史,不只覆盖最后一次失败。
|
||||
|
||||
索引建议:接口记录按时间/状态/来源,业务头按源单号,job 按 `(flow_code,status,next_check_at)` 和 `(input_id,input_version)`,节点按现有 `(job_id,node_code,attempt_no)`,事件按 `(job_id,created_at)`,推送按来源唯一键及 `(push_status,next_retry_at)`。
|
||||
|
||||
## 11. 实施影响与验收
|
||||
|
||||
### 11.1 现有代码到新设计的映射
|
||||
|
||||
| 现有位置 | 已核对现状 | 实施改动 |
|
||||
| --- | --- | --- |
|
||||
| [main.py 建单](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:499) | 供应商+日期键,固定首节点,只复用未完成作业 | 新接收/登记/领取按 flow 和 INPUT_ID;已完成作业也防重 |
|
||||
| [schema.sql](/Users/yuuuu/Documents/AutoRpa/state_center/sql/schema.sql:11) | job 无 flow/input 关联;已有节点、日志及证据表 | 第 4 章 Oracle 接口表及推送台账;job 扩展字段/索引,supplier_name/order_date 允许空;job_event 错误保存在现有 detail_json |
|
||||
| [db.py](/Users/yuuuu/Documents/AutoRpa/state_center/app/db.py:22) | Oracle 默认 autocommit=True | 对登记、领取、结果及推进使用显式短事务 |
|
||||
| [main.py 完成回报](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:712) | 保存节点、编号和推进分步执行;节点定义全局固定 | 同事务回报,校验 execution_token/attempt;按 flow 定义完成规则 |
|
||||
| [main.py 重试](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:812) | 已保留 attempt 历史、重试当前节点 | 保留机制,支持共用输入关联及 UNKNOWN 核对 |
|
||||
| [main.py 人工提交](/Users/yuuuu/Documents/AutoRpa/state_center/app/main.py:1346) | 通用确认可直接推进;RETRY_CURRENT_NODE 分支当前只记录事件 | 按第 10.3 节校验结果和动作,实现真实重试及核查恢复 |
|
||||
| [agent.py](/Users/yuuuu/Documents/AutoRpa/rpa_yisaiqi/agent.py:1) | 通用代理主要调用 mock;存在节点硬编码与默认 attempt=1 | 四条独立入口、真实 runner、领取结果中的 attempt/token |
|
||||
| [contract_create_node.py](/Users/yuuuu/Documents/AutoRpa/rpa_yisaiqi/contract_create_node.py:241) | 已校验空明细及正数量,仍要求旧 customerCode | 适配共用接口数据和客商定位;保留有效输入检查 |
|
||||
| [contract_flow_steps.py](/Users/yuuuu/Documents/AutoRpa/rpa_yisaiqi/contract_flow_steps.py:788) | 缺价补 1.00,同码首行及前 30 行定位 | 移除缺价补值;按来源明细/目标行定位并核对实际导入结果 |
|
||||
| [Web 节点详情](/Users/yuuuu/Documents/AutoRpa/state_center_web/src/components/JobNodeDetailList.tsx:1) | 已展示时间、尝试次数、错误及证据 | 复用组件,补三个独立状态、输入关联、接收列表及恢复动作 |
|
||||
|
||||
当前 Oracle 配置主要服务货品主数据读取;新接口表写入连接和权限需现场配置。当前代码不等于已具备四条真实平台执行能力,平台查询/导出及返利页面模块需要核对或实现。
|
||||
|
||||
### 11.2 实施顺序
|
||||
|
||||
1. 按共同输入契约核对第 4、7 章字段及合同/返利范围;确认现场接口表复用方式。
|
||||
2. 完成接收校验、Oracle 同事务保存及接收查询,验证失败重传和重复消息。
|
||||
3. 增加 flow/input 关联、作业登记、独立状态、领取及结果事务,跑通“同一输入两个 job”。
|
||||
4. 接平台订单导出、共享文件、台账及推送回执。
|
||||
5. 接合同页面保存和链路更新;返利读取同 INPUT_ID 与合同号完成提交。
|
||||
6. 接逐条选品;补齐异常及运维动作,配置四条排期后联调。
|
||||
|
||||
### 11.3 验收用例
|
||||
|
||||
| 编号 | 场景 | 预期结果 |
|
||||
| --- | --- | --- |
|
||||
| AT-01 | 一次完整 ORDER_COMMON 推送 | 一份头明细;登记两个相同 INPUT_ID/版本的合同、返利 job |
|
||||
| AT-02 | 仅返利必填缺失 | 当次返回字段错误,无业务输入;补齐后可重传 |
|
||||
| AT-03 | 相同请求重发、相同对象换消息 ID 重发 | 返回原输入,不新增执行作业 |
|
||||
| AT-04 | 同已成功来源/版本传不同内容 | 冲突可查,旧数据和外部结果不被覆盖 |
|
||||
| AT-05 | Oracle 已提交,Oracle 登记失败 | 接收记录显示登记失败;后续幂等补登记,不要求上游再推 |
|
||||
| AT-06 | 返利脚本先于合同脚本启动 | WAIT_CONTRACT;无额外供数请求、无返利提交 |
|
||||
| AT-07 | 合同保存成功,链路更新失败 | 保存 SUCCESS、链路 FAILED;只恢复链路,返利可按保存条件领取 |
|
||||
| AT-08 | 合同完成后返利按另一排期启动 | 读取同一接口数据和本方合同号,字段来源可追溯 |
|
||||
| AT-09 | 平台订单重复导出到不同文件 | 原件保留,已接收同源订单不重复入单 |
|
||||
| AT-10 | 推送部分成功/超时 | 按实际回执记录逐单结果;UNKNOWN 不直接按失败重发 |
|
||||
| AT-11 | 选品合格/不合格/查询失败 | 分别 ADDED、SKIPPED、FAILED;本批结束,不等付款 |
|
||||
| AT-12 | 脚本中断和重复点击 | 不重复领取;已有结果可查,确认停止并核对后恢复当前步骤 |
|
||||
| AT-13 | 合同已保存但回报丢失 | 重发完成键或核对目标结果,不再次建合同 |
|
||||
| AT-14 | 未尽调或返利提交失败 | 保留输入及原业务待办,其他记录继续;人工结果核对可留痕 |
|
||||
| AT-15 | 46 个 Excel RPA 字段 | 共用接口来源、固定值及日期按映射填报,缺价不补 1.00 |
|
||||
| AT-16 | 运维按 MESSAGE_ID、INPUT_ID、合同号查询 | 能串起接收、两流程、尝试、文件、请求和人工动作 |
|
||||
| AT-17 | 未授权恢复、旧 token 回报 | 拒绝写入;旧成功结果不被覆盖 |
|
||||
| AT-18 | 新旧流程并存 | 旧在途单保留原节点;新流程不误入对码/OCR/M5 |
|
||||
|
||||
文档检查与业务验收分开:当前只完成设计;以上需要在测试数据库、实际共享目录及外部测试系统执行,mock 成功不代表真实业务通过。
|
||||
|
||||
## 12. 未决事项、风险及对其他模块的影响
|
||||
|
||||
### 12.1 联调前需要确认
|
||||
|
||||
| 项目 | 已明确 | 剩余信息 |
|
||||
| --- | --- | --- |
|
||||
| 合同返利供数 | 一次推送共用 | 正式头明细、业务范围标识、消息 ID/版本;一组数据是否有多合同/多返利范围 |
|
||||
| 业务字段 | Excel 来源及默认值 | 实际 JSON 列名;采购/返利数量是否同字段,价格口径及精度 |
|
||||
| 订单出站接口 | 本方归档后推送 | 接收 URL、字段、批量/逐单回执、幂等和查询能力 |
|
||||
| 链路更新 | 合同保存成功后更新对应节点 | 节点代码、链路关联、合同标识字段及成功返回码 |
|
||||
| 返利可操作性 | 用同一份输入加已保存合同号 | 目标协议生成时间、是否依赖审批或链路更新;未确认前不新增业务审批门槛 |
|
||||
| 平台和共享目录 | 两平台分别导出归档 | 真实账号、订单状态/范围、文件样例、共享路径及四条排期 |
|
||||
| 盈利 | 5% 以上盈利 | 计算基数及边界;不直接套用旧方案 0.95 公式 |
|
||||
| 现场库和模板 | 接口输入与处理记录分开 | 现有 Oracle 表、权限、正式模板和确有要求的附件 |
|
||||
|
||||
### 12.2 可能问题与控制位置
|
||||
|
||||
| 问题 | 影响 | 对应设计 |
|
||||
| --- | --- | --- |
|
||||
| 共用一个 processed 标志 | 合同处理后返利漏取 | 第 5 章分离合同保存、链路、返利标志 |
|
||||
| 两份接口输入被分别修改 | 合同/返利货品数量不一致 | 同 INPUT_ID/版本只保留一份原始头明细 |
|
||||
| 成功接收后没有 job | 数据落表但长期未执行 | REGISTER_STATUS 和取数前补登记;运维可查 |
|
||||
| 接口/页面结果未知被当失败 | 重复订单、合同或返利 | UNKNOWN 核对后恢复,不无条件重做 |
|
||||
| 把链路失败当合同未保存 | 再建一份合同 | 保存节点结果与链路节点独立 |
|
||||
| 当前合同缺价补值或明细定位错误 | 错金额、同码行覆盖或漏行 | 接收校验及第 11.1 节代码修正;单规格规则不消除多订单行的问题 |
|
||||
| 原人工确认直接推进 | 未核实外部结果就完成作业 | 第 10.3 节按动作和证据校验 |
|
||||
| 新旧流程改全局状态机 | 旧对码、OCR、M5 在途单无法继续 | flow/version 分开,旧单保持原链;切换前确定同一来源由哪套办理 |
|
||||
| 两库或共享目录不可用 | 接收失败、原文不可查、文件不可达 | 独立错误可见;执行记录保留,不伪装成空数据或成功 |
|
||||
|
||||
本稿的表与字段是详细设计建议,尚未核对现场字典。对外契约尚未提供的部分保留为明确缺口;不以增加业务操作、复制数据或自动猜规则来填补。
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
# 福建洁达采购 RPA 概要设计
|
||||
|
||||
| 文档属性 | 内容 |
|
||||
| --- | --- |
|
||||
| 版本 | V3.1 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 状态 | 设计评审稿;未实施代码或建表 |
|
||||
| 适用系统 | 当前 AutoRpa 状态中心、RPA 执行程序及福建洁达采购业务 |
|
||||
| 本版核心关系 | 订单中心一次推送合同/返利共用数据;合同先处理,返利按自己的排期读取同一份数据 |
|
||||
| 文档范围 | 概要设计、表与状态详细设计、接口及时序、异常恢复、运维查询、实施与验收 |
|
||||
| 替代关系 | 整合原 Codex 设计稿 V1.9 和技术实现说明 V2.3;旧方案仅供历史参考 |
|
||||
|
||||
**阅读顺序:** 业务评审看第 1–3 章;开发看第 4–9 章;运维看第 10 章;实施验收及未决问题见第 11–12 章。
|
||||
|
||||
|
||||
> 详细的数据模型、接口字段、状态、时序、异常、运维和验收见[《福建洁达采购 RPA 详细设计》](./2026-09-13-jieda-rpa-detailed-design.md)。
|
||||
|
||||
## 1. 设计依据与范围
|
||||
|
||||
### 1.1 已确认业务规则
|
||||
|
||||
| 编号 | 规则 |
|
||||
| --- | --- |
|
||||
| BR-01 | 四条脚本分别运行,自动时间和频率不同,也支持业务点击;一次 HTTP 接收不等待四条流程全部执行 |
|
||||
| BR-02 | 选品读取未处理销售明细,在 1 号药城和药帮忙均按同一套采购条件执行:按批准文号查询,依次校验自营仓、盈利、有效期、库存,再按仓库距离选择;合格加购物车,不合格跳过,处理完本批结束;不等待人工付款 |
|
||||
| BR-03 | 本业务相同批准文号只有一个规格,不增加规格、厂家、包装或单位换算匹配 |
|
||||
| BR-04 | 平台订单导出到各平台对应共享文件夹,按订单中心接口格式组装并推送;对码由订单中心内部处理 |
|
||||
| BR-05 | 订单中心一次传来的完整业务数据供合同、返利共用,不拆成两次独立供数,不重复保存两份相同头明细 |
|
||||
| BR-06 | 接收时检查上游应传的必填字段,缺项当次返回失败和字段位置;通过并落表后返回成功 |
|
||||
| BR-07 | 合同保存成功后调用订单中心链路接口更新对应节点;返利读取同一份接口数据,并使用本方已取得的合同号 |
|
||||
| BR-08 | 手工单暂不做;合同/返利字段、固定值及原业务人工分支按提供材料执行 |
|
||||
| BR-09 | RPA 将平台订单的对码原始信息随订单报文传给订单中心;订单中心返回或后续提供标准货品结果,本方不自行判断货品编码 |
|
||||
|
||||
### 1.2 参考材料
|
||||
|
||||
| 材料 | 用途 |
|
||||
| --- | --- |
|
||||
| [合同系统录入流程](/Users/yuuuu/Documents/AutoRpa/资料/合同系统录入流程.md) | 合单、尽调、提交、回写;页面步骤指向 rpa_yisaiqi |
|
||||
| [选品条件 20260902](/Users/yuuuu/Documents/AutoRpa/资料/采购平台选品条件优先项_20260902.txt) | 自营仓、盈利、效期、库存和仓库顺序 |
|
||||
| [合同返利字段映射 20260911](/Users/yuuuu/Documents/AutoRpa/资料/合同返利系统字段映射_20260911.xlsx) | 两个工作表的 46 个 RPA 字段及 Sheet3 导入列 |
|
||||
| [仓库一期技术设计](/Users/yuuuu/Documents/AutoRpa/资料/采购主链RPA一期设计文档.md) | 参考概要/详细设计、实体、接口、状态和审计的组织方式;不沿用其旧业务范围 |
|
||||
| [现有数据库结构](/Users/yuuuu/Documents/AutoRpa/state_center/sql/schema.sql) | 判断 job、节点、待办、日志及证据记录可复用部分 |
|
||||
|
||||
本次会话的业务确认优先于旧文档。下文表名、内部代码、API 路径及列类型为本方建议,正式外部字段名、节点代码和现场 Oracle 结构仍需按第 12 章核对。
|
||||
|
||||
## 2. 概要设计:架构与分工
|
||||
|
||||
### 2.1 总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
OC[订单中心] -->|销售数据 / 合同返利共用数据| API[FastAPI 接收校验]
|
||||
API -->|校验成功同事务落表| IFDB[(Oracle 接口头明细及接收记录)]
|
||||
S[四条独立排期 / 点击脚本] --> RPA[RPA 执行程序]
|
||||
RPA <-->|领取数据 / 回报结果| SC[状态中心]
|
||||
SC -->|读取同一份输入| IFDB
|
||||
SC <--> BDB[(Oracle 作业、节点、调用记录)]
|
||||
RPA -->|查品加购 / 导出订单| PL[采购平台]
|
||||
PL --> F[对应共享文件夹]
|
||||
F -->|组装报文并推送| OC
|
||||
RPA -->|录入保存| CT[合同系统]
|
||||
RPA -->|保存后调用链路接口| OC
|
||||
RPA -->|同一接口数据加合同号| RB[返利中心]
|
||||
UI[运维列表 / 详情 / 日志] --> SC
|
||||
```
|
||||
|
||||
接口数据与业务处理记录分开:**Oracle 接口表回答“订单中心传了什么”,Oracle 业务记录回答“本方做到哪一步、结果是什么、谁操作过”。** RPA 通过状态中心读取接口数据,数据库连接集中在后端。
|
||||
|
||||
### 2.2 接口表与业务表的选择
|
||||
|
||||
| 方式 | 特点 | 本期选择 |
|
||||
| --- | --- | --- |
|
||||
| 仅一张接口表加一个 processed 标志 | 简单,但合同处理后可能被整体标记完成,返利漏取;也无法表达多次失败尝试 | 不采用 |
|
||||
| 接口表再完整复制成合同表、返利表 | 三份字段容易不一致,需要额外同步 | 不采用 |
|
||||
| 接口头明细保留一份输入,业务作业/节点保存独立状态与结果 | 合同、返利通过相同 INPUT_ID 关联;历史操作复用现有日志 | **采用** |
|
||||
|
||||
现有 `job / job_node` 就是本期业务处理表,无需再复制合同、返利输入。为无 job 的接收失败补接收记录;为跨导出文件识别同一订单,补逐订单推送台账。这两项有明确查询及防重用途。
|
||||
|
||||
### 2.3 模块职责及运行方式
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| FastAPI | 校验、保存接收数据、取数、原子领取、保存回报、查询及运维动作 |
|
||||
| Oracle 接口库 | 接收尝试、销售头明细、合同/返利共用头明细;成功输入保留原值 |
|
||||
| Oracle 状态中心 | 各流程 job、节点尝试、业务编号、订单推送状态、操作日志、证据索引 |
|
||||
| 四条 RPA 脚本 | 本批逐条执行;平台订单和合同链路接口由对应脚本通过适配器调用并回报 |
|
||||
| 现有 Web | 接收查询、共用数据处理总览、节点尝试、文件和运维动作 |
|
||||
|
||||
四条脚本分别配置时间、频率及本轮条数上限,单执行机每条脚本设单实例启动控制。FastAPI 常驻。脚本启动时调用后端登记未建立的 job,再领取本流程数据;订单导出先生成并登记 export_batch_id,恢复失败批次沿用该 ID;本批结束释放本机运行锁。没有数据时记录本轮零条并退出,不新增第五条业务或后台维护流程。
|
||||
|
||||
## 3. 概要设计:业务流程与完成边界
|
||||
|
||||
| 流程 | 输入 | 正常处理 | 完成含义 |
|
||||
| --- | --- | --- | --- |
|
||||
| PROCURE | 销售接口表的一条明细 | 按批准文号查询,按采购条件加购或跳过 | 该行处理完;不代表付款成交 |
|
||||
| ORDER_PUSH | 对应平台导出的订单文件 | 保存共享目录、提取对码原始信息、组装、推送、记录回执 | 应推送订单确认接收;不等待内部对码 |
|
||||
| CONTRACT | 共用订单接口头明细 | 录入并保存合同,保存合同号,更新订单中心链路 | 保存及链路更新分别记录;链路失败不重建合同 |
|
||||
| REBATE | **同一共用订单接口头明细**+本方合同保存结果 | 按合同号查协议、填总单、导细单、提交 | 返利提交成功;不要求订单中心再次推数据 |
|
||||
|
||||
### 3.1 合同与返利的关联
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
I[一次推送:INPUT_ID = I001] --> C[合同 job:INPUT_ID = I001]
|
||||
I --> R[返利 job:INPUT_ID = I001]
|
||||
C --> CS[合同保存成功:合同号 HT001]
|
||||
CS --> CH[合同链路更新]
|
||||
CS -->|提供合同号| R
|
||||
R --> RS[返利提交结果]
|
||||
```
|
||||
|
||||
合同、返利是两个独立作业,绑定同一 INPUT_ID 和 INPUT_VERSION。合同保存号只从对应合同 job 取得,不能按同品种或同一天猜配。
|
||||
|
||||
返利默认在“合同已确认保存成功且合同号存在”后可领取;链路回传结果另列显示,**不擅自把链路成功增加为返利前置条件**。若目标返利协议暂不可查,记录等待原因,下次返利运行再查询。是否必须审批通过由目标系统及接口方确认,不新增审批动作。
|
||||
|
||||
### 3.2 选品规则
|
||||
|
||||
1 号药城和药帮忙共用以下选品判断逻辑。平台只负责登录、搜索、读取商品信息和执行加购,不能改变判断优先级。自营仓、5% 以上盈利、有效期大于等于 12 个月、库存满足采购数量四项都通过后,按下列仓库顺序选择。不合格记跳过,平台技术错误记失败。
|
||||
|
||||
广东壹号药业有限公司、昆山亿方药业有限公司、湖北亿昊药业有限公司、天津壹号药业有限公司、重庆亿昊药业有限公司、重庆壹号医药有限公司、湖南壹号药业股份有限公司、辽宁亿昊药业有限公司、河南壹号医药有限公司、甘肃壹号云医药有限责任公司企业店、陕西壹号药业有限公司。
|
||||
|
|
@ -1,498 +0,0 @@
|
|||
# 洁达选品:改造商城查价工程(详细方案)
|
||||
|
||||
> Windows 按本文改同事原工程。概要口径见 [2026-09-14-jieda-procure-crawler-overview.md](./2026-09-14-jieda-procure-crawler-overview.md)。
|
||||
> 不要改仓库一期状态机,不要把 ±10% 比价逻辑留在主流程里。
|
||||
|
||||
| 文档属性 | 内容 |
|
||||
| --- | --- |
|
||||
| 版本 | V1.0 |
|
||||
| 日期 | 2026-09-14 |
|
||||
| 执行环境 | Windows + 艺赛旗 Python 应用(`isa-rpa` + `uv`) |
|
||||
| 基线代码 | 同事工程 `main.py`(约 907 行) |
|
||||
| 建议工作副本 | 拷一份到本仓库 `rpa_yisaiqi/procure_mall/`,或 Windows 本地独立目录;不要在原 zip 上直接当唯一真源 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 原工程:留什么、删什么、补什么
|
||||
|
||||
### 1.1 原样保留(不要重写)
|
||||
|
||||
| 函数 / 能力 | 作用 |
|
||||
| --- | --- |
|
||||
| `SecretClient` + `YAOEX_SECRET_NAME` / `YBM_SECRET_NAME` | 密码箱登录 |
|
||||
| `get_yaoex_auth` / `ensure_yaoex_logged_in` | 1 号药城登录,抽 `token / userId / buyerCode` |
|
||||
| `get_ybm_cookies` / `ensure_ybm_logged_in` | 药帮忙登录,抽 cookie |
|
||||
| `build_yaoex_search_request` / `query_yaoex_products` | `homeSearchList`,`keyword=批准文号`,`per=20`,`deadLineStart=12` |
|
||||
| `decrypt_yaoex_price` / `build_yaoex_price_decrypt_key` | 价格 AES 解密 |
|
||||
| `build_ybm_search_payload` / `query_ybm_products` | `list-products`,`queryWord=批准文号`,`isThirdCompany=0`,`isEffectPeriodOverOneYear=1` |
|
||||
| `post_json_with_retry` | 3 次重试 |
|
||||
| 按批准文号缓存 | 同一文号不打两次 |
|
||||
|
||||
搜索接口:
|
||||
|
||||
- 1 号药城:`POST https://gateway-b2b.fangkuaiyi.com/home/search/homeSearchList`
|
||||
- 药帮忙:`POST https://www.ybm100.com/new-front/search/list-products`
|
||||
|
||||
字段映射继续用原代码:
|
||||
|
||||
| 结果字段 | 1 号药城 | 药帮忙 |
|
||||
| --- | --- | --- |
|
||||
| 平台索引ID | `spuCode` | `productInfo.skuId` |
|
||||
| 平台名称 | `shortName` | `productInfo.showName` |
|
||||
| 平台规格 | `spec` | `productInfo.spec` |
|
||||
| 平台价格 | `decrypt(price)` | `price`,空则 `fob`;库存 0 时价当 0 |
|
||||
| 效期 | `deadLine` | `productInfo.nearEffect` |
|
||||
| 商家 | `vendorName` | `productInfo.shopName` |
|
||||
| 库存 | `stockCount` | `productInfo.availableQty` |
|
||||
|
||||
### 1.2 必须删除或停用
|
||||
|
||||
- `is_in_purchase_price_band`
|
||||
- `filter_rows_by_purchase_price_band`
|
||||
- `main()` 里「价格区间过滤后总行数」那段
|
||||
- 把全部候选行当最终输出
|
||||
|
||||
### 1.3 必须新增
|
||||
|
||||
- 选品规则引擎(两平台共用)
|
||||
- 按行决策:每行每平台 0 或 1 个仓
|
||||
- 加购适配器(接口优先,页面兜底)
|
||||
- `--dry-run` / `--limit` / `--platform`
|
||||
- 新结果表(结论 + 原因,不再是比价明细大表)
|
||||
|
||||
---
|
||||
|
||||
## 2. 建议拆文件
|
||||
|
||||
原 `main.py` 已 900 行,不要再往里堆加购和规则。Windows 按下列拆;若艺赛旗工程限制只能单文件,也必须把规则和加购写成独立函数,禁止写进 `query_*`。
|
||||
|
||||
```text
|
||||
procure_mall/
|
||||
pyproject.toml # 沿用原工程
|
||||
main.py # 只编排:读入 → 登录 → 循环行 → 写结果
|
||||
search_yaoex.py # 从 main.py 搬登录+查询+解密
|
||||
search_ybm.py # 从 main.py 搬登录+查询
|
||||
http_util.py # post_json_with_retry 等
|
||||
rules.py # 合格判定 + 仓库排序
|
||||
cart_yaoex.py # 1 号药城加购
|
||||
cart_ybm.py # 药帮忙加购
|
||||
io_excel.py # 读输入、写结果
|
||||
tests/test_rules.py # 规则单测,不连商城
|
||||
```
|
||||
|
||||
`main.py` 目标骨架:
|
||||
|
||||
```python
|
||||
def main() -> None:
|
||||
args = parse_args() # --dry-run 默认 True;--limit N;--platform all|yaoex|ybm
|
||||
rows = load_input_rows(args.input_path)
|
||||
if args.limit:
|
||||
rows = rows[: args.limit]
|
||||
|
||||
yaoex_auth = asyncio.run(get_yaoex_auth()) if args.platform in ("all", "yaoex") else None
|
||||
ybm_cookies = asyncio.run(get_ybm_cookies()) if args.platform in ("all", "ybm") else None
|
||||
|
||||
decisions = []
|
||||
for purchase in rows:
|
||||
if yaoex_auth is not None:
|
||||
decisions.append(run_one_platform(purchase, "1号药城", yaoex_auth, dry_run=args.dry_run))
|
||||
if ybm_cookies is not None:
|
||||
decisions.append(run_one_platform(purchase, "药帮忙", ybm_cookies, dry_run=args.dry_run))
|
||||
|
||||
write_decision_file(decisions)
|
||||
```
|
||||
|
||||
单行单平台:
|
||||
|
||||
```python
|
||||
def run_one_platform(purchase, mall_name, auth, dry_run=True) -> dict:
|
||||
try:
|
||||
candidates = search_candidates(mall_name, auth, purchase["批准文号"])
|
||||
except Exception as exc:
|
||||
return decision(purchase, mall_name, status="FAILED", reason=f"QUERY_FAILED:{exc}")
|
||||
|
||||
picked, rejected = select_warehouse(candidates, purchase)
|
||||
if picked is None:
|
||||
return decision(purchase, mall_name, status="SKIPPED", reason=rejected_summary(rejected), raw=candidates)
|
||||
|
||||
if dry_run:
|
||||
return decision(purchase, mall_name, status="ADDED", reason="DRY_RUN", picked=picked)
|
||||
|
||||
try:
|
||||
cart_ref = add_to_cart(mall_name, auth, picked, qty=purchase["采购数量"])
|
||||
except Exception as exc:
|
||||
return decision(purchase, mall_name, status="FAILED", reason=f"CART_FAILED:{exc}", picked=picked)
|
||||
|
||||
return decision(purchase, mall_name, status="ADDED", reason="CART_OK", picked=picked, cart_ref=cart_ref)
|
||||
```
|
||||
|
||||
查询失败不得改成 SKIPPED。
|
||||
|
||||
---
|
||||
|
||||
## 3. 输入 / 输出
|
||||
|
||||
### 3.1 输入 Excel(改模板,不要沿用只有采购价格的比价模板)
|
||||
|
||||
路径建议:`桌面/福建商城选品测试/选品输入.xlsx`
|
||||
第一行表头,从第 2 行起数据。
|
||||
|
||||
| 列名 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 行号 | 否 | 没有则按 Excel 行号 |
|
||||
| 批准文号 | 是 | 查询关键字;空行跳过 |
|
||||
| 销售价格 | 是 | 毛利基准。缺或无法转数字 → 该行两平台都 SKIPPED,原因 `NO_SALE_PRICE` |
|
||||
| 采购数量 | 是 | 库存门槛和加购数量。缺或 ≤0 → SKIPPED,原因 `NO_QTY` |
|
||||
| 通用名 | 否 | 回填 |
|
||||
| 规格 | 否 | 回填,不参与匹配 |
|
||||
| 生产厂家 | 否 | 回填,不参与匹配 |
|
||||
|
||||
本期**不要用采购价格做合格判断**。模板里可以留着采购价格列,规则引擎忽略它。
|
||||
|
||||
`--limit 5` 只跑前 5 条有效行。
|
||||
|
||||
### 3.2 输出 Excel
|
||||
|
||||
路径:`桌面/福建商城选品测试/结果/选品结果_YYYYMMDD_HHMMSS.xlsx`
|
||||
一行 = 一个输入行 × 一个平台。
|
||||
|
||||
| 列名 | 说明 |
|
||||
| --- | --- |
|
||||
| 行号 | 输入行 |
|
||||
| 批准文号 | |
|
||||
| 通用名 | |
|
||||
| 销售价格 | |
|
||||
| 采购数量 | |
|
||||
| 商城 | `1号药城` / `药帮忙` |
|
||||
| 结论 | `ADDED` / `SKIPPED` / `FAILED` |
|
||||
| 原因 | 见 3.3 |
|
||||
| 选中商家 | ADDED 时必填 |
|
||||
| 平台索引ID | sku/spu |
|
||||
| 平台名称 | |
|
||||
| 平台规格 | |
|
||||
| 平台价格 | |
|
||||
| 效期原文 | |
|
||||
| 库存 | |
|
||||
| 毛利率 | `(销售价-平台价)/销售价`,保留 4 位小数 |
|
||||
| dry_run | `Y/N` |
|
||||
| 加购凭证 | 接口回执或页面摘要;dry-run 为空 |
|
||||
| 候选商家清单 | 可选,分号拼接,便于核对为什么没选上 |
|
||||
|
||||
另写一份 `未匹配商家_时间戳.txt`:所有出现过、但不在 11 家名单里的商家名(去重)。用来决定要不要加别名。
|
||||
|
||||
### 3.3 原因码
|
||||
|
||||
| 原因码 | 结论 | 何时 |
|
||||
| --- | --- | --- |
|
||||
| `NO_SALE_PRICE` | SKIPPED | 销售价缺失 |
|
||||
| `NO_QTY` | SKIPPED | 采购数量缺失或 ≤0 |
|
||||
| `NO_HIT` | SKIPPED | 接口无货或空列表 |
|
||||
| `NO_SELF_WAREHOUSE` | SKIPPED | 有货但不在 11 家 |
|
||||
| `MARGIN_LT_5PCT` | SKIPPED | 自营仓有货但毛利不够(可与其它原因并列,取「没有任何候选过齐 1~4」) |
|
||||
| `EXPIRY_LT_12M` | SKIPPED | 效期不够 |
|
||||
| `STOCK_LT_QTY` | SKIPPED | 库存不够 |
|
||||
| `NO_CANDIDATE_AFTER_RULES` | SKIPPED | 1~4 筛完为空(推荐作为总原因,明细放候选清单) |
|
||||
| `DRY_RUN` | ADDED | 规则通过,未真正加购 |
|
||||
| `CART_OK` | ADDED | 加购成功 |
|
||||
| `QUERY_FAILED:...` | FAILED | 搜索接口失败(已重试) |
|
||||
| `CART_FAILED:...` | FAILED | 加购失败 |
|
||||
| `CART_UNKNOWN:...` | FAILED | 加购后无法确认是否进车 |
|
||||
|
||||
SKIPPED 的总原因用 `NO_CANDIDATE_AFTER_RULES`,并在「候选商家清单」里带每条被淘汰原因。不要只写一个模糊的「不合格」。
|
||||
|
||||
---
|
||||
|
||||
## 4. 规则引擎(`rules.py`)
|
||||
|
||||
两平台只把候选翻译成同一结构,再进引擎。平台代码禁止自己改优先级。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Candidate:
|
||||
mall_name: str
|
||||
sku_id: str
|
||||
name: str
|
||||
spec: str
|
||||
price: float
|
||||
expiry_raw: str
|
||||
expiry_months: float | None
|
||||
vendor_raw: str
|
||||
vendor_norm: str
|
||||
stock: float
|
||||
extra: dict
|
||||
|
||||
@dataclass
|
||||
class PurchaseLine:
|
||||
approval_no: str
|
||||
sale_price: float
|
||||
qty: float
|
||||
```
|
||||
|
||||
### 4.1 商家规范化
|
||||
|
||||
```text
|
||||
去首尾空格
|
||||
去掉前缀 "[自营]"(全角空格也去)
|
||||
压缩连续空格为单空格
|
||||
```
|
||||
|
||||
规范化后必须 **等于** 下面 11 个字符串之一,顺序即优先级(下标越小越优先):
|
||||
|
||||
```python
|
||||
WAREHOUSE_ORDER = [
|
||||
"广东壹号药业有限公司",
|
||||
"昆山亿方药业有限公司",
|
||||
"湖北亿昊药业有限公司",
|
||||
"天津壹号药业有限公司",
|
||||
"重庆亿昊药业有限公司",
|
||||
"重庆壹号医药有限公司",
|
||||
"湖南壹号药业股份有限公司",
|
||||
"辽宁亿昊药业有限公司",
|
||||
"河南壹号医药有限公司",
|
||||
"甘肃壹号云医药有限责任公司企业店",
|
||||
"陕西壹号药业有限公司",
|
||||
]
|
||||
```
|
||||
|
||||
本期不做别名、不做包含匹配。对不上的 `vendor_raw` 记入未匹配清单。
|
||||
|
||||
### 4.2 价格
|
||||
|
||||
```text
|
||||
毛利 = (销售价格 - 平台价) / 销售价格
|
||||
合格:毛利 >= 0.05
|
||||
即:平台价 <= 销售价格 * 0.95
|
||||
```
|
||||
|
||||
平台价无法转数字 → 该候选淘汰。销售价 ≤ 0 → 整行 SKIPPED。
|
||||
**禁止**用采购价、禁止 ±10% 区间。
|
||||
|
||||
### 4.3 效期
|
||||
|
||||
目标:剩余 ≥ 12 个月。解析顺序:
|
||||
|
||||
1. 纯数字(含小数):当作月数。
|
||||
2. 含「月」:抽出数字当月数。
|
||||
3. 日期(`YYYY-MM-DD` / `YYYY/MM/DD` / `YYYY-MM`):`(到期日 - 今天) / 30.4375`,到期日按该月最后一天。
|
||||
4. 都不是:`expiry_months = None`,该候选淘汰(原因 `EXPIRY_UNPARSEABLE`)。
|
||||
|
||||
接口虽已带 12 个月过滤,仍以本函数为准。
|
||||
|
||||
### 4.4 库存
|
||||
|
||||
`stock >= qty`。库存无法转数字或为 0 → 淘汰。药帮忙无货把价格改成 0 的旧逻辑可保留在映射层,引擎侧会因库存/价格再淘汰一次。
|
||||
|
||||
### 4.5 择仓
|
||||
|
||||
```text
|
||||
passed = [c for c in candidates if 自营仓 and 毛利 and 效期 and 库存]
|
||||
if not passed: return None
|
||||
return min(passed, key=lambda c: WAREHOUSE_ORDER.index(c.vendor_norm))
|
||||
```
|
||||
|
||||
同一仓库多条 SKU:取平台价更低的;再相同取库存更大的。不要两条都加购。
|
||||
|
||||
### 4.6 必须先写的单测(不连网)
|
||||
|
||||
文件:`tests/test_rules.py`。Windows 在改 `main.py` 前先把这些测过。
|
||||
|
||||
| 用例 | 期望 |
|
||||
| --- | --- |
|
||||
| 11 家名单内、毛利 6%、效期 13 月、库存够 | 选出该仓 |
|
||||
| 毛利刚好 5% | 选出(`>= 0.05`) |
|
||||
| 毛利 4.9% | 淘汰 |
|
||||
| 商家不在名单 | 淘汰 |
|
||||
| 商家带 `[自营] 广东壹号药业有限公司` | 视为广东壹号 |
|
||||
| 广东和昆山都合格 | 选广东 |
|
||||
| 只有昆山合格 | 选昆山 |
|
||||
| 库存 9、采购 10 | 淘汰 |
|
||||
| 效期 11 个月 | 淘汰 |
|
||||
| 效期 `2027-09-14` 且今天起 ≥12 个月 | 过 |
|
||||
| 空候选 | SKIPPED 语义(函数返回 None) |
|
||||
| 销售价用 100、平台价 96 | 淘汰(毛利 4%) |
|
||||
| 销售价 100、平台价 95 | 过 |
|
||||
|
||||
用采购价 ±10% 的旧例子(平台价 96、采购价 90)在新规则下若销售价是 100 应淘汰。加一条单测钉死,防止有人把 ±10% 加回来。
|
||||
|
||||
命令:
|
||||
|
||||
```bat
|
||||
cd /d 工程目录
|
||||
uv run pytest tests/test_rules.py -v
|
||||
```
|
||||
|
||||
期望:全绿。未装 pytest 则 `uv add --dev pytest`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 加购(原工程没有,这是新工作)
|
||||
|
||||
### 5.1 抓接口(每个平台做一次,记录进 `docs/cart-api.md` 或工程内 `CART_API.md`)
|
||||
|
||||
在 Windows 上用已经登录的账号:
|
||||
|
||||
1. 打开对应商城,F12 → Network,过滤 XHR/Fetch。
|
||||
2. 搜索一个确定能加的自营仓商品。
|
||||
3. 点「加入购物车」,看新出现的请求。
|
||||
4. 记录:URL、方法、请求头(cookie / token 哪些必要)、body、成功响应长什么样。
|
||||
5. 把 body 里的 sku/spu、数量、商家 ID 标出来。
|
||||
|
||||
1 号药城登录态用 `ycgltoken` 等;药帮忙用整份 cookie。加购请求必须带上搜品时同一套认证,不要重新登录。
|
||||
|
||||
若加购接口有 CSRF / 签名 / 人机验证:不要硬解,改页面点击(5.3)。
|
||||
|
||||
### 5.2 接口加购(优先)
|
||||
|
||||
`cart_yaoex.add(auth, candidate, qty) -> cart_ref`
|
||||
`cart_ybm.add(cookies, candidate, qty) -> cart_ref`
|
||||
|
||||
- 数量 = 采购数量。
|
||||
- 成功:响应明确成功,或随后查购物车能看到该 sku + 数量。
|
||||
- 响应含糊:结论 `FAILED`,原因 `CART_UNKNOWN`,不要报 ADDED。
|
||||
- 已在车里:视为成功(幂等),不要再加一次导致数量翻倍。先查车再加。
|
||||
|
||||
### 5.3 页面加购(接口抓不到才用)
|
||||
|
||||
只对规则已经选出的那 **1 条** 操作:
|
||||
|
||||
1. 打开商品页或搜索结果中该 sku。
|
||||
2. 填数量。
|
||||
3. 点加入购物车。
|
||||
4. 等成功提示或打开购物车核对。
|
||||
|
||||
禁止:重新用页面搜索批准文号再人工挑仓。仓已经由规则选定。
|
||||
|
||||
页面选择器用艺赛旗 `BrowserSession`,登录态复用。不要为加购再登一次(除非会话丢了)。
|
||||
|
||||
### 5.4 dry-run
|
||||
|
||||
默认 `--dry-run`。`ADDED + DRY_RUN` 表示规则通过。关 dry-run 必须显式 `--execute-cart`。
|
||||
|
||||
第一次真正加购只用 `--limit 1 --platform yaoex --execute-cart`,人工打开购物车核对后再加量。
|
||||
|
||||
---
|
||||
|
||||
## 6. Windows 环境与运行
|
||||
|
||||
### 6.1 环境
|
||||
|
||||
- 艺赛旗 Python 应用,能 `uv run main.py`
|
||||
- 密码箱已有:
|
||||
- `yaoex_GYJDLHT_login`
|
||||
- `yaobangmang_18146141930_login`
|
||||
- 账号必须是业务允许加购的测试/正式采购账号。真正加购前跟业务确认,避免测出一车真货。
|
||||
- 桌面目录:`福建商城选品测试\`
|
||||
|
||||
### 6.2 命令
|
||||
|
||||
```bat
|
||||
:: 只判定,不加购(默认)
|
||||
uv run main.py --dry-run --limit 5
|
||||
|
||||
:: 只跑一个平台
|
||||
uv run main.py --dry-run --limit 5 --platform yaoex
|
||||
|
||||
:: 确认规则后,真加购 1 条
|
||||
uv run main.py --execute-cart --limit 1 --platform yaoex
|
||||
```
|
||||
|
||||
把命令写进 `README.md`,替换原「查价结果」说明。
|
||||
|
||||
### 6.3 验收顺序(必须按这次序,不要一上来一千条真加购)
|
||||
|
||||
1. `pytest tests/test_rules.py` 全绿。
|
||||
2. `--dry-run --limit 5 --platform yaoex`:结果表 5 行,结论只有 ADDED/SKIPPED/FAILED,没有 ±10% 残留。
|
||||
3. 同样 5 条跑药帮忙。
|
||||
4. 人工抽 2 条 ADDED:对照网页,商家是否 11 家之一、毛利是否 ≥5%、效期、库存。
|
||||
5. `--execute-cart --limit 1` 一个平台,打开购物车看 sku 和数量。
|
||||
6. 再跑同一条,确认不会把数量翻倍(幂等)。
|
||||
7. 再 `--limit 20` dry-run,看耗时。预估一千条:约 `20 条耗时 × 50`,写入结果 README。登录应只发生两次(每平台一次)。
|
||||
|
||||
失败则停在当前步,不要跳到一千条。
|
||||
|
||||
---
|
||||
|
||||
## 7. 异常
|
||||
|
||||
| 情况 | 处理 |
|
||||
| --- | --- |
|
||||
| 密码箱空 | 立即失败退出,不写半份结果 |
|
||||
| 登录失败 | 该平台本批中止;另一平台仍可跑(已登录的话) |
|
||||
| 单文号查询 3 次仍失败 | 该行该平台 FAILED,继续下一行 |
|
||||
| 加购超时 | FAILED `CART_UNKNOWN`,不要当 SKIPPED |
|
||||
| 极验登录点一次不够 | 沿用原等待逻辑;仍失败则整平台中止,记登录错误 |
|
||||
| 关闭 dry-run 但加购模块未实现 | 启动时报错退出,禁止假装 ADDED |
|
||||
|
||||
脚本不点支付、不打开结算页。
|
||||
|
||||
---
|
||||
|
||||
## 8. Windows 任务清单
|
||||
|
||||
按序做。每步可单独验证。
|
||||
|
||||
### 任务 1:工作副本和输入模板
|
||||
|
||||
- [ ] 拷贝同事工程到工作目录(仓库 `rpa_yisaiqi/procure_mall/` 或 Windows 本地)
|
||||
- [ ] 桌面建 `福建商城选品测试\选品输入.xlsx`,表头按 3.1
|
||||
- [ ] 放 5 条真实批准文号、销售价格、采购数量(问业务要,不要用假文号)
|
||||
- [ ] `uv run python -c "import isa_rpa, requests, openpyxl; print('ok')"` 能过
|
||||
|
||||
### 任务 2:规则引擎 + 单测
|
||||
|
||||
- [ ] 新建 `rules.py`、`tests/test_rules.py`
|
||||
- [ ] 实现规范化、毛利、效期、库存、择仓
|
||||
- [ ] `uv run pytest tests/test_rules.py -v` 全绿
|
||||
- [ ] 确认没有 import 原 ±10% 函数
|
||||
|
||||
### 任务 3:搜品接到按行决策(仍不加购)
|
||||
|
||||
- [ ] 拆出或保留 `query_yaoex_products` / `query_ybm_products`
|
||||
- [ ] 候选映射成 `Candidate`
|
||||
- [ ] `main.py` 改为按行循环,默认 dry-run
|
||||
- [ ] 删除 `filter_rows_by_purchase_price_band` 调用
|
||||
- [ ] 写新结果表(3.2)
|
||||
- [ ] 跑 `uv run main.py --dry-run --limit 5`
|
||||
- [ ] 人工核对 2 条 ADDED、2 条 SKIPPED
|
||||
|
||||
### 任务 4:抓加购接口
|
||||
|
||||
- [ ] 1 号药城 F12 抓「加入购物车」,写入 `CART_API.md`
|
||||
- [ ] 药帮忙同样写一份
|
||||
- [ ] 判定:接口可用 / 必须页面点击
|
||||
|
||||
### 任务 5:实现加购(默认仍 dry-run)
|
||||
|
||||
- [ ] `cart_yaoex.py` / `cart_ybm.py`
|
||||
- [ ] 先查车再加,避免数量翻倍
|
||||
- [ ] `--execute-cart --limit 1` 单平台
|
||||
- [ ] 人工打开购物车核对
|
||||
- [ ] 再跑同一条,数量不翻倍
|
||||
|
||||
### 任务 6:收尾
|
||||
|
||||
- [ ] README:命令、dry-run 默认、输入输出路径、密码箱名称
|
||||
- [ ] `--limit 20` dry-run 耗时写进 README
|
||||
- [ ] 把 `未匹配商家` 列表交给业务看要不要补别名(本期不改代码硬匹配)
|
||||
- [ ] 交付:工程、5 条结果 xlsx、加购成功截图、CART_API.md
|
||||
|
||||
---
|
||||
|
||||
## 9. 与后续 Oracle / 状态中心的边界
|
||||
|
||||
本期 `decision` 字典就是以后 `PROCURE_ITEM_RUN` 的 `output_json`。字段先对齐:`result=ADDED|SKIPPED|FAILED`、原因、商家、sku、价格、数量。
|
||||
|
||||
不要在本期接 FastAPI、不要写 MySQL、不要为选品新建 Oracle 表。下一期只换输入输出通道,不换规则和搜品。
|
||||
|
||||
---
|
||||
|
||||
## 10. 未决(不影响 Windows 开工)
|
||||
|
||||
| 项 | 本期默认 | 谁拍板 |
|
||||
| --- | --- | --- |
|
||||
| 两平台都合格是否都加购 | 都加 | 业务;要互斥再改 `main` 循环 |
|
||||
| 毛利 刚好 5% | 算合格 `>=` | 已按设计说明「不低于 5%」 |
|
||||
| 商家名不完全等于 11 家 | 淘汰并记未匹配清单 | 业务补别名后再改 `WAREHOUSE_ORDER` 映射 |
|
||||
| 真正加购用测试账号还是正式采购账号 | 必须业务书面确认后再 `--execute-cart` | 业务 / 运维 |
|
||||
|
||||
加购接口在改代码前未知,这是正常的。任务 4 的产出就是答案,不是阻塞任务 1~3 的理由。
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# 洁达选品:改造商城查价工程(概要)
|
||||
|
||||
| 文档属性 | 内容 |
|
||||
| --- | --- |
|
||||
| 版本 | V1.0 |
|
||||
| 日期 | 2026-09-14 |
|
||||
| 状态 | 本期实施依据;Windows 机按本文和详细方案改同事原工程 |
|
||||
| 基线工程 | 同事「1号药城与药帮忙货品价格库存下载」`isa-rpa` Python 应用(`main.py`) |
|
||||
| 详细方案 | [2026-09-14-jieda-procure-crawler-detail.md](./2026-09-14-jieda-procure-crawler-detail.md) |
|
||||
| 业务口径 | [洁达四流程概要 V3.1](./2026-09-13-jieda-rpa-overview-design.md) 的 **BR-02 / BR-03 / 第 3.2 节** |
|
||||
| 选品条件原文 | [采购平台选品条件优先项 20260902](/Users/yuuuu/Documents/AutoRpa/资料/采购平台选品条件优先项_20260902.txt) |
|
||||
|
||||
本期只做 **PROCURE 选品加购的第一刀**:在原查价工程上改,不接状态中心、不接 Oracle、不改合同/返利。先跑通「登录搜品 → 按行判定 → 合格加购」。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要做成什么
|
||||
|
||||
一次脚本运行:
|
||||
|
||||
1. 读入待选品行(批准文号、销售价格、采购数量)。
|
||||
2. 浏览器登录 1 号药城、药帮忙各一次,拿 cookie/token。
|
||||
3. 按批准文号打现有搜索接口,拉候选(商家、价、效期、库存)。
|
||||
4. 用洁达选品规则,每个平台每行最多选出 **1 家仓**。
|
||||
5. 合格则加入该平台购物车;不合格记跳过;查询失败记失败。
|
||||
6. 写出结果表。人工付款仍由业务在平台做,脚本不等、不点支付。
|
||||
|
||||
完成标准:Windows 机用 5~20 个品种能稳定跑完;结果表能看出每行 `ADDED / SKIPPED / FAILED`;合格行购物车里能看到对应商品和数量。默认 `--dry-run` 只判定不加购,确认规则后再关。
|
||||
|
||||
## 2. 为什么改原工程,不另起
|
||||
|
||||
原工程已经有:双平台登录、搜索 API、1 号药城价格解密、按文号缓存、接口重试。缺的是选品决策和加购。合同那套 Studio 逐步点击不适合一千个品种。
|
||||
|
||||
原工程里必须删掉的:
|
||||
|
||||
- `filter_rows_by_purchase_price_band`(采购价 ±10%)
|
||||
- 把所有命中行倒进 Excel 当最终答案
|
||||
- 用「采购价格」做是否合格的依据
|
||||
|
||||
保留的:登录、`query_yaoex_products`、`query_ybm_products`、`decrypt_yaoex_price`、密码箱、重试。
|
||||
|
||||
## 3. 架构
|
||||
|
||||
```text
|
||||
Excel 输入(本期)
|
||||
→ 登录一次 / 平台
|
||||
→ 搜索接口(按批准文号,带缓存)
|
||||
→ 选品规则引擎(纯 Python,两平台同一套)
|
||||
→ 加购适配器(先接口,抓不到再点页面;仅合格行)
|
||||
→ 结果 Excel(每行一个平台一个结论)
|
||||
```
|
||||
|
||||
平台只负责登录、搜索、读商品、执行加购。**不能改规则优先级。**
|
||||
|
||||
本期输入仍用桌面 Excel,是为了在 Windows 上先试,不堵在 Oracle。列必须有 **销售价格**,不能再用采购价冒充。下一期再把输入/回报换成状态中心领取。
|
||||
|
||||
## 4. 按行怎么跑
|
||||
|
||||
一行输入 × 一个平台 = 一条结论。一次脚本把两个平台都跑完。
|
||||
|
||||
```text
|
||||
登录两个商城(各一次)
|
||||
for 每一行输入:
|
||||
for 平台 in [1号药城, 药帮忙]:
|
||||
搜批准文号
|
||||
规则选出 0 或 1 个仓
|
||||
合格 → 加购(dry-run 则跳过动作)→ ADDED
|
||||
无合格仓 → SKIPPED + 原因
|
||||
接口/登录级错误 → FAILED + 原因(不是 SKIPPED)
|
||||
写结果表,退出
|
||||
```
|
||||
|
||||
**两边都合格就两边都加购。** 同一销售行可能进两个购物车。若业务要互斥,只改这一条策略,不要改规则引擎。
|
||||
|
||||
登录失败整批中止。单个文号查询失败只失败该平台该行,继续后面的行。
|
||||
|
||||
## 5. 合格标准(两平台同一套)
|
||||
|
||||
按批准文号查询,不配规格、厂家、包装。前 4 项必须同时过,再按仓库名单取第一家。
|
||||
|
||||
| 顺序 | 条件 | 判定 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 自营仓 | 商家名规范化后,必须落在下列 11 家里 |
|
||||
| 2 | 价格 | `(销售价格 - 平台价) / 销售价格 >= 0.05`,即平台价 ≤ 销售价 × 0.95。基准是输入里的**销售价格** |
|
||||
| 3 | 效期 | ≥ 12 个月 |
|
||||
| 4 | 库存 | 平台库存 ≥ 本行采购数量 |
|
||||
| 5 | 仓库距离 | 在 1~4 都过的候选里,按下列顺序取第一家 |
|
||||
|
||||
仓库顺序(不可改、不可按页面默认排序):
|
||||
|
||||
1. 广东壹号药业有限公司
|
||||
2. 昆山亿方药业有限公司
|
||||
3. 湖北亿昊药业有限公司
|
||||
4. 天津壹号药业有限公司
|
||||
5. 重庆亿昊药业有限公司
|
||||
6. 重庆壹号医药有限公司
|
||||
7. 湖南壹号药业股份有限公司
|
||||
8. 辽宁亿昊药业有限公司
|
||||
9. 河南壹号医药有限公司
|
||||
10. 甘肃壹号云医药有限责任公司企业店
|
||||
11. 陕西壹号药业有限公司
|
||||
|
||||
商家名先去掉 `[自营]`、首尾空格再比。对不上名单 = 本候选淘汰,不是整行失败。对不上的原始商家名写入结果,方便以后补别名。本期不做模糊匹配。
|
||||
|
||||
效期:接口已带「12 个月」过滤,代码仍要再验。解析不了的效期,该候选淘汰。
|
||||
|
||||
查询失败、登录失败、加购后不确定是否进车,记 `FAILED` / `UNKNOWN`,**禁止记成 SKIPPED**。
|
||||
|
||||
## 6. 加购
|
||||
|
||||
原工程没有购物车代码。合格之后才加购,数量 = 本行采购数量。
|
||||
|
||||
1. 用 F12 抓「加入购物车」请求(详细方案有步骤)。
|
||||
2. 抓到则复用登录态打接口。
|
||||
3. 抓不到则只对合格行点页面,禁止一千条都去点搜索。
|
||||
4. `--dry-run` 默认开启:只写「将加购」不真正加。Windows 先用 dry-run 对规则,再用 1~2 行关 dry-run 验车。
|
||||
|
||||
脚本不打开结算、不点支付。
|
||||
|
||||
## 7. 本期不做
|
||||
|
||||
- 状态中心领取 / start / complete / fail
|
||||
- Oracle 接口表、行 job
|
||||
- 平台订单导出、合同、返利
|
||||
- 人工付款、待复核清单替代加购(新方案是合格就加购)
|
||||
- 把查价 ±10% 留作并行规则
|
||||
|
||||
## 8. Windows 交付物
|
||||
|
||||
- 改过的 `isa-rpa` 工程(可运行)
|
||||
- 输入模板(含销售价格、采购数量、批准文号)
|
||||
- 结果表(每行每平台:结论、原因、选中商家、平台价、效期、库存、是否 dry-run)
|
||||
- 加购接口记录(URL、方法、关键字段;或「改用页面点击」的说明)
|
||||
- 用 5 个品种跑通的截图或结果文件
|
||||
|
||||
详细到函数、表头、命令、测试步骤,见详细方案。
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
# 合同班:表 3 合单录入 + 接口 4 组包
|
||||
|
||||
| 文档属性 | 内容 |
|
||||
| --- | --- |
|
||||
| 日期 | 2026-10-05 |
|
||||
| 状态 | 已锁定,本轮实施依据 |
|
||||
| 范围 | `rpa_contract/` 从表 3 领取、按 `supplierId` 合单、调现有页面 runner、尽调闸、写表 4 并回写表 3;`rpa_oms/app/outbound/` 接口 4 组包 |
|
||||
| 对照 | `docs/RPA订单中心接口开发口径_20260930.md` 第 4 节、第 6 节;`docs/合同系统录入流程.md`;Excel `4合同协议录入结果接口` |
|
||||
| 不变 | 接口 3 收件、表 3/4 DDL、页面步骤主体(登录/起草/货品/提交/读号)、接口 2 组包形态 |
|
||||
|
||||
合同班另起进程。HTTP 收件不同步跑作业。接口 4 真实 path 仍空:只组包,不发 HTTP。
|
||||
|
||||
---
|
||||
|
||||
## 1. 要做成什么
|
||||
|
||||
1. 合同班启动瞬间冻结本班可录名单:表 3 `process_status` 0→3。
|
||||
2. 按 `supplierId` 合成一份合同;头字段取该组按 `fid` 的第一行;货品行全收。
|
||||
3. `contractEntryStatus=0` 且已有 `contractNo` 的行不领取,留给返利班。
|
||||
4. 注入 runner 调现有合同页面流。单测不打开浏览器。
|
||||
5. 提交成功:表 4 `actionCode=1`、`contractStatus=1`、带合同号;表 3 `process_status=1`。同一合同下多个 `omsId` 回同一套号和状态。
|
||||
6. 失败(未尽调 / 页面挂):表 4 `contractStatus=3` 带原因;表 3 `process_status=2`。
|
||||
7. 接口 4 从表 4 取 `push_status=0` 组装 `{"data":[...]}`,一批最多 500。不改 `push_status`,不 POST。
|
||||
|
||||
完成标准(内存替身,不连库、不开浏览器):
|
||||
|
||||
- `cd rpa_contract` → `uv run pytest tests/test_job.py::test_job_merges_same_supplier_and_writes_table4 -q`
|
||||
- 同一供应商两行、两个 `omsId`:runner 调一次;表 4 两行同一合同号;表 3 两条 `process_status=1`
|
||||
- 带 `contractNo` 的协议重录行不领
|
||||
- `cd rpa_oms` → `uv run pytest tests/test_contract_agreement_outbound.py::test_assembles_unpushed_rows_as_document_envelope -q`
|
||||
|
||||
---
|
||||
|
||||
## 2. 不做什么
|
||||
|
||||
- 不实现返利班、不打开返利中心、不回 `contractStatus=2` / `actionCode=2`
|
||||
- 不向订单中心发 HTTP,不改 `push_status`
|
||||
- 不在接口 3 请求路径里录合同
|
||||
- 不按 `omsId` 或供应商名称合单
|
||||
- 不把下午新进行追加进上午已录合同(冻结快照自然保证)
|
||||
- 不默认合并/上传 PDF(接口 3 这条链没有平台扫描件)
|
||||
- 不改表结构、不改接口 1/2/3
|
||||
- 不改 `rpa_selection/`、`rpa_cart/`、`rpa_rebate/`
|
||||
- 合同班不 import `rpa_oms`(作业写表 4;组包在 oms 另调,对齐选品写表 2 / 接口 2 另组)
|
||||
- 不把 JSON 干跑入口 `main.py --input-json` 拆掉
|
||||
|
||||
---
|
||||
|
||||
## 3. 已锁定决策
|
||||
|
||||
| 项 | 决定 |
|
||||
| --- | --- |
|
||||
| 包边界 | 领取/合单/落表在 `rpa_contract`;接口 4 组包在 `rpa_oms/app/outbound/contract_agreement.py` |
|
||||
| 冻结 | `claim_pending` 一次领完本班可录行,0→3;过程中新进的仍是 0,下一班再处理 |
|
||||
| 可录 | `process_status=0` 且**不是**(`contract_entry_status=0` 且 `contract_no` 非空) |
|
||||
| 协议重录行 | 永不领取,保持 `process_status=0` |
|
||||
| 合单键 | `supplier_id`。头字段 = 该组 `fid` 升序第一行 |
|
||||
| 采购员 / 计划员 | 只存在表 3 行上,本轮不填页面(现有 flow 无对应控件) |
|
||||
| 表 4 粒度 | 每个 `oms_id` 一行。同一 `oms_id` 多货品仍一行。`org_code` 跟该 `oms_id` 在表 3 的原值 |
|
||||
| 表 4 `fid` | 序列触发器生成,INSERT 不绑这一列 |
|
||||
| 表 4 审计 | `create_user_id`/`update_user_id` = 0;名称 = `合同班`;`push_status=0` |
|
||||
| 客商 | `customerCode` = 第一行 `supplier_mdm_code`;`customerName` = `supplier_name` |
|
||||
| 日期 | 页面签订日/开始日/结束日 = 第一行 `contract_sign_date` / `contract_start_date` / `contract_end_date`(`YYYY-MM-DD`) |
|
||||
| S08 兼容 | `ContractFlowContext` 增加可选 `contract_sign_date`、`contract_start_date`。未给时回退 `order_date`(JSON 干跑不破) |
|
||||
| PDF | `source_type=""`,不走医药城合并上传 |
|
||||
| 尽调 | S06 之后扫页面正文含「未尽调」则失败,原因固定「客户未尽调」,不提交。无独立选择器 |
|
||||
| runner | `Callable[[ContractDraft], ContractOutcome]`。生产包装 `run_contract_create_flow`;单测注入 |
|
||||
| 崩溃 | 停在表 3 `process_status=3`,人工改回 0 再跑;未写表 4 |
|
||||
| 组包范围 | 全部 `push_status=0`,不按 `actionCode` 过滤(返利以后写 `actionCode=2` 也走同一函数) |
|
||||
| 组包拆批 | 同一非空 `contractNo` 不拆批,算法对齐接口 2 的 `omsId`。`contractNo` 为空(失败行)按行独立,不合成一组 |
|
||||
| `omsId` | JSON 整数 |
|
||||
| HTTP | 不做。path 空 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 领取与合单
|
||||
|
||||
### 4.1 行模型
|
||||
|
||||
`PurchaseContractRow`(表 3 已有列,camelCase 不进作业):
|
||||
|
||||
`fid, oms_id, supplier_id, supplier_name, contract_sign_date, contract_start_date, contract_end_date, payment_term, goods_id, goods_name, reg_no, spec, unit, qty, unit_price, purchaser_id, purchaser_name, planner_id, planner_name, contract_entry_status, contract_no, process_status, org_code, goods_mdm_code, supplier_mdm_code`
|
||||
|
||||
### 4.2 SQL 语义
|
||||
|
||||
```
|
||||
WHERE process_status = 0
|
||||
AND NOT (NVL(contract_entry_status, -1) = 0
|
||||
AND contract_no IS NOT NULL
|
||||
AND LENGTH(TRIM(contract_no)) > 0)
|
||||
ORDER BY fid
|
||||
```
|
||||
|
||||
领取:对命中行 `UPDATE ... SET process_status = 3, process_time = SYSTIMESTAMP, update_time = SYSTIMESTAMP WHERE fid = :fid AND process_status = 0`。`rowcount=1` 才算领到。可选 `limit` 仅供单测。
|
||||
|
||||
`contract_no` 空白串视为空。
|
||||
|
||||
### 4.3 分组
|
||||
|
||||
对已领取列表按 `fid` 升序扫描,`OrderedDict` 按 `supplier_id` 归组。组内顺序保持 `fid` 升序。每个 `supplier_id` 一份合同。
|
||||
|
||||
### 4.4 草稿映射
|
||||
|
||||
第一行 → 头:
|
||||
|
||||
| 草稿 | 来源 |
|
||||
| --- | --- |
|
||||
| `supplier_name` / `customer_name` | `supplier_name` |
|
||||
| `customer_code` | `supplier_mdm_code` |
|
||||
| `order_date` | `contract_sign_date`(标题用) |
|
||||
| `contract_sign_date` / `contract_start_date` / `contract_end_date` | 对应三列,格式 `YYYY-MM-DD` |
|
||||
| `payment_days` | `str(payment_term)` |
|
||||
| `is_prepayment` | `否` |
|
||||
| `payment_method` | `账期支付` |
|
||||
| `contract_category` | `采购协议` |
|
||||
| `source_type` | `""` |
|
||||
| `amount` | `sum(qty * unit_price)`,四位小数字符串 |
|
||||
| `belong_year` | 签订日年份 |
|
||||
|
||||
每一行 → `ContractGoodsLine`:
|
||||
|
||||
| 字段 | 来源 |
|
||||
| --- | --- |
|
||||
| `operation_code` | `goods_id`(弹窗实际搜 `goods_code`) |
|
||||
| `goods_code` | `goods_id` |
|
||||
| `goods_name` | `goods_name` |
|
||||
| `approval_no` | `reg_no` |
|
||||
| `spec` / `unit` | 同名 |
|
||||
| `quantity` | `qty` |
|
||||
| `mdm_code` | `goods_mdm_code` |
|
||||
| `unit_price` | 四位小数字符串 |
|
||||
|
||||
第一行 `supplier_mdm_code` 为空:整组业务失败,原因「缺少供应商MDM码」,不调 runner。
|
||||
|
||||
---
|
||||
|
||||
## 5. 页面改动(只两处)
|
||||
|
||||
现有 `run_contract_create_flow` 继续串 S02–S13。本轮只加:
|
||||
|
||||
1. **S08 日期**(`fill_contract_amount_and_dates`):签订日 = `contract_sign_date or order_date`;开始日 = `contract_start_date or order_date`;结束日仍用 `contract_end_date`。
|
||||
2. **尽调闸**(S06 之后、S07 之前):`check_customer_diligence` 读页面正文。含「未尽调」则抛 `DiligenceError`(`errors.py`,`NodeError` 子类,文案「客户未尽调」)。生产 runner 让它冒泡到 job。
|
||||
|
||||
不改货品弹窗搜索、不改读号策略、不改 JSON 干跑提交前停住的行为。
|
||||
|
||||
---
|
||||
|
||||
## 6. 班次结果
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class ContractOutcome:
|
||||
kind: str # "ok" | "diligence" | "fail"
|
||||
contract_no: str | None
|
||||
reason: str | None
|
||||
```
|
||||
|
||||
| `kind` | 表 3 `process_status` / `process_result` | 表 4 |
|
||||
| --- | --- | --- |
|
||||
| `ok` 且有合同号 | `1` / 成功说明 | `action_code=1`,`contract_status=1`,`contract_no`,失败原因空 |
|
||||
| `diligence` | `2` / `客户未尽调` | `action_code=1`,`contract_status=3`,`contract_fail_reason=客户未尽调`,`contract_no` 空 |
|
||||
| `fail` 或其他异常 | `2` / 原因(截到 1000) | `contract_status=3`,`contract_fail_reason` 截到 500,`contract_no` 空 |
|
||||
| runner 返回 `ok` 但合同号空 | 同 `fail`,「submit 模式未读取到真实合同编号」 | 同 `fail` |
|
||||
|
||||
同一组所有表 3 `fid` 写同一套处理状态。表 4 按组内**去重后的 `oms_id`**(首次出现顺序)各插一行,合同号和状态相同;`agreement_*` 全空;`org_code` 取该 `oms_id` 对应表 3 行。
|
||||
|
||||
一次 `save_group`:先插表 4,再改表 3。中途崩溃停在 3、表 4 可能部分写入——Oracle 同一连接一事务提交。内存替身同样先写 4 再改 3,一次调用内完成。
|
||||
|
||||
生产 runner:由 `ContractDraft` 填 `ContractFlowContext`(含三个日期),调 `run_contract_create_flow`,读到号 → `kind=ok`;捕获 `DiligenceError` → `kind=diligence`;其他 `NodeError` / 异常 → `kind=fail`。
|
||||
|
||||
---
|
||||
|
||||
## 7. 接口 4 组包
|
||||
|
||||
对标 `app/outbound/cart_settlement.py`。
|
||||
|
||||
```python
|
||||
load_contract_agreement_envelope(repo, limit=500) -> {"data": [...]}
|
||||
```
|
||||
|
||||
`repo.list_unpushed()`:`WHERE push_status = 0 ORDER BY fid`。
|
||||
|
||||
每条 camelCase:
|
||||
|
||||
```
|
||||
fid, omsId, actionCode, contractStatus, contractFailReason,
|
||||
contractNo, agreementStatus, agreementFailReason,
|
||||
agreementId, agreementDtlId, orgCode
|
||||
```
|
||||
|
||||
- `fid` / `omsId` / `actionCode` / 状态码:JSON 整数(库值 `None` 则字段为 `null`)
|
||||
- 字符串字段原样,空为 `null`
|
||||
- 不改 `push_status`,不发 HTTP
|
||||
|
||||
拆批:按 `contract_no` 分组(仅非空)。空 `contract_no` 每行单独一组。一组放不进当前批且批已有内容则停;批为空则整组放入(可超过 500,对齐接口 2)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 文件落点
|
||||
|
||||
**`rpa_contract/`**(作业扁平,与选品同款):
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `group.py` | 可录判定、按 `supplier_id` 分组、草稿映射、金额四位小数 |
|
||||
| `job.py` | `claim_pending` → 分组 → runner → `save_group` |
|
||||
| `repo.py` | `MemoryContractRepo` + `OracleContractRepo`:领取、插表 4、回写表 3 |
|
||||
| `persist.py` | 由 `ContractOutcome` + 组行生成表 4 dict、表 3 状态 |
|
||||
| `db.py` | 拷选品连接模式;空缺复用 `rpa_oms/.env`;用户名戳 `合同班` |
|
||||
| `errors.py` | 新增 `DiligenceError` |
|
||||
| `contract_flow_steps.py` | S08 日期回退;S06 后尽调扫描 |
|
||||
| `run_batch.py` | 班次入口:`from_env` + 生产 runner。保留现有 `main.py` JSON 干跑 |
|
||||
| `pyproject.toml` | 加 `oracledb` |
|
||||
| `tests/test_job.py` | 点名合单/跳过重录/未尽调 |
|
||||
| `tests/test_group.py` | 头字段取第一行、金额合计、空白 `contract_no` 仍可领 |
|
||||
|
||||
**`rpa_oms/`**:
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `app/outbound/contract_agreement.py` | `load_` / `build_contract_agreement_envelope` |
|
||||
| `app/outbound/memory_repo.py` | 增 `MemoryContractAgreementRepo.list_unpushed` |
|
||||
| `app/outbound/oracle_repo.py` | 增 `OracleContractAgreementRepo`(或同文件新类)只读未推行 |
|
||||
| `tests/test_contract_agreement_outbound.py` | 点名组包 |
|
||||
| `examples/contract_agreement_batch.json` | 与 Excel 请求示例同形(`omsId` 用整数) |
|
||||
| `README.md` | 接口 4:组包已做,path 空,不发 HTTP |
|
||||
|
||||
`rpa_contract/README.md`:合单/尽调/写表 4 本轮已接;接口 4 组包在 oms,本班不推。
|
||||
|
||||
---
|
||||
|
||||
## 9. 验收用例(点名)
|
||||
|
||||
### 9.1 `test_job_merges_same_supplier_and_writes_table4`
|
||||
|
||||
两行同一 `supplier_id`,`oms_id` 分别为 `2026100501`、`2026100502`,`process_status=0`。第三行 `contract_entry_status=0` 且 `contract_no="HT-OLD"`。runner 记调用次数并返回 `kind=ok, contract_no="HT20261005001"`。
|
||||
|
||||
断言:
|
||||
|
||||
- runner 调用 1 次;草稿两行货品;客商编码为第一行 MDM
|
||||
- 表 4 两行:同一 `contract_no`,`action_code=1`,`contract_status=1`,`push_status=0`,`oms_id` 各一
|
||||
- 前两行表 3 `process_status=1`;第三行仍为 0(未领)
|
||||
|
||||
### 9.2 `test_job_marks_diligence_fail`
|
||||
|
||||
一组一行。runner 返回 `kind=diligence, reason="客户未尽调"`。
|
||||
|
||||
断言:表 3 `process_status=2`,`process_result` 含「客户未尽调」;表 4 `contract_status=3`,`contract_no` 空,`contract_fail_reason="客户未尽调"`。
|
||||
|
||||
### 9.3 `test_assembles_unpushed_rows_as_document_envelope`
|
||||
|
||||
内存两行 `push_status=0`:一行 `action_code=1, contract_status=1, contract_no="HT1"`;一行 `action_code=2, agreement_status=1`。另有一行 `push_status=1` 不进包。
|
||||
|
||||
断言信封 `data` 两条,camelCase 与第 7 节字段一致,`omsId` 为 `int`,已推行不出现。
|
||||
|
||||
另测(对齐接口 2 `_take_complete_groups`):同一 `contract_no` 三行、`limit=2`、当前批为空 → 整组三条都放入(允许超过 limit)。两个不同 `contract_no` 各一行、`limit=1` → 只出第一组。
|
||||
|
||||
---
|
||||
|
||||
## 10. 风险与边界
|
||||
|
||||
- 尽调靠正文扫描,页面若换文案会漏拦。本轮不补选择器。
|
||||
- 货品弹窗仍按 `goods_id` / 品名搜,不改成 MDM。
|
||||
- 表 4 没有表 3 `fid` 列,回执关联键是表 4 自己的 `fid` 和 `omsId`。
|
||||
- 真库领取/落表在 Windows 上做,本轮验收不连库。
|
||||
- 接口 4 path、Token、是否带 `orgSecret` 仍按口径第 6 节后置,本轮不发明 URL。
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
# rpa_contract
|
||||
|
||||
合同班。从表 3 领取、按 `supplierId` 合单、调页面录入、写表 4。接口 4 组包在 `rpa_oms`,本班不推、不改 `push_status`。
|
||||
合同班。页面录入脚本在本包;从表 3 合单、写表 4、推接口 4 尚未接。
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| 读 | 表 3 `rpa_purchase_contract_data`(`process_status=0`;跳过 `contractEntryStatus=0` 且已有 `contractNo` 的协议重录行) |
|
||||
| 写 | 表 4 `rpa_contract_agreement_result`(`actionCode=1`,提交成功 `contractStatus=1` 起草) |
|
||||
| 口 | 口 3 收件在 `rpa_oms`;口 4 组包在 oms,path 空 |
|
||||
| 读 | 表 3 `rpa_purchase_contract_data`(`process_status=0`) |
|
||||
| 写 | 表 4 `rpa_contract_agreement_result`(接口 4 `actionCode=1`,`contractStatus=1` 起草) |
|
||||
| 口 | 口 3 收件在 `rpa_oms`;口 4 回写做到时由本班调用 |
|
||||
|
||||
一天两班(中午、下午)。启动瞬间冻结本班名单(`process_status` 0→3)。按 `supplierId` 合单;头字段取该班该供应商第一行。下午同一供应商另开一份。
|
||||
一天两班(中午、下午)。启动瞬间冻结本班名单。按 `supplierId` 合单;头字段取该班该供应商第一行。下午同一供应商另开一份。
|
||||
|
||||
口径:`docs/RPA订单中心接口开发口径_20260930.md` 第 4 节;合单细节见 `docs/合同系统录入流程.md`。
|
||||
|
||||
|
|
@ -25,33 +25,25 @@
|
|||
1. 打开合同系统并登录
|
||||
2. 进起草,套非格式采购协议模板
|
||||
3. 签约方:客户编码、客户名称
|
||||
4. 尽调:页面正文含「未尽调」则本单失败,不提交
|
||||
5. 基础信息:合同版本类型、合同类别、结算类型、合同背景概述、合同性质=主合同
|
||||
6. 金额与日期:签订日 / 开始日 / 结束日用接口 3 第一行三日期(JSON 干跑未给则回退 `orderDate`)
|
||||
7. 货品明细
|
||||
8. 付款与标志:这期一律账期,不勾预付款;页面「账期支付」对应选项「电汇」
|
||||
4. 基础信息:合同版本类型、合同类别、结算类型、合同背景概述、合同性质=主合同
|
||||
5. 金额与日期:签订日 / 开始日 / 结束日
|
||||
6. 货品明细
|
||||
7. 付款与标志:这期一律账期,不勾预付款;页面「账期支付」对应选项「电汇」
|
||||
8. 尽调:口径有,脚本尚未加这一闸
|
||||
9. 点提交,不要点暂存
|
||||
10. 从「我的合同」读正式合同号
|
||||
|
||||
## 运行
|
||||
|
||||
JSON 干跑(不领表):
|
||||
|
||||
```bash
|
||||
cd rpa_contract
|
||||
uv sync --python 3.12
|
||||
uv run pytest tests/test_job.py::test_job_merges_same_supplier_and_writes_table4 -q
|
||||
uv run pytest -v
|
||||
uv run python main.py --input-json examples/contract_create_input_dry_run.json
|
||||
```
|
||||
|
||||
班次(领表 3):
|
||||
|
||||
```bash
|
||||
uv run python run_batch.py
|
||||
```
|
||||
|
||||
`submitMode=dry_run` 跑到提交前,不点提交。真起草用 `examples/contract_draft_yyc.json` / `examples/contract_draft_xyy.json`。
|
||||
|
||||
## 本期不做
|
||||
|
||||
返利班、向订单中心发 HTTP、改 `push_status`。
|
||||
从表 3 按班次合单、尽调闸、接口 4 回写。
|
||||
|
|
|
|||
|
|
@ -141,8 +141,6 @@ def build_flow_context(spec: ContractCreateInput, draft: ContractDraft) -> Contr
|
|||
amount=draft.amount,
|
||||
contract_title=draft.contract_title,
|
||||
contract_end_date=draft.contract_end_date,
|
||||
contract_sign_date=draft.contract_sign_date,
|
||||
contract_start_date=draft.contract_start_date,
|
||||
is_major_contract=draft.is_major_contract,
|
||||
is_prepayment=draft.is_prepayment,
|
||||
belong_year=draft.belong_year,
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ class ContractDraft:
|
|||
amount: str = ""
|
||||
contract_title: str = ""
|
||||
contract_end_date: str = ""
|
||||
contract_sign_date: str = ""
|
||||
contract_start_date: str = ""
|
||||
is_major_contract: str = "否"
|
||||
is_prepayment: str = "否"
|
||||
belong_year: str = ""
|
||||
|
|
@ -89,8 +87,6 @@ def contract_draft_from_dict(data: dict[str, Any]) -> ContractDraft:
|
|||
amount=str(data.get("amount", "")),
|
||||
contract_title=str(data.get("contractTitle") or data.get("docSubject") or ""),
|
||||
contract_end_date=str(data.get("contractEndDate", "")),
|
||||
contract_sign_date=str(data.get("contractSignDate", "")),
|
||||
contract_start_date=str(data.get("contractStartDate", "")),
|
||||
is_major_contract=str(data.get("isMajorContract", "否")),
|
||||
is_prepayment=str(data.get("isPrepayment", "否")),
|
||||
belong_year=str(data.get("belongYear", "")),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from typing import Protocol
|
|||
|
||||
from accept import AcceptanceReport, accept_log_path, evidence_shot
|
||||
from errors import (
|
||||
DiligenceError,
|
||||
LoginFailed,
|
||||
NodeError,
|
||||
PdfMergeUnsupported,
|
||||
|
|
@ -70,8 +69,6 @@ class ContractFlowContext:
|
|||
amount: str = ""
|
||||
contract_title: str = ""
|
||||
contract_end_date: str = ""
|
||||
contract_sign_date: str = ""
|
||||
contract_start_date: str = ""
|
||||
is_major_contract: str = "否"
|
||||
is_prepayment: str = "否"
|
||||
belong_year: str = ""
|
||||
|
|
@ -107,7 +104,6 @@ def run_contract_create_flow(browser: BrowserAdapter, ctx: ContractFlowContext)
|
|||
enter_contract_create(browser)
|
||||
select_contract_template(browser, ctx)
|
||||
fill_contract_signatory_info(browser, ctx)
|
||||
check_customer_diligence(browser, ctx)
|
||||
fill_contract_basic_info(browser, ctx)
|
||||
fill_contract_amount_and_dates(browser, ctx)
|
||||
fill_contract_goods_lines(browser, ctx)
|
||||
|
|
@ -535,30 +531,14 @@ def _select_settlement_type(browser: BrowserAdapter, value: str) -> None:
|
|||
raise NodeError(f"结算方式下拉未选中,仍停留在「==请选择==」,期望「{value}」")
|
||||
|
||||
|
||||
def sign_date_for(ctx: ContractFlowContext) -> str:
|
||||
return (ctx.contract_sign_date or ctx.order_date or "").strip()
|
||||
|
||||
|
||||
def begin_date_for(ctx: ContractFlowContext) -> str:
|
||||
return (ctx.contract_start_date or ctx.order_date or "").strip()
|
||||
|
||||
|
||||
def check_customer_diligence(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
text = browser.read_body_text() or ""
|
||||
if "未尽调" in text:
|
||||
raise DiligenceError("客户未尽调")
|
||||
|
||||
|
||||
def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowContext) -> None:
|
||||
amount_type = (ctx.amount_type or "合同标的金额").strip()
|
||||
if amount_type:
|
||||
browser.select_xform(X["amount_type"], amount_type)
|
||||
if ctx.amount:
|
||||
browser.fill_first([F["amount"]], ctx.amount)
|
||||
sign_date = sign_date_for(ctx)
|
||||
begin_date = begin_date_for(ctx)
|
||||
browser.fill_first([F["sign_date"]], sign_date)
|
||||
browser.fill_first([F["begin_date"]], begin_date)
|
||||
browser.fill_first([F["sign_date"]], ctx.order_date)
|
||||
browser.fill_first([F["begin_date"]], ctx.order_date)
|
||||
if ctx.contract_end_date:
|
||||
browser.fill_first([F["end_date"]], ctx.contract_end_date)
|
||||
evidence_shot(browser, "contract-amount-and-dates-filled")
|
||||
|
|
@ -567,8 +547,8 @@ def fill_contract_amount_and_dates(browser: BrowserAdapter, ctx: ContractFlowCon
|
|||
"金额日期",
|
||||
[
|
||||
("amount", _try_read(browser, [F["amount"]]), ctx.amount),
|
||||
("signDate", _try_read(browser, [F["sign_date"]]), sign_date),
|
||||
("beginDate", _try_read(browser, [F["begin_date"]]), begin_date),
|
||||
("signDate", _try_read(browser, [F["sign_date"]]), ctx.order_date),
|
||||
("beginDate", _try_read(browser, [F["begin_date"]]), ctx.order_date),
|
||||
("endDate", _try_read(browser, [F["end_date"]]), ctx.contract_end_date),
|
||||
],
|
||||
browser=browser,
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
"""Oracle 11.2 thick 连接。环境变量与 rpa_oms 相同。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_client_ready = False
|
||||
_PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
_OMS_ENV = _PROJECT_ROOT.parent / "rpa_oms" / ".env"
|
||||
|
||||
|
||||
class RepoUnavailable(Exception):
|
||||
def __init__(self, message: str = "数据库不可用"):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def load_runtime_env(paths: list[Path] | None = None) -> None:
|
||||
"""先读合同 .env,空缺再补 rpa_oms/.env。不覆盖已有非空环境变量。"""
|
||||
env_paths = paths if paths is not None else [_PROJECT_ROOT / ".env", _OMS_ENV]
|
||||
for path in env_paths:
|
||||
_fill_env(path)
|
||||
|
||||
|
||||
def _fill_env(path: Path) -> None:
|
||||
if not path.is_file():
|
||||
return
|
||||
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
||||
value = value[1:-1]
|
||||
if not key or not value:
|
||||
continue
|
||||
current = os.environ.get(key, "")
|
||||
if not current:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def oracle_dsn() -> tuple[str, str, str]:
|
||||
user = os.getenv("RPA_ORACLE_USER", "")
|
||||
password = os.getenv("RPA_ORACLE_PASSWORD", "")
|
||||
host = os.getenv("RPA_ORACLE_HOST", "")
|
||||
if not user or not password or not host:
|
||||
raise RepoUnavailable("未配置 RPA_ORACLE_HOST / RPA_ORACLE_USER / RPA_ORACLE_PASSWORD")
|
||||
port = os.getenv("RPA_ORACLE_PORT", "1521")
|
||||
service = os.getenv("RPA_ORACLE_SERVICE", "rpa")
|
||||
return user, password, f"{host}:{port}/{service}"
|
||||
|
||||
|
||||
def connect_from_env():
|
||||
user, password, dsn = oracle_dsn()
|
||||
_ensure_client()
|
||||
import oracledb
|
||||
|
||||
return oracledb.connect(user=user, password=password, dsn=dsn)
|
||||
|
||||
|
||||
def _ensure_client() -> None:
|
||||
global _client_ready
|
||||
if _client_ready:
|
||||
return
|
||||
import oracledb
|
||||
|
||||
if os.getenv("RPA_ORACLE_THICK", "1") == "1":
|
||||
lib_dir = os.getenv("RPA_ORACLE_CLIENT_LIB", "")
|
||||
kwargs = {"lib_dir": lib_dir} if lib_dir else {}
|
||||
oracledb.init_oracle_client(**kwargs)
|
||||
_client_ready = True
|
||||
|
|
@ -29,9 +29,3 @@ class UnknownRpaError(NodeError):
|
|||
error_code = "RPA_UNKNOWN_ERROR"
|
||||
retryable = True
|
||||
route_role = "OPS"
|
||||
|
||||
|
||||
class DiligenceError(NodeError):
|
||||
error_code = "BIZ_CUSTOMER_NOT_DILIGENCE"
|
||||
retryable = False
|
||||
route_role = "PURCHASER"
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
"""合同班合单:可录判定、按供应商分组、草稿映射。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from contract_draft import ContractDraft
|
||||
from contract_flow_steps import ContractGoodsLine
|
||||
from repo import PurchaseContractRow
|
||||
|
||||
|
||||
def is_claimable(row: PurchaseContractRow) -> bool:
|
||||
if int(row.process_status or 0) != 0:
|
||||
return False
|
||||
if row.contract_entry_status is not None and int(row.contract_entry_status) == 0:
|
||||
if (row.contract_no or "").strip():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def group_by_supplier(rows: list[PurchaseContractRow]) -> list[list[PurchaseContractRow]]:
|
||||
groups: OrderedDict[str, list[PurchaseContractRow]] = OrderedDict()
|
||||
for row in sorted(rows, key=lambda item: item.fid):
|
||||
groups.setdefault(row.supplier_id, []).append(row)
|
||||
return list(groups.values())
|
||||
|
||||
|
||||
def missing_mdm_reason(rows: list[PurchaseContractRow]) -> str | None:
|
||||
if not rows:
|
||||
return "缺少供应商MDM码"
|
||||
if not (rows[0].supplier_mdm_code or "").strip():
|
||||
return "缺少供应商MDM码"
|
||||
return None
|
||||
|
||||
|
||||
def money4(value) -> str:
|
||||
amount = Decimal(str(value)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
||||
return f"{amount:.4f}"
|
||||
|
||||
|
||||
def iso_date(value) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, datetime):
|
||||
return value.date().isoformat()
|
||||
if isinstance(value, date):
|
||||
return value.isoformat()
|
||||
return str(value)[:10]
|
||||
|
||||
|
||||
def draft_from_group(rows: list[PurchaseContractRow]) -> ContractDraft:
|
||||
first = rows[0]
|
||||
sign = iso_date(first.contract_sign_date)
|
||||
total = sum(
|
||||
(Decimal(str(row.qty)) * Decimal(str(row.unit_price)) for row in rows),
|
||||
Decimal("0"),
|
||||
)
|
||||
goods = [
|
||||
ContractGoodsLine(
|
||||
operation_code=row.goods_id,
|
||||
goods_code=row.goods_id,
|
||||
goods_name=row.goods_name,
|
||||
approval_no=row.reg_no,
|
||||
spec=row.spec,
|
||||
unit=row.unit,
|
||||
quantity=float(row.qty),
|
||||
mdm_code=row.goods_mdm_code or "",
|
||||
unit_price=money4(row.unit_price),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
return ContractDraft(
|
||||
supplier_name=first.supplier_name,
|
||||
order_date=sign,
|
||||
source_type="",
|
||||
customer_code=(first.supplier_mdm_code or "").strip(),
|
||||
customer_name=first.supplier_name,
|
||||
contract_category="采购协议",
|
||||
amount=money4(total),
|
||||
contract_sign_date=sign,
|
||||
contract_start_date=iso_date(first.contract_start_date),
|
||||
contract_end_date=iso_date(first.contract_end_date),
|
||||
is_prepayment="否",
|
||||
belong_year=sign[:4],
|
||||
payment_method="账期支付",
|
||||
payment_days=str(first.payment_term),
|
||||
goods_lines=goods,
|
||||
)
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
"""合同班次:领取表 3 → 按供应商合单 → runner 录合同 → 写表 4。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from contract_draft import ContractDraft
|
||||
from errors import DiligenceError, NodeError
|
||||
from group import draft_from_group, group_by_supplier, missing_mdm_reason
|
||||
from persist import KIND_DILIGENCE, KIND_FAIL, KIND_OK, ContractOutcome, build_save
|
||||
|
||||
Runner = Callable[[ContractDraft], ContractOutcome]
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobSummary:
|
||||
claimed: int = 0
|
||||
groups: int = 0
|
||||
ok: int = 0
|
||||
failed: int = 0
|
||||
|
||||
|
||||
def run_batch(repo, *, runner: Runner, limit: int | None = None) -> JobSummary:
|
||||
claimed = repo.claim_pending(limit=limit)
|
||||
summary = JobSummary(claimed=len(claimed), groups=0)
|
||||
for rows in group_by_supplier(claimed):
|
||||
summary.groups += 1
|
||||
outcome = _run_group(rows, runner)
|
||||
repo.save_group(build_save(rows, outcome))
|
||||
if outcome.kind == KIND_OK and (outcome.contract_no or "").strip():
|
||||
summary.ok += 1
|
||||
else:
|
||||
summary.failed += 1
|
||||
return summary
|
||||
|
||||
|
||||
def _run_group(rows, runner: Runner) -> ContractOutcome:
|
||||
reason = missing_mdm_reason(rows)
|
||||
if reason:
|
||||
return ContractOutcome(KIND_FAIL, None, reason)
|
||||
try:
|
||||
outcome = runner(draft_from_group(rows))
|
||||
except Exception as exc:
|
||||
return ContractOutcome(KIND_FAIL, None, str(exc))
|
||||
if outcome.kind == KIND_OK and not (outcome.contract_no or "").strip():
|
||||
return ContractOutcome(KIND_FAIL, None, "submit 模式未读取到真实合同编号")
|
||||
return outcome
|
||||
|
||||
|
||||
def outcome_from_run(run: Callable[[], str]) -> ContractOutcome:
|
||||
try:
|
||||
number = (run() or "").strip()
|
||||
except DiligenceError:
|
||||
return ContractOutcome(KIND_DILIGENCE, None, "客户未尽调")
|
||||
except NodeError as exc:
|
||||
return ContractOutcome(KIND_FAIL, None, str(exc))
|
||||
except Exception as exc:
|
||||
return ContractOutcome(KIND_FAIL, None, str(exc))
|
||||
if not number:
|
||||
return ContractOutcome(KIND_FAIL, None, "submit 模式未读取到真实合同编号")
|
||||
return ContractOutcome(KIND_OK, number, None)
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
"""合同班结果:ContractOutcome → 表 4 行 + 表 3 状态。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from repo import PurchaseContractRow
|
||||
|
||||
KIND_OK = "ok"
|
||||
KIND_DILIGENCE = "diligence"
|
||||
KIND_FAIL = "fail"
|
||||
STAMP = "合同班"
|
||||
PROCESS_RESULT_MAX = 1000
|
||||
FAIL_REASON_MAX = 500
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContractOutcome:
|
||||
kind: str
|
||||
contract_no: str | None
|
||||
reason: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroupSave:
|
||||
fids: list[int]
|
||||
process_status: int
|
||||
process_result: str
|
||||
table4_rows: list[dict]
|
||||
|
||||
|
||||
def clip(text: str, max_len: int) -> str:
|
||||
text = (text or "").strip()
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[: max_len - 1] + "…"
|
||||
|
||||
|
||||
def build_save(rows: list[PurchaseContractRow], outcome: ContractOutcome) -> GroupSave:
|
||||
kind = outcome.kind
|
||||
number = (outcome.contract_no or "").strip() or None
|
||||
if kind == KIND_OK and number:
|
||||
process_status = 1
|
||||
process_result = "合同已提交"
|
||||
contract_status = 1
|
||||
fail_reason = None
|
||||
contract_no = number
|
||||
elif kind == KIND_DILIGENCE:
|
||||
process_status = 2
|
||||
process_result = clip("客户未尽调", PROCESS_RESULT_MAX)
|
||||
contract_status = 3
|
||||
fail_reason = "客户未尽调"
|
||||
contract_no = None
|
||||
else:
|
||||
reason = outcome.reason or "合同录入失败"
|
||||
if kind == KIND_OK and not number:
|
||||
reason = "submit 模式未读取到真实合同编号"
|
||||
process_status = 2
|
||||
process_result = clip(reason, PROCESS_RESULT_MAX)
|
||||
contract_status = 3
|
||||
fail_reason = clip(reason, FAIL_REASON_MAX)
|
||||
contract_no = None
|
||||
|
||||
seen: dict[int, PurchaseContractRow] = {}
|
||||
for row in rows:
|
||||
seen.setdefault(row.oms_id, row)
|
||||
table4 = [
|
||||
{
|
||||
"oms_id": row.oms_id,
|
||||
"action_code": 1,
|
||||
"contract_status": contract_status,
|
||||
"contract_fail_reason": fail_reason,
|
||||
"contract_no": contract_no,
|
||||
"agreement_status": None,
|
||||
"agreement_fail_reason": None,
|
||||
"agreement_id": None,
|
||||
"agreement_dtl_id": None,
|
||||
"push_status": 0,
|
||||
"org_code": row.org_code,
|
||||
"create_user_id": 0,
|
||||
"create_user_name": STAMP,
|
||||
"update_user_id": 0,
|
||||
"update_user_name": STAMP,
|
||||
}
|
||||
for row in seen.values()
|
||||
]
|
||||
return GroupSave(
|
||||
fids=[row.fid for row in rows],
|
||||
process_status=process_status,
|
||||
process_result=process_result,
|
||||
table4_rows=table4,
|
||||
)
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
[project]
|
||||
name = "rpa-contract"
|
||||
version = "0.1.0"
|
||||
description = "洁达合同班。表 3 合单录入 → 表 4。接口 4 组包在 rpa_oms。"
|
||||
description = "洁达合同班。表 3 合单录入 → 表 4。页面脚本已在本包;合单与接口 4 尚未接。"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"oracledb>=2.4.1",
|
||||
"pypdf>=5.1.0",
|
||||
"selenium>=4.15.0",
|
||||
"webdriver-manager>=4.0.0",
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
"""表 3 领取、表 4 写入。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from db import RepoUnavailable, connect_from_env, oracle_dsn
|
||||
|
||||
|
||||
@dataclass
|
||||
class PurchaseContractRow:
|
||||
fid: int
|
||||
oms_id: int
|
||||
supplier_id: str
|
||||
supplier_name: str
|
||||
contract_sign_date: date | None
|
||||
contract_start_date: date | None
|
||||
contract_end_date: date | None
|
||||
payment_term: int
|
||||
goods_id: str
|
||||
goods_name: str
|
||||
reg_no: str
|
||||
spec: str
|
||||
unit: str
|
||||
qty: Decimal | float | int
|
||||
unit_price: Decimal | float | int
|
||||
purchaser_id: str
|
||||
purchaser_name: str
|
||||
planner_id: str
|
||||
planner_name: str
|
||||
contract_entry_status: int | None
|
||||
contract_no: str | None
|
||||
process_status: int
|
||||
org_code: str | None
|
||||
goods_mdm_code: str | None
|
||||
supplier_mdm_code: str | None
|
||||
process_result: str = ""
|
||||
|
||||
|
||||
class MemoryContractRepo:
|
||||
def __init__(self, rows: list[PurchaseContractRow]):
|
||||
self.table3 = {row.fid: row for row in rows}
|
||||
self.table4: list[dict] = []
|
||||
self._next_fid = 1
|
||||
|
||||
def claim_pending(self, limit: int | None = None) -> list[PurchaseContractRow]:
|
||||
from group import is_claimable
|
||||
|
||||
pending = sorted(
|
||||
(row for row in self.table3.values() if is_claimable(row)),
|
||||
key=lambda row: row.fid,
|
||||
)
|
||||
if limit is not None:
|
||||
pending = pending[:limit]
|
||||
for row in pending:
|
||||
row.process_status = 3
|
||||
return pending
|
||||
|
||||
def save_group(self, payload: Any) -> None:
|
||||
for row in payload.table4_rows:
|
||||
item = dict(row)
|
||||
item["fid"] = self._next_fid
|
||||
self._next_fid += 1
|
||||
self.table4.append(item)
|
||||
for fid in payload.fids:
|
||||
target = self.table3[fid]
|
||||
target.process_status = payload.process_status
|
||||
target.process_result = payload.process_result
|
||||
|
||||
|
||||
_SELECT_PENDING = """
|
||||
SELECT
|
||||
fid, oms_id, supplier_id, supplier_name,
|
||||
contract_sign_date, contract_start_date, contract_end_date,
|
||||
payment_term, goods_id, goods_name, reg_no, spec, unit,
|
||||
qty, unit_price, purchaser_id, purchaser_name, planner_id, planner_name,
|
||||
contract_entry_status, contract_no, process_status, org_code,
|
||||
goods_mdm_code, supplier_mdm_code,
|
||||
NVL(process_result, '') AS process_result
|
||||
FROM rpa_purchase_contract_data
|
||||
WHERE process_status = 0
|
||||
AND NOT (NVL(contract_entry_status, -1) = 0
|
||||
AND contract_no IS NOT NULL
|
||||
AND LENGTH(TRIM(contract_no)) > 0)
|
||||
ORDER BY fid
|
||||
"""
|
||||
|
||||
_SELECT_PENDING_LIMIT = f"""
|
||||
SELECT * FROM (
|
||||
{_SELECT_PENDING}
|
||||
) WHERE ROWNUM <= :limit
|
||||
"""
|
||||
|
||||
_CLAIM_SQL = """
|
||||
UPDATE rpa_purchase_contract_data
|
||||
SET process_status = 3,
|
||||
process_time = SYSTIMESTAMP,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid AND process_status = 0
|
||||
"""
|
||||
|
||||
_INSERT_TABLE4_SQL = """
|
||||
INSERT INTO rpa_contract_agreement_result (
|
||||
oms_id, action_code, contract_status, contract_fail_reason, contract_no,
|
||||
agreement_status, agreement_fail_reason, agreement_id, agreement_dtl_id,
|
||||
push_status, create_time, update_time,
|
||||
create_user_id, create_user_name, update_user_id, update_user_name, org_code
|
||||
) VALUES (
|
||||
:oms_id, :action_code, :contract_status, :contract_fail_reason, :contract_no,
|
||||
:agreement_status, :agreement_fail_reason, :agreement_id, :agreement_dtl_id,
|
||||
0, SYSTIMESTAMP, SYSTIMESTAMP,
|
||||
0, '合同班', 0, '合同班', :org_code
|
||||
)
|
||||
"""
|
||||
|
||||
_COMPLETE_SQL = """
|
||||
UPDATE rpa_purchase_contract_data
|
||||
SET process_status = :process_status,
|
||||
process_result = :process_result,
|
||||
process_time = SYSTIMESTAMP,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid AND process_status = 3
|
||||
"""
|
||||
|
||||
_TABLE4_BIND_KEYS = (
|
||||
"oms_id",
|
||||
"action_code",
|
||||
"contract_status",
|
||||
"contract_fail_reason",
|
||||
"contract_no",
|
||||
"agreement_status",
|
||||
"agreement_fail_reason",
|
||||
"agreement_id",
|
||||
"agreement_dtl_id",
|
||||
"org_code",
|
||||
)
|
||||
|
||||
|
||||
def _row_from_record(cols: list[str], rec) -> PurchaseContractRow:
|
||||
data = dict(zip(cols, rec))
|
||||
return PurchaseContractRow(
|
||||
fid=int(data["fid"]),
|
||||
oms_id=int(data["oms_id"]),
|
||||
supplier_id=data["supplier_id"],
|
||||
supplier_name=data["supplier_name"],
|
||||
contract_sign_date=data["contract_sign_date"],
|
||||
contract_start_date=data["contract_start_date"],
|
||||
contract_end_date=data["contract_end_date"],
|
||||
payment_term=int(data["payment_term"]),
|
||||
goods_id=data["goods_id"],
|
||||
goods_name=data["goods_name"],
|
||||
reg_no=data["reg_no"],
|
||||
spec=data["spec"],
|
||||
unit=data["unit"],
|
||||
qty=data["qty"],
|
||||
unit_price=data["unit_price"],
|
||||
purchaser_id=data["purchaser_id"],
|
||||
purchaser_name=data["purchaser_name"],
|
||||
planner_id=data["planner_id"],
|
||||
planner_name=data["planner_name"],
|
||||
contract_entry_status=data["contract_entry_status"],
|
||||
contract_no=data["contract_no"],
|
||||
process_status=int(data["process_status"]),
|
||||
org_code=data["org_code"],
|
||||
goods_mdm_code=data["goods_mdm_code"],
|
||||
supplier_mdm_code=data["supplier_mdm_code"],
|
||||
process_result=data.get("process_result") or "",
|
||||
)
|
||||
|
||||
|
||||
class OracleContractRepo:
|
||||
def __init__(self, connect: Callable):
|
||||
self._connect = connect
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "OracleContractRepo":
|
||||
oracle_dsn()
|
||||
return cls(connect=connect_from_env)
|
||||
|
||||
def claim_pending(self, limit: int | None = None) -> list[PurchaseContractRow]:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if limit is None:
|
||||
cur.execute(_SELECT_PENDING)
|
||||
else:
|
||||
cur.execute(_SELECT_PENDING_LIMIT, {"limit": int(limit)})
|
||||
cols = [item[0].lower() for item in cur.description]
|
||||
fetched = [_row_from_record(cols, rec) for rec in cur.fetchall()]
|
||||
claimed: list[PurchaseContractRow] = []
|
||||
for row in fetched:
|
||||
cur.execute(_CLAIM_SQL, {"fid": row.fid})
|
||||
if cur.rowcount == 1:
|
||||
row.process_status = 3
|
||||
claimed.append(row)
|
||||
conn.commit()
|
||||
return claimed
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def save_group(self, payload: Any) -> None:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
for row in payload.table4_rows:
|
||||
cur.execute(
|
||||
_INSERT_TABLE4_SQL,
|
||||
{key: row.get(key) for key in _TABLE4_BIND_KEYS},
|
||||
)
|
||||
for fid in payload.fids:
|
||||
cur.execute(
|
||||
_COMPLETE_SQL,
|
||||
{
|
||||
"process_status": payload.process_status,
|
||||
"process_result": payload.process_result,
|
||||
"fid": fid,
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
"""合同班次入口:领表 3、调页面流、写表 4。不推接口 4。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from apa_browser_adapter import ApaBrowserAdapter
|
||||
from contract_create_node import CONTRACT_URL, ContractCreateInput, build_flow_context
|
||||
from contract_draft import ContractDraft
|
||||
from contract_flow_steps import run_contract_create_flow
|
||||
from db import load_runtime_env
|
||||
from job import outcome_from_run, run_batch
|
||||
from persist import ContractOutcome
|
||||
from repo import OracleContractRepo
|
||||
|
||||
|
||||
def production_runner(draft: ContractDraft) -> ContractOutcome:
|
||||
spec = ContractCreateInput(
|
||||
job_id="contract-batch",
|
||||
supplier_name=draft.supplier_name,
|
||||
order_date=draft.order_date,
|
||||
source_type=draft.source_type or "",
|
||||
submit_mode="submit",
|
||||
)
|
||||
ctx = build_flow_context(spec, draft)
|
||||
browser = ApaBrowserAdapter(url=ctx.contract_url or CONTRACT_URL, flow="CONTRACT_CREATE")
|
||||
|
||||
def run() -> str:
|
||||
return run_contract_create_flow(browser, ctx)
|
||||
|
||||
return outcome_from_run(run)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
load_runtime_env()
|
||||
repo = OracleContractRepo.from_env()
|
||||
summary = run_batch(repo, runner=production_runner)
|
||||
print(
|
||||
f"claimed={summary.claimed} groups={summary.groups} "
|
||||
f"ok={summary.ok} failed={summary.failed}"
|
||||
)
|
||||
return 0 if summary.failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
from contract_flow_steps import (
|
||||
ContractFlowContext,
|
||||
begin_date_for,
|
||||
check_customer_diligence,
|
||||
sign_date_for,
|
||||
)
|
||||
from errors import DiligenceError
|
||||
import pytest
|
||||
|
||||
|
||||
def _ctx(**overrides) -> ContractFlowContext:
|
||||
data = dict(
|
||||
contract_url="http://example",
|
||||
username="u",
|
||||
password="p",
|
||||
supplier_name="示例",
|
||||
order_date="2026-09-28",
|
||||
source_type="",
|
||||
)
|
||||
data.update(overrides)
|
||||
return ContractFlowContext(**data)
|
||||
|
||||
|
||||
class _FakeBrowser:
|
||||
def __init__(self, text: str):
|
||||
self._text = text
|
||||
|
||||
def read_body_text(self) -> str:
|
||||
return self._text
|
||||
|
||||
|
||||
def test_sign_and_begin_fall_back_to_order_date():
|
||||
ctx = _ctx()
|
||||
assert sign_date_for(ctx) == "2026-09-28"
|
||||
assert begin_date_for(ctx) == "2026-09-28"
|
||||
|
||||
|
||||
def test_sign_and_begin_use_contract_dates_when_set():
|
||||
ctx = _ctx(contract_sign_date="2026-10-01", contract_start_date="2026-10-02")
|
||||
assert sign_date_for(ctx) == "2026-10-01"
|
||||
assert begin_date_for(ctx) == "2026-10-02"
|
||||
|
||||
|
||||
def test_diligence_raises_when_page_says_not_done():
|
||||
with pytest.raises(DiligenceError, match="客户未尽调"):
|
||||
check_customer_diligence(_FakeBrowser("客户未尽调,不能起草"), _ctx())
|
||||
|
||||
|
||||
def test_diligence_passes_when_text_absent():
|
||||
check_customer_diligence(_FakeBrowser("客户已尽调"), _ctx())
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from group import (
|
||||
draft_from_group,
|
||||
group_by_supplier,
|
||||
is_claimable,
|
||||
missing_mdm_reason,
|
||||
money4,
|
||||
)
|
||||
from repo import PurchaseContractRow
|
||||
|
||||
|
||||
def _row(**overrides) -> PurchaseContractRow:
|
||||
data = dict(
|
||||
fid=1,
|
||||
oms_id=2026100501,
|
||||
supplier_id="SUP001",
|
||||
supplier_name="示例供应商",
|
||||
contract_sign_date=date(2026, 10, 1),
|
||||
contract_start_date=date(2026, 10, 2),
|
||||
contract_end_date=date(2027, 9, 30),
|
||||
payment_term=90,
|
||||
goods_id="G0001",
|
||||
goods_name="阿莫西林胶囊",
|
||||
reg_no="国药准字H20201234",
|
||||
spec="0.25g*24粒",
|
||||
unit="盒",
|
||||
qty=Decimal("10.0000"),
|
||||
unit_price=Decimal("100.0000"),
|
||||
purchaser_id="P001",
|
||||
purchaser_name="张三",
|
||||
planner_id="PL001",
|
||||
planner_name="李四",
|
||||
contract_entry_status=None,
|
||||
contract_no=None,
|
||||
process_status=0,
|
||||
org_code="901",
|
||||
goods_mdm_code="QX0100167583",
|
||||
supplier_mdm_code="DW0100046361",
|
||||
)
|
||||
data.update(overrides)
|
||||
return PurchaseContractRow(**data)
|
||||
|
||||
|
||||
def test_protocol_rerun_is_not_claimable():
|
||||
row = _row(contract_entry_status=0, contract_no="HT-OLD")
|
||||
assert is_claimable(row) is False
|
||||
|
||||
|
||||
def test_blank_contract_no_is_still_claimable():
|
||||
assert is_claimable(_row(contract_entry_status=0, contract_no=" ")) is True
|
||||
assert is_claimable(_row(contract_entry_status=0, contract_no="")) is True
|
||||
assert is_claimable(_row(contract_entry_status=0, contract_no=None)) is True
|
||||
|
||||
|
||||
def test_processing_or_done_is_not_claimable():
|
||||
assert is_claimable(_row(process_status=3)) is False
|
||||
assert is_claimable(_row(process_status=1)) is False
|
||||
|
||||
|
||||
def test_group_by_supplier_keeps_fid_order_and_first_header():
|
||||
rows = [
|
||||
_row(fid=2, supplier_id="B", payment_term=30, supplier_mdm_code="MDM-B"),
|
||||
_row(fid=1, supplier_id="A", payment_term=90, supplier_mdm_code="MDM-A"),
|
||||
_row(fid=3, supplier_id="A", payment_term=60, oms_id=2026100502, goods_id="G0002"),
|
||||
]
|
||||
groups = group_by_supplier(rows)
|
||||
assert [g[0].supplier_id for g in groups] == ["A", "B"]
|
||||
assert [r.fid for r in groups[0]] == [1, 3]
|
||||
draft = draft_from_group(groups[0])
|
||||
assert draft.customer_code == "MDM-A"
|
||||
assert draft.payment_days == "90"
|
||||
assert draft.order_date == "2026-10-01"
|
||||
assert draft.contract_sign_date == "2026-10-01"
|
||||
assert draft.contract_start_date == "2026-10-02"
|
||||
assert draft.contract_end_date == "2027-09-30"
|
||||
assert draft.amount == "2000.0000"
|
||||
assert draft.source_type == ""
|
||||
assert draft.is_prepayment == "否"
|
||||
assert len(draft.goods_lines) == 2
|
||||
assert draft.goods_lines[0].goods_code == "G0001"
|
||||
assert draft.goods_lines[0].operation_code == "G0001"
|
||||
assert draft.goods_lines[0].unit_price == "100.0000"
|
||||
assert draft.belong_year == "2026"
|
||||
|
||||
|
||||
def test_missing_mdm_reason():
|
||||
rows = [_row(supplier_mdm_code="")]
|
||||
assert missing_mdm_reason(rows) == "缺少供应商MDM码"
|
||||
assert missing_mdm_reason([_row()]) is None
|
||||
|
||||
|
||||
def test_money4():
|
||||
assert money4(Decimal("10.5")) == "10.5000"
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from errors import DiligenceError, NodeError
|
||||
from job import outcome_from_run, run_batch
|
||||
from persist import KIND_DILIGENCE, KIND_FAIL, KIND_OK, ContractOutcome, build_save
|
||||
from repo import MemoryContractRepo, PurchaseContractRow
|
||||
|
||||
|
||||
def _row(**overrides) -> PurchaseContractRow:
|
||||
data = dict(
|
||||
fid=1,
|
||||
oms_id=2026100501,
|
||||
supplier_id="SUP001",
|
||||
supplier_name="示例供应商",
|
||||
contract_sign_date=date(2026, 10, 1),
|
||||
contract_start_date=date(2026, 10, 2),
|
||||
contract_end_date=date(2027, 9, 30),
|
||||
payment_term=90,
|
||||
goods_id="G0001",
|
||||
goods_name="阿莫西林胶囊",
|
||||
reg_no="国药准字H20201234",
|
||||
spec="0.25g*24粒",
|
||||
unit="盒",
|
||||
qty=Decimal("10.0000"),
|
||||
unit_price=Decimal("100.0000"),
|
||||
purchaser_id="P001",
|
||||
purchaser_name="张三",
|
||||
planner_id="PL001",
|
||||
planner_name="李四",
|
||||
contract_entry_status=None,
|
||||
contract_no=None,
|
||||
process_status=0,
|
||||
org_code="901",
|
||||
goods_mdm_code="QX0100167583",
|
||||
supplier_mdm_code="DW0100046361",
|
||||
)
|
||||
data.update(overrides)
|
||||
return PurchaseContractRow(**data)
|
||||
|
||||
|
||||
def test_claim_pending_sets_processing_and_skips_protocol_rerun():
|
||||
repo = MemoryContractRepo(
|
||||
[
|
||||
_row(),
|
||||
_row(fid=2, oms_id=2026100502, process_status=1),
|
||||
_row(fid=3, oms_id=2026100503, contract_entry_status=0, contract_no="HT-OLD"),
|
||||
]
|
||||
)
|
||||
claimed = repo.claim_pending()
|
||||
assert [row.fid for row in claimed] == [1]
|
||||
assert repo.table3[1].process_status == 3
|
||||
assert repo.table3[3].process_status == 0
|
||||
|
||||
|
||||
def test_save_group_inserts_one_table4_row_per_oms_id():
|
||||
repo = MemoryContractRepo(
|
||||
[
|
||||
_row(),
|
||||
_row(fid=2, oms_id=2026100501, goods_id="G0002"),
|
||||
_row(fid=3, oms_id=2026100502, goods_id="G0003"),
|
||||
]
|
||||
)
|
||||
repo.claim_pending()
|
||||
outcome = ContractOutcome(KIND_OK, "HT20261005001", None)
|
||||
payload = build_save(list(repo.table3.values()), outcome)
|
||||
repo.save_group(payload)
|
||||
assert [row["oms_id"] for row in repo.table4] == [2026100501, 2026100502]
|
||||
assert {row["contract_no"] for row in repo.table4} == {"HT20261005001"}
|
||||
assert {row["action_code"] for row in repo.table4} == {1}
|
||||
assert {row["contract_status"] for row in repo.table4} == {1}
|
||||
assert {row["push_status"] for row in repo.table4} == {0}
|
||||
assert repo.table3[1].process_status == 1
|
||||
assert repo.table3[3].process_status == 1
|
||||
|
||||
|
||||
def test_job_merges_same_supplier_and_writes_table4():
|
||||
repo = MemoryContractRepo(
|
||||
[
|
||||
_row(),
|
||||
_row(fid=2, oms_id=2026100502, goods_id="G0002"),
|
||||
_row(fid=3, oms_id=2026100503, contract_entry_status=0, contract_no="HT-OLD"),
|
||||
]
|
||||
)
|
||||
calls: list = []
|
||||
|
||||
def runner(draft):
|
||||
calls.append(draft)
|
||||
return ContractOutcome(KIND_OK, "HT20261005001", None)
|
||||
|
||||
run_batch(repo, runner=runner)
|
||||
assert len(calls) == 1
|
||||
assert [line.goods_code for line in calls[0].goods_lines] == ["G0001", "G0002"]
|
||||
assert calls[0].customer_code == "DW0100046361"
|
||||
assert [row["oms_id"] for row in repo.table4] == [2026100501, 2026100502]
|
||||
assert {row["contract_no"] for row in repo.table4} == {"HT20261005001"}
|
||||
assert {row["action_code"] for row in repo.table4} == {1}
|
||||
assert {row["contract_status"] for row in repo.table4} == {1}
|
||||
assert {row["push_status"] for row in repo.table4} == {0}
|
||||
assert repo.table3[1].process_status == 1
|
||||
assert repo.table3[2].process_status == 1
|
||||
assert repo.table3[3].process_status == 0
|
||||
|
||||
|
||||
def test_job_marks_diligence_fail():
|
||||
repo = MemoryContractRepo([_row()])
|
||||
|
||||
def runner(draft):
|
||||
return ContractOutcome(KIND_DILIGENCE, None, "客户未尽调")
|
||||
|
||||
run_batch(repo, runner=runner)
|
||||
assert repo.table3[1].process_status == 2
|
||||
assert "客户未尽调" in repo.table3[1].process_result
|
||||
assert repo.table4[0]["contract_status"] == 3
|
||||
assert repo.table4[0]["contract_no"] is None
|
||||
assert repo.table4[0]["contract_fail_reason"] == "客户未尽调"
|
||||
|
||||
|
||||
def test_job_skips_runner_when_mdm_missing():
|
||||
repo = MemoryContractRepo([_row(supplier_mdm_code="")])
|
||||
called = {"n": 0}
|
||||
|
||||
def runner(draft):
|
||||
called["n"] += 1
|
||||
return ContractOutcome(KIND_OK, "HT-X", None)
|
||||
|
||||
run_batch(repo, runner=runner)
|
||||
assert called["n"] == 0
|
||||
assert repo.table3[1].process_status == 2
|
||||
assert "缺少供应商MDM码" in repo.table3[1].process_result
|
||||
|
||||
|
||||
def test_outcome_from_run_maps_diligence():
|
||||
def run():
|
||||
raise DiligenceError("客户未尽调")
|
||||
|
||||
out = outcome_from_run(run)
|
||||
assert out.kind == KIND_DILIGENCE
|
||||
assert out.reason == "客户未尽调"
|
||||
assert out.contract_no is None
|
||||
|
||||
|
||||
def test_outcome_from_run_maps_empty_number_to_fail():
|
||||
out = outcome_from_run(lambda: "")
|
||||
assert out.kind == KIND_FAIL
|
||||
assert "未读取到真实合同编号" in (out.reason or "")
|
||||
|
||||
|
||||
def test_outcome_from_run_ok():
|
||||
out = outcome_from_run(lambda: "HT9")
|
||||
assert out == ContractOutcome(KIND_OK, "HT9", None)
|
||||
|
||||
|
||||
def test_outcome_from_run_maps_node_error():
|
||||
def run():
|
||||
raise NodeError("客商加载失败")
|
||||
|
||||
out = outcome_from_run(run)
|
||||
assert out.kind == KIND_FAIL
|
||||
assert "客商加载失败" in (out.reason or "")
|
||||
|
||||
|
|
@ -29,77 +29,27 @@ dependencies = [
|
|||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
|
||||
|
|
@ -245,69 +195,6 @@ wheels = [
|
|||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "50.0.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
|
|
@ -335,50 +222,6 @@ wheels = [
|
|||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oracledb"
|
||||
version = "26.0.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/f3/22113415f48b6608ada31ebb46047b3b8ac361dba393d1b5ed1f768d8f6a/oracledb-26.0.1.tar.gz", hash = "sha256:786397a6b37e94ebfa6c1c6cd026755eb8fccfd39e15bcb64cb5879491882aef", size = 910463, upload-time = "2026-09-22T21:23:20.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/fa/349ee179b3e106f210ac070d199dd341f37e2af9f5faf2563b80d8aa6ab1/oracledb-26.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eea4d0824fa82b03c0d90aad81f10dbbacdaf351979c0130190a362a539e63ae", size = 4908265, upload-time = "2026-09-22T21:23:49.524Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/68/2dc6a99d385835290825c0d6689097af29637db8754197040b95814bb5cd/oracledb-26.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c05707b2ba57d8210bdf062ba3419900591f3369d1532fb116af51445c2993d", size = 2448388, upload-time = "2026-09-22T21:23:50.993Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/4b/ce48ed072c2328b429a6bc87a35ddd735945151145d9fe49c1e177b0da83/oracledb-26.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36cbf2be185412fee95be96aa6f187a73fd5181bb03186b53d5b70ac18fa354b", size = 2663296, upload-time = "2026-09-22T21:23:52.426Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/c8/73166ff29389c5ce3272fcc534360ab65248f90fcdf175a99b5fa2bb8466/oracledb-26.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5ad1b6f4db3d8a43182e7734922342fac5901456b34acead92f6fc60cc59882", size = 2493289, upload-time = "2026-09-22T21:23:53.949Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/c0/f6b4f82d6dc7f7d7654c0e4889caa1abbe019ec76daa1b4b4ee80859172b/oracledb-26.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aecc4cdd03cb5a5ad51f43680d16d74132ae7df65c991e7bd69bbe2ccc0fd209", size = 2694730, upload-time = "2026-09-22T21:23:55.609Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/da/a3ad79e18c5b27b357de005ce9d79dff1ef1646b7c55b6e6557b238f03bd/oracledb-26.0.1-cp312-cp312-win32.whl", hash = "sha256:f55d6fd9ebc3e8bf3bf4828fcd6ae152b363859628d27f92a4764111adfb1001", size = 1578393, upload-time = "2026-09-22T21:23:56.975Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/c6/8e2ad66bda2cda6d085f3516ad8f7834b1d7d4c9da21f96834467525b551/oracledb-26.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c6f2f0d7290b15f1a166c0d24187e7746a6f08ea6e886e521110f1bf8d2002c", size = 1970972, upload-time = "2026-09-22T21:23:58.352Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/ce/98c3a83142a562e40135abb8503b59eba7af1135c11c8077986a289f3a35/oracledb-26.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:c31b6f7440047c867a1b41cbaace7867db0750ecab4dc6454c9f501991152deb", size = 1622016, upload-time = "2026-09-22T21:23:59.996Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/d7/2a911476339d9fa6eb83e6a4639b007c13af26d7261907ecc2d8722a6712/oracledb-26.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:339dd6c4f0d50dab4026a36ae89d902240b2a879a17cdf18e3feadc113957b4e", size = 4911060, upload-time = "2026-09-22T21:24:01.757Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/e8/97e3b2c283ab996efe1d12b6afa86042c746b0ce4e408942bb4b7c35e39a/oracledb-26.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5907bb5c9df123e417ddf358f540554dfc6766064c2feb84121c099bc4e039a3", size = 2445641, upload-time = "2026-09-22T21:24:03.587Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/f7/bbf75b91248ea489e8393ae84e9985b37c895781c658b6d9e5d83d02c1c1/oracledb-26.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16d08328bf9b02e980ad4d5f3e0e99596f9a42aaf6bd1e244ea85be753f2c292", size = 2646590, upload-time = "2026-09-22T21:24:05.22Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/32/cbdacb302a2ca047e36e0f804c493391844aad83822d428184fa1d4203d5/oracledb-26.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50e42111d5c620f60478e4571a5847c335286e8650e63a8789f8bd51035f90da", size = 2499129, upload-time = "2026-09-22T21:24:06.801Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/1e/ce86b581a4f1c8d5844019f84eacc9f4084c882ba676a90c94d7eaeb749e/oracledb-26.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0453f8f1a6748c982dcdac8af09bcd81971979c7ec9d1d151e15595f58d9164b", size = 2675979, upload-time = "2026-09-22T21:24:08.153Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/7c/49e6af631b84db68d801b6d03ff58c4fa7b767ffa8a8402de643584e9d63/oracledb-26.0.1-cp313-cp313-win32.whl", hash = "sha256:93e6a7cda6ad7b14e2ce8689c9694872b3d69d57015aab070028c9a93faf3bf1", size = 1576622, upload-time = "2026-09-22T21:24:09.492Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/10/ba7a5ce82fda9bbfcf222930b9a6f9a967c2b3d49f787ba7f4f99b598c6f/oracledb-26.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:6cf0c4d8662072917e2f98598cb97a18e157785bd0f294fbcf770d822ca239a9", size = 1970210, upload-time = "2026-09-22T21:24:10.806Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/dc/96ce57dad3b1bb3d3c30be936d0f6daea0a02649d8936b878921004b15e1/oracledb-26.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:22bb1e47a01a5259eead9202be8f631682b69be08ee29d8e532ed9b9364bd7bd", size = 1621513, upload-time = "2026-09-22T21:24:12.246Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/e6/7cf147ea1daf0c7c86fcd77acce1a09b0d4ab75539a4677a2034f112c860/oracledb-26.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2201184b46bbec476a7ac29a72f14ee4ed75d692ade3b6c5d672fdbca71e6b2", size = 4961776, upload-time = "2026-09-22T21:24:13.967Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/7e/1ada40fce6ec550718e6eb1b8be9bfcc6e9697f19b6244ed6e0b89aa8da8/oracledb-26.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bddb6bd7156df750045b419ffc400cfe429276f1a17d83913f10adefd0baa9a", size = 2486660, upload-time = "2026-09-22T21:24:16.397Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/2a/a4cf6f8081b1ab3498c82e3917b2174843bf22e2fa4c41e98ad1e0b66d47/oracledb-26.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33b22800a9b02ee0ead00e6f3073b810f7dc51c1e82fc9fc5475c09812420df", size = 2664849, upload-time = "2026-09-22T21:24:18.16Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/f2/50a32fa2d8c6cbb0e5fb69c262b01e7412ad08e430192404af9d2b9f4bea/oracledb-26.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:124f0072e6c7df277c561ec3a4c42cb2c02c30e9ce15e75a460b4abcfee41046", size = 2544330, upload-time = "2026-09-22T21:24:19.627Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/90/9b6dd9bbc1899ca3a982e9012f25be2a70e4795eacc6f8582c340c64fe1c/oracledb-26.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4c5094d5ba60b0f67ce7c74eb0fbc738d827103c0ba2db5f8483db405ab4e9c4", size = 2693190, upload-time = "2026-09-22T21:24:21.228Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/42/0df9265f2f562283c88590a6bcd442084cef80c832ff598bdbab994a3cb8/oracledb-26.0.1-cp314-cp314-win32.whl", hash = "sha256:03424ccfbd25c402133c2a23c5c1a630e57effda7b962085d91c41d765af1b25", size = 1602133, upload-time = "2026-09-22T21:24:22.733Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/d4/1b90c5e252a53dbc71833217746f2673e9e2ce0e06115bac1dc42ae347d0/oracledb-26.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:ee8f4736b72f38ade26f9770f7b40e8662bfa825ef48cefcc55cef0c30525d4b", size = 2027835, upload-time = "2026-09-22T21:24:24.105Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/4e/9febea61cbf476c2ae5d290fb3f823f1e46668e3ac15b8af5307531e33ad/oracledb-26.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:13b0dfbbc503c6107d6c08b1658df260c2c077bc5574d9545159df0a0301fe7d", size = 1680743, upload-time = "2026-09-22T21:24:25.475Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/bb/71f7861bfbf586757873259a858f370030a137b4172865992c3cc9a462b1/oracledb-26.0.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9e77a6b3b01a3a2f4b786939f59d862fbe73fbc4c68cdf87aa45a5398725a16c", size = 4960764, upload-time = "2026-09-22T21:24:27.244Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/c0/86aa68f08e5c1d25b864ac5777c559bf0bc8911e5d8262b70bec5e05cc21/oracledb-26.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d722f185f9c05ecccb2702a58a1e5ecfd309ae92cad0c38373e0969e83bf2bd", size = 2488466, upload-time = "2026-09-22T21:24:29.177Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/f4/af94230a3c2c6679323f3578a0a5281cfd87f72994f66e6044b5ff6c95e9/oracledb-26.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfcf6382bc48e21e448ca2b71644cc75626056a638bca50181acdc8adc69dd3c", size = 2685915, upload-time = "2026-09-22T21:24:30.823Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/a7/aa99f8e60961e80795ca926ead66c66d98c3aee78d970320eacdfa843614/oracledb-26.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d42a2e0caea9bcf7d260d430b225d72cff6dffa0d6729be7a8823db2678e0b74", size = 2544920, upload-time = "2026-09-22T21:24:32.667Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/65/b636a18a578cffd5819df08ae80deeb931f1215d2d2b18112a37e9d2bbea/oracledb-26.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a073cc8bd74a7ce32d12f4bc898dd143971981b52ecd2f72c37b4474bff76cb", size = 2713492, upload-time = "2026-09-22T21:24:34.22Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4a/e758838b14540a94599487d0d15ce4390edba5a257e53f2cea66f94bd138/oracledb-26.0.1-cp315-cp315-win32.whl", hash = "sha256:f4188bf8eedfc05ef266835e98ce304fda92d6bc87dd776d5f9a8b510d8b54ba", size = 1601872, upload-time = "2026-09-22T21:24:35.591Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/b1/51b2a51abd89ba43dc0f9ee14d456a9b002d0b9c31840c7de55387e555eb/oracledb-26.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:9d508d2df7bb9a4802247ec95d06064e2babf8ae9e1f91d89c349bcd30df44ed", size = 2026829, upload-time = "2026-09-22T21:24:37.15Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/6b/b099ff2c51447cbf0d2f1c33ea7de1abf96d9bf4c3d93b2a5dff48728965/oracledb-26.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:7c9541d7cf2d324e9516c955c033235bbf25b7d96ce41a856b3bff2eab3494ed", size = 1680535, upload-time = "2026-09-22T21:24:38.551Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outcome"
|
||||
version = "1.3.0.post0"
|
||||
|
|
@ -490,7 +333,6 @@ name = "rpa-contract"
|
|||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "oracledb" },
|
||||
{ name = "pypdf" },
|
||||
{ name = "requests" },
|
||||
{ name = "selenium" },
|
||||
|
|
@ -504,7 +346,6 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "oracledb", specifier = ">=2.4.1" },
|
||||
{ name = "pypdf", specifier = ">=5.1.0" },
|
||||
{ name = "requests", specifier = ">=2.31.0" },
|
||||
{ name = "selenium", specifier = ">=4.15.0" },
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
| 1 | 订单中心 → 本方 | `rpa_sales_goods_mapping_data` | `POST /api/oms/v1/sales-goods` |
|
||||
| 2 | 本方 → 订单中心 | `rpa_cart_settlement_goods_data` | `app/outbound/cart_settlement.py` 取 `push_status=0` 组装报文。路径仍空,不发 HTTP |
|
||||
| 3 | 订单中心 → 本方 | `rpa_purchase_contract_data` | `POST /api/oms/v1/purchase-contracts`,按对方 `fid` 幂等 |
|
||||
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | `app/outbound/contract_agreement.py` 取 `push_status=0` 组装报文。路径仍空,不发 HTTP |
|
||||
| 4 | 本方 → 订单中心 | `rpa_contract_agreement_result` | 未做。将来由合同/返利班调用 |
|
||||
|
||||
入站(1、3):
|
||||
|
||||
|
|
@ -27,15 +27,6 @@ from app.outbound.oracle_repo import OracleCartSettlementRepo
|
|||
envelope = load_cart_settlement_envelope(OracleCartSettlementRepo.from_env())
|
||||
```
|
||||
|
||||
接口 4 出站组装:`load_contract_agreement_envelope(repo)`,同一非空 `contractNo` 不拆批,一批最多 500 条。不改 `push_status`。
|
||||
|
||||
```python
|
||||
from app.outbound.contract_agreement import load_contract_agreement_envelope
|
||||
from app.outbound.oracle_repo import OracleContractAgreementRepo
|
||||
|
||||
envelope = load_contract_agreement_envelope(OracleContractAgreementRepo.from_env())
|
||||
```
|
||||
|
||||
运行时只落 / 读 Oracle。单测用内存替身,不连库:`uv run pytest -v`。
|
||||
|
||||
## 真库
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
"""出站:从表组装订单中心报文。本期接口 2、4 只取数组装,不发 HTTP。"""
|
||||
"""出站:从表组装订单中心报文。本期接口 2 只取数组装,不发 HTTP。"""
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
"""接口 4:从表 4 取未推行,组装文档报文。
|
||||
|
||||
同一非空 contractNo 必须在同一批 data 里。一批最多 500 条。
|
||||
空 contractNo(失败行)按行独立。不改 push_status,不发 HTTP。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
MAX_BATCH = 500
|
||||
|
||||
|
||||
def load_contract_agreement_envelope(repo, limit: int = MAX_BATCH) -> dict:
|
||||
return build_contract_agreement_envelope(repo.list_unpushed(), limit=limit)
|
||||
|
||||
|
||||
def build_contract_agreement_envelope(rows: list[dict], limit: int = MAX_BATCH) -> dict:
|
||||
chosen = _take_complete_groups(rows, limit)
|
||||
return {"data": [_to_item(row) for row in chosen]}
|
||||
|
||||
|
||||
def _group_key(row: dict):
|
||||
number = row.get("contract_no")
|
||||
if number is None or str(number).strip() == "":
|
||||
return ("fid", row["fid"])
|
||||
return ("no", str(number).strip())
|
||||
|
||||
|
||||
def _take_complete_groups(rows: list[dict], limit: int) -> list[dict]:
|
||||
groups: OrderedDict = OrderedDict()
|
||||
for row in rows:
|
||||
groups.setdefault(_group_key(row), []).append(row)
|
||||
batch: list[dict] = []
|
||||
for group in groups.values():
|
||||
if len(batch) + len(group) > limit and batch:
|
||||
break
|
||||
batch.extend(group)
|
||||
return batch
|
||||
|
||||
|
||||
def _to_item(row: dict) -> dict:
|
||||
return {
|
||||
"fid": _as_int(row["fid"]),
|
||||
"omsId": _as_int(row["oms_id"]),
|
||||
"actionCode": _as_int(row["action_code"]),
|
||||
"contractStatus": _as_int(row.get("contract_status")),
|
||||
"contractFailReason": _as_str(row.get("contract_fail_reason")),
|
||||
"contractNo": _as_str(row.get("contract_no")),
|
||||
"agreementStatus": _as_int(row.get("agreement_status")),
|
||||
"agreementFailReason": _as_str(row.get("agreement_fail_reason")),
|
||||
"agreementId": _as_str(row.get("agreement_id")),
|
||||
"agreementDtlId": _as_str(row.get("agreement_dtl_id")),
|
||||
"orgCode": _as_str(row.get("org_code")),
|
||||
}
|
||||
|
||||
|
||||
def _as_int(value):
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
|
||||
def _as_str(value):
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value)
|
||||
if text.strip() == "":
|
||||
return None
|
||||
return text
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""内存表 2 / 表 4。只给单测用。"""
|
||||
"""内存表 2。只给单测用。"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
|
|
@ -8,11 +8,3 @@ class MemoryCartSettlementRepo:
|
|||
|
||||
def list_unpushed(self) -> list[dict]:
|
||||
return [dict(row) for row in self._rows if row.get("push_status", 0) == 0]
|
||||
|
||||
|
||||
class MemoryContractAgreementRepo:
|
||||
def __init__(self, rows: list[dict] | None = None):
|
||||
self._rows = [dict(row) for row in (rows or [])]
|
||||
|
||||
def list_unpushed(self) -> list[dict]:
|
||||
return [dict(row) for row in self._rows if row.get("push_status", 0) == 0]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""接口 2 / 4:读 push_status=0。不更新推送状态。"""
|
||||
"""接口 2:从表 2 读取 push_status=0。不更新推送状态。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
|
@ -52,50 +52,3 @@ class OracleCartSettlementRepo:
|
|||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
_LIST_AGREEMENT_SQL = """
|
||||
SELECT
|
||||
fid, oms_id, action_code, contract_status, contract_fail_reason,
|
||||
contract_no, agreement_status, agreement_fail_reason,
|
||||
agreement_id, agreement_dtl_id, org_code, push_status
|
||||
FROM rpa_contract_agreement_result
|
||||
WHERE push_status = 0
|
||||
ORDER BY fid
|
||||
"""
|
||||
|
||||
|
||||
class OracleContractAgreementRepo:
|
||||
def __init__(self, connect: Callable):
|
||||
self._connect = connect
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "OracleContractAgreementRepo":
|
||||
user = os.getenv("RPA_ORACLE_USER", "")
|
||||
password = os.getenv("RPA_ORACLE_PASSWORD", "")
|
||||
host = os.getenv("RPA_ORACLE_HOST", "")
|
||||
if not user or not password or not host:
|
||||
raise RepoUnavailable("未配置 RPA_ORACLE_HOST / RPA_ORACLE_USER / RPA_ORACLE_PASSWORD")
|
||||
port = os.getenv("RPA_ORACLE_PORT", "1521")
|
||||
service = os.getenv("RPA_ORACLE_SERVICE", "rpa")
|
||||
dsn = f"{host}:{port}/{service}"
|
||||
|
||||
def connect():
|
||||
_ensure_client()
|
||||
import oracledb
|
||||
|
||||
return oracledb.connect(user=user, password=password, dsn=dsn)
|
||||
|
||||
return cls(connect=connect)
|
||||
|
||||
def list_unpushed(self) -> list[dict]:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(_LIST_AGREEMENT_SQL)
|
||||
cols = [item[0].lower() for item in cur.description]
|
||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
except Exception as exc:
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"data": [
|
||||
{
|
||||
"fid": 300001,
|
||||
"omsId": 20260928000001,
|
||||
"actionCode": 1,
|
||||
"contractStatus": 2,
|
||||
"contractFailReason": null,
|
||||
"contractNo": "HT20260928001",
|
||||
"agreementStatus": null,
|
||||
"agreementFailReason": null,
|
||||
"agreementId": null,
|
||||
"agreementDtlId": null,
|
||||
"orgCode": "901"
|
||||
},
|
||||
{
|
||||
"fid": 300002,
|
||||
"omsId": 20260928000001,
|
||||
"actionCode": 2,
|
||||
"contractStatus": null,
|
||||
"contractFailReason": null,
|
||||
"contractNo": "HT20260928001",
|
||||
"agreementStatus": 1,
|
||||
"agreementFailReason": null,
|
||||
"agreementId": "AGR20260928001",
|
||||
"agreementDtlId": "AGRD20260928001",
|
||||
"orgCode": "901"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
"""接口 4:从表 4 取 push_status=0,组装文档报文。不连 Oracle,不发 HTTP。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.outbound.contract_agreement import (
|
||||
MAX_BATCH,
|
||||
build_contract_agreement_envelope,
|
||||
load_contract_agreement_envelope,
|
||||
)
|
||||
from app.outbound.memory_repo import MemoryContractAgreementRepo
|
||||
|
||||
|
||||
def _row(**overrides):
|
||||
row = {
|
||||
"fid": 300001,
|
||||
"oms_id": 20260928000001,
|
||||
"action_code": 1,
|
||||
"contract_status": 1,
|
||||
"contract_fail_reason": None,
|
||||
"contract_no": "HT1",
|
||||
"agreement_status": None,
|
||||
"agreement_fail_reason": None,
|
||||
"agreement_id": None,
|
||||
"agreement_dtl_id": None,
|
||||
"org_code": "901",
|
||||
"push_status": 0,
|
||||
}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
|
||||
def test_assembles_unpushed_rows_as_document_envelope():
|
||||
repo = MemoryContractAgreementRepo(
|
||||
[
|
||||
_row(),
|
||||
_row(
|
||||
fid=300002,
|
||||
action_code=2,
|
||||
contract_status=None,
|
||||
contract_no="HT1",
|
||||
agreement_status=1,
|
||||
agreement_id="AGR1",
|
||||
agreement_dtl_id="AGRD1",
|
||||
),
|
||||
_row(fid=300003, push_status=1, contract_no="HT-SKIP"),
|
||||
]
|
||||
)
|
||||
envelope = load_contract_agreement_envelope(repo)
|
||||
assert envelope == {
|
||||
"data": [
|
||||
{
|
||||
"fid": 300001,
|
||||
"omsId": 20260928000001,
|
||||
"actionCode": 1,
|
||||
"contractStatus": 1,
|
||||
"contractFailReason": None,
|
||||
"contractNo": "HT1",
|
||||
"agreementStatus": None,
|
||||
"agreementFailReason": None,
|
||||
"agreementId": None,
|
||||
"agreementDtlId": None,
|
||||
"orgCode": "901",
|
||||
},
|
||||
{
|
||||
"fid": 300002,
|
||||
"omsId": 20260928000001,
|
||||
"actionCode": 2,
|
||||
"contractStatus": None,
|
||||
"contractFailReason": None,
|
||||
"contractNo": "HT1",
|
||||
"agreementStatus": 1,
|
||||
"agreementFailReason": None,
|
||||
"agreementId": "AGR1",
|
||||
"agreementDtlId": "AGRD1",
|
||||
"orgCode": "901",
|
||||
},
|
||||
]
|
||||
}
|
||||
assert isinstance(envelope["data"][0]["omsId"], int)
|
||||
|
||||
|
||||
def test_assembles_contract_success_and_agreement_rows():
|
||||
repo = MemoryContractAgreementRepo(
|
||||
[
|
||||
_row(contract_status=2),
|
||||
_row(
|
||||
fid=300002,
|
||||
action_code=2,
|
||||
contract_status=None,
|
||||
contract_no="HT1",
|
||||
agreement_status=1,
|
||||
agreement_id="AGR1",
|
||||
agreement_dtl_id="AGRD1",
|
||||
),
|
||||
]
|
||||
)
|
||||
envelope = load_contract_agreement_envelope(repo)
|
||||
assert envelope["data"][0]["actionCode"] == 1
|
||||
assert envelope["data"][0]["contractStatus"] == 2
|
||||
assert envelope["data"][1]["actionCode"] == 2
|
||||
assert envelope["data"][1]["agreementStatus"] == 1
|
||||
assert envelope["data"][1]["agreementId"] == "AGR1"
|
||||
assert envelope["data"][1]["agreementDtlId"] == "AGRD1"
|
||||
|
||||
|
||||
def test_oversized_contract_no_group_enters_when_batch_empty():
|
||||
rows = [
|
||||
_row(fid=1, oms_id=1, contract_no="HT-BIG"),
|
||||
_row(fid=2, oms_id=2, contract_no="HT-BIG"),
|
||||
_row(fid=3, oms_id=3, contract_no="HT-BIG"),
|
||||
]
|
||||
envelope = build_contract_agreement_envelope(rows, limit=2)
|
||||
assert [item["fid"] for item in envelope["data"]] == [1, 2, 3]
|
||||
|
||||
|
||||
def test_does_not_split_contract_no_when_batch_already_has_rows():
|
||||
rows = [
|
||||
_row(fid=1, oms_id=1, contract_no="HT-A"),
|
||||
_row(fid=2, oms_id=2, contract_no="HT-B"),
|
||||
_row(fid=3, oms_id=3, contract_no="HT-B"),
|
||||
]
|
||||
envelope = build_contract_agreement_envelope(rows, limit=2)
|
||||
assert [item["fid"] for item in envelope["data"]] == [1]
|
||||
|
||||
|
||||
def test_empty_contract_no_rows_are_independent():
|
||||
rows = [
|
||||
_row(fid=1, oms_id=1, contract_no=None, contract_status=3),
|
||||
_row(fid=2, oms_id=2, contract_no="", contract_status=3),
|
||||
_row(fid=3, oms_id=3, contract_no="HT-C"),
|
||||
]
|
||||
envelope = build_contract_agreement_envelope(rows, limit=1)
|
||||
assert [item["fid"] for item in envelope["data"]] == [1]
|
||||
|
||||
|
||||
def test_batch_cap_is_500():
|
||||
assert MAX_BATCH == 500
|
||||
|
||||
|
||||
def test_payload_omits_push_status():
|
||||
item = build_contract_agreement_envelope([_row()])["data"][0]
|
||||
assert "pushStatus" not in item
|
||||
assert "push_status" not in item
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
REBATE_URL=http://192.168.170.144:8001
|
||||
REBATE_USERNAME=
|
||||
REBATE_PASSWORD=
|
||||
# 页面 ACCEPT 要搜的合同号;空则用哨兵号验收「搜不到」
|
||||
REBATE_ACCEPT_CONTRACT_NO=
|
||||
|
|
@ -1,44 +1,15 @@
|
|||
# rpa_rebate
|
||||
|
||||
返利班。领表 4 起草行、表 3 只录协议行,按合同号查返利中心。搜到再录协议、写表 4。查号走页面;录协议仍用替身。接口 4 组包在 `rpa_oms`,本班不推、不改 `push_status`。
|
||||
返利班。本包尚未实现。
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| 读 | 表 4 `action_code=1 AND contract_status=1`(已有合同号、尚未回成功);表 3 `contract_entry_status=0` 且已有 `contractNo` |
|
||||
| 写 | 表 4:搜到则起草行 `contractStatus=2`;录协议后插 `actionCode=2`、`agreementStatus=1` |
|
||||
| 口 | 口 4(组包在 oms,path 空) |
|
||||
| 读 | 表 3 / 表 4:已有合同号尚未回合同成功的单,以及只录协议的单 |
|
||||
| 写 | 表 4:搜到合同号则 `contractStatus=2`;录协议后 `actionCode=2`、`agreementStatus=1` |
|
||||
| 口 | 口 4 |
|
||||
|
||||
与合同同一天两班,建议合同班跑完再跑。查号挂在本班,不单开查状态任务。搜不到不重录合同,戳改回 `合同班`,留给下一班再搜。
|
||||
|
||||
口径:`docs/RPA订单中心接口开发口径_20260930.md` 第 5 节。
|
||||
|
||||
## 运行
|
||||
|
||||
内存替身(不连库、不开浏览器):
|
||||
|
||||
```bash
|
||||
cd rpa_rebate
|
||||
uv sync --python 3.12
|
||||
uv run pytest tests/test_job.py::test_job_finds_contract_and_writes_agreement -q
|
||||
uv run pytest tests/test_finder.py::test_find_flow_writes_accept_for_missing_number -q
|
||||
```
|
||||
|
||||
页面查号(不开录协议、不连 Oracle)。账号放本机 `.env`:
|
||||
|
||||
```bash
|
||||
cd rpa_rebate
|
||||
uv run python run_find.py
|
||||
```
|
||||
|
||||
验收看控制台 `ACCEPT SUMMARY REBATE_FIND` 或 `screenshots/REBATE_FIND/ACCEPT.txt`。默认搜哨兵号,期望 `found=no`。要搜真号:`REBATE_ACCEPT_CONTRACT_NO=合同号`。
|
||||
|
||||
接口 4 组包(`rpa_oms`,不发 HTTP):
|
||||
|
||||
```bash
|
||||
cd rpa_oms
|
||||
uv run pytest tests/test_contract_agreement_outbound.py::test_assembles_unpushed_rows_as_document_envelope -q
|
||||
```
|
||||
与合同同一天两班,建议合同班跑完再跑。查号挂在本班,不单开查状态任务。搜不到不重录合同,留给下一班再搜。
|
||||
|
||||
## 本期不做
|
||||
|
||||
录协议(总单 / 导入 / 送审)、映射表默认值、向订单中心发 HTTP、改 `push_status`。
|
||||
返利中心查号、录协议、送审、接口 4 回写。
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
"""Shared ACCEPT report for every RPA flow (any platform / page).
|
||||
|
||||
New flows (ORDER_FETCH, REBATE_*, M5, ...) must use this instead of
|
||||
reading screenshots. Screenshots stay on disk for humans; this text is
|
||||
the agent oracle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
|
||||
from errors import NodeError
|
||||
|
||||
PACKAGE_ROOT = Path(__file__).resolve().parent
|
||||
SCREENSHOT_ROOT = PACKAGE_ROOT / "screenshots"
|
||||
|
||||
|
||||
class HasScreenshot(Protocol):
|
||||
def screenshot(self, name: str) -> str: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class AcceptLine:
|
||||
step: str
|
||||
title: str
|
||||
ok: bool
|
||||
facts: dict[str, str]
|
||||
mismatches: list[str]
|
||||
|
||||
|
||||
def safe_flow_name(flow: str) -> str:
|
||||
text = (flow or "default").strip() or "default"
|
||||
return "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in text)
|
||||
|
||||
|
||||
def artifact_dir(flow: str) -> Path:
|
||||
return SCREENSHOT_ROOT / safe_flow_name(flow)
|
||||
|
||||
|
||||
def evidence_shot(browser: HasScreenshot, name: str, *, failed: bool = False) -> str:
|
||||
"""Save a screenshot only on failure. Set RPA_SCREENSHOT=always to keep every frame."""
|
||||
mode = (os.environ.get("RPA_SCREENSHOT") or "fail").strip().lower()
|
||||
if not failed and mode != "always":
|
||||
return ""
|
||||
return browser.screenshot(name)
|
||||
|
||||
|
||||
def accept_log_path(flow: str = "default") -> Path:
|
||||
override = os.environ.get("RPA_ACCEPT_LOG")
|
||||
if override:
|
||||
return Path(override)
|
||||
return artifact_dir(flow) / "ACCEPT.txt"
|
||||
|
||||
|
||||
def field_match(actual: str, expected: str) -> bool:
|
||||
actual_text = (actual or "").strip()
|
||||
expected_text = (expected or "").strip()
|
||||
if not expected_text:
|
||||
return True
|
||||
if actual_text == expected_text:
|
||||
return True
|
||||
compact_a = actual_text.replace(" ", "").replace(",", "")
|
||||
compact_e = expected_text.replace(" ", "").replace(",", "")
|
||||
if compact_a == compact_e or compact_a.replace("-", "") == compact_e.replace("-", ""):
|
||||
return True
|
||||
try:
|
||||
return float(compact_a) == float(compact_e)
|
||||
except ValueError:
|
||||
pass
|
||||
yes = {"是", "1", "Y", "YES", "TRUE"}
|
||||
no = {"否", "2", "0", "N", "NO", "FALSE"}
|
||||
if compact_a.upper() in yes and compact_e.upper() in yes:
|
||||
return True
|
||||
if compact_a.upper() in no and compact_e.upper() in no:
|
||||
return True
|
||||
return compact_e in compact_a or compact_a in compact_e
|
||||
|
||||
|
||||
def format_accept(line: AcceptLine) -> str:
|
||||
status = "ok" if line.ok else "FAIL"
|
||||
facts = " ".join(f"{key}={value}" for key, value in line.facts.items())
|
||||
extra = f" {' | '.join(line.mismatches)}" if line.mismatches else ""
|
||||
return f"ACCEPT {line.step} {status} {line.title} {facts}{extra}".rstrip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AcceptanceReport:
|
||||
"""DOM / page-state checklist. One instance per flow run."""
|
||||
|
||||
flow: str = "default"
|
||||
lines: list[AcceptLine] = field(default_factory=list)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.lines = []
|
||||
|
||||
def check(
|
||||
self,
|
||||
step: str,
|
||||
title: str,
|
||||
pairs: list[tuple[str, object, object]],
|
||||
*,
|
||||
browser: HasScreenshot | None = None,
|
||||
screenshot_on_fail: str = "",
|
||||
) -> None:
|
||||
facts: dict[str, str] = {}
|
||||
mismatches: list[str] = []
|
||||
for name, actual, expected in pairs:
|
||||
actual_text = "" if actual is None else str(actual).strip()
|
||||
expected_text = "" if expected is None else str(expected).strip()
|
||||
facts[name] = actual_text or "(empty)"
|
||||
if expected_text and not field_match(actual_text, expected_text):
|
||||
mismatches.append(f"{name} expected={expected_text!r} actual={actual_text!r}")
|
||||
ok = not mismatches
|
||||
self.lines.append(AcceptLine(step, title, ok, facts, mismatches))
|
||||
print(format_accept(self.lines[-1]), flush=True)
|
||||
self.dump()
|
||||
if ok:
|
||||
return
|
||||
if browser is not None and screenshot_on_fail:
|
||||
evidence_shot(browser, screenshot_on_fail, failed=True)
|
||||
raise NodeError(f"ACCEPT {step} FAIL " + "; ".join(mismatches))
|
||||
|
||||
def note(
|
||||
self,
|
||||
step: str,
|
||||
title: str,
|
||||
facts: dict[str, object],
|
||||
*,
|
||||
ok: bool = True,
|
||||
raise_error: bool = True,
|
||||
) -> None:
|
||||
rendered = {key: ("" if value is None else str(value).strip()) or "(empty)" for key, value in facts.items()}
|
||||
self.lines.append(AcceptLine(step, title, ok, rendered, [] if ok else ["failed"]))
|
||||
print(format_accept(self.lines[-1]), flush=True)
|
||||
self.dump()
|
||||
if not ok and raise_error:
|
||||
raise NodeError(f"ACCEPT {step} FAIL {title} " + " ".join(f"{k}={v}" for k, v in rendered.items()))
|
||||
|
||||
def dump(self) -> None:
|
||||
path = accept_log_path(self.flow)
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(self.summary_text(), encoding="utf-8")
|
||||
except OSError as exc:
|
||||
print(f"ACCEPT log write skipped: {exc}", flush=True)
|
||||
|
||||
def summary_text(self) -> str:
|
||||
passed = sum(1 for line in self.lines if line.ok)
|
||||
rows = [f"ACCEPT SUMMARY {self.flow} {passed}/{len(self.lines)} ok"]
|
||||
rows.extend(format_accept(line) for line in self.lines)
|
||||
return "\n".join(rows) + "\n"
|
||||
|
|
@ -1,504 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
Selenium Chrome 浏览器适配。通过 WebDriver 操控 Chrome。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _is_inside_repo(path: Path, repo_root: Path) -> bool:
|
||||
try:
|
||||
path.resolve().relative_to(repo_root.resolve())
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def resolve_chrome_profile_base(requested: str | None = None, *, repo_root: Path | None = None) -> Path:
|
||||
"""Chrome user-data dir must stay outside the repo so list_dir never walks 3GB."""
|
||||
root = (repo_root or REPO_ROOT).resolve()
|
||||
outside = (root.parent / "rpa-chrome-profile").resolve()
|
||||
raw = os.getenv("RPA_CHROME_PROFILE") if requested is None else requested
|
||||
if raw:
|
||||
candidate = Path(raw).expanduser().resolve()
|
||||
if _is_inside_repo(candidate, root):
|
||||
print(f"RPA_CHROME_PROFILE 指向仓库内 {candidate},已改用 {outside}", flush=True)
|
||||
return outside
|
||||
return candidate
|
||||
return outside
|
||||
|
||||
|
||||
class ApaBrowserAdapter:
|
||||
"""Selenium Chrome 浏览器适配器,实现 BrowserAdapter 协议"""
|
||||
|
||||
def __init__(self, url: str = "", *, flow: str = "default"):
|
||||
self._url = url
|
||||
self._flow = flow
|
||||
self._driver = None
|
||||
self._is_open = False
|
||||
|
||||
def _get_driver(self):
|
||||
"""创建或获取 Chrome WebDriver"""
|
||||
if self._driver is not None:
|
||||
return self._driver
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
|
||||
profile_base = resolve_chrome_profile_base()
|
||||
if _is_inside_repo(profile_base, REPO_ROOT):
|
||||
raise RuntimeError(f"拒绝在仓库内创建 Chrome 用户目录: {profile_base}")
|
||||
# 每次进程单独目录:上次 detach 留下的 Chrome 会锁住同一 profile
|
||||
profile_dir = profile_base / str(os.getpid())
|
||||
os.makedirs(profile_dir, exist_ok=True)
|
||||
profile_dir = str(profile_dir)
|
||||
|
||||
options = Options()
|
||||
options.page_load_strategy = "eager"
|
||||
# 不自动关闭浏览器,方便调试
|
||||
options.add_experimental_option("detach", True)
|
||||
# 避免被检测为自动化
|
||||
options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"])
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||
options.add_argument("--no-first-run")
|
||||
options.add_argument("--no-default-browser-check")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--disable-extensions")
|
||||
options.add_argument("--disable-popup-blocking")
|
||||
options.add_argument("--start-maximized")
|
||||
# 独立目录,避免和日常 Chrome 抢用户数据目录导致卡住
|
||||
options.add_argument("--user-data-dir=" + profile_dir)
|
||||
|
||||
chromedriver = os.getenv("CHROMEDRIVER_PATH")
|
||||
if chromedriver and os.path.exists(chromedriver):
|
||||
service = Service(chromedriver)
|
||||
else:
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
service = Service(ChromeDriverManager().install())
|
||||
self._driver = webdriver.Chrome(service=service, options=options)
|
||||
self._driver.set_page_load_timeout(45)
|
||||
self._driver.set_script_timeout(30)
|
||||
return self._driver
|
||||
|
||||
def _ensure_open(self):
|
||||
if self._driver is None:
|
||||
driver = self._get_driver()
|
||||
if self._url:
|
||||
driver.get(self._url)
|
||||
self._is_open = True
|
||||
|
||||
# ---- 内部查找方法 ----
|
||||
|
||||
def _find(self, selector: str, timeout: int = 5):
|
||||
"""
|
||||
智能查找元素:支持 CSS、XPath、text=、label=、aria=、title=、:contains()
|
||||
"""
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
import re
|
||||
|
||||
# 1. text=xxx → 查找包含对应文本的元素
|
||||
if selector.startswith("text="):
|
||||
text_val = selector[5:].strip('"').strip("'")
|
||||
xpath = f'//*[contains(text(),"{text_val}")]'
|
||||
return self._driver.find_element(By.XPATH, xpath)
|
||||
|
||||
# 2. label=xxx → 找到对应标签的输入框
|
||||
if selector.startswith("label="):
|
||||
text_val = selector[6:].strip('"').strip("'")
|
||||
xpath = f'//label[contains(text(),"{text_val}")]/following-sibling::*[1][self::input or self::select or self::textarea]'
|
||||
try:
|
||||
return self._driver.find_element(By.XPATH, xpath)
|
||||
except NoSuchElementException:
|
||||
# Fallback: find the label's "for" attribute
|
||||
try:
|
||||
for_id = self._driver.find_element(
|
||||
By.XPATH, f'//label[contains(text(),"{text_val}")]'
|
||||
).get_attribute("for")
|
||||
if for_id:
|
||||
return self._driver.find_element(By.ID, for_id)
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
raise
|
||||
|
||||
# 3. aria=xxx → aria-label 属性
|
||||
if selector.startswith("aria="):
|
||||
text_val = selector[5:].strip('"').strip("'")
|
||||
return self._driver.find_element(By.CSS_SELECTOR, f'[aria-label="{text_val}"]')
|
||||
|
||||
# 4. title=xxx → title 属性
|
||||
if selector.startswith("title="):
|
||||
text_val = selector[6:].strip('"').strip("'")
|
||||
return self._driver.find_element(By.CSS_SELECTOR, f'[title="{text_val}"]')
|
||||
|
||||
# 5. :contains() → 转 XPath(支持 button:contains(文本) / a:contains("文本"))
|
||||
if ":contains(" in selector:
|
||||
m = re.match(r'(.+?):contains\(["\']?(.+?)["\']?\)', selector)
|
||||
if m:
|
||||
tag_part, text_val = m.groups()
|
||||
xpath = f'//{tag_part}[contains(.,"{text_val}")]'
|
||||
return self._driver.find_element(By.XPATH, xpath)
|
||||
|
||||
# 6. 显式 XPath(以 / 或 // 开头)优先按 XPath 查找
|
||||
if selector.startswith("//") or selector.startswith("/"):
|
||||
return self._driver.find_element(By.XPATH, selector)
|
||||
|
||||
# 7. 标准 CSS selector
|
||||
return self._driver.find_element(By.CSS_SELECTOR, selector)
|
||||
|
||||
# ---- BrowserAdapter 协议实现 ----
|
||||
|
||||
def open_url(self, url: str) -> None:
|
||||
self._url = url
|
||||
driver = self._get_driver()
|
||||
try:
|
||||
driver.get(url)
|
||||
except Exception as exc:
|
||||
# eager 仍可能因长轮询资源超时;页面文档已出来就继续
|
||||
current = ""
|
||||
try:
|
||||
current = driver.current_url or ""
|
||||
except Exception:
|
||||
current = ""
|
||||
if not current or current in ("data:,", "about:blank"):
|
||||
raise RuntimeError(f"打开页面失败:{url} ({exc})") from exc
|
||||
self._is_open = True
|
||||
|
||||
def _set_value_via_js(self, el, value: str) -> None:
|
||||
"""通过原生 value setter + 事件注入设置值,兼容 LUI/React 受控组件"""
|
||||
self._driver.execute_script(
|
||||
"""
|
||||
var el = arguments[0];
|
||||
var value = arguments[1];
|
||||
var proto = el.tagName === 'TEXTAREA'
|
||||
? window.HTMLTextAreaElement.prototype
|
||||
: window.HTMLInputElement.prototype;
|
||||
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
||||
setter.call(el, value);
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
""",
|
||||
el,
|
||||
str(value),
|
||||
)
|
||||
|
||||
def fill_first(self, selectors: list, value: str) -> None:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
# 先尝试原生输入;LUI 受控组件用 JS 注入兜底
|
||||
try:
|
||||
el.clear()
|
||||
el.send_keys(str(value))
|
||||
except Exception:
|
||||
pass
|
||||
if el.get_attribute("value") != str(value):
|
||||
self._set_value_via_js(el, str(value))
|
||||
time.sleep(0.3)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"fill_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def click_native(self, selectors: list) -> None:
|
||||
"""真实鼠标点击,给只监听 mousedown 的 LUI 页签/工具栏用。"""
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
self._driver.execute_script(
|
||||
"arguments[0].scrollIntoView({block:'center', inline:'center'});",
|
||||
el,
|
||||
)
|
||||
time.sleep(0.2)
|
||||
ActionChains(self._driver).move_to_element(el).pause(0.15).click().perform()
|
||||
time.sleep(0.4)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"click_native failed: {selectors}, last: {last_error}")
|
||||
|
||||
def click_first(self, selectors: list) -> None:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
# LUI/委托事件用 JS 点击最可靠
|
||||
self._driver.execute_script("arguments[0].click()", el)
|
||||
time.sleep(0.5)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"click_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def double_click_first(self, selectors: list) -> None:
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
ActionChains(self._driver).double_click(el).perform()
|
||||
time.sleep(0.5)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"double_click_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
try:
|
||||
WebDriverWait(self._driver, timeout_seconds).until(
|
||||
EC.text_to_be_present_in_element((By.TAG_NAME, "body"), text)
|
||||
)
|
||||
except Exception as e:
|
||||
raise TimeoutError(f"wait_for_text '{text}' timed out: {e}")
|
||||
|
||||
def wait_for_element(self, selectors: list, timeout_seconds: int = 15) -> None:
|
||||
self._ensure_open()
|
||||
deadline = time.time() + timeout_seconds
|
||||
last_error = None
|
||||
while time.time() < deadline:
|
||||
for sel in selectors:
|
||||
try:
|
||||
self._find(sel, timeout=1)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError(f"wait_for_element timed out: {selectors}, last: {last_error}")
|
||||
|
||||
def element_exists(self, selectors: list) -> bool:
|
||||
self._ensure_open()
|
||||
for sel in selectors:
|
||||
try:
|
||||
self._find(sel, timeout=1)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def switch_to_frame(self, selectors: list) -> None:
|
||||
"""切换到匹配到的 iframe。优先可见弹窗,避免切到上次留下的隐藏 iframe。"""
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
last_error = None
|
||||
found = []
|
||||
for sel in selectors:
|
||||
try:
|
||||
if sel.startswith("//") or sel.startswith("/"):
|
||||
found.extend(self._driver.find_elements(By.XPATH, sel))
|
||||
else:
|
||||
found.extend(self._driver.find_elements(By.CSS_SELECTOR, sel))
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
visible = []
|
||||
hidden = []
|
||||
for el in found:
|
||||
try:
|
||||
(visible if el.is_displayed() else hidden).append(el)
|
||||
except Exception:
|
||||
hidden.append(el)
|
||||
for el in visible + hidden:
|
||||
try:
|
||||
self._driver.switch_to.default_content()
|
||||
self._driver.switch_to.frame(el)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
raise RuntimeError(f"switch_to_frame failed: {selectors}, last: {last_error}")
|
||||
|
||||
def switch_to_default_content(self) -> None:
|
||||
"""切回主文档(顶层 frame)。"""
|
||||
self._ensure_open()
|
||||
self._driver.switch_to.default_content()
|
||||
|
||||
def execute_script(self, script: str, *args):
|
||||
self._ensure_open()
|
||||
return self._driver.execute_script(script, *args)
|
||||
|
||||
def select_xform(self, name: str, value: str) -> None:
|
||||
"""选中 Landray xform fancy-select(.fs-wrap / .fs-option)。"""
|
||||
self._ensure_open()
|
||||
result = self._driver.execute_script(
|
||||
"""
|
||||
const name = arguments[0];
|
||||
const wanted = String(arguments[1] || '').trim();
|
||||
const flag = document.querySelector('xformflag[property="' + name + '"]');
|
||||
if (!flag) {
|
||||
return {ok: false, error: 'flag not found: ' + name};
|
||||
}
|
||||
const norm = (s) => String(s || '').replace(/\\s+/g, '').trim();
|
||||
const wantedN = norm(wanted);
|
||||
const opts = Array.from(flag.querySelectorAll('.fs-option'));
|
||||
let target = opts.find((o) => {
|
||||
const text = norm((o.querySelector('.fs-option-label') || o).textContent);
|
||||
const val = norm(o.getAttribute('data-value'));
|
||||
return text === wantedN || val === wantedN;
|
||||
});
|
||||
if (!target && wantedN.length >= 2) {
|
||||
target = opts.find((o) => {
|
||||
const text = norm((o.querySelector('.fs-option-label') || o).textContent);
|
||||
return text.indexOf(wantedN) >= 0;
|
||||
});
|
||||
}
|
||||
if (!target) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'option not found: ' + wanted,
|
||||
available: opts.map((o) => ((o.querySelector('.fs-option-label') || o).textContent || '').trim()),
|
||||
};
|
||||
}
|
||||
const labelWrap = flag.querySelector('.fs-label-wrap');
|
||||
if (labelWrap) { labelWrap.click(); }
|
||||
target.click();
|
||||
const input = flag.querySelector('input[name="' + name + '"]');
|
||||
const selected = target.getAttribute('data-value') || '';
|
||||
if (input) {
|
||||
const proto = window.HTMLInputElement.prototype;
|
||||
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
||||
setter.call(input, selected);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (typeof __xformDispatch === 'function') {
|
||||
try { __xformDispatch(selected, input); } catch (e) {}
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
value: selected,
|
||||
label: ((flag.querySelector('.fs-label') || {}).textContent || '').trim(),
|
||||
};
|
||||
""",
|
||||
name,
|
||||
str(value),
|
||||
)
|
||||
if not result or not result.get("ok"):
|
||||
raise RuntimeError(f"select_xform failed: {name}={value} {result}")
|
||||
time.sleep(0.3)
|
||||
|
||||
def select_first(self, selectors: list, value: str) -> None:
|
||||
self._ensure_open()
|
||||
from selenium.webdriver.support.ui import Select
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
# 原生 <select>
|
||||
try:
|
||||
Select(el).select_by_visible_text(value)
|
||||
time.sleep(0.3)
|
||||
if self._selected_text_matches(el, value):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
# 兜底:JS 定位 option 并设置选中
|
||||
self._select_via_js(el, value)
|
||||
time.sleep(0.3)
|
||||
if self._selected_text_matches(el, value):
|
||||
return
|
||||
raise RuntimeError(f"select_first 未选中目标项: {value}")
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"select_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def _selected_text_matches(self, el, value: str) -> bool:
|
||||
try:
|
||||
script = (
|
||||
"var el = arguments[0];"
|
||||
"if (el.selectedIndex != null) {"
|
||||
" var o = el.options[el.selectedIndex];"
|
||||
" return o ? (o.textContent || o.text || '').trim() : '';"
|
||||
"}"
|
||||
"return (el.value || '').trim();"
|
||||
)
|
||||
selected = self._driver.execute_script(script, el)
|
||||
return (selected or "").strip() == str(value).strip()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _select_via_js(self, el, value: str) -> None:
|
||||
self._driver.execute_script(
|
||||
"""
|
||||
var el = arguments[0];
|
||||
var value = arguments[1];
|
||||
var options = el.options || [];
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var t = (options[i].textContent || options[i].text || '').trim();
|
||||
if (t === value || options[i].value === value) {
|
||||
el.selectedIndex = i;
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
""",
|
||||
el,
|
||||
str(value),
|
||||
)
|
||||
|
||||
def upload_file_first(self, selectors: list, file_path: str) -> None:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
el.send_keys(file_path)
|
||||
time.sleep(1)
|
||||
return
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"upload_file_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def read_first(self, selectors: list, value_name: str = "") -> str:
|
||||
self._ensure_open()
|
||||
last_error = None
|
||||
for sel in selectors:
|
||||
try:
|
||||
el = self._find(sel)
|
||||
result = el.text or el.get_attribute("value") or ""
|
||||
if result:
|
||||
return result
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise RuntimeError(f"read_first failed: {selectors}, last: {last_error}")
|
||||
|
||||
def read_body_text(self) -> str:
|
||||
self._ensure_open()
|
||||
try:
|
||||
return self._driver.find_element("tag name", "body").text
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def screenshot(self, name: str) -> str:
|
||||
from accept import artifact_dir
|
||||
|
||||
save_dir = artifact_dir(self._flow)
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = save_dir / f"{name}.png"
|
||||
if self._driver:
|
||||
self._driver.save_screenshot(str(path))
|
||||
return str(path)
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
"""Oracle 11.2 thick 连接。环境变量与 rpa_oms 相同。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
_client_ready = False
|
||||
_PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
_OMS_ENV = _PROJECT_ROOT.parent / "rpa_oms" / ".env"
|
||||
|
||||
|
||||
class RepoUnavailable(Exception):
|
||||
def __init__(self, message: str = "数据库不可用"):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def load_runtime_env(paths: list[Path] | None = None) -> None:
|
||||
"""先读返利 .env,空缺再补 rpa_oms/.env。不覆盖已有非空环境变量。"""
|
||||
env_paths = paths if paths is not None else [_PROJECT_ROOT / ".env", _OMS_ENV]
|
||||
for path in env_paths:
|
||||
_fill_env(path)
|
||||
|
||||
|
||||
def _fill_env(path: Path) -> None:
|
||||
if not path.is_file():
|
||||
return
|
||||
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
||||
value = value[1:-1]
|
||||
if not key or not value:
|
||||
continue
|
||||
current = os.environ.get(key, "")
|
||||
if not current:
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def oracle_dsn() -> tuple[str, str, str]:
|
||||
user = os.getenv("RPA_ORACLE_USER", "")
|
||||
password = os.getenv("RPA_ORACLE_PASSWORD", "")
|
||||
host = os.getenv("RPA_ORACLE_HOST", "")
|
||||
if not user or not password or not host:
|
||||
raise RepoUnavailable("未配置 RPA_ORACLE_HOST / RPA_ORACLE_USER / RPA_ORACLE_PASSWORD")
|
||||
port = os.getenv("RPA_ORACLE_PORT", "1521")
|
||||
service = os.getenv("RPA_ORACLE_SERVICE", "rpa")
|
||||
return user, password, f"{host}:{port}/{service}"
|
||||
|
||||
|
||||
def connect_from_env():
|
||||
user, password, dsn = oracle_dsn()
|
||||
_ensure_client()
|
||||
import oracledb
|
||||
|
||||
return oracledb.connect(user=user, password=password, dsn=dsn)
|
||||
|
||||
|
||||
def _ensure_client() -> None:
|
||||
global _client_ready
|
||||
if _client_ready:
|
||||
return
|
||||
import oracledb
|
||||
|
||||
if os.getenv("RPA_ORACLE_THICK", "1") == "1":
|
||||
lib_dir = os.getenv("RPA_ORACLE_CLIENT_LIB", "")
|
||||
kwargs = {"lib_dir": lib_dir} if lib_dir else {}
|
||||
oracledb.init_oracle_client(**kwargs)
|
||||
_client_ready = True
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"""Page-flow errors. Codes stay for logs."""
|
||||
|
||||
|
||||
class NodeError(RuntimeError):
|
||||
error_code = "UI_ELEMENT_NOT_FOUND"
|
||||
retryable = False
|
||||
route_role = "OPS"
|
||||
|
||||
|
||||
class LoginFailed(NodeError):
|
||||
error_code = "SYS_LOGIN_FAIL"
|
||||
retryable = True
|
||||
route_role = "OPS"
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"""返利班次:领表 4 起草行 / 表 3 只录协议行 → 查号 → 录协议替身 → 写表 4。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from persist import (
|
||||
KIND_FAIL,
|
||||
AgreementOutcome,
|
||||
RebateGroup,
|
||||
agreement_ok,
|
||||
build_found,
|
||||
build_not_found,
|
||||
)
|
||||
from repo import Claimed
|
||||
|
||||
Finder = Callable[[str], bool]
|
||||
Recorder = Callable[[str], AgreementOutcome]
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobSummary:
|
||||
claimed: int = 0
|
||||
groups: int = 0
|
||||
ok: int = 0
|
||||
not_found: int = 0
|
||||
failed: int = 0
|
||||
|
||||
|
||||
def run_batch(
|
||||
repo,
|
||||
*,
|
||||
finder: Finder,
|
||||
recorder: Recorder,
|
||||
limit: int | None = None,
|
||||
) -> JobSummary:
|
||||
claimed = repo.claim_pending(limit=limit)
|
||||
groups = group_claimed(claimed)
|
||||
summary = JobSummary(
|
||||
claimed=len(claimed.drafts) + len(claimed.protocols),
|
||||
groups=len(groups),
|
||||
)
|
||||
for group in groups:
|
||||
try:
|
||||
found = bool(finder(group.contract_no))
|
||||
except Exception:
|
||||
repo.save_group(build_not_found(group))
|
||||
summary.not_found += 1
|
||||
continue
|
||||
if not found:
|
||||
repo.save_group(build_not_found(group))
|
||||
summary.not_found += 1
|
||||
continue
|
||||
try:
|
||||
outcome = recorder(group.contract_no)
|
||||
except Exception as exc:
|
||||
outcome = AgreementOutcome(KIND_FAIL, None, None, str(exc))
|
||||
if outcome is None:
|
||||
outcome = AgreementOutcome(KIND_FAIL, None, None, "协议录入失败")
|
||||
repo.save_group(build_found(group, outcome))
|
||||
if agreement_ok(outcome):
|
||||
summary.ok += 1
|
||||
else:
|
||||
summary.failed += 1
|
||||
return summary
|
||||
|
||||
|
||||
def group_claimed(claimed: Claimed) -> list[RebateGroup]:
|
||||
groups: OrderedDict[str, RebateGroup] = OrderedDict()
|
||||
|
||||
def bucket(contract_no: str) -> RebateGroup:
|
||||
number = (contract_no or "").strip()
|
||||
if number not in groups:
|
||||
groups[number] = RebateGroup(contract_no=number)
|
||||
return groups[number]
|
||||
|
||||
for row in claimed.drafts:
|
||||
bucket(row["contract_no"]).drafts.append(row)
|
||||
for row in claimed.protocols:
|
||||
bucket(row["contract_no"]).protocols.append(row)
|
||||
return list(groups.values())
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
"""返利班结果:查号 / 录协议 → 表 4 更新与插入、表 3 状态。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
KIND_OK = "ok"
|
||||
KIND_FAIL = "fail"
|
||||
STAMP_CONTRACT = "合同班"
|
||||
STAMP_CLAIMING = "查号中"
|
||||
STAMP_DONE = "返利班"
|
||||
PROCESS_RESULT_MAX = 1000
|
||||
FAIL_REASON_MAX = 500
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgreementOutcome:
|
||||
kind: str
|
||||
agreement_id: str | None
|
||||
agreement_dtl_id: str | None
|
||||
reason: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RebateGroup:
|
||||
contract_no: str
|
||||
drafts: list[dict] = field(default_factory=list)
|
||||
protocols: list[dict] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroupSave:
|
||||
table4_updates: list[dict]
|
||||
table4_inserts: list[dict]
|
||||
table4_release_fids: list[int]
|
||||
table3_updates: list[dict]
|
||||
|
||||
|
||||
def clip(text: str, max_len: int) -> str:
|
||||
text = (text or "").strip()
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[: max_len - 1] + "…"
|
||||
|
||||
|
||||
def agreement_ok(outcome: AgreementOutcome) -> bool:
|
||||
return outcome.kind == KIND_OK and bool((outcome.agreement_id or "").strip())
|
||||
|
||||
|
||||
def build_not_found(group: RebateGroup) -> GroupSave:
|
||||
return GroupSave(
|
||||
table4_updates=[],
|
||||
table4_inserts=[],
|
||||
table4_release_fids=[int(row["fid"]) for row in group.drafts],
|
||||
table3_updates=[
|
||||
{"fid": int(row["fid"]), "process_status": 0, "process_result": ""}
|
||||
for row in group.protocols
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def build_found(group: RebateGroup, outcome: AgreementOutcome) -> GroupSave:
|
||||
ok = agreement_ok(outcome)
|
||||
number = (group.contract_no or "").strip()
|
||||
updates = [
|
||||
{
|
||||
"fid": int(row["fid"]),
|
||||
"contract_status": 2,
|
||||
"update_user_name": STAMP_DONE,
|
||||
}
|
||||
for row in group.drafts
|
||||
]
|
||||
inserts: list[dict] = []
|
||||
seen_protocol: dict[int, dict] = {}
|
||||
for row in group.protocols:
|
||||
seen_protocol.setdefault(int(row["oms_id"]), row)
|
||||
for row in seen_protocol.values():
|
||||
inserts.append(_action1_success(row, number))
|
||||
seen_oms: dict[int, dict] = {}
|
||||
for row in group.drafts + group.protocols:
|
||||
seen_oms.setdefault(int(row["oms_id"]), row)
|
||||
for row in seen_oms.values():
|
||||
inserts.append(_action2(row, number, outcome, ok))
|
||||
if ok:
|
||||
process_status = 1
|
||||
process_result = "协议已录入"
|
||||
else:
|
||||
process_status = 2
|
||||
process_result = clip(outcome.reason or "协议录入失败", PROCESS_RESULT_MAX)
|
||||
table3_updates = [
|
||||
{
|
||||
"fid": int(row["fid"]),
|
||||
"process_status": process_status,
|
||||
"process_result": process_result,
|
||||
}
|
||||
for row in group.protocols
|
||||
]
|
||||
return GroupSave(
|
||||
table4_updates=updates,
|
||||
table4_inserts=inserts,
|
||||
table4_release_fids=[],
|
||||
table3_updates=table3_updates,
|
||||
)
|
||||
|
||||
|
||||
def _action1_success(row: dict, contract_no: str) -> dict:
|
||||
return {
|
||||
"oms_id": int(row["oms_id"]),
|
||||
"action_code": 1,
|
||||
"contract_status": 2,
|
||||
"contract_fail_reason": None,
|
||||
"contract_no": contract_no,
|
||||
"agreement_status": None,
|
||||
"agreement_fail_reason": None,
|
||||
"agreement_id": None,
|
||||
"agreement_dtl_id": None,
|
||||
"push_status": 0,
|
||||
"org_code": row.get("org_code"),
|
||||
"create_user_id": 0,
|
||||
"create_user_name": STAMP_DONE,
|
||||
"update_user_id": 0,
|
||||
"update_user_name": STAMP_DONE,
|
||||
}
|
||||
|
||||
|
||||
def _action2(row: dict, contract_no: str, outcome: AgreementOutcome, ok: bool) -> dict:
|
||||
if ok:
|
||||
status = 1
|
||||
fail_reason = None
|
||||
agreement_id = (outcome.agreement_id or "").strip() or None
|
||||
agreement_dtl_id = (outcome.agreement_dtl_id or "").strip() or None
|
||||
else:
|
||||
status = 2
|
||||
fail_reason = clip(outcome.reason or "协议录入失败", FAIL_REASON_MAX)
|
||||
agreement_id = None
|
||||
agreement_dtl_id = None
|
||||
return {
|
||||
"oms_id": int(row["oms_id"]),
|
||||
"action_code": 2,
|
||||
"contract_status": None,
|
||||
"contract_fail_reason": None,
|
||||
"contract_no": contract_no,
|
||||
"agreement_status": status,
|
||||
"agreement_fail_reason": fail_reason,
|
||||
"agreement_id": agreement_id,
|
||||
"agreement_dtl_id": agreement_dtl_id,
|
||||
"push_status": 0,
|
||||
"org_code": row.get("org_code"),
|
||||
"create_user_id": 0,
|
||||
"create_user_name": STAMP_DONE,
|
||||
"update_user_id": 0,
|
||||
"update_user_name": STAMP_DONE,
|
||||
}
|
||||
|
|
@ -1,28 +1,13 @@
|
|||
[project]
|
||||
name = "rpa-rebate"
|
||||
version = "0.1.0"
|
||||
description = "洁达返利班。表 4 查号 / 表 3 只录协议 → 表 4。查号走页面,录协议仍用替身。接口 4 组包在 rpa_oms。"
|
||||
readme = "README.md"
|
||||
description = "洁达返利班。按合同号查号、录协议 → 表 4。本包尚未实现。"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"oracledb>=2.4.1",
|
||||
"selenium>=4.15.0",
|
||||
"webdriver-manager>=4.0.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q"
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
default = true
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
"""生产 finder:登录一次,按合同号搜返利中心。Callable[[str], bool]。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from rebate_flow_steps import (
|
||||
DEFAULT_URL,
|
||||
RebateFlowContext,
|
||||
enter_purchase_agreement,
|
||||
login_rebate_center,
|
||||
open_rebate_center,
|
||||
search_contract,
|
||||
)
|
||||
|
||||
|
||||
class RebateFinder:
|
||||
def __init__(self, browser, ctx: RebateFlowContext | None = None, **kwargs):
|
||||
self._browser = browser
|
||||
self._ctx = ctx or RebateFlowContext(
|
||||
rebate_url=kwargs["rebate_url"],
|
||||
username=kwargs["username"],
|
||||
password=kwargs["password"],
|
||||
)
|
||||
self._ready = False
|
||||
|
||||
def __call__(self, contract_no: str) -> bool:
|
||||
self._ensure_session()
|
||||
return search_contract(self._browser, contract_no)
|
||||
|
||||
def _ensure_session(self) -> None:
|
||||
if self._ready:
|
||||
return
|
||||
open_rebate_center(self._browser, self._ctx)
|
||||
login_rebate_center(self._browser, self._ctx)
|
||||
enter_purchase_agreement(self._browser)
|
||||
self._ready = True
|
||||
|
||||
|
||||
def context_from_env() -> RebateFlowContext:
|
||||
return RebateFlowContext(
|
||||
rebate_url=(os.getenv("REBATE_URL") or DEFAULT_URL).strip(),
|
||||
username=(os.getenv("REBATE_USERNAME") or "").strip(),
|
||||
password=os.getenv("REBATE_PASSWORD") or "",
|
||||
)
|
||||
|
||||
|
||||
def production_finder() -> RebateFinder:
|
||||
from apa_browser_adapter import ApaBrowserAdapter
|
||||
|
||||
ctx = context_from_env()
|
||||
browser = ApaBrowserAdapter(url=ctx.rebate_url, flow="REBATE_FIND")
|
||||
return RebateFinder(browser, ctx)
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
"""返利中心查号:打开、登录、进采购协议、按合同号搜。不录协议。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
from accept import AcceptanceReport, evidence_shot
|
||||
from errors import LoginFailed
|
||||
|
||||
FLOW = "REBATE_FIND"
|
||||
DEFAULT_URL = "http://192.168.170.144:8001"
|
||||
SENTINEL_CONTRACT_NO = "__NO_SUCH_CONTRACT__"
|
||||
|
||||
_ACCEPT = AcceptanceReport(flow=FLOW)
|
||||
|
||||
_PORTAL_MARKERS = ("采购协议", "待审批", "待审批事项", "待办")
|
||||
EMPTY_MARKERS = ("暂无数据", "没有找到", "查询无结果", "无匹配数据", "未找到相关")
|
||||
|
||||
LOGIN = {
|
||||
"user": [
|
||||
'input[name="username"]',
|
||||
'input[name="userName"]',
|
||||
'input[placeholder*="用户名"]',
|
||||
'input[placeholder*="账号"]',
|
||||
'input[placeholder*="工号"]',
|
||||
'input[type="text"]',
|
||||
],
|
||||
"password": [
|
||||
'input[name="password"]',
|
||||
'input[placeholder*="密码"]',
|
||||
'input[type="password"]',
|
||||
],
|
||||
"submit": [
|
||||
"text=登录",
|
||||
"button:contains(登录)",
|
||||
'button[type="submit"]',
|
||||
'input[type="submit"]',
|
||||
'input[value="登录"]',
|
||||
],
|
||||
}
|
||||
|
||||
MENU = {
|
||||
"entry": [
|
||||
"text=采购协议管理",
|
||||
"text=采购协议",
|
||||
"a:contains(采购协议管理)",
|
||||
"span:contains(采购协议管理)",
|
||||
],
|
||||
}
|
||||
|
||||
SEARCH = {
|
||||
"contract_no": [
|
||||
"label=合同编号",
|
||||
"label=合同号",
|
||||
'input[placeholder*="合同编号"]',
|
||||
'input[placeholder*="合同号"]',
|
||||
'input[name*="contractNo"]',
|
||||
'input[name*="contract"]',
|
||||
],
|
||||
"button": [
|
||||
"text=查询",
|
||||
"button:contains(查询)",
|
||||
"a:contains(查询)",
|
||||
"text=搜索",
|
||||
"button:contains(搜索)",
|
||||
],
|
||||
}
|
||||
|
||||
_TABLE_SCRIPT = """
|
||||
const nodes = Array.from(document.querySelectorAll(
|
||||
'table tbody, table, .el-table__body, .el-table, .ant-table-tbody, .ant-table'
|
||||
));
|
||||
return nodes.map((n) => (n.innerText || '').trim()).filter(Boolean).join('\\n');
|
||||
"""
|
||||
|
||||
|
||||
class BrowserAdapter(Protocol):
|
||||
def open_url(self, url: str) -> None: ...
|
||||
def fill_first(self, selectors: list[str], value: str) -> None: ...
|
||||
def click_first(self, selectors: list[str]) -> None: ...
|
||||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None: ...
|
||||
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None: ...
|
||||
def element_exists(self, selectors: list[str]) -> bool: ...
|
||||
def read_body_text(self) -> str: ...
|
||||
def execute_script(self, script: str, *args): ...
|
||||
def screenshot(self, name: str) -> str: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RebateFlowContext:
|
||||
rebate_url: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
def classify_search_result(result_text: str, contract_no: str, *, from_table: bool = True) -> bool:
|
||||
number = (contract_no or "").strip()
|
||||
if not number:
|
||||
return False
|
||||
text = result_text or ""
|
||||
if from_table:
|
||||
if any(marker in text for marker in EMPTY_MARKERS) and number not in text:
|
||||
return False
|
||||
return number in text
|
||||
if any(marker in text for marker in EMPTY_MARKERS):
|
||||
return False
|
||||
return text.count(number) >= 2
|
||||
|
||||
|
||||
def read_result_table(browser: BrowserAdapter) -> tuple[str, bool]:
|
||||
try:
|
||||
text = browser.execute_script(_TABLE_SCRIPT)
|
||||
except Exception:
|
||||
text = None
|
||||
if isinstance(text, str) and text.strip():
|
||||
return text, True
|
||||
return (browser.read_body_text() or ""), False
|
||||
|
||||
|
||||
def open_rebate_center(browser: BrowserAdapter, ctx: RebateFlowContext) -> None:
|
||||
browser.open_url(ctx.rebate_url)
|
||||
|
||||
|
||||
def _is_logged_in(browser: BrowserAdapter) -> bool:
|
||||
body = browser.read_body_text() or ""
|
||||
return any(marker in body for marker in _PORTAL_MARKERS)
|
||||
|
||||
|
||||
def login_rebate_center(browser: BrowserAdapter, ctx: RebateFlowContext) -> None:
|
||||
try:
|
||||
if _is_logged_in(browser):
|
||||
return
|
||||
if not (ctx.username or "").strip() or ctx.password == "":
|
||||
raise LoginFailed("未配置 REBATE_USERNAME / REBATE_PASSWORD")
|
||||
browser.fill_first(LOGIN["user"], ctx.username)
|
||||
browser.fill_first(LOGIN["password"], ctx.password)
|
||||
browser.click_first(LOGIN["submit"])
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
if _is_logged_in(browser):
|
||||
return
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError("登录后未进入返利中心")
|
||||
except LoginFailed:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise LoginFailed(f"返利中心登录失败:{exc}") from exc
|
||||
|
||||
|
||||
def enter_purchase_agreement(browser: BrowserAdapter) -> None:
|
||||
body = browser.read_body_text() or ""
|
||||
if "合同编号" in body or "合同号" in body:
|
||||
return
|
||||
browser.click_first(MENU["entry"])
|
||||
browser.wait_for_text("查询", timeout_seconds=15)
|
||||
|
||||
|
||||
def search_contract(browser: BrowserAdapter, contract_no: str) -> bool:
|
||||
number = (contract_no or "").strip()
|
||||
if not number:
|
||||
return False
|
||||
browser.fill_first(SEARCH["contract_no"], number)
|
||||
browser.click_first(SEARCH["button"])
|
||||
deadline = time.time() + 15
|
||||
last_text = ""
|
||||
last_from_table = False
|
||||
while time.time() <= deadline:
|
||||
text, from_table = read_result_table(browser)
|
||||
last_text, last_from_table = text, from_table
|
||||
found = classify_search_result(text, number, from_table=from_table)
|
||||
empty = any(marker in (text or "") for marker in EMPTY_MARKERS)
|
||||
if found or empty:
|
||||
return found
|
||||
time.sleep(0.4)
|
||||
return classify_search_result(last_text, number, from_table=last_from_table)
|
||||
|
||||
|
||||
def run_find_flow(
|
||||
browser: BrowserAdapter,
|
||||
ctx: RebateFlowContext,
|
||||
contract_no: str,
|
||||
*,
|
||||
expect_found: bool | None = None,
|
||||
) -> bool:
|
||||
_ACCEPT.reset()
|
||||
try:
|
||||
open_rebate_center(browser, ctx)
|
||||
_ACCEPT.note("S02", "打开返利中心", {"url": ctx.rebate_url}, ok=True)
|
||||
try:
|
||||
login_rebate_center(browser, ctx)
|
||||
_ACCEPT.note("S03", "登录", {"user": ctx.username}, ok=True)
|
||||
except Exception as exc:
|
||||
evidence_shot(browser, "rebate-login-failed", failed=True)
|
||||
_ACCEPT.note(
|
||||
"S03",
|
||||
"登录",
|
||||
{"user": ctx.username, "error": exc},
|
||||
ok=False,
|
||||
raise_error=False,
|
||||
)
|
||||
raise
|
||||
enter_purchase_agreement(browser)
|
||||
_ACCEPT.note("S04", "进入采购协议", {"menu": "采购协议管理"}, ok=True)
|
||||
found = search_contract(browser, contract_no)
|
||||
found_text = "yes" if found else "no"
|
||||
if expect_found is None:
|
||||
_ACCEPT.note(
|
||||
"S05",
|
||||
"查号",
|
||||
{"contractNo": contract_no, "found": found_text},
|
||||
ok=True,
|
||||
)
|
||||
else:
|
||||
_ACCEPT.check(
|
||||
"S05",
|
||||
"查号",
|
||||
[
|
||||
("contractNo", contract_no, contract_no),
|
||||
("found", found_text, "yes" if expect_found else "no"),
|
||||
],
|
||||
browser=browser,
|
||||
screenshot_on_fail="rebate-find-mismatch",
|
||||
)
|
||||
return found
|
||||
finally:
|
||||
print(_ACCEPT.summary_text(), end="", flush=True)
|
||||
_ACCEPT.dump()
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
"""表 4 起草行领取、表 3 只录协议行领取、表 4 写入。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from db import RepoUnavailable, connect_from_env, oracle_dsn
|
||||
from persist import STAMP_CLAIMING, STAMP_CONTRACT, STAMP_DONE, GroupSave
|
||||
|
||||
ACTION_CONTRACT = 1
|
||||
CONTRACT_DRAFT = 1
|
||||
PROCESS_PENDING = 0
|
||||
PROCESS_CLAIMING = 3
|
||||
ENTRY_PROTOCOL_ONLY = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Claimed:
|
||||
drafts: list[dict] = field(default_factory=list)
|
||||
protocols: list[dict] = field(default_factory=list)
|
||||
|
||||
|
||||
def _has_contract_no(row: dict) -> bool:
|
||||
return bool((row.get("contract_no") or "").strip())
|
||||
|
||||
|
||||
def is_draft(row: dict) -> bool:
|
||||
if int(row.get("action_code") or 0) != ACTION_CONTRACT:
|
||||
return False
|
||||
if int(row.get("contract_status") or 0) != CONTRACT_DRAFT:
|
||||
return False
|
||||
if not _has_contract_no(row):
|
||||
return False
|
||||
stamp = row.get("update_user_name") or STAMP_CONTRACT
|
||||
return stamp == STAMP_CONTRACT
|
||||
|
||||
|
||||
def is_protocol_only(row: dict) -> bool:
|
||||
if int(row.get("process_status") or 0) != PROCESS_PENDING:
|
||||
return False
|
||||
entry = row.get("contract_entry_status")
|
||||
if entry is None or int(entry) != ENTRY_PROTOCOL_ONLY:
|
||||
return False
|
||||
return _has_contract_no(row)
|
||||
|
||||
|
||||
class MemoryRebateRepo:
|
||||
def __init__(
|
||||
self,
|
||||
table4: list[dict] | None = None,
|
||||
table3: list[dict] | None = None,
|
||||
):
|
||||
self.table4: dict[int, dict] = {
|
||||
int(row["fid"]): dict(row) for row in (table4 or [])
|
||||
}
|
||||
self.table3: dict[int, dict] = {
|
||||
int(row["fid"]): dict(row) for row in (table3 or [])
|
||||
}
|
||||
self._next_fid = (max(self.table4) + 1) if self.table4 else 1
|
||||
|
||||
def claim_pending(self, limit: int | None = None) -> Claimed:
|
||||
drafts = sorted(
|
||||
(row for row in self.table4.values() if is_draft(row)),
|
||||
key=lambda row: int(row["fid"]),
|
||||
)
|
||||
protocols = sorted(
|
||||
(row for row in self.table3.values() if is_protocol_only(row)),
|
||||
key=lambda row: int(row["fid"]),
|
||||
)
|
||||
if limit is not None:
|
||||
if len(drafts) >= limit:
|
||||
drafts = drafts[:limit]
|
||||
protocols = []
|
||||
else:
|
||||
protocols = protocols[: max(0, limit - len(drafts))]
|
||||
for row in drafts:
|
||||
row["update_user_name"] = STAMP_CLAIMING
|
||||
for row in protocols:
|
||||
row["process_status"] = PROCESS_CLAIMING
|
||||
return Claimed(drafts=list(drafts), protocols=list(protocols))
|
||||
|
||||
def save_group(self, payload: GroupSave) -> None:
|
||||
for item in payload.table4_updates:
|
||||
target = self.table4[int(item["fid"])]
|
||||
target["contract_status"] = item["contract_status"]
|
||||
target["update_user_name"] = item["update_user_name"]
|
||||
for fid in payload.table4_release_fids:
|
||||
self.table4[int(fid)]["update_user_name"] = STAMP_CONTRACT
|
||||
for row in payload.table4_inserts:
|
||||
item = dict(row)
|
||||
item["fid"] = self._next_fid
|
||||
self._next_fid += 1
|
||||
self.table4[item["fid"]] = item
|
||||
for item in payload.table3_updates:
|
||||
target = self.table3[int(item["fid"])]
|
||||
target["process_status"] = item["process_status"]
|
||||
target["process_result"] = item["process_result"]
|
||||
|
||||
|
||||
_SELECT_DRAFTS = """
|
||||
SELECT
|
||||
fid, oms_id, action_code, contract_status, contract_fail_reason,
|
||||
contract_no, agreement_status, agreement_fail_reason,
|
||||
agreement_id, agreement_dtl_id, org_code, push_status,
|
||||
NVL(update_user_name, '合同班') AS update_user_name
|
||||
FROM rpa_contract_agreement_result
|
||||
WHERE action_code = 1
|
||||
AND contract_status = 1
|
||||
AND contract_no IS NOT NULL
|
||||
AND LENGTH(TRIM(contract_no)) > 0
|
||||
AND NVL(update_user_name, '合同班') = '合同班'
|
||||
ORDER BY fid
|
||||
"""
|
||||
|
||||
_SELECT_DRAFTS_LIMIT = f"""
|
||||
SELECT * FROM (
|
||||
{_SELECT_DRAFTS}
|
||||
) WHERE ROWNUM <= :limit
|
||||
"""
|
||||
|
||||
_CLAIM_DRAFT_SQL = """
|
||||
UPDATE rpa_contract_agreement_result
|
||||
SET update_user_name = :stamp,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid
|
||||
AND action_code = 1
|
||||
AND contract_status = 1
|
||||
AND NVL(update_user_name, :contract_stamp) = :contract_stamp
|
||||
"""
|
||||
|
||||
_SELECT_PROTOCOLS = """
|
||||
SELECT
|
||||
fid, oms_id, contract_no, contract_entry_status, process_status, org_code,
|
||||
NVL(process_result, '') AS process_result
|
||||
FROM rpa_purchase_contract_data
|
||||
WHERE process_status = 0
|
||||
AND NVL(contract_entry_status, -1) = 0
|
||||
AND contract_no IS NOT NULL
|
||||
AND LENGTH(TRIM(contract_no)) > 0
|
||||
ORDER BY fid
|
||||
"""
|
||||
|
||||
_SELECT_PROTOCOLS_LIMIT = f"""
|
||||
SELECT * FROM (
|
||||
{_SELECT_PROTOCOLS}
|
||||
) WHERE ROWNUM <= :limit
|
||||
"""
|
||||
|
||||
_CLAIM_PROTOCOL_SQL = """
|
||||
UPDATE rpa_purchase_contract_data
|
||||
SET process_status = 3,
|
||||
process_time = SYSTIMESTAMP,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid AND process_status = 0
|
||||
"""
|
||||
|
||||
_UPDATE_DRAFT_SQL = """
|
||||
UPDATE rpa_contract_agreement_result
|
||||
SET contract_status = :contract_status,
|
||||
update_user_id = 0,
|
||||
update_user_name = :stamp,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid AND update_user_name = :claim_stamp
|
||||
"""
|
||||
|
||||
_RELEASE_DRAFT_SQL = """
|
||||
UPDATE rpa_contract_agreement_result
|
||||
SET update_user_name = :stamp,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid AND update_user_name = :claim_stamp
|
||||
"""
|
||||
|
||||
_INSERT_TABLE4_SQL = """
|
||||
INSERT INTO rpa_contract_agreement_result (
|
||||
oms_id, action_code, contract_status, contract_fail_reason, contract_no,
|
||||
agreement_status, agreement_fail_reason, agreement_id, agreement_dtl_id,
|
||||
push_status, create_time, update_time,
|
||||
create_user_id, create_user_name, update_user_id, update_user_name, org_code
|
||||
) VALUES (
|
||||
:oms_id, :action_code, :contract_status, :contract_fail_reason, :contract_no,
|
||||
:agreement_status, :agreement_fail_reason, :agreement_id, :agreement_dtl_id,
|
||||
0, SYSTIMESTAMP, SYSTIMESTAMP,
|
||||
0, '返利班', 0, '返利班', :org_code
|
||||
)
|
||||
"""
|
||||
|
||||
_COMPLETE_PROTOCOL_SQL = """
|
||||
UPDATE rpa_purchase_contract_data
|
||||
SET process_status = :process_status,
|
||||
process_result = :process_result,
|
||||
process_time = SYSTIMESTAMP,
|
||||
update_time = SYSTIMESTAMP
|
||||
WHERE fid = :fid AND process_status = 3
|
||||
"""
|
||||
|
||||
_TABLE4_BIND_KEYS = (
|
||||
"oms_id",
|
||||
"action_code",
|
||||
"contract_status",
|
||||
"contract_fail_reason",
|
||||
"contract_no",
|
||||
"agreement_status",
|
||||
"agreement_fail_reason",
|
||||
"agreement_id",
|
||||
"agreement_dtl_id",
|
||||
"org_code",
|
||||
)
|
||||
|
||||
|
||||
def _records(cur) -> list[dict]:
|
||||
cols = [item[0].lower() for item in cur.description]
|
||||
return [dict(zip(cols, rec)) for rec in cur.fetchall()]
|
||||
|
||||
|
||||
class OracleRebateRepo:
|
||||
def __init__(self, connect: Callable):
|
||||
self._connect = connect
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "OracleRebateRepo":
|
||||
oracle_dsn()
|
||||
return cls(connect=connect_from_env)
|
||||
|
||||
def claim_pending(self, limit: int | None = None) -> Claimed:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
drafts = self._claim_drafts(cur, limit)
|
||||
remaining = None if limit is None else max(0, int(limit) - len(drafts))
|
||||
protocols: list[dict] = []
|
||||
if remaining is None or remaining > 0:
|
||||
protocols = self._claim_protocols(cur, remaining)
|
||||
conn.commit()
|
||||
return Claimed(drafts=drafts, protocols=protocols)
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def save_group(self, payload: GroupSave) -> None:
|
||||
conn = self._connect()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
for item in payload.table4_updates:
|
||||
cur.execute(
|
||||
_UPDATE_DRAFT_SQL,
|
||||
{
|
||||
"fid": int(item["fid"]),
|
||||
"contract_status": item["contract_status"],
|
||||
"stamp": STAMP_DONE,
|
||||
"claim_stamp": STAMP_CLAIMING,
|
||||
},
|
||||
)
|
||||
for fid in payload.table4_release_fids:
|
||||
cur.execute(
|
||||
_RELEASE_DRAFT_SQL,
|
||||
{
|
||||
"fid": int(fid),
|
||||
"stamp": STAMP_CONTRACT,
|
||||
"claim_stamp": STAMP_CLAIMING,
|
||||
},
|
||||
)
|
||||
for row in payload.table4_inserts:
|
||||
cur.execute(
|
||||
_INSERT_TABLE4_SQL,
|
||||
{key: row.get(key) for key in _TABLE4_BIND_KEYS},
|
||||
)
|
||||
for item in payload.table3_updates:
|
||||
cur.execute(
|
||||
_COMPLETE_PROTOCOL_SQL,
|
||||
{
|
||||
"fid": int(item["fid"]),
|
||||
"process_status": item["process_status"],
|
||||
"process_result": item["process_result"],
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
raise RepoUnavailable(str(exc)) from exc
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _claim_drafts(self, cur, limit: int | None) -> list[dict]:
|
||||
if limit is None:
|
||||
cur.execute(_SELECT_DRAFTS)
|
||||
else:
|
||||
cur.execute(_SELECT_DRAFTS_LIMIT, {"limit": int(limit)})
|
||||
fetched = _records(cur)
|
||||
claimed: list[dict] = []
|
||||
for row in fetched:
|
||||
cur.execute(
|
||||
_CLAIM_DRAFT_SQL,
|
||||
{
|
||||
"fid": row["fid"],
|
||||
"stamp": STAMP_CLAIMING,
|
||||
"contract_stamp": STAMP_CONTRACT,
|
||||
},
|
||||
)
|
||||
if cur.rowcount == 1:
|
||||
row["update_user_name"] = STAMP_CLAIMING
|
||||
claimed.append(row)
|
||||
return claimed
|
||||
|
||||
def _claim_protocols(self, cur, limit: int | None) -> list[dict]:
|
||||
if limit is None:
|
||||
cur.execute(_SELECT_PROTOCOLS)
|
||||
else:
|
||||
cur.execute(_SELECT_PROTOCOLS_LIMIT, {"limit": int(limit)})
|
||||
fetched = _records(cur)
|
||||
claimed: list[dict] = []
|
||||
for row in fetched:
|
||||
cur.execute(_CLAIM_PROTOCOL_SQL, {"fid": row["fid"]})
|
||||
if cur.rowcount == 1:
|
||||
row["process_status"] = PROCESS_CLAIMING
|
||||
claimed.append(row)
|
||||
return claimed
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
"""页面查号入口。不连 Oracle、不录协议。验收只看 ACCEPT.txt。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from apa_browser_adapter import ApaBrowserAdapter
|
||||
from db import load_runtime_env
|
||||
from rebate_finder import context_from_env
|
||||
from rebate_flow_steps import SENTINEL_CONTRACT_NO, run_find_flow
|
||||
|
||||
|
||||
def main() -> int:
|
||||
load_runtime_env()
|
||||
contract_no = (os.getenv("REBATE_ACCEPT_CONTRACT_NO") or SENTINEL_CONTRACT_NO).strip()
|
||||
expect_found = None if contract_no != SENTINEL_CONTRACT_NO else False
|
||||
ctx = context_from_env()
|
||||
browser = ApaBrowserAdapter(url=ctx.rebate_url, flow="REBATE_FIND")
|
||||
found = run_find_flow(browser, ctx, contract_no, expect_found=expect_found)
|
||||
if expect_found is False and found:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
from accept import accept_log_path
|
||||
from job import run_batch
|
||||
from persist import KIND_OK, STAMP_CONTRACT, STAMP_DONE, AgreementOutcome
|
||||
from rebate_flow_steps import (
|
||||
SENTINEL_CONTRACT_NO,
|
||||
RebateFlowContext,
|
||||
classify_search_result,
|
||||
run_find_flow,
|
||||
search_contract,
|
||||
)
|
||||
from rebate_finder import RebateFinder
|
||||
from repo import MemoryRebateRepo
|
||||
|
||||
|
||||
class FakeBrowser:
|
||||
def __init__(self, *, table_text="", body_text="", login_body=""):
|
||||
self.table_text = table_text
|
||||
self.body_text = body_text
|
||||
self.login_body = login_body or body_text
|
||||
self.opened: list[str] = []
|
||||
self.filled: list[tuple[list[str], str]] = []
|
||||
self.clicked: list[list[str]] = []
|
||||
self.script_count = 0
|
||||
|
||||
def open_url(self, url: str) -> None:
|
||||
self.opened.append(url)
|
||||
|
||||
def fill_first(self, selectors: list[str], value: str) -> None:
|
||||
self.filled.append((list(selectors), value))
|
||||
|
||||
def click_first(self, selectors: list[str]) -> None:
|
||||
self.clicked.append(list(selectors))
|
||||
|
||||
def click_native(self, selectors: list[str]) -> None:
|
||||
self.click_first(selectors)
|
||||
|
||||
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None:
|
||||
haystack = f"{self.body_text}\n{self.table_text}\n{self.login_body}"
|
||||
if text not in haystack:
|
||||
raise TimeoutError(text)
|
||||
|
||||
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None:
|
||||
return None
|
||||
|
||||
def element_exists(self, selectors: list[str]) -> bool:
|
||||
return True
|
||||
|
||||
def read_body_text(self) -> str:
|
||||
return self.body_text
|
||||
|
||||
def execute_script(self, script: str, *args):
|
||||
self.script_count += 1
|
||||
return self.table_text
|
||||
|
||||
def screenshot(self, name: str) -> str:
|
||||
return name
|
||||
|
||||
def read_first(self, selectors: list[str], value_name: str = "") -> str:
|
||||
return ""
|
||||
|
||||
def switch_to_frame(self, selectors: list[str]) -> None:
|
||||
return None
|
||||
|
||||
def switch_to_default_content(self) -> None:
|
||||
return None
|
||||
|
||||
def select_first(self, selectors: list[str], value: str) -> None:
|
||||
return None
|
||||
|
||||
def select_xform(self, name: str, value: str) -> None:
|
||||
return None
|
||||
|
||||
def upload_file_first(self, selectors: list[str], file_path: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_table_row_with_contract_no_is_found():
|
||||
assert classify_search_result("HT1\n重庆亿昊药业有限公司", "HT1", from_table=True) is True
|
||||
|
||||
|
||||
def test_empty_table_is_not_found():
|
||||
assert classify_search_result("暂无数据", "HT1", from_table=True) is False
|
||||
|
||||
|
||||
def test_search_box_echo_is_not_found():
|
||||
browser = FakeBrowser(
|
||||
table_text="暂无数据",
|
||||
body_text="合同编号 HT1 查询 暂无数据",
|
||||
)
|
||||
assert search_contract(browser, "HT1") is False
|
||||
assert browser.filled[-1][1] == "HT1"
|
||||
assert browser.clicked
|
||||
|
||||
|
||||
def test_search_table_hit_is_found():
|
||||
browser = FakeBrowser(table_text="合同编号 供应商\nHT1 重庆亿昊药业有限公司")
|
||||
assert search_contract(browser, "HT1") is True
|
||||
|
||||
|
||||
def test_page_finder_found_writes_agreement_like_stub():
|
||||
repo = MemoryRebateRepo(
|
||||
table4=[
|
||||
{
|
||||
"fid": 1,
|
||||
"oms_id": 2026100501,
|
||||
"action_code": 1,
|
||||
"contract_status": 1,
|
||||
"contract_fail_reason": None,
|
||||
"contract_no": "HT1",
|
||||
"agreement_status": None,
|
||||
"agreement_fail_reason": None,
|
||||
"agreement_id": None,
|
||||
"agreement_dtl_id": None,
|
||||
"org_code": "901",
|
||||
"push_status": 0,
|
||||
"create_user_name": STAMP_CONTRACT,
|
||||
"update_user_name": STAMP_CONTRACT,
|
||||
}
|
||||
]
|
||||
)
|
||||
browser = FakeBrowser(
|
||||
table_text="HT1 重庆亿昊",
|
||||
body_text="采购协议管理 合同编号 查询",
|
||||
login_body="采购协议管理",
|
||||
)
|
||||
finder = RebateFinder(
|
||||
browser,
|
||||
rebate_url="http://example",
|
||||
username="4795",
|
||||
password="secret",
|
||||
)
|
||||
|
||||
def recorder(contract_no: str) -> AgreementOutcome:
|
||||
return AgreementOutcome(KIND_OK, "AGR1", "AGRD1", None)
|
||||
|
||||
summary = run_batch(repo, finder=finder, recorder=recorder)
|
||||
assert summary.ok == 1
|
||||
assert repo.table4[1]["contract_status"] == 2
|
||||
assert repo.table4[1]["update_user_name"] == STAMP_DONE
|
||||
agreements = [row for row in repo.table4.values() if row["action_code"] == 2]
|
||||
assert len(agreements) == 1
|
||||
assert len(browser.opened) == 1
|
||||
|
||||
|
||||
def test_find_flow_writes_accept_for_missing_number(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("RPA_ACCEPT_LOG", raising=False)
|
||||
monkeypatch.setattr("accept.SCREENSHOT_ROOT", tmp_path)
|
||||
browser = FakeBrowser(
|
||||
table_text="暂无数据",
|
||||
body_text="采购协议管理 合同编号 查询",
|
||||
)
|
||||
ctx = RebateFlowContext(rebate_url="http://example", username="u", password="p")
|
||||
found = run_find_flow(browser, ctx, SENTINEL_CONTRACT_NO, expect_found=False)
|
||||
assert found is False
|
||||
text = accept_log_path("REBATE_FIND").read_text(encoding="utf-8")
|
||||
assert "ACCEPT SUMMARY REBATE_FIND 4/4 ok" in text
|
||||
assert "ACCEPT S05 ok 查号" in text
|
||||
assert f"contractNo={SENTINEL_CONTRACT_NO}" in text
|
||||
assert "found=no" in text
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
from job import run_batch
|
||||
from persist import KIND_OK, STAMP_CONTRACT, STAMP_DONE, AgreementOutcome
|
||||
from repo import MemoryRebateRepo
|
||||
|
||||
|
||||
def _t4(**overrides) -> dict:
|
||||
row = {
|
||||
"fid": 1,
|
||||
"oms_id": 2026100501,
|
||||
"action_code": 1,
|
||||
"contract_status": 1,
|
||||
"contract_fail_reason": None,
|
||||
"contract_no": "HT1",
|
||||
"agreement_status": None,
|
||||
"agreement_fail_reason": None,
|
||||
"agreement_id": None,
|
||||
"agreement_dtl_id": None,
|
||||
"org_code": "901",
|
||||
"push_status": 0,
|
||||
"create_user_name": STAMP_CONTRACT,
|
||||
"update_user_name": STAMP_CONTRACT,
|
||||
}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
|
||||
def _t3(**overrides) -> dict:
|
||||
row = {
|
||||
"fid": 10,
|
||||
"oms_id": 2026100503,
|
||||
"contract_no": "HT-OLD",
|
||||
"contract_entry_status": 0,
|
||||
"process_status": 0,
|
||||
"process_result": "",
|
||||
"org_code": "901",
|
||||
}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
|
||||
def test_job_finds_contract_and_writes_agreement():
|
||||
repo = MemoryRebateRepo(
|
||||
table4=[
|
||||
_t4(),
|
||||
_t4(fid=2, oms_id=2026100502),
|
||||
_t4(fid=3, contract_status=2, contract_no="HT-DONE", update_user_name=STAMP_DONE),
|
||||
]
|
||||
)
|
||||
looked: list[str] = []
|
||||
recorded: list[str] = []
|
||||
|
||||
def finder(contract_no: str) -> bool:
|
||||
looked.append(contract_no)
|
||||
return True
|
||||
|
||||
def recorder(contract_no: str) -> AgreementOutcome:
|
||||
recorded.append(contract_no)
|
||||
return AgreementOutcome(KIND_OK, "AGR1", "AGRD1", None)
|
||||
|
||||
summary = run_batch(repo, finder=finder, recorder=recorder)
|
||||
assert summary.claimed == 2
|
||||
assert summary.ok == 1
|
||||
assert looked == ["HT1"]
|
||||
assert recorded == ["HT1"]
|
||||
drafts = [row for row in repo.table4.values() if row["fid"] in (1, 2)]
|
||||
assert {row["contract_status"] for row in drafts} == {2}
|
||||
assert {row["action_code"] for row in drafts} == {1}
|
||||
assert {row["update_user_name"] for row in drafts} == {STAMP_DONE}
|
||||
assert {row["push_status"] for row in drafts} == {0}
|
||||
agreements = [row for row in repo.table4.values() if row["action_code"] == 2]
|
||||
assert sorted(row["oms_id"] for row in agreements) == [2026100501, 2026100502]
|
||||
assert {row["contract_no"] for row in agreements} == {"HT1"}
|
||||
assert {row["agreement_status"] for row in agreements} == {1}
|
||||
assert {row["agreement_id"] for row in agreements} == {"AGR1"}
|
||||
assert {row["agreement_dtl_id"] for row in agreements} == {"AGRD1"}
|
||||
assert {row["push_status"] for row in agreements} == {0}
|
||||
assert repo.table4[3]["contract_status"] == 2
|
||||
assert repo.table4[3]["action_code"] == 1
|
||||
|
||||
|
||||
def test_job_leaves_not_found_for_next_shift():
|
||||
repo = MemoryRebateRepo(table4=[_t4()])
|
||||
recorded = {"n": 0}
|
||||
|
||||
def recorder(contract_no: str) -> AgreementOutcome:
|
||||
recorded["n"] += 1
|
||||
return AgreementOutcome(KIND_OK, "AGR1", "AGRD1", None)
|
||||
|
||||
run_batch(repo, finder=lambda _no: False, recorder=recorder)
|
||||
assert recorded["n"] == 0
|
||||
row = repo.table4[1]
|
||||
assert row["contract_status"] == 1
|
||||
assert row["action_code"] == 1
|
||||
assert row["update_user_name"] == STAMP_CONTRACT
|
||||
assert [item["action_code"] for item in repo.table4.values()] == [1]
|
||||
|
||||
|
||||
def test_job_claims_protocol_only_and_writes_table4():
|
||||
repo = MemoryRebateRepo(table3=[_t3(), _t3(fid=11, process_status=1, contract_no="HT-SKIP")])
|
||||
recorded: list[str] = []
|
||||
|
||||
def recorder(contract_no: str) -> AgreementOutcome:
|
||||
recorded.append(contract_no)
|
||||
return AgreementOutcome(KIND_OK, "AGR-OLD", "AGRD-OLD", None)
|
||||
|
||||
run_batch(repo, finder=lambda _no: True, recorder=recorder)
|
||||
assert recorded == ["HT-OLD"]
|
||||
assert repo.table3[10]["process_status"] == 1
|
||||
assert repo.table3[11]["process_status"] == 1
|
||||
success = [row for row in repo.table4.values() if row["action_code"] == 1]
|
||||
agreements = [row for row in repo.table4.values() if row["action_code"] == 2]
|
||||
assert len(success) == 1
|
||||
assert success[0]["oms_id"] == 2026100503
|
||||
assert success[0]["contract_status"] == 2
|
||||
assert success[0]["contract_no"] == "HT-OLD"
|
||||
assert success[0]["push_status"] == 0
|
||||
assert len(agreements) == 1
|
||||
assert agreements[0]["agreement_status"] == 1
|
||||
assert agreements[0]["agreement_id"] == "AGR-OLD"
|
||||
assert agreements[0]["agreement_dtl_id"] == "AGRD-OLD"
|
||||
|
|
@ -1,626 +0,0 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.7.22"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.5.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "50.0.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.20"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oracledb"
|
||||
version = "26.0.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/f3/22113415f48b6608ada31ebb46047b3b8ac361dba393d1b5ed1f768d8f6a/oracledb-26.0.1.tar.gz", hash = "sha256:786397a6b37e94ebfa6c1c6cd026755eb8fccfd39e15bcb64cb5879491882aef", size = 910463, upload-time = "2026-09-22T21:23:20.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/fa/349ee179b3e106f210ac070d199dd341f37e2af9f5faf2563b80d8aa6ab1/oracledb-26.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eea4d0824fa82b03c0d90aad81f10dbbacdaf351979c0130190a362a539e63ae", size = 4908265, upload-time = "2026-09-22T21:23:49.524Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/68/2dc6a99d385835290825c0d6689097af29637db8754197040b95814bb5cd/oracledb-26.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c05707b2ba57d8210bdf062ba3419900591f3369d1532fb116af51445c2993d", size = 2448388, upload-time = "2026-09-22T21:23:50.993Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/4b/ce48ed072c2328b429a6bc87a35ddd735945151145d9fe49c1e177b0da83/oracledb-26.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36cbf2be185412fee95be96aa6f187a73fd5181bb03186b53d5b70ac18fa354b", size = 2663296, upload-time = "2026-09-22T21:23:52.426Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/c8/73166ff29389c5ce3272fcc534360ab65248f90fcdf175a99b5fa2bb8466/oracledb-26.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5ad1b6f4db3d8a43182e7734922342fac5901456b34acead92f6fc60cc59882", size = 2493289, upload-time = "2026-09-22T21:23:53.949Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/c0/f6b4f82d6dc7f7d7654c0e4889caa1abbe019ec76daa1b4b4ee80859172b/oracledb-26.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aecc4cdd03cb5a5ad51f43680d16d74132ae7df65c991e7bd69bbe2ccc0fd209", size = 2694730, upload-time = "2026-09-22T21:23:55.609Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/da/a3ad79e18c5b27b357de005ce9d79dff1ef1646b7c55b6e6557b238f03bd/oracledb-26.0.1-cp312-cp312-win32.whl", hash = "sha256:f55d6fd9ebc3e8bf3bf4828fcd6ae152b363859628d27f92a4764111adfb1001", size = 1578393, upload-time = "2026-09-22T21:23:56.975Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/c6/8e2ad66bda2cda6d085f3516ad8f7834b1d7d4c9da21f96834467525b551/oracledb-26.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c6f2f0d7290b15f1a166c0d24187e7746a6f08ea6e886e521110f1bf8d2002c", size = 1970972, upload-time = "2026-09-22T21:23:58.352Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/ce/98c3a83142a562e40135abb8503b59eba7af1135c11c8077986a289f3a35/oracledb-26.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:c31b6f7440047c867a1b41cbaace7867db0750ecab4dc6454c9f501991152deb", size = 1622016, upload-time = "2026-09-22T21:23:59.996Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/d7/2a911476339d9fa6eb83e6a4639b007c13af26d7261907ecc2d8722a6712/oracledb-26.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:339dd6c4f0d50dab4026a36ae89d902240b2a879a17cdf18e3feadc113957b4e", size = 4911060, upload-time = "2026-09-22T21:24:01.757Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/e8/97e3b2c283ab996efe1d12b6afa86042c746b0ce4e408942bb4b7c35e39a/oracledb-26.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5907bb5c9df123e417ddf358f540554dfc6766064c2feb84121c099bc4e039a3", size = 2445641, upload-time = "2026-09-22T21:24:03.587Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/f7/bbf75b91248ea489e8393ae84e9985b37c895781c658b6d9e5d83d02c1c1/oracledb-26.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16d08328bf9b02e980ad4d5f3e0e99596f9a42aaf6bd1e244ea85be753f2c292", size = 2646590, upload-time = "2026-09-22T21:24:05.22Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/32/cbdacb302a2ca047e36e0f804c493391844aad83822d428184fa1d4203d5/oracledb-26.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50e42111d5c620f60478e4571a5847c335286e8650e63a8789f8bd51035f90da", size = 2499129, upload-time = "2026-09-22T21:24:06.801Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/1e/ce86b581a4f1c8d5844019f84eacc9f4084c882ba676a90c94d7eaeb749e/oracledb-26.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0453f8f1a6748c982dcdac8af09bcd81971979c7ec9d1d151e15595f58d9164b", size = 2675979, upload-time = "2026-09-22T21:24:08.153Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/7c/49e6af631b84db68d801b6d03ff58c4fa7b767ffa8a8402de643584e9d63/oracledb-26.0.1-cp313-cp313-win32.whl", hash = "sha256:93e6a7cda6ad7b14e2ce8689c9694872b3d69d57015aab070028c9a93faf3bf1", size = 1576622, upload-time = "2026-09-22T21:24:09.492Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/10/ba7a5ce82fda9bbfcf222930b9a6f9a967c2b3d49f787ba7f4f99b598c6f/oracledb-26.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:6cf0c4d8662072917e2f98598cb97a18e157785bd0f294fbcf770d822ca239a9", size = 1970210, upload-time = "2026-09-22T21:24:10.806Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/dc/96ce57dad3b1bb3d3c30be936d0f6daea0a02649d8936b878921004b15e1/oracledb-26.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:22bb1e47a01a5259eead9202be8f631682b69be08ee29d8e532ed9b9364bd7bd", size = 1621513, upload-time = "2026-09-22T21:24:12.246Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/e6/7cf147ea1daf0c7c86fcd77acce1a09b0d4ab75539a4677a2034f112c860/oracledb-26.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2201184b46bbec476a7ac29a72f14ee4ed75d692ade3b6c5d672fdbca71e6b2", size = 4961776, upload-time = "2026-09-22T21:24:13.967Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/7e/1ada40fce6ec550718e6eb1b8be9bfcc6e9697f19b6244ed6e0b89aa8da8/oracledb-26.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bddb6bd7156df750045b419ffc400cfe429276f1a17d83913f10adefd0baa9a", size = 2486660, upload-time = "2026-09-22T21:24:16.397Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/2a/a4cf6f8081b1ab3498c82e3917b2174843bf22e2fa4c41e98ad1e0b66d47/oracledb-26.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33b22800a9b02ee0ead00e6f3073b810f7dc51c1e82fc9fc5475c09812420df", size = 2664849, upload-time = "2026-09-22T21:24:18.16Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/f2/50a32fa2d8c6cbb0e5fb69c262b01e7412ad08e430192404af9d2b9f4bea/oracledb-26.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:124f0072e6c7df277c561ec3a4c42cb2c02c30e9ce15e75a460b4abcfee41046", size = 2544330, upload-time = "2026-09-22T21:24:19.627Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/90/9b6dd9bbc1899ca3a982e9012f25be2a70e4795eacc6f8582c340c64fe1c/oracledb-26.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4c5094d5ba60b0f67ce7c74eb0fbc738d827103c0ba2db5f8483db405ab4e9c4", size = 2693190, upload-time = "2026-09-22T21:24:21.228Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/42/0df9265f2f562283c88590a6bcd442084cef80c832ff598bdbab994a3cb8/oracledb-26.0.1-cp314-cp314-win32.whl", hash = "sha256:03424ccfbd25c402133c2a23c5c1a630e57effda7b962085d91c41d765af1b25", size = 1602133, upload-time = "2026-09-22T21:24:22.733Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/d4/1b90c5e252a53dbc71833217746f2673e9e2ce0e06115bac1dc42ae347d0/oracledb-26.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:ee8f4736b72f38ade26f9770f7b40e8662bfa825ef48cefcc55cef0c30525d4b", size = 2027835, upload-time = "2026-09-22T21:24:24.105Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/4e/9febea61cbf476c2ae5d290fb3f823f1e46668e3ac15b8af5307531e33ad/oracledb-26.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:13b0dfbbc503c6107d6c08b1658df260c2c077bc5574d9545159df0a0301fe7d", size = 1680743, upload-time = "2026-09-22T21:24:25.475Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/bb/71f7861bfbf586757873259a858f370030a137b4172865992c3cc9a462b1/oracledb-26.0.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9e77a6b3b01a3a2f4b786939f59d862fbe73fbc4c68cdf87aa45a5398725a16c", size = 4960764, upload-time = "2026-09-22T21:24:27.244Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/c0/86aa68f08e5c1d25b864ac5777c559bf0bc8911e5d8262b70bec5e05cc21/oracledb-26.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d722f185f9c05ecccb2702a58a1e5ecfd309ae92cad0c38373e0969e83bf2bd", size = 2488466, upload-time = "2026-09-22T21:24:29.177Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/f4/af94230a3c2c6679323f3578a0a5281cfd87f72994f66e6044b5ff6c95e9/oracledb-26.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfcf6382bc48e21e448ca2b71644cc75626056a638bca50181acdc8adc69dd3c", size = 2685915, upload-time = "2026-09-22T21:24:30.823Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/a7/aa99f8e60961e80795ca926ead66c66d98c3aee78d970320eacdfa843614/oracledb-26.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d42a2e0caea9bcf7d260d430b225d72cff6dffa0d6729be7a8823db2678e0b74", size = 2544920, upload-time = "2026-09-22T21:24:32.667Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/65/b636a18a578cffd5819df08ae80deeb931f1215d2d2b18112a37e9d2bbea/oracledb-26.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a073cc8bd74a7ce32d12f4bc898dd143971981b52ecd2f72c37b4474bff76cb", size = 2713492, upload-time = "2026-09-22T21:24:34.22Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4a/e758838b14540a94599487d0d15ce4390edba5a257e53f2cea66f94bd138/oracledb-26.0.1-cp315-cp315-win32.whl", hash = "sha256:f4188bf8eedfc05ef266835e98ce304fda92d6bc87dd776d5f9a8b510d8b54ba", size = 1601872, upload-time = "2026-09-22T21:24:35.591Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/b1/51b2a51abd89ba43dc0f9ee14d456a9b002d0b9c31840c7de55387e555eb/oracledb-26.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:9d508d2df7bb9a4802247ec95d06064e2babf8ae9e1f91d89c349bcd30df44ed", size = 2026829, upload-time = "2026-09-22T21:24:37.15Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/6b/b099ff2c51447cbf0d2f1c33ea7de1abf96d9bf4c3d93b2a5dff48728965/oracledb-26.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:7c9541d7cf2d324e9516c955c033235bbf25b7d96ce41a856b3bff2eab3494ed", size = 1680535, upload-time = "2026-09-22T21:24:38.551Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outcome"
|
||||
version = "1.3.0.post0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.3"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.21.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pysocks"
|
||||
version = "1.7.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.4"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpa-rebate"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "oracledb" },
|
||||
{ name = "selenium" },
|
||||
{ name = "webdriver-manager" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "oracledb", specifier = ">=2.4.1" },
|
||||
{ name = "selenium", specifier = ">=4.15.0" },
|
||||
{ name = "webdriver-manager", specifier = ">=4.0.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=8.0.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "selenium"
|
||||
version = "4.50.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "trio" },
|
||||
{ name = "trio-websocket" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3", extra = ["socks"] },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/6c/09a5bdf96a5f850a2bf69ee670a246ae30a132dca3975c7fb081aae09338/selenium-4.50.0.tar.gz", hash = "sha256:80402fb6f48e0ee2183ec1d58b257a593dcd5c8130c1e6419ee7509f2128be5d", size = 1051762, upload-time = "2026-09-30T19:49:33.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/d3/fedaee179dcd47a7033bb5a74b7d5742da18e46d4ef3b279277c887de64a/selenium-4.50.0-py3-none-any.whl", hash = "sha256:f724a38a7fd8b0561c6767c620bd9b2c7bd8012d4cb664b2ad23064e5d67d0df", size = 12080288, upload-time = "2026-09-30T19:49:30.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sortedcontainers"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trio"
|
||||
version = "0.34.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
|
||||
{ name = "idna" },
|
||||
{ name = "outcome" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "sortedcontainers" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/dc/a2d25ed73ad49cfd79bf18d262577c3731c98e382284e28d522f49a0df35/trio-0.34.0.tar.gz", hash = "sha256:63b9485408bdfdde544fced107045a8c0086cdc4bd0ef2f797b9e0dd111b964b", size = 607457, upload-time = "2026-08-11T00:33:42.198Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/1f/555f1364bed52a92a864181962b77f1b15adadeacf23b86105324363e461/trio-0.34.0-py3-none-any.whl", hash = "sha256:6c7c9f49917694dcdcd5f67abd168df5599eca480d61f29854d17a61a75c2f05", size = 511840, upload-time = "2026-08-11T00:33:40.552Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trio-websocket"
|
||||
version = "0.12.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "outcome" },
|
||||
{ name = "trio" },
|
||||
{ name = "wsproto" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
socks = [
|
||||
{ name = "pysocks" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webdriver-manager"
|
||||
version = "4.1.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/e5/78d362c3507473d27708068fa66c2439b55127571351a096c7ec8eb85e9c/webdriver_manager-4.1.2.tar.gz", hash = "sha256:230515ff4dc4c2feff1add25306ca262421b31016d77ded305e8dd921266445f", size = 38948, upload-time = "2026-06-04T06:00:00.895Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/2c/7414ab135e99df7d9cc4a4afbd21b46906a9397aa2519beac50f6517138e/webdriver_manager-4.1.2-py3-none-any.whl", hash = "sha256:552bbb4e6f95e6e8b772565d2023e7c0ec5ff1736db0b186922d0466fc78c3de", size = 32512, upload-time = "2026-06-04T05:59:59.565Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.9.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/cb/a5abcc2891249f393827c650c6296660ce40374ac22d99ab9aea41f9d2a2/websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", size = 84110, upload-time = "2026-08-31T14:08:40.964Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/d2/cc4dc1271e464942db7ee278baae2daa99ee77cb2af744025c04da585a3e/websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce", size = 95786, upload-time = "2026-08-31T14:08:39.899Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wsproto"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
|
||||
]
|
||||
Loading…
Reference in New Issue