oh-my-muse/docs/agent-specs/2026-06-07-P1R7eAccountSourceOwnerPropagation执行版.md

787 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# P1R7e Account Source Owner Propagation 执行版
## 结论
P1R-7e 只执行 Account/Member source owner propagation 的第四 owner 切片:把 Account quota adjustment terminal fact 转成 Account 本域 Events publish outbox再由 Account worker 调用 `EventsPublishApi` 写入 `muse_unified_event`,最终通过 `/app-api/muse/events` 对被调整账户用户 SSE 可见。
本执行版不允许把 Events / P1R-7 / Account 推进到 `completed`,不允许修改 OpenAPI、coverage scanner 或 coverage report不允许把 security event、quota request、integration call 或 account audit 表复用为 Events publish outbox。
```mermaid
flowchart LR
Command["adminCreateQuotaAdjustment<br/>commandId + expected snapshot"] --> Quota["muse_member_quota<br/>limit / used / revision"]
Command --> Audit["muse_account_audit<br/>admin operation audit"]
Command --> Log["muse_member_entitlement_audit_log<br/>one row per resourceType"]
Log --> Outbox["muse_account_event_publish_outbox<br/>one row per entitlement audit log"]
Outbox --> Worker["Account publish worker<br/>claim/retry/dead_letter"]
Worker --> Api["EventsPublishApi<br/>events-api only"]
Api --> Unified["muse_unified_event<br/>notification/quota_alert"]
Unified --> Stream["/app-api/muse/events<br/>account owner visible SSE"]
```
## Review Gate
P1R-7e 审阅版已通过 fresh review
- Gibbs spec compliance reviewPASS无 P0/P1/P2。
- Ampere quality / feasibility reviewPASS无 P0/P1/P2。
旧 Goodall feasibility review 对早期 security event 候选 FAIL。该反馈已验证有效并已在审阅版中通过改选 `quota adjustment -> quota_alert` 关闭;旧 FAIL 不计入当前最终审阅版 PASS gate。
P1R-7e 执行版第三轮 fresh review 已通过:
- Pauli execution spec compliance reviewPASS无 P0/P1/P2已验证 scope 未扩大、protected staged/unstaged diff gate、V20 migration 编号前提、P1R mixed gate、Surefire XML 计数、隔离 coverage scanner 和 completed 边界。
- Hooke execution quality / feasibility reviewPASS无 P0/P1/P2已验证 `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` 同事务 audit log / outbox 接入点、Events `quota_alert` 合同、payload/resourceRef/sourceRevision/timestamp 合同、最终验证假绿口。
旧执行版 review 中Pascal PASS / Schrodinger FAIL、Feynman PASS / Kant FAIL 均因后续执行版修订失效,不计入当前最终 PASS gate其中有效反馈已经关闭到本执行版。本轮双 PASS 只允许进入 fresh implementer 实现,不代表 P1R-7e 实现完成,也不代表 Events / P1R-7 / Account completed。
非阻塞风险已在本执行版冻结:
- 每条 `muse_member_entitlement_audit_log` 发布一条 `quota_alert`,不做 command 聚合。
- `resourceRef.resourceId` 固定为数值型 entitlement audit log id。
- `sourceRevision` 固定使用 Events 现有 `__none__`,不新增 audit log revision。
## 边界
### 本轮目标
- 在 Member(Account) owner 内新增独立 Events publish outbox。
- 只接入 `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` 首次成功路径。
- 只发布 `muse_member_entitlement_audit_log(change_type=quota_adjustment)` 形成的用户可见 quota alert。
- 每条 entitlement audit log 对应一条 outbox 和一条 Events notification。
- 通过 Account worker 调用 `EventsPublishApi`
- 用 focused tests、P1R mixed gates、Flyway `_test`、dependency gate、dependency tree 和隔离 coverage scanner 证明链路进入 `needs_verification` 证据层。
### 本轮非目标
- 不发布 `muse_member_security_event`
- 不发布 `muse_account_security_event_ack`
- 不发布 `muse_account_quota_request`
- 不发布 `muse_account_integration_call`
- 不发布 `muse_member_usage_record`
- 不发布 New-API binding、call attribution job、export task、download credential 或 file service 事件。
- 不发布 admin reason 原文、requestHash、before/after snapshot、operatorUserId、correlationId、external provider detail、error stack 或 credential。
- 不抽取 AI / Knowledge / Market / Account 共享 outbox 框架。
- 不为历史 entitlement audit log 做 backfill。
- 不修改 `docs/api-contracts/*/openapi.yaml`
- 不修改 `muse-cloud/scripts/p1r-audit-api-coverage.py`
- 不修改 `docs/superpowers/reports/p1r-api-coverage.json``.md`
### 受保护文件
实现代理不得修改以下文件:
- `docs/api-contracts/account/openapi.yaml`
- `docs/api-contracts/market/openapi.yaml`
- `docs/api-contracts/ai/openapi.yaml`
- `docs/api-contracts/knowledge/openapi.yaml`
- `docs/api-contracts/events/openapi.yaml`
- `muse-cloud/scripts/p1r-audit-api-coverage.py`
- `docs/superpowers/reports/p1r-api-coverage.json`
- `docs/superpowers/reports/p1r-api-coverage.md`
## 已验证事实
- 正确 worktree 是 `/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0`
- 当前分支是 `dev/1.0.0`,远端跟踪是 `origin/dev/1.0.0`
- `git pull --ff-only origin dev/1.0.0` 返回 `Already up to date.`
- 当前 HEAD 为 `e8ed7d7 test(p1r): 收口 Market 事件传播真实链路门禁`
- P1R-7b 已提交并 push`3db5fbe``230152c`
- P1R-7c 已提交并 push`cef686b``e55618f`
- P1R-7d 已提交并 push`b36b153``e8ed7d7`
- P1R-7e 审阅版路径为 `docs/agent-specs/2026-06-07-P1R7eAccountSourceOwnerPropagation审阅版.md`
- P1R-7e 审阅版 fresh review 已双 PASSGibbs spec PASSAmpere quality PASS。
- 当前 coverage 边界仍为 `completedOperations=100``needsVerificationOperations=133``incompleteOperations=0``genericPersistenceOperations=0``ssePlaceholderOperations=0`
- Events `streamEvents` 当前是 `dedicated / needs_verification`
- Account 33 operations 当前是 `dedicated / needs_verification`
- 当前受保护文件 diff 为空。
- 当前最新 migration 是 `muse-cloud/sql/muse/V19__extend_market_events_publish_outbox.sql`P1R-7e 下一 migration 编号应为 `V20`。若实现前 live 文件列表已出现更高版本,必须停止并修订本执行版的 migration 编号。
- `EventsPublishReqDTO` 必填 `commandId``tenantId``ownerUserId``sourceOwner``sourceType``sourceId``eventType``payloadSummary``emittedAt`
- `EventsPublishServiceImpl` 当前先按 `commandId` 回放,再按 source tuple 回放。
- `EventsPublishServiceImpl` 当前已声明 `notification` event type。
- `EventsPublishServiceImpl` 当前已声明 `quota_alert` notification type。
- Events OpenAPI `SSENotificationEvent.data.resourceRef.resourceId` 为 int64。
- `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` 成功后写 `muse_member_entitlement_audit_log(changeType=quota_adjustment)`,每个 adjustment 一条 log。
- `MemberEntitlementAuditLogDO` 包含 `id``accountUserId``changeType``sourceOwner``sourceId``idempotencyKey``beforeValueSnapshot``deltaValueSnapshot``afterValueSnapshot``reasonCode``reasonMessage``operatorUserId``auditEventId`
- `MemberEntitlementAuditLogMapper.selectPageByAccountUserId` 已把 quota ledger 固定为 `quota_adjustment`,并按 OpenAPI source owner allowlist 过滤。
- `muse-module-member-server/pom.xml` 当前没有 `muse-module-events-api` 依赖;实现只允许新增 events-api direct dependency不得新增 events-server dependency。
- `MemberSecurityEventDO` 虽有 App 读侧和 ack但当前高敏导出生产写入路径被 fail-closed 阻断,不是 P1R-7e 第一切片 source fact。
## 推断
- P1R-7e 可复用 P1R-7b/P1R-7c/P1R-7d 的 outbox 状态机、worker claim、stale claim guard、dependency tree gate 和 Flyway `_test` 模式。
- Account 第一切片按 entitlement audit log 逐条发布,比按 command 聚合更稳source fact 粒度、idempotencyKey 粒度和 resourceType 粒度一致。
- Account outbox 与 entitlement audit log 必须保持两张表 / 两套状态机,否则 Account ledger 查询事实与 Events publish 补偿状态会互相污染。
## 假设
- P1R-7e 允许 Member server 新增 `muse-module-events-api` direct dependency。
- P1R-7e 允许新增 `muse_account_event_publish_outbox`
- P1R-7e 第一批通知的 SSE owner 是 `MemberEntitlementAuditLogDO.accountUserId`,不是 admin operator。
- P1R-7e 第一批使用 Events 现有 `notification/quota_alert`,不新增 OpenAPI event type 或 notification type。
## 设计合同
### 事件合同
第一批只允许以下 Account source fact 进入 publish outbox
| Account source fact | changeType | Events eventType | notification type | SSE owner | message |
|---|---|---|---|---|---|
| `muse_member_entitlement_audit_log` | `quota_adjustment` | `notification` | `quota_alert` | `accountUserId` | `额度已调整` |
多 resourceType 同一 command 时,必须按 entitlement audit log 逐条发布,不允许同时做 command 聚合。
### 幂等合同
- 每条 entitlement audit log 只生成一条 outbox。
- `commandId` 格式:`acct_evt:` + SHA-256(`tenantId|auditLogId|accountUserId|quota_alert`) 前 32 位 hex。
- `outboxId` 格式:`acct_out:` + SHA-256(`tenantId|auditLogId|accountUserId|quota_alert`) 前 32 位 hex。
- Events source tuple
- `sourceOwner = account`
- `sourceType = account_quota_adjustment`
- `sourceId = entitlementAuditLog.id`
- `sourceRevision = __none__`
- `eventType = notification`
- 同一 entitlement audit log 重放必须得到同一 Events `eventId/sequenceNo`
- 同一 command 下不同 resourceType 的 entitlement audit log 必须生成不同 commandId 和不同 source tuple。
- Command replay 策略:`adminCreateQuotaAdjustment` 命中 Account command replay 时不补历史 outbox只有首次成功写入 entitlement audit log 的事务内创建 outbox。历史 backfill 若需要,必须另起任务。
### Payload 合同
Internal outbox 允许保存以下字段:
- `auditLogId`
- `sourceCommandId`
- `accountUserId`
- `operatorUserId`
- `resourceType`
- `deltaAmount`
- `sourceOwner`
- `auditEventId`
- `requestHash`
- `sourceRevision`
- `message`
- `resourceRefType`
- `resourceRefId`
Events `payloadSummary` 只能发布:
```json
{
"type": "quota_alert",
"message": "额度已调整",
"resourceRef": {
"resourceType": "account_quota",
"resourceId": 8001
},
"timestamp": "2026-06-07T12:00:00"
}
```
约束:
- `resourceRef.resourceId` 必须是数值型 Long / Integer 语义,值为 entitlement audit log id。
- DTO `EventsPublishReqDTO.resourceId` 是 String可以传 `String.valueOf(auditLogId)`payload `resourceRef.resourceId` 必须保持 Number不能是 String。
- `message` 第一切片只允许 `额度已调整`
- `timestamp` 使用 entitlement audit log 的 `createTime`,缺失时才回退 outbox `createTime`
- 不得发布 `reasonMessage``reasonCode``beforeValueSnapshot``deltaValueSnapshot``afterValueSnapshot`、operatorUserId、correlationId、external provider detail、requestHash、error stack。
- `payloadSummary` 必须是 exact allowlist顶层只能包含 `type``message``resourceRef``timestamp``resourceRef` 只能包含 `resourceType``resourceId`
### 状态机合同
Outbox 本域状态只允许:
- `queued`
- `running`
- `retryable`
- `published`
- `dead_letter`
状态流转:
```mermaid
stateDiagram-v2
[*] --> queued
queued --> running: claim
retryable --> running: retry due
running --> running: expired lease reclaim
running --> published: Events accepted
running --> dead_letter: Events rejected/blocked
running --> retryable: temporary failure and attempts remain
running --> dead_letter: retry exhausted
```
Worker claim 要求:
- 使用 PostgreSQL `FOR UPDATE SKIP LOCKED` 或等价 `UPDATE ... RETURNING` 原子领取。
- 调度线程无请求租户上下文时必须先忽略租户拦截器领取,再使用 outbox 行内 `tenantId` 恢复租户上下文发布。
- Claim 阶段唯一递增 `attemptCount`
- 临时失败只释放 claim、设置 `nextRetryAt` 和安全错误摘要,不二次递增 attempt count。
- `markPublished` / `markRetryable` / `markDeadLetter` 必须绑定 `publish_status='running' AND attempt_count=#{claimedAttemptCount}`
- terminal update 返回 0 时必须记录 stale claim 诊断日志,包含 tenantId、accountUserId、outboxId、auditLogId、attempt、targetStatus、errorCode。
- Worker 默认关闭:`muse.account.events.publish-worker.enabled=false`
- 调度配置键:
- `muse.account.events.publish-worker.initial-delay-ms`
- `muse.account.events.publish-worker.fixed-delay-ms`
- `muse.account.events.publish-worker.max-attempt`
- `muse.account.events.publish-worker.claim-timeout-seconds`
### 失败路径合同
| 路径 | 结果 |
|---|---|
| 无 entitlement audit log | 不创建 outbox |
| changeType 非 `quota_adjustment` | 不创建 outbox |
| accountUserId null 或 <= 0 | 不创建 outbox记录安全日志 |
| audit log id null | 不创建 outbox记录安全日志 |
| payload 构造缺 required 字段 | 创建 outbox 前拒绝并记录安全日志,不创建 outbox不发布错误码固定为 `ACCOUNT_EVENTS_PAYLOAD_INVALID` |
| Events accepted | outbox -> published回写 eventId / sequenceNo |
| Events duplicate accepted | outbox -> published回写既有 eventId / sequenceNo |
| Events rejected / blocked | outbox -> dead_letter |
| CommonResult error / exception | attempt 未耗尽 -> retryable耗尽 -> dead_letter |
| command replay | 不补历史 outbox不重复发布 |
## 涉及文件
### 生产代码
- Modify: `muse-cloud/muse-module-member/muse-module-member-server/pom.xml`
- Modify: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountQuotaServiceImpl.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxService.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxServiceImpl.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishWorker.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPayloads.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/dataobject/account/AccountEventPublishOutboxDO.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountEventPublishOutboxMapper.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/framework/config/MuseAccountEventsConfiguration.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/framework/config/MuseAccountEventsProperties.java`
- Create: `muse-cloud/sql/muse/V20__extend_account_events_publish_outbox.sql`
### 测试代码
- Modify: `muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountQuotaServiceTest.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxServiceTest.java`
- Create: `muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishWorkerTest.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishDependencyTest.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishEndToEndTest.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishFlywayMigrationIT.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishMigrationSqlTest.java`
### 文档
- Modify: `docs/agent-specs/.agent`
- Create: `docs/memorys/YYYY-MM-DD-P1R7eAccountSourceOwnerPropagation真实链路.md`
## 实施任务
### Task 0fresh implementer preflight
- 在 worktree 根执行:
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
git status --short --branch
git log --oneline -5
git pull --ff-only origin dev/1.0.0
git diff --quiet -- docs/api-contracts/account/openapi.yaml docs/api-contracts/market/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/events/openapi.yaml muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md
git diff --cached --quiet -- docs/api-contracts/account/openapi.yaml docs/api-contracts/market/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/events/openapi.yaml muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md
ls muse-cloud/sql/muse/V*__*.sql | sort -V | tail
```
- 若最新 migration 已不是 V19停止并修订 V20 编号。
- 若 protected diff 非空,停止并汇报。
- 若远端有更新且无法 fast-forward停止并汇报。
完成条件:
- 记录 dirty baseline。
- 确认 protected diff 为空。
- 确认 V20 编号仍可用。
### Task 1依赖与配置骨架
目标:
- Member server 只新增 `muse-module-events-api` direct dependency。
- 新增 Account Events worker 配置,默认关闭。
文件:
- Modify: `muse-cloud/muse-module-member/muse-module-member-server/pom.xml`
- Create: `MuseAccountEventsConfiguration.java`
- Create: `MuseAccountEventsProperties.java`
要求:
- 不新增 `muse-module-events-server`
- 配置 prefix`muse.account.events`
- 默认:
- `publish-worker.enabled=false`
- `publish-worker.max-attempt=5`
- `publish-worker.claim-timeout-seconds=60`
- 配置类必须可被 Spring 扫描。
Focused tests
- 配置默认值。
- worker disabled 时 `dispatchOnce()` 不 claim。
### Task 2V20 outbox schema
目标:
- 新增 `muse_account_event_publish_outbox`,只服务 Account -> Events publish compensation。
文件:
- Create: `muse-cloud/sql/muse/V20__extend_account_events_publish_outbox.sql`
- Create: `AccountEventPublishOutboxDO.java`
- Create: `AccountEventPublishOutboxMapper.java`
- Create: `P1rAccountEventsPublishMigrationSqlTest.java`
表字段必须包含:
- `id`
- `tenant_id`
- `outbox_id`
- `command_id`
- `source_command_id`
- `audit_log_id`
- `account_user_id`
- `operator_user_id`
- `resource_type`
- `delta_amount`
- `source_owner`
- `audit_event_id`
- `request_hash`
- `source_revision`
- `event_type`
- `notification_type`
- `resource_ref_type`
- `resource_ref_id`
- `payload_summary`
- `publish_status`
- `attempt_count`
- `max_attempt`
- `next_retry_at`
- `claimed_at`
- `claim_expires_at`
- `last_error_code`
- `last_error_message`
- `published_event_id`
- `published_sequence_no`
- `creator`
- `updater`
- `create_time`
- `update_time`
- `deleted`
唯一约束:
- `(tenant_id, outbox_id)`
- `(tenant_id, command_id)`
- `(tenant_id, audit_log_id, notification_type)`
Check 约束:
- `event_type = 'notification'`
- `notification_type = 'quota_alert'`
- `publish_status in ('queued','running','retryable','published','dead_letter')`
- `account_user_id > 0`
- `audit_log_id > 0`
- `attempt_count >= 0`
- `max_attempt > 0`
- `resource_ref_id > 0`
索引:
- claim index`publish_status, next_retry_at, claim_expires_at`
- owner status index`account_user_id, publish_status`
- audit log index`audit_log_id`
Trigger
- `update_time` trigger。
Mapper 方法:
- `insertIgnore`
- `selectByTenantIdAndOutboxId`
- `selectByTenantIdAndCommandId`
- `selectByTenantIdAndAuditLogIdAndNotificationType`
- `claimNextPublishOutbox`
- `markPublished`
- `markRetryable`
- `markDeadLetter`
SQL gate
- 断言字段、唯一约束、check 约束、索引、trigger。
- 断言非法 `event_type``notification_type``publish_status``account_user_id``audit_log_id``resource_ref_id` 在 Flyway `_test` 中被 DB 拒绝。
### Task 3Account outbox service
目标:
- 在 Account quota adjustment 首次成功事务内,为每条 entitlement audit log 创建一条 outbox。
文件:
- Create: `AccountEventPublishOutboxService.java`
- Create: `AccountEventPublishOutboxServiceImpl.java`
- Create: `AccountEventPayloads.java`
- Modify: `AccountQuotaServiceImpl.java`
- Test: `AccountEventPublishOutboxServiceTest.java`
- Test: `AccountQuotaServiceTest.java`
接入点:
- `AccountQuotaServiceImpl.adminCreateQuotaAdjustment`
- 在每个 `MemberEntitlementAuditLogDO` insert 后调用 outbox service。
- command replay 返回时不得创建 outbox。
Outbox service 输入:
- `MemberEntitlementAuditLogDO log`
- source command id必须优先由调用方传入原始 `reqVO.getCommandId()`;只有回查历史 log 且调用方无法传入时,才允许从 `log.idempotencyKey``lastIndexOf(':')` 切出 commandId禁止用第一个冒号切分。
- request hash允许 outbox 内部保存,不允许 Events payload 发布。
Payload 构造:
```json
{
"type": "quota_alert",
"message": "额度已调整",
"resourceRef": {
"resourceType": "account_quota",
"resourceId": 8001
},
"timestamp": "2026-06-07T12:00:00"
}
```
Payload validator
- 顶层 exact keys`type/message/resourceRef/timestamp`
- `resourceRef` exact keys`resourceType/resourceId`
- `type=quota_alert`
- `message` 只允许 `额度已调整`
- `resourceType=account_quota`
- `resourceId` 必须是 Number。
- 不允许 requestHash、reason、snapshot、operator、correlation、error 等字段。
错误码:
- payload invalid 固定为 `ACCOUNT_EVENTS_PAYLOAD_INVALID`
Focused tests
- 首次 successful quota adjustment 为每个 audit log 创建 outbox。
- command replay 不创建 outbox。
- invalid owner 不创建 outbox并记录安全日志。
- payload exact allowlist敏感字段负断言。
- 多 resourceType 产生多条不同 outbox。
- configured maxAttempt 生效。
- timestamp 使用 audit log createTime缺失时回退 outbox createTime。
### Task 4Account publish worker
目标:
- Claim Account outbox构造 `EventsPublishReqDTO`,调用 `EventsPublishApi`,按结果更新 outbox 状态。
文件:
- Create: `AccountEventPublishWorker.java`
- Test: `AccountEventPublishWorkerTest.java`
Events request
- `commandId = outbox.commandId`
- `tenantId = outbox.tenantId`
- `ownerUserId = outbox.accountUserId`
- `sourceOwner = account`
- `sourceType = account_quota_adjustment`
- `sourceId = String.valueOf(outbox.auditLogId)`
- `sourceRevision = __none__`
- `eventType = notification`
- `resourceType = outbox.resourceRefType`
- `resourceId = String.valueOf(outbox.resourceRefId)`
- `payloadSummary = parsed exact allowlist payload`
- `emittedAt = outbox.createTime`
状态处理:
- accepted -> `published`
- duplicate accepted -> `published`
- rejected / blocked -> `dead_letter`
- CommonResult error -> `retryable` 或耗尽后 `dead_letter`
- exception -> `retryable` 或耗尽后 `dead_letter`
- payload invalid -> `dead_letter`,不调用 Events
- unknown status -> `dead_letter`
Focused tests
- disabled 不 claim。
- enabled claim 使用配置 claim timeout。
- accepted 回写 eventId / sequenceNo。
- duplicate accepted 回写既有 eventId / sequenceNo。
- rejected / blocked dead_letter。
- CommonResult error 非最后一次 retryable最后一次 dead_letter。
- exception 非最后一次 retryable最后一次 dead_letter。
- invalid payload dead_letter 且不调用 Events。
- owner invalid dead_letter 且不调用 Events。
- terminal update 使用 claimed attempt ownership。
- stale terminal update 返回 0 时记录诊断日志。
### Task 5P1R executable gates
目标:
- 用 server 侧 gate 证明依赖方向、focused E2E、V20 migration 和既有 owner gate 不回退。
文件:
- Create: `P1rAccountEventsPublishDependencyTest.java`
- Create: `P1rAccountEventsPublishEndToEndTest.java`
- Create: `P1rAccountEventsPublishFlywayMigrationIT.java`
- Create: `P1rAccountEventsPublishMigrationSqlTest.java`
Dependency test
- Member server 有 direct `muse-module-events-api`
- Member server 无 `muse-module-events-server`
- Events server 无 AI / Knowledge / Market / Member / Content server。
E2E focused test
- 构造 quota adjustment source fact。
- Account outbox service 创建 outbox。
- Worker 发布到 fake / in-memory Events publish API 或真实 Events service harness。
- `muse_unified_event` 中 owner 为 accountUserId。
- SSE visible query 对 account owner 可见,对其他 owner 不可见。
- payload 不含 requestHash、reason、snapshot、operator、correlation。
- 多 resourceType 产生多条独立通知。
- rejected -> outbox dead_lettervisible query 为空。
- stale claim terminal update 被忽略。
Flyway `_test`
- 使用真实 PostgreSQL `_test` 数据库。
- 跑到 V20。
- 验证 table / indexes / constraints / trigger。
- 验证非法 insert 被 DB 拒绝。
### Task 6最终验证、review 和留痕
最终验证顺序必须先 build再 tests避免 `clean` 清理 surefire XML。
#### 1. Full reactor build
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o clean install -DskipTests -Dspring-boot.repackage.skip=true
```
期望62/62 或 live reactor 全量 modules success。
#### 2. Account focused tests
先断言新增测试文件存在:
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
test -f muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxServiceTest.java && \
test -f muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishWorkerTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishMigrationSqlTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishDependencyTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishEndToEndTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishFlywayMigrationIT.java
```
运行:
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test \
-pl muse-module-member/muse-module-member-server -am \
-Dtest=AccountQuotaServiceTest,AccountEventPublishOutboxServiceTest,AccountEventPublishWorkerTest \
-Dsurefire.failIfNoSpecifiedTests=false
```
必须检查 surefire XML目标测试类 `tests>0` 且 failures/errors/skipped 均为 0。
#### 3. P1R mixed executable gates
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test \
-pl muse-server -am \
-Dtest=P1rAccountEventsPublishMigrationSqlTest,P1rAccountEventsPublishDependencyTest,P1rAccountEventsPublishEndToEndTest,P1rAccountRealApiGateTest,P1rAccountRouteOwnershipTest,P1rMarketEventsPublishMigrationSqlTest,P1rMarketEventsPublishDependencyTest,P1rMarketEventsPublishEndToEndTest,P1rKnowledgeEventsPublishMigrationSqlTest,P1rKnowledgeEventsPublishDependencyTest,P1rKnowledgeEventsPublishEndToEndTest,P1rAiEventsPublishMigrationSqlTest,P1rAiEventsPublishDependencyTest,P1rAiEventsPublishEndToEndTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \
-Dsurefire.failIfNoSpecifiedTests=false
```
必须检查 surefire XML新增 Account 三个 P1R gate 和既有 Account owner gate 测试类 `tests>0` 且 failures/errors/skipped 均为 0
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
targets = [
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountRouteOwnershipTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rApiCoverageReportTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rEventsRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rEventsRouteOwnershipTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiRouteOwnershipTest.xml",
]
for target in targets:
path = Path(target)
if not path.exists():
raise SystemExit(f"missing surefire xml: {target}")
root = ET.parse(path).getroot()
tests = int(root.attrib.get("tests", "0"))
failures = int(root.attrib.get("failures", "0"))
errors = int(root.attrib.get("errors", "0"))
skipped = int(root.attrib.get("skipped", "0"))
if tests <= 0 or failures or errors or skipped:
raise SystemExit(f"bad surefire result: {target} tests={tests} failures={failures} errors={errors} skipped={skipped}")
PY
```
#### 4. Flyway `_test`
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
. ~/.config/muse-repo/infra.env && \
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD" && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test -pl muse-server -am \
-Dtest=P1rAccountEventsPublishFlywayMigrationIT \
-Dflyway.postgresql.transactional.lock=false \
-Dp1r.flyway.locations=filesystem:sql/muse \
-Dp1r.flyway.url="jdbc:postgresql://$MUSE_POSTGRES_HOST:$MUSE_POSTGRES_PORT/muse_p1r7e_account_events_publish_test" \
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
-Djava.net.useSystemProxies=false \
-DsocksProxyHost= \
-DsocksProxyPort= \
-Dhttp.proxyHost= \
-Dhttp.proxyPort= \
-Dhttps.proxyHost= \
-Dhttps.proxyPort= \
-Dsurefire.failIfNoSpecifiedTests=false
```
期望:
- migrations_executed = 20。
- target_schema_version = 20。
- `muse_account_event_publish_outbox` 存在。
- 目标测试 XML tests > 0failures/errors/skipped 均为 0。
#### 5. Dependency tree gate
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o -pl muse-module-member/muse-module-member-server -DskipTests dependency:tree > /tmp/p1r7e-member-server-dependency-tree.txt && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o -pl muse-module-events/muse-module-events-server -DskipTests dependency:tree > /tmp/p1r7e-events-server-dependency-tree.txt && \
rg "muse-module-events-api" /tmp/p1r7e-member-server-dependency-tree.txt && \
! rg "muse-module-events-server" /tmp/p1r7e-member-server-dependency-tree.txt && \
! rg "muse-module-(ai|knowledge|market|member|content)-server" /tmp/p1r7e-events-server-dependency-tree.txt
```
#### 6. Isolated coverage scanner
只允许在 `/tmp` 隔离副本运行 scanner
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
tmpdir=$(mktemp -d /tmp/p1r7e-coverage-scan.XXXXXX) && \
rsync -a --delete --exclude .git --exclude 'muse-cloud/**/target' ./ "$tmpdir"/ && \
cd "$tmpdir" && \
python3 -m venv .venv && \
. .venv/bin/activate && \
python -m pip install --quiet PyYAML && \
python muse-cloud/scripts/p1r-audit-api-coverage.py --check
```
期望:
- scanner exit 0。
- completed=100。
- needsVerification=133。
- incomplete=0。
- genericPersistence=0。
- ssePlaceholder=0。
- Events `streamEvents=dedicated / needs_verification`
- Account 33 operations 仍为 `dedicated / needs_verification`
#### 7. Protected diff and whitespace
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
git diff --check && \
git diff --quiet -- docs/api-contracts/account/openapi.yaml docs/api-contracts/market/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/events/openapi.yaml muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md && \
git diff --cached --quiet -- docs/api-contracts/account/openapi.yaml docs/api-contracts/market/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/events/openapi.yaml muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md
```
#### 8. Memory
新增:
- `docs/memorys/2026-06-07-P1R7eAccountSourceOwnerPropagation真实链路.md`
必须记录:
- 目标和非目标。
- 关键实现文件。
- focused tests 数量。
- P1R mixed gates 数量。
- Flyway `_test` 结果。
- dependency tree gate。
- isolated coverage scanner 目录和 summary。
- protected diff。
- fresh implementation review 结果。
- 明确说明仍不代表 Events / P1R-7 / Account completed。
#### 9. Fresh implementation review
实现完成和最终验证后必须重新派发:
- fresh spec / correctness review。
- fresh quality / data-integrity / testing review。
双 PASS 前不得提交、push 或推进 completed。
## 完成条件
P1R-7e 实现完成条件:
- Account quota adjustment terminal fact -> Account outbox -> worker -> `EventsPublishApi` -> `muse_unified_event` -> SSE owner-visible focused evidence 成立。
- Member server 只依赖 events-api不依赖 events-server。
- Events server 不反向依赖 AI / Knowledge / Market / Member / Content server。
- V20 SQL 静态 gate 和 Flyway `_test` 通过。
- focused tests、P1R mixed gates、dependency tree、isolated coverage scanner、protected diff、`git diff --check` 均通过。
- fresh implementation spec review + quality review 双 PASS。
仍不得称为 completed
- Account 33 operations。
- Events `streamEvents`
- 总 P1R-7。
- Market / Knowledge / AI 之外任何未单独批准的域。