build / build-push (push) Successful in 1m26s
含10-22精修六类修复: 续租renewRunning/结果INCOMPLETE_RESULT门/终态TASK_TERMINAL/liveGrant复用ACL/hosted幂等重放绑定/错误明细整条重序列化。 合规审计与证据: 工作区docs/tasks/REFINEMENT_AUDIT.md + evidence/refinement-20261022/(patch+指纹)。SQL未执行、测试未运行(仅-skipTests编译)。
301 lines
25 KiB
Markdown
301 lines
25 KiB
Markdown
# 答卷投递 v2 与可信建件(answer-delivery-v2)
|
||
|
||
任务书:`docs/tasks/TASK_2.md`(【任务 2】可信 FC 提交 + 完整答卷投影 + SH→FL 办件编排)。
|
||
本文档是**契约与运维权威**:写清"v2 与 v1 差在哪、双方字段闭集、故障状态表、broker 隔离的实值来源、
|
||
可信关联与恢复时序、撤权竞态的生效边界"。代码事实以本文为准,与代码分叉时**先改文档再改代码**。
|
||
|
||
依赖:TASK_1《consumer-access-v3.md》的 `FormConsumerAccessService`(owner/read/subscribe 与
|
||
`grantVersion`)是唯一授权事实源;本契约不写第二套 ACL,只消费它。
|
||
|
||
---
|
||
|
||
## 1. 协议版本差额(先说清"新增了什么、旧的那条怎么算")
|
||
|
||
| 维度 | v1(既有,冻结) | v2(本任务新增) |
|
||
|---|---|---|
|
||
| exchange | `fc.events.v1`(topic,广播) | `fc.answer.delivery.v2`(topic,按消费方私有 routing key) |
|
||
| 消息形状 | 十字段闭集、**不含值文档**(`FcEventPublisher.Envelope`) | `SnapshotEnvelope` 十五字段,**含完整 snapshot** |
|
||
| 版本键 | `version` = 钉住的 schema 版本 | `formVersion`(schema 版本)+ **`answerRevision`**(对外快照单调修订号) |
|
||
| 可靠性 | afterCommit 尽力发、失败仅 warn、"拉取即事实源" | **答卷同事务落 outbox**,afterCommit 只唤醒扫描;publisher confirm + mandatory return |
|
||
| 消费方判据 | 广播 key 用资源 `app_code` | routing key 用**显式 consumerAppKey + 租户域**:`fc.snapshot.v2.<appKey>.<schoolId>` |
|
||
| 授权 | 无(能连 broker 就能看到元数据) | 逐订阅授权 + 每条带 `grantVersion`,发布前复核 |
|
||
| 目标端点 | 无(消费方靠拉取) | 新增 `POST /oapi/form/submission-context`、`GET /oapi/form/submission-context/{intentId}/receipt`、`POST /oapi/form/answer-delivery/receipt`、`POST /oapi/form/answer-delivery/grants/verify` |
|
||
|
||
**v1 一条都不改**:字段闭集、routing key 形态、失败静默的取舍全部保留(`FcEventPublisherTest`
|
||
的十字段断言仍然成立)。差异登记见 §9 待修清单第 3 条。
|
||
|
||
## 2. 快照修订号 `answerRevision`
|
||
|
||
`answer` 新增 `answer_revision BIGINT NOT NULL DEFAULT 0`:创建=1,此后**每个会改变对外快照的路径**
|
||
在同事务内 +1,并与不可变 outbox 行一起落库。覆盖点(一处不漏):
|
||
|
||
| 路径 | 位置 | 事件类型 |
|
||
|---|---|---|
|
||
| 提交(手工/代录/导入/hosted) | `AnswerService#doSubmit` | `created` |
|
||
| 本人编辑值 | `AnswerReadService#editBySubmitter` | `updated` |
|
||
| 管理员编辑值 | `AnswerAdminService#doEdit` | `updated` |
|
||
| **改业务状态(含批量)** | `AnswerAdminService#doChangeStatus` | `updated` |
|
||
| 软删(本人 + 管理面 + 批量) | `softDeleteBySubmitter` / `doSoftDelete` | `tombstoned` |
|
||
|
||
并发控制是两道闸,缺一都可能让"同一 revision 对应两份内容":
|
||
|
||
1. **行级 CAS**:`AnswerRepository#compareAndSetRevision(id, expected, next)`(JPQL UPDATE 自带行锁;
|
||
必须在改动实体字段**之前**调用,否则自动 flush 会先把新号写进去、CAS 自我拒绝);
|
||
2. **outbox 唯一键** `uk_delivery_revision(answer_uuid, answer_revision, consumer_app_key,
|
||
consumer_school_id)`:CAS 没挡住的极端形态在这里撞键,整事务回滚。
|
||
撞键被翻译成 `BizException(CUSTOM)`,**故意不**透传 `DataIntegrityViolationException`——
|
||
提交链路外层会把 DIVE 当"幂等重放"返回原答卷,那等于把并发丢更新伪装成重放成功。
|
||
|
||
没上 JPA `@Version`:那会让存量的分离实体 `save()` 路径(并发 IT、批量导入)突然开始抛
|
||
`OptimisticLockException`,属于跨批次行为变更,不在本任务授权范围内。
|
||
|
||
## 3. 规范化与哈希:`fc.canon.v1`(跨端契约,两端各一份实现)
|
||
|
||
实现:form-center `AnswerCanonicalizer`、svc-hall `AnswerProjectionStore`(消费端复算)。
|
||
规则闭集(要改语义就升算法号,不许原地改):
|
||
|
||
1. 对象键按 **UTF-16 代码元字典序**(`String.compareTo`)升序递归输出——钉比较器而不是"码点序",
|
||
两端都是 JVM,同一个比较器才逐字一致;
|
||
2. 数组顺序保留(多选/排序是业务语义);
|
||
3. 数字 `decimalValue().stripTrailingZeros().toPlainString()`(`100.0→100`、`1E+2→100`,无指数);
|
||
4. 字符串只转义引号、反斜杠与控制字符(`\b\f\n\r\t` 短转义,其余 C0 用反斜杠+u+00+两位大写十六进制),
|
||
非 ASCII(含中文)原样输出,哈希取 UTF-8 字节;
|
||
5. **空值策略:显式 null 成员保留**(缺失键与 `"k":null` 是两种事实,合并会丢"字段被清空"这次变化);
|
||
6. 快照顶层 `intent` 成员**不参与哈希**(它只对单个 consumer 可见,留着就会出现"同 revision 两份 hash");
|
||
7. 时间恒为 `yyyy-MM-dd'T'HH:mm:ss`(不用 `ISO_LOCAL_DATE_TIME`:整分时它会省掉秒段,跨端就比不了);
|
||
8. 投递时间、重试计数、消费端扩展列都是信封/本地列,不进快照。
|
||
|
||
### 跨端夹具(逐字比对,两端测试钉同一串)
|
||
|
||
输入快照(十个成员):`schId=5, appCode="demo", code="honor_apply", submitterId=1001, subjectId=7,
|
||
status="processed", values={"a":{"z":1,"y":null},"b":[2,1],"n":100.0,"e":true},
|
||
answerNo=null, createTime="2026-10-21T10:00:00", updateTime=null`
|
||
|
||
规范串(`FcAnswerDeliveryTest.CANONICAL_VECTOR` = `ShAnswerFlowTest.CANONICAL_VECTOR`):
|
||
|
||
```json
|
||
{"answerNo":null,"appCode":"demo","code":"honor_apply","createTime":"2026-10-21T10:00:00","schId":5,"status":"processed","subjectId":7,"submitterId":1001,"updateTime":null,"values":{"a":{"y":null,"z":1},"b":[2,1],"e":true,"n":100}}
|
||
```
|
||
|
||
`contentHash` = 上述字节串的 SHA-256 小写十六进制(64 位)。
|
||
消费端复算不一致 ⇒ 毒载荷拒绝;与**已存投影行**不一致 ⇒ 冲突待核验不覆盖(§5)。
|
||
|
||
### 信封示例(闭集十五字段;`intent` 只在 `snapshot` 内、只对目标 consumer 出现)
|
||
|
||
```json
|
||
{"protocolVersion":"fc.answer.snapshot.v2","eventId":"5f0a…","eventType":"updated",
|
||
"sourceAppKey":"100021","consumerAppKey":"100018","consumerSchoolId":5,"grantVersion":7,
|
||
"formUuid":"f0e1…","formVersion":4,"answerUuid":"a1b2…","answerRevision":3,"isDeleted":false,
|
||
"contentHash":"<64hex>","occurredAt":"2026-10-21T10:05:00+08:00",
|
||
"snapshot":{"schId":5,"appCode":"demo","code":"honor_apply","submitterId":1001,"subjectId":1001,
|
||
"status":"processed","values":{},"answerNo":null,"createTime":"…","updateTime":"…",
|
||
"intent":{"intentId":"svchall-0123…","consumerAppKey":"100018","consumerSchoolId":5,
|
||
"submitterId":1001,"formVersion":4,"linkedRevision":1}}}
|
||
```
|
||
|
||
不进快照的:`anon_ip`/`anon_ua`(审计数据,契约 v2 C8/C12)、库内 `id`(契约 §1)、附件字节
|
||
(值里只有 `{id,fileName,key}`,P34)。**无订阅的简单表单不产生任何投递行**,答卷照常提交。
|
||
|
||
## 4. 可信提交上下文(A.1~A.4)
|
||
|
||
```
|
||
SH POST /api/apply/intents(body 只有 siteCode/itemCode/clientKey)
|
||
└ 会话取人与校 → 事项入口判定:已发布 ∧ applyUrl 为空 ∧ onlineFlag=1
|
||
∧ TASK_1 成对绑定(fcFormUuid+fcFormVersion) ∧ flowGuid 就绪 ← 任一不过:不建件、不调流程
|
||
└ 落 fc_apply_intent(uk: 校/人/事项/clientKey)
|
||
└ 签名 POST /oapi/form/submission-context(intentId=svchall-<自然键哈希>)
|
||
FC:resolveSignedCaller → requireRead + requireSubscribe
|
||
→ 按 (formUuid,formVersion) 精确取版本行 + 表单当前开放填报
|
||
→ embedOrigin / callbackUrl 取服务端配置(缺键即拒,浏览器 URL 一律不采信)
|
||
→ 同 (appKey,intentId) 回到同一行;令牌在该行上轮换(旧令牌即失效)
|
||
→ 存 SHA-256 摘要 + 过期 + 绑定坐标(默认 30 分钟)
|
||
└ 返回 fillPageUrl = {FC 前端基址}/f/{code}?mode=embed&appCode=…&ctx=<token>
|
||
|
||
被嵌页(form-center front Fill.vue,hosted 通道)
|
||
└ GET /api/form/hosted/schema?contextToken → 按令牌钉的那一版取 schema(绝不回落当前发布版)
|
||
└ POST /api/form/hosted/submit(body 同 /api/form/submit + contextToken)
|
||
FC 在答卷事务内:redeem(存在→未过期→会话用户=断言填报人→会话校=上下文租户域,
|
||
错人/错校/过期/不存在一律拒)→ 坐标逐字核对 → 落答卷(answer_revision=1)
|
||
→ 原子关联 submission_context.answer_uuid(uk 双向唯一)→ 同事务写 outbox
|
||
→ 同 token 同幂等键=原答卷重放;换幂等键重用=拒绝
|
||
└ 成功只 postMessage {proto:"fc",v:1,type:"success",hosted:true} 给**服务端锁定的精确 origin**
|
||
(不带 answerId、不带版本号;referrer 回落与同 hostname 降级都不可用)
|
||
|
||
SH POST /api/apply/intents/{id}/confirm("可确认"信号只触发这一步)
|
||
└ 签名 GET /oapi/form/submission-context/{intentId}/receipt
|
||
FC:只向所属消费应用返回 + 读取时再核一次 requireRead
|
||
→ PENDING(还没答卷,不建件) / SUBMITTED(坐标+提交者+revision+hash+快照)
|
||
└ 逐字复核 consumerAppKey / 学校 / 提交者 / formUuid+formVersion → 建件(一次)
|
||
```
|
||
|
||
**`bizId`/`answerId` 在浏览器可提交的形状里根本没有字段位**——"不能用浏览器传入 bizId 直接建件"
|
||
是结构事实,不是审查约定。匿名与普通提交仍是中心自用的两条既有通道;hosted 首批只允许登录本人,
|
||
匿名答卷的 `submitter_id=0` 与上下文断言的真人对不上,"匿名完成消息用于本人办件"这条路径
|
||
在 redeem 处就被结构性挡住。
|
||
|
||
## 5. 投影、inbox 与顺序无关合并(A.5/B.5)
|
||
|
||
三张表各管一件事,**没有第五张"双轨表"**:
|
||
|
||
| 表 | 唯一键 | 管什么 |
|
||
|---|---|---|
|
||
| `fc_apply_intent` | `(sch,submitter,item,clientKey)`、`(consumerAppKey,fcIntentId)`、`answer_uuid`、`apply_id` | 业务意图;"一次意图一个办件"的库级形态 |
|
||
| `fc_answer_projection` | `(answer_uuid, sch_id)`、`intent_id` | 答卷在本校的唯一投影 + **意图↔答卷↔办件的独立关联表**(保存 formUuid/formVersion/answerRevision/flowGuid) |
|
||
| `fc_answer_inbox` | `(event_id, consumer_app_key)` | 每封事件怎么处置的台账(APPLIED/IGNORED/CONFLICT/DEFERRED/REJECTED)+ 回执上报状态 |
|
||
|
||
规则(`AnswerProjectionStore#applySnapshot`,一个事务内完成,DB 提交成功才 manual ack):
|
||
|
||
- 更高 revision 才应用;更低忽略;同 revision 同 hash 幂等;同 revision 异 hash ⇒ `conflict_flag=1`
|
||
留证**不覆盖**(投影与台账同时记,等人工/对账核验);
|
||
- 墓碑只是"更高 revision 的一种取值",本地保留 revision;旧 `created` 复活不了它;
|
||
- `biz_ext_json` 是业务端扩展列,中心快照永不写;
|
||
- 每条事件**不回查答案值文档**(全文就在消息里);应用结果经 §4 的成对签名端点回执,
|
||
FC 按 `eventId + 消费方 + revision` 逐字复核并幂等记录;
|
||
- **没有已验证意图就不建件**:事件先到 ⇒ 只落 `UNLINKED` 投影;确认先到 ⇒ 意图行落
|
||
`ANSWERED/APPLIED`;后到的一方撞进同一组唯一键,两条路径收敛到同一个 intent/answer,
|
||
不会各建一个办件。
|
||
|
||
## 6. broker 隔离配置(实值来源,缺一条就别开 `RABBITMQ_HOST`)
|
||
|
||
v2 快照带完整值文档,"能连上 broker 的任何人可见"是不可接受的。以下每一项都是**部署契约的
|
||
待填实值**,不是建议:
|
||
|
||
| 项 | 实值来源 | 必须为真的判据 |
|
||
|---|---|---|
|
||
| vhost | projman 注入 `RABBITMQ_VHOST`(form-center 与 svc-hall 同一 vhost,与其他租户隔离) | 该 vhost 内不存在任何非授权消费者可绑的队列;跨应用共享 vhost 上**不得**发明文快照 |
|
||
| 发布方凭据 | `RABBITMQ_USER/PASS`(form-center 侧) | 账号权限:仅对 `fc.answer.delivery.v2` 有 `write`,对 `configure` 只覆盖自身 exchange 声明 |
|
||
| 消费方凭据 | svc-hall 侧同名 env | 只能 `configure`/`write`/`read` 自己前缀的队列与 binding:`svc-hall.fc.answer.snapshot.v2*`、`fc.snapshot.v2.<自身 appKey>.*`;**禁止** `#` 或他人前缀的通配绑定 |
|
||
| 队列与死信 | `FcAnswerMqConfiguration` 声明 | `svc-hall.fc.answer.snapshot.v2`(durable)+ `.dlq`(有界重试 3 次后不 requeue 落此,可查) |
|
||
| confirm/return | form-center `application.yml`:`publisher-confirm-type=correlated`、`publisher-returns=true`、`template.mandatory=true` | 拿不到逐条 confirm 就**不发送**(宁可积压 PENDING,也不按未确认投递) |
|
||
| 隔离能力确认 | 运维在交付单上逐项签字 | 上述任一项不成立时,v2 全量快照**不得**启用;此时只保留"签名收据"这条建件路径(本机联调即该形态) |
|
||
|
||
## 7. 故障状态表(两侧 + 恢复;按码处置,不靠文案)
|
||
|
||
### 7.1 form-center 投递行 `answer_delivery.state`
|
||
|
||
| 状态 | 含义 | 进入条件 | 离开 |
|
||
|---|---|---|---|
|
||
| `PENDING` | 已落库待发 | 答卷事务内入队;或未配 broker 时长期停留 | 扫描取出→发送 |
|
||
| `BROKER_ACCEPTED` | **总线收下** | confirm ack=true | 终态(业务侧结论看 `consumer_applied_at`) |
|
||
| `FAILED` | 发送异常/不可路由/broker nack | AmqpException、mandatory return、ack=false | 退避 `20s×2^n`(base/cap 可配)后重扫 |
|
||
| `DEAD` | 有界重试耗尽 | retry_count ≥ max-retries(8) | 人工/`failedPage` 处置 |
|
||
| `GRANT_REVOKED` | 发布前 grant 复核未过 | 订阅撤销或 `grantVersion` 进位 | 终态,**不重放旧 grant** |
|
||
|
||
**两个时刻分列**:`broker_accepted_at`(只由 confirm 回调写)与 `consumer_applied_at` +
|
||
`consumer_outcome`(只由消费方签名回执写)。broker 的 ack 永远不构成业务成功证据;
|
||
回执自称 `APPLIED` 但 hash 与信封不符 ⇒ 记 `CONFLICT`;同 eventId 重复上报不同结论 ⇒ 保留首次。
|
||
|
||
### 7.2 svc-hall inbox `state` / `report_state`
|
||
|
||
`APPLIED` / `IGNORED`(低 revision 或同版同 hash 重放)/ `CONFLICT`(同版异 hash,不覆盖)/
|
||
`DEFERRED`(授权无法确认,暂缓写入)/ `REJECTED`(闭集或哈希不过,毒载荷 ack 后留档);
|
||
`report_state`=`PENDING/SENT/FAILED` 由重报扫描追平(本地已落库但中心没收到确认时,
|
||
只补确认,不回滚事实)。
|
||
|
||
### 7.3 svc-hall `flow_apply_command.state`
|
||
|
||
| 状态 | 进入 | 处置 |
|
||
|---|---|---|
|
||
| `PENDING` | 与办件同短事务落库 | 事务外发送(HTTP 不占长事务) |
|
||
| `UNKNOWN` | **任何**发送异常(超时/断连/4xx/5xx 一律算未知) | 先 `GET /oapi/flow/instance` 按业务键查 |
|
||
| `DONE` | 发起成功,或查得远端 10/20/30/40 并回填真实状态 | 终态 |
|
||
| `FAILED` | 退避中(含"流程中心未接入",等接入后自愈) | `next_retry_at` 到点重扫 |
|
||
| `DEAD` | 重试预算耗尽(默认 6 次) | 人工处置;不无限重投 |
|
||
|
||
关键取舍:**不靠异常类型猜"远端有没有受理"**。查到 30/40 只回填镜像状态、**绝不重发 apply**
|
||
(重发会命中中心 R4 重启,把已拒绝/已撤回的件重新跑起来);只有查无才用同一 `requestToken` 补发。
|
||
|
||
### 7.4 flow-center 发起守卫分支(`FlowApplyRetryModels.ApplyResultV2.branch`,稳定码)
|
||
|
||
| 码 | 判据 | 是否重启流程 |
|
||
|---|---|---|
|
||
| `CREATED` | 无既有实例 | 新建 + START 推进 |
|
||
| `IDEMPOTENT` | 命中 10/20 | 否(既有语义逐字不变) |
|
||
| `SAME_REQUEST` | `requestToken` 与行上相同 | **否**——同一次命令的重投 |
|
||
| `STALE_ROUND` | 请求 `attempt` ≤ 行 `apply_round`(NULL 按 1) | 否 |
|
||
| `REPLAY_ONLY` | `retryOnly=true` 命中既有实例(含 30/40) | 否 |
|
||
| `RESTARTED` | 更高 `attempt` + 新 token 命中 30/40 | 是,走既有 R4 |
|
||
| `CONCURRENT_CREATED` | 唯一键撞键后事务边界外重读命中 | 否(且先核归属) |
|
||
|
||
归属复核三条:行 `apply_app_key`、`submitter_id`、流程线 `flowGuid` 与请求**任一明确不符即拒**,
|
||
不把别的应用相同 `bizType+bizId` 的办件当自己的结果。存量行(token/round 为 NULL)的守卫**缺省是关**。
|
||
|
||
### 7.5 时序
|
||
|
||
```
|
||
提交点(答卷事务)→ revision+1 → outbox 行 → [提交] → afterCommit 唤醒扫描 → 发送(persistent)
|
||
├ broker ack → broker_accepted_at (≠ 业务成功)
|
||
├ broker nack → FAILED + 退避
|
||
└ 不可路由/异常 → FAILED + 退避 →(耗尽)DEAD
|
||
消费方:manual ack 只在 DB 提交后;签名回执 → consumer_applied_at / consumer_outcome
|
||
进程崩溃窗口:outbox / flow_apply_command 都是库内行 → 重启后扫描续跑(不依赖内存闭包)
|
||
```
|
||
|
||
## 8. TASK_3 必须一次交付的扩展点(签名即契约,后续只调用不编辑)
|
||
|
||
form-center:`AnswerSnapshotService.currentAuthorized(ConsumerPrincipal, Long consumerSchoolId, String answerUuid)`、
|
||
`AnswerDeliveryService.replayAuthorized(ConsumerPrincipal, String answerUuid, Long expectedRevision, String operationId)`
|
||
(稳定码 `QUEUED/ALREADY_DELIVERED/GRANT_REVOKED/REVISION_MISMATCH/NO_DELIVERY_ROW`,
|
||
`operationId` 幂等吸收)、`AnswerDeliveryService.failedPage(ConsumerPrincipal, Long, Integer, Integer)`。
|
||
svc-hall:普通消费保留 `AnswerProjectionStore.applySnapshot(SnapshotEnvelope)`;对账修复与分页改用下方§8.1明确的
|
||
`repairAuthoritative(...)`、`manifestPage(...)`(只出轻量摘要与本地创建时间,不拉值文档)、
|
||
`retireAbsent(AuthoritativeAbsenceProof)`(只做退役标记,不物理删、不改 revision;
|
||
`expectedRevision` 与本地不符时**拒绝退役**并留证)。
|
||
快照适配与对账共用 §3 的 canonicalization/hash 契约,两端不得各写一套。
|
||
|
||
### 8.1 Task2 前置修正:对账与普通消费分离(2026-10-21)
|
||
|
||
- 普通 `applySnapshot` 继续按MQ规则处理,同revision异hash不覆盖,且可能关联意图并建件;**TASK_3不得使用它执行修复**。
|
||
- 新 `RepairResult repairAuthoritative(SnapshotEnvelope envelope, Long expectedRevision, String expectedHash, Integer expectedIsDeleted, String proofId)` 是服务端内部入口。TASK_3须先经平台签名的FC任务端点读取当前快照并核验task/consumer/school/form/answer/version/grant;proofId仅为审计引用,不是签名或浏览器可提交的授权凭据。使用现有fc.canon.v1复算hash,并绕过MQ授权缓存重新核验订阅且grantVersion必须相等。
|
||
- expectedRevision/hash/isDeleted是本地读取的完整三元组;全部null仅表示观察到缺失。已有行用单条数据库条件更新检查三元组与school/form/answer仍匹配,更新仅限快照列与修复留证,保留bizExt/intent/apply/flow;缺失行插入UNLINKED投影。修复不调用建件、不写inbox或消费回执、不改意图。唯一键竞争须回滚并重新读取。
|
||
- SH投影新增本地 `projection_version` 乐观锁列(与FC revision无关);权威条件更新同时递增该列,已读取旧实体的MQ/收据写入将触发乐观锁冲突,不能把权威修复覆盖回旧值。冲突必须整笔回滚,在事务外重新读取后有界重试。已有TASK_2库先执行 `svc-hall/server/sql/add_fc_projection_version.sql`,新库直接使用更新后的 `fc_projection_and_apply_intent.sql`,两脚本不可重复套用;本次未执行数据库升级。
|
||
- 权威修复信封eventId最多36字符,可用任务/答卷操作的UUID;proofId最多128字符。新鲜授权响应必须同时匹配form/consumer/school及相等grantVersion,不能用别校/别应用响应或未来纪元放行。
|
||
- 稳定返回:`APPLIED`、`LOCAL_CHANGED`(期望已变化,重新读取后有界重试)、`LOCAL_AHEAD`(本地revision更高,向FC复核且不降级)、`DEFERRED`(新鲜授权核验不通过,不写入)。同revision异hash仅在此权威入口可修复,普通MQ规则不变。
|
||
- `ManifestPage manifestPage(Long schId, String formUuid, String cursor, Integer size)` 使用answerUuid升序keyset,初始cursor=null,按返回nextCursor继续;size默认200/上限500,多读一条确定completed。返回 `{rows,nextCursor,completed}`;rows为answerUuid/revision/contentHash/isDeleted/schId/createTime,结束时completed=true且nextCursor=null,不暴露库内id。
|
||
- 本地分页不提供一致性快照证明。TASK_3须保存扫描结果/检查点,并用完整FC物化manifest、新鲜缺席证明及任务起点校验退役候选;本地createTime为空或晚于任务起点时不可退役。分页中途新增/变动不得被“没看到”解释为中心不存在。
|
||
- 新增规则门禁 `ShReconcileExtensionTest`;MySQL实际条件更新/唯一键并发、签名任务端点及Rabbit运行期行为仍需联调验证,规则单测不替代它们。
|
||
|
||
## 9. 待修清单(本轮刻意不修;跨校那条必须在本任务联调验收前解决)
|
||
|
||
1. **跨校授权缺"调用方属于该租户域"的事实源(TASK_1 遗留,优先级最高)**。
|
||
`FormConsumerAccessService` 判的是"(formUuid, consumerSchoolId, consumerAppKey) 三元键上有没有生效
|
||
grant",而 `consumerSchoolId` 一直是**调用方申报**的:grant 创建侧只限制"校管理员只能授给本校",
|
||
超管可代任意校授,于是"这个 appKey 到底能不能在 school=6 下消费数据"没有第二重证明。
|
||
本任务的可信建件把这条链放大成写侧风险——SH 申报的是事项所在校,中心照单核。
|
||
修向(择一,需授权):FC 侧建立 appKey↔可服务学校集合的事实源(UserBase 注册信息),
|
||
或把 grant 创建限定为"消费方应用注册校"。**本轮按边界要求不改 TASK_1 的 ACL/目录文件**。
|
||
2. **托管填报的资格口径两侧不同**:SH 入口判"事项发布 + 在线办理 + FC 绑定",FC 仍按表单
|
||
`audience` 门判"这个会话身份能不能填这张表"。若中心侧表单人群配得比事项服务对象严
|
||
(例如只含教师),学生会话会出现"能打开被嵌页、提交被 403"。修向:要么 SH 入口镜像 audience,
|
||
要么 hosted 通道把资格判据换成"意图授权"。本轮按任务书口径**保留 audience 门不动**
|
||
(它同时是 FC 自用通道的判据,改它要单独授权)。
|
||
3. **v1 与 v2 的事件口径分叉**:改业务状态现在只产生 v2 的 `updated` 投递,
|
||
v1 `fc.events.v1` 仍不发状态变更事件(v1 的 `type` 闭集与消费方约定都在冻结范围外)。
|
||
需要显式决定"v1 冻结并声明"还是"v1 同步补发",否则仍依赖 v1 的旧消费方看不到 status 变化。
|
||
4. **应急对账闸不认识新命令表**:`FlowReconcileJob` 仍只看 `service_apply.flow_status`,
|
||
不知道 `flow_apply_command` 的 `UNKNOWN` 行(该文件构造器被基线测试逐位钉住,本轮不动)。
|
||
大面积事故时开闸对账与恢复扫描可能同时推进同一条链路,需要后续统一到一个处置出口。
|
||
5. **回调"更高版本 ⇒ 新一轮"的判定还没与 attempt 映射对齐**(任务书 C.4 后半句,本轮未做):
|
||
`SvcFlowCallbackHandler#advance` 仍按 `instanceVersion > last_instance_version` 认定新轮,
|
||
而本轮新增的持久轮次在 `flow_apply_command.attempt` 上,两侧没有交叉核对。
|
||
当前实际影响有限(R4 重启只由用户显式重提触发,且 `markResubmitted` 会清空旧轮结果),
|
||
但"版本更高"在严格口径下**不自动证明**是新一轮——需要回调侧按 intent/attempt 映射复核后再放行。
|
||
本轮按"不顺手改既有接收器"处理,留待与 §9#4 一并在统一处置出口里做。
|
||
|
||
## 10. 未实现范围与证据边界(照实说)
|
||
|
||
- **问卷类独立后端未定位**:本文 §3~§7 是可直接执行的消费协议与适配要求(信封、canonicalization、
|
||
inbox/投影唯一键、回执、撤权竞态)。**未宣称"三端之外所有消费者已接入"**,也不虚构该消费端源码。
|
||
- **部门实施冻结**:新办件首次发起只做既有规则的成员就绪检查;部门来源切换、未来节点
|
||
冻结/改派/挂起、当前科室停用行为细则**不在本任务**(按已知限制记录,不声称实现 D10 全部细则)。
|
||
- 前端只做到**构建级验证**(`vite build` 两条绿),本机浏览器通道不可用 ⇒ 没有 iframe 视觉证据;
|
||
"消息来源三重校验(精确 origin + `event.source` + proto/v)"目前只有代码与协议级依据。
|
||
- 测试是**规则单测**(FC 21 / SH 26 / FL 12 条,全绿)。桩事务能证"答卷写与 outbox 写在同一事务回调内、
|
||
回调抛出后两者都不落地、afterCommit 只唤醒扫描",**证不了 MySQL 的原子性**;
|
||
manual ack、死信路由、publisher confirm 的 broker 行为在本机无 rabbit 的情况下未跑通。
|
||
这两类都要在联调环境补做,才算 §6/§7 的完整验收。
|
||
- DDL 三个文件(`form-center/service/sql/answer_delivery_v2.sql`、
|
||
`svc-hall/server/sql/fc_projection_and_apply_intent.sql`、
|
||
`flow-center/service/src/main/resources/sql/add_flw_apply_request_identity.sql`)
|
||
都是**非幂等**脚本,生产需人工执行;`ddl-auto=update` 的开发库会自动追加,两侧不能长期分叉。
|