From c64f3d8b80e61def30018f0ee0206851efe51c23 Mon Sep 17 00:00:00 2001 From: break Date: Fri, 7 Aug 2026 11:02:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E5=B1=80=E5=AE=A1=E8=AE=A1=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/.openspec-target | 1 + .../skills/openspec-apply-change/SKILL.md | 45 +- .../skills/openspec-archive-change/SKILL.md | 182 + .../skills/openspec-explore/SKILL.md | 62 +- .agents/skills/openspec-propose/SKILL.md | 149 + .agents/skills/openspec-sync-specs/SKILL.md | 262 + .../skills/openspec-update-change/SKILL.md | 91 + .claude/commands/opsx/apply.md | 39 +- .claude/commands/opsx/archive.md | 114 +- .claude/commands/opsx/explore.md | 42 +- .claude/commands/opsx/propose.md | 89 +- .claude/commands/opsx/sync.md | 156 +- .claude/commands/opsx/update.md | 29 +- .claude/skills/openspec-apply-change/SKILL.md | 37 +- .../skills/openspec-archive-change/SKILL.md | 106 +- .claude/skills/openspec-explore/SKILL.md | 40 +- .claude/skills/openspec-propose/SKILL.md | 85 +- .claude/skills/openspec-sync-specs/SKILL.md | 152 +- .../skills/openspec-update-change/SKILL.md | 25 +- .opencode/package-lock.json | 115 - .opencode/skills/comment-standards/SKILL.md | 139 - .opencode/skills/hurl-test/SKILL.md | 952 ---- .../skills/openspec-api-contract/SKILL.md | 97 - .../skills/openspec-archive-change/SKILL.md | 114 - .../skills/openspec-lock-consensus/SKILL.md | 281 -- .opencode/skills/openspec-propose/SKILL.md | 110 - .scratch/tech-global-audit/审计覆盖基线.md | 70 +- .scratch/tech-global-audit/审计覆盖清单.json | 4322 ++++++++++------- AI系统级规划提示词规范.md | 522 -- CONTEXT_RESET_PLAN.md | 477 ++ README.md | 4 +- cmd/audit-retention-simulate/main.go | 320 ++ cmd/worker/main.go | 17 +- docs/admin-openapi.yaml | 1294 ++++- .../从零构建Agent友好项目最佳实践.md | 556 +++ .../多视角审计中心功能总结.md | 267 + .../平台基础审计调查接口.md | 2 +- .../归档灰度操作手册.md | 166 + .../跨视角调查与前端导航契约.md | 372 +- .../application/auditarchive/retention.go | 38 + internal/bootstrap/handlers.go | 3 +- internal/bootstrap/services.go | 19 +- internal/bootstrap/stores.go | 2 - internal/bootstrap/worker_services.go | 7 +- internal/bootstrap/worker_stores.go | 3 - internal/governance/auditcoverage/scanner.go | 26 +- internal/infrastructure/audit/batch.go | 5 + internal/infrastructure/audit/registry.go | 29 +- internal/infrastructure/audit/writer.go | 18 +- .../cardobservation/series_runner.go | 10 +- .../integrationlog/repository.go | 57 +- internal/model/dto/audit_dto.go | 104 +- internal/query/audit/events.go | 138 +- internal/query/audit/finance.go | 54 +- internal/query/audit/resources.go | 37 +- internal/query/audit/risks.go | 52 +- internal/query/audit/subject_activities.go | 96 +- internal/query/audit/timeline.go | 50 +- internal/query/integration/logs.go | 191 +- internal/query/integration/overview.go | 52 +- internal/routes/audit.go | 32 +- internal/service/account/service.go | 7 - internal/service/account_audit/service.go | 42 - internal/service/agent_recharge/service.go | 8 - internal/service/asset/lifecycle_service.go | 339 +- .../service/asset/manual_adjustment_audit.go | 94 + internal/service/asset/service.go | 126 +- internal/service/asset_audit/service.go | 26 - internal/service/device/audit.go | 287 -- internal/service/device/service.go | 3 - internal/service/device_import/audit.go | 5 - internal/service/device_import/service.go | 3 - internal/service/iot_card/audit.go | 235 - internal/service/iot_card/card_snapshot.go | 16 + internal/service/iot_card/polling_audit.go | 85 + internal/service/iot_card/service.go | 147 +- .../service/iot_card/stop_resume_service.go | 3 - internal/service/iot_card_import/audit.go | 5 - internal/service/iot_card_import/service.go | 3 - .../service/polling/asset_polling_service.go | 209 +- .../postgres/account_operation_log_store.go | 25 - .../postgres/asset_operation_log_store.go | 5 - internal/task/audit_monthly_retention.go | 46 +- opencode.json | 37 - .../build-multi-view-audit-center/design.md | 2 +- .../asset-audit-readable-content/spec.md | 28 + .../build-multi-view-audit-center/tasks.md | 18 +- pkg/config/config.go | 5 +- pkg/config/defaults/config.yaml | 2 + pkg/config/loader.go | 1 + pkg/constants/audit.go | 22 +- pkg/queue/types.go | 1 - pkg/sanitizer/sanitizer.go | 44 +- worker-多实例升级方案.md | 550 --- 94 files changed, 8641 insertions(+), 6714 deletions(-) create mode 100644 .agents/skills/.openspec-target rename {.opencode => .agents}/skills/openspec-apply-change/SKILL.md (52%) create mode 100644 .agents/skills/openspec-archive-change/SKILL.md rename {.opencode => .agents}/skills/openspec-explore/SKILL.md (58%) create mode 100644 .agents/skills/openspec-propose/SKILL.md create mode 100644 .agents/skills/openspec-sync-specs/SKILL.md create mode 100644 .agents/skills/openspec-update-change/SKILL.md delete mode 100644 .opencode/package-lock.json delete mode 100644 .opencode/skills/comment-standards/SKILL.md delete mode 100644 .opencode/skills/hurl-test/SKILL.md delete mode 100644 .opencode/skills/openspec-api-contract/SKILL.md delete mode 100644 .opencode/skills/openspec-archive-change/SKILL.md delete mode 100644 .opencode/skills/openspec-lock-consensus/SKILL.md delete mode 100644 .opencode/skills/openspec-propose/SKILL.md delete mode 100644 AI系统级规划提示词规范.md create mode 100644 CONTEXT_RESET_PLAN.md create mode 100644 cmd/audit-retention-simulate/main.go create mode 100644 docs/engineering/从零构建Agent友好项目最佳实践.md create mode 100644 docs/feature-504-multi-view-audit-center/多视角审计中心功能总结.md create mode 100644 docs/feature-504-multi-view-audit-center/归档灰度操作手册.md delete mode 100644 internal/service/account_audit/service.go create mode 100644 internal/service/asset/manual_adjustment_audit.go delete mode 100644 internal/service/device/audit.go delete mode 100644 internal/service/iot_card/audit.go create mode 100644 internal/service/iot_card/card_snapshot.go create mode 100644 internal/service/iot_card/polling_audit.go delete mode 100644 internal/store/postgres/account_operation_log_store.go delete mode 100644 opencode.json delete mode 100644 worker-多实例升级方案.md diff --git a/.agents/skills/.openspec-target b/.agents/skills/.openspec-target new file mode 100644 index 0000000..2695308 --- /dev/null +++ b/.agents/skills/.openspec-target @@ -0,0 +1 @@ +codex diff --git a/.opencode/skills/openspec-apply-change/SKILL.md b/.agents/skills/openspec-apply-change/SKILL.md similarity index 52% rename from .opencode/skills/openspec-apply-change/SKILL.md rename to .agents/skills/openspec-apply-change/SKILL.md index 9f31f2c..74109b6 100644 --- a/.opencode/skills/openspec-apply-change/SKILL.md +++ b/.agents/skills/openspec-apply-change/SKILL.md @@ -1,17 +1,20 @@ --- name: openspec-apply-change description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. +allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.2.0" + generatedBy: "1.8.0" --- Implement tasks from an OpenSpec change. -**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name (e.g., `$openspec-apply-change (Codex) or /openspec-apply-change (other agents) add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -20,9 +23,9 @@ Implement tasks from an OpenSpec change. If a name is provided, use it. Otherwise: - Infer from conversation context if the user mentioned a change - Auto-select if only one active change exists - - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one - Always announce: "Using change: " and how to override (e.g., `/opsx-apply `). + Always announce: "Using change: " and how to override (e.g., `$openspec-apply-change (Codex) or /openspec-apply-change (other agents) `). 2. **Check status to understand the schema** ```bash @@ -30,6 +33,7 @@ Implement tasks from an OpenSpec change. ``` Parse the JSON to understand: - `schemaName`: The workflow being used (e.g., "spec-driven") + - `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) 3. **Get apply instructions** @@ -39,23 +43,43 @@ Implement tasks from an OpenSpec change. ``` This returns: - - Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs) + - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs) - Progress (total, complete, remaining) - Task list with status - Dynamic instruction based on current state + - Optional `context`: current required project instruction input from the selected root + - Optional `operationGuidance`: current advisory guidance for apply **Handle states:** - - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change + - If `state: "blocked"` (missing artifacts): show message, suggest using `$openspec-continue-change (Codex) or /openspec-continue-change (other agents)` (if it is not installed, run `openspec status --change "" --json` to see the next artifact and `openspec instructions --change "" --json` for how to create it) - If `state: "all_done"`: congratulate, suggest archive - Otherwise: proceed to implementation + Treat `context` as a required prompt-level input. Read and consider it, and + apply relevant project facts, conventions, and constraints while implementing. + Treat `operationGuidance` as optional additive advice. Read and consider every + entry, and follow entries that are applicable and compatible with the built-in + workflow. + + Keep both fields separate from CLI-returned state, missing artifacts, tasks, + progress, `contextFiles`, and the built-in `instruction`. They are not + evidence of task completion, do not replace the built-in instruction, and do + not permit bypassing a blocked state. If context conflicts with the built-in + instruction, an explicit user choice, or a CLI-controlled value, report the + conflict and preserve the controlling value. If guidance is inapplicable or + conflicts with those controlling inputs, do not follow it and explain why. + These are prompt-level behavior contracts, not enforceable checks. + 4. **Read context files** - Read the files listed in `contextFiles` from the apply instructions output. + Read every file path listed under `contextFiles` from the apply instructions output. The files depend on the schema being used: - **spec-driven**: proposal, specs, design, tasks - Other schemas: follow the contextFiles from CLI output + Do not copy `context` or `operationGuidance` verbatim into implementation + files or planning artifacts unless the user separately asks for that content. + 5. **Show current progress** Display: @@ -115,7 +139,7 @@ Working on task 4/7: - [x] Task 2 ... -All tasks complete! Ready to archive this change. +All tasks complete! You can archive this change with `$openspec-archive-change (Codex) or /openspec-archive-change (other agents)`. ``` **Output On Pause (Issue Encountered)** @@ -147,6 +171,11 @@ What would you like to do? - Update task checkbox immediately after completing each task - Pause on errors, blockers, or unclear requirements - don't guess - Use contextFiles from CLI output, don't assume specific file names +- Do not use context or operation guidance as proof that a task is complete +- Apply relevant project context; report conflicts with controlling workflow inputs +- Consider every guidance entry; explain any inapplicable or conflicting advice +- Do not copy runtime context or operation guidance into implementation files or planning artifacts +- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria **Fluid Workflow Integration** diff --git a/.agents/skills/openspec-archive-change/SKILL.md b/.agents/skills/openspec-archive-change/SKILL.md new file mode 100644 index 0000000..17d952d --- /dev/null +++ b/.agents/skills/openspec-archive-change/SKILL.md @@ -0,0 +1,182 @@ +--- +name: openspec-archive-change +description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. +allowed-tools: Bash(openspec:*) +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.8.0" +--- + +Archive a completed change in the experimental workflow. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one + + When prompting, show only active changes (not already archived). + Include the schema used for each change if available. + + Always announce: "Using change: " and how to override (e.g., `$openspec-archive-change (Codex) or /openspec-archive-change (other agents) `). + + **Load current archive inputs before the existing archive checks:** + + After resolving the selected change and planning root, run: + ```bash + openspec instructions archive --change "" --json + ``` + Keep the same selected-root flags on this command. This lookup is advisory and + optional: it only supplies extra prompt inputs, so it must never block archiving. + If it exits non-zero or returns invalid JSON — for example on an older CLI that + does not support this command yet — continue the archive workflow with no + context and no operation guidance. Do not report an error and do not stop. + + A successful response may omit both optional fields. Treat `context` as a + required prompt-level input: read and consider it, and apply relevant project + facts, conventions, and constraints. Treat `operationGuidance` as optional + additive advice: read and consider every entry, and follow entries that are + applicable and compatible with the built-in archive workflow. + + Keep both fields separate from built-in steps, explicit user choices, resolved + paths, CLI checks, and command contracts. If context conflicts with one of those + controlling inputs, report the conflict and preserve the controlling value. If + guidance is inapplicable or conflicts with a controlling input, do not follow it + and explain why. Do not infer replacement paths, skipped prompts, or flags from + either field, and do not copy their text verbatim into specs, change artifacts, + or archive summaries unless the user separately asks for it. These are + prompt-level behavior contracts, not enforceable checks. + +2. **Check artifact completion status** + + Run `openspec status --change "" --json` to check artifact completion. + + Parse the JSON to understand: + - `schemaName`: The workflow being used + - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context + - `artifacts`: List of artifacts with their status (`done`, `skipped`, or other) + + **If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs): + - Display warning listing incomplete artifacts + - Ask the user to confirm they want to proceed + - Proceed if user confirms + +3. **Check task completion status** + + Read the tasks file (typically `tasks.md`) to check for incomplete tasks. + + Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete). + + **If incomplete tasks found:** + - Display warning showing count of incomplete tasks + - Ask the user to confirm they want to proceed + - Proceed if user confirms + + **If no tasks file exists:** Proceed without task-related warning. + +4. **Assess delta spec sync state** + + Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only + delta-spec source. If the `specs` entry is missing or + `existingOutputPaths` is empty, proceed without a sync prompt and do not infer + delta specs from other artifacts. + + **If delta specs exist:** + - Compare each delta spec with its corresponding main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path) + - Determine what changes would be applied (adds, modifications, removals, renames) + - Show a combined summary before prompting + + **Prompt options:** + - If changes needed: "Sync now (recommended)", "Archive without syncing" + - If already synced: "Archive now", "Sync anyway", "Cancel" + + Route on the answer: + - "Cancel" — stop, do not archive + - "Archive without syncing" or "Archive now" — proceed to archive + - "Sync now" or "Sync anyway" — sync, then verify (below) + - Anything else — ask again rather than archiving + + Before a selected sync writes any main spec, run + `openspec instructions specs --change "" --json` once with the same + selected-root flags. Require a zero exit status and valid artifact-instruction + JSON. If the lookup fails or returns invalid JSON, report the error and stop + before writing any main spec or moving the change. A valid response with omitted + `rules` is the no-rules case. Apply returned `rules` only to the content and + form of main specs produced by this merge; do not use them as archive guidance, + change CLI behavior, or copy the rule text into any output file. + + Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result. + + Then re-run the comparison from the top of this step against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced: + - ADDED requirements present + - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact + - REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match + - RENAMED requirements present under the new name and absent under the old one + + If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again. + +5. **Perform the archive** + + Create an `archive` directory under `planningHome.changesDir` if it doesn't exist: + ```bash + mkdir -p "/archive" + ``` + + Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-`. Never stack a second date (same rule as `openspec archive`). + + **Check if target already exists:** + - If yes: Fail with error, suggest renaming existing archive or using different date + - If no: Move `changeRoot` to the archive directory + + ```bash + mv "" "/archive/" + ``` + +6. **Display summary** + + Show archive completion summary including: + - Change name + - Schema that was used + - Archive location + - Whether specs were synced (if applicable) + - Note about any warnings (incomplete artifacts/tasks) + +**Output On Success** + +```markdown +## Archive Complete + +**Change:** +**Schema:** +**Archived to:** the archive path derived from `planningHome.changesDir`// +**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped"> + +<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")> +``` + +**Guardrails** +- Announce the selected change; prompt for selection when it is ambiguous +- Use artifact graph (openspec status --json) for completion checking +- Don't block archive on warnings - just inform and confirm +- Preserve .openspec.yaml when moving to archive (it moves with the directory) +- Show clear summary of what happened +- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven) +- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot` +- If delta specs exist, always run the sync assessment and show the combined summary before prompting +- Apply relevant runtime context and report conflicts; operation guidance remains advisory +- Consider every guidance entry and explain any inapplicable or conflicting advice +- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged +- Artifact rules constrain only the specs being written and are never operation guidance +- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files diff --git a/.opencode/skills/openspec-explore/SKILL.md b/.agents/skills/openspec-explore/SKILL.md similarity index 58% rename from .opencode/skills/openspec-explore/SKILL.md rename to .agents/skills/openspec-explore/SKILL.md index 2510ac4..60b89df 100644 --- a/.opencode/skills/openspec-explore/SKILL.md +++ b/.agents/skills/openspec-explore/SKILL.md @@ -1,20 +1,23 @@ --- name: openspec-explore description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. +allowed-tools: Bash(openspec:*) license: MIT compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.2.0" + generatedBy: "1.8.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. For a new change, scaffold it first as described below. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + --- ## The Stance @@ -56,10 +59,10 @@ Depending on what the user brings, you might: │ Use ASCII diagrams liberally │ ├─────────────────────────────────────────┤ │ │ -│ ┌────────┐ ┌────────┐ │ -│ │ State │────────▶│ State │ │ -│ │ A │ │ B │ │ -│ └────────┘ └────────┘ │ +│ ┌────────┐ ┌────────┐ │ +│ │ State │────────▶│ State │ │ +│ │ A │ │ B │ │ +│ └────────┘ └────────┘ │ │ │ │ System diagrams, state machines, │ │ data flows, architecture sketches, │ @@ -91,6 +94,12 @@ This tells you: - Their names, schemas, and status - What the user might be working on +Then read the project's own context from the resolved root - `/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists: +- `context`: project background - tech stack, conventions, constraints +- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact + +Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create. + ### When no change exists Think freely. When insights crystallize, you might offer: @@ -98,15 +107,23 @@ Think freely. When insights crystallize, you might offer: - "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize +If the user asks you to capture the exploration as a new change, transition seamlessly into the requested capture: + +1. Run `openspec new change ""` (with `--store ` when applicable) before creating any artifacts. Never create a new change directory under `openspec/changes/` by hand; the CLI scaffold creates required metadata such as `.openspec.yaml`. Keep the selected `--store ` on every applicable follow-up `status` and `instructions` command. +2. Run `openspec status --change "" --json` (append the confirmed `--store ""` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is `ready`, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own `instruction` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) for that prerequisite whether it is `ready` or `blocked`. If its own `instruction` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves. +3. Follow the returned `template` and `instruction` fields. Read completed dependency files listed in `dependencies`, and apply `context` and `rules` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to `resolvedOutputPath`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists. +4. After creating each artifact, re-run `openspec status --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) and continue until every requested artifact is `done`, `skipped`, or was deliberately skipped because its own `instruction` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still `blocked` only because you deliberately skipped a conditional prerequisite, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture. + +Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. + ### When a change exists If the user mentions a change or you detect one is relevant: -1. **Read existing artifacts for context** - - `openspec/changes//proposal.md` - - `openspec/changes//design.md` - - `openspec/changes//tasks.md` - - etc. +1. **Resolve and read existing artifacts for context** + - Run `openspec status --change "" --json`. + - Use `changeRoot`, `artifactPaths`, and `actionContext` from the status JSON. + - Read existing files from `artifactPaths..existingOutputPaths`. 2. **Reference them naturally in conversation** - "Your design mentions using Redis, but we just realized SQLite fits better..." @@ -114,14 +131,16 @@ If the user mentions a change or you detect one is relevant: 3. **Offer to capture when decisions are made** - | Insight Type | Where to Capture | - |--------------|------------------| - | New requirement discovered | `specs//spec.md` | - | Requirement changed | `specs//spec.md` | - | Design decision made | `design.md` | - | Scope changed | `proposal.md` | - | New work identified | `tasks.md` | - | Assumption invalidated | Relevant artifact | + `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities. + + | Insight Type | Where to Capture | + |----------------------------|-------------------------------------| + | New requirement discovered | `specs//spec.md` | + | Requirement changed | `specs//spec.md` | + | Design decision made | `design.md` | + | Scope changed | `proposal.md` | + | New work identified | `tasks.md` | + | Assumption invalidated | Relevant artifact | Example offers: - "That's a design decision. Capture it in design.md?" @@ -201,7 +220,7 @@ You: [reads codebase] **User is stuck mid-implementation:** ``` -User: /opsx-explore add-auth-system +User: $openspec-explore (Codex) or /openspec-explore (other agents) add-auth-system The OAuth integration is more complex than expected You: [reads change artifacts] @@ -227,7 +246,7 @@ User: A CLI tool that tracks local dev environments You: That changes everything. ┌─────────────────────────────────────────────────┐ - │ CLI TOOL DATA STORAGE │ + │ CLI TOOL DATA STORAGE │ └─────────────────────────────────────────────────┘ Key constraints: @@ -283,6 +302,7 @@ But this summary is optional. Sometimes the thinking IS the value. - **Don't rush** - Discovery is thinking time, not task time - **Don't force structure** - Let patterns emerge naturally - **Don't auto-capture** - Offer to save insights, don't just do it +- **Don't manually scaffold changes** - Never create a new change directory under `openspec/changes/` by hand. Always use `openspec new change ""` (with `--store ` when applicable) so required metadata such as `.openspec.yaml` is created before writing artifacts. - **Do visualize** - A good diagram is worth many paragraphs - **Do explore the codebase** - Ground discussions in reality - **Do question assumptions** - Including the user's and your own diff --git a/.agents/skills/openspec-propose/SKILL.md b/.agents/skills/openspec-propose/SKILL.md new file mode 100644 index 0000000..60c21fa --- /dev/null +++ b/.agents/skills/openspec-propose/SKILL.md @@ -0,0 +1,149 @@ +--- +name: openspec-propose +description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. +allowed-tools: Bash(openspec:*) +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.8.0" +--- + +Propose a new change - create the change and generate all artifacts in one step. + +**Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow. + +I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is: +- proposal.md (what & why) +- `specs//spec.md` (what the system must do - a delta, not the main spec) +- design.md (how) +- tasks.md (implementation steps) + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities. + +When the user is ready to implement, they must start the apply workflow explicitly. + +--- + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. + +**Steps** + +1. **Understand the request and clarify material ambiguity** + + If no clear input is provided, ask the user (open-ended, no preset options): + > "What change do you want to work on? Describe what you want to build or fix." + + From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). + + **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. + + If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts. + +2. **Determine the workflow schema** + + Use the configured default schema unless the user explicitly requests a different workflow. + + **Use a different schema only if the user:** + - Explicitly requests a specific schema by name → use `--schema ` + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store ""`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; `schemas` does not accept `--store`. If context reports only `no_openspec_root`, run `openspec schemas --json` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + + Otherwise, omit `--schema` to preserve the configured default. + +3. **Create the change directory** + + Choose one schema form below. If a registered store is selected, append `--store ""` to that command and each later OpenSpec command shown below that accepts `--store`. + + Using the configured default: + ```bash + openspec new change "" + ``` + + Using an explicitly requested schema: + ```bash + openspec new change "" --schema "" + ``` + This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`. + +4. **Get the artifact build order** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to get: + - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) + - `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on) + - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. + +5. **Create every artifact in the required set** + + Use a todo list to track progress through the artifacts. + + Loop through artifacts in dependency order (artifacts with no pending dependencies first): + + a. **For each artifact that is `ready` (dependencies satisfied)**: + - Get instructions: + ```bash + openspec instructions --change "" --json + ``` + - The instructions JSON includes: + - `context`: Project background (constraints for you - do NOT include in output) + - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) + - `template`: The structure to use for your output file + - `instruction`: Schema-specific guidance for this artifact type + - `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact + - `resolvedOutputPath`: Resolved path or pattern to write the artifact + - `dependencies`: Completed artifacts to read for context + - Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them) + - If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath` + - Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path + - Apply `context` and `rules` as constraints - but do NOT copy them into the file + - Show brief progress: "Created " + + b. **Continue until every artifact in the required set exists (not just `apply.requires`)** + - After creating each artifact, re-run `openspec status --change "" --json` + - The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone + - `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on + - An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one + - Create every artifact in the required set that is missing, then re-check - creating one can unblock others + - Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions --change "" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it + - Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway + - Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped + + c. **If an artifact requires user input** (unclear context): + - Ask the user to clarify + - Then continue with creation + +6. **Show final status** + ```bash + openspec status --change "" + ``` + +**Output** + +After completing all artifacts, summarize: +- Change name and location +- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why +- What's ready: "All artifacts needed for implementation are ready." +- Prompt: "The artifacts are ready for review. When you are ready, run `$openspec-apply-change (Codex) or /openspec-apply-change (other agents)` or ask me to apply this change." + +**Artifact Creation Guidelines** + +- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names +- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly +- The schema defines what each artifact should contain - follow it +- Read dependency artifacts for context before creating new ones +- Use `template` as the structure for your output file - fill in its sections +- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file + - Do NOT copy ``, ``, `` blocks into the artifact + - These guide what you write, but should never appear in the output + +**Guardrails** +- The request that invoked this workflow authorizes planning only. Any implementation or apply instruction in that request does not carry forward. Do NOT implement the change, start the apply workflow, or edit project code during this workflow. After presenting the artifacts, stop and wait for a new user request to start the apply workflow +- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires` +- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them) +- Ask about ambiguities that would materially change scope, externally observable behavior, compatibility, or acceptance criteria; for minor details, make reasonable assumptions and record them +- If a change with that name already exists, ask if user wants to continue it or create a new one +- Verify each artifact file exists after writing before proceeding to next diff --git a/.agents/skills/openspec-sync-specs/SKILL.md b/.agents/skills/openspec-sync-specs/SKILL.md new file mode 100644 index 0000000..6da5900 --- /dev/null +++ b/.agents/skills/openspec-sync-specs/SKILL.md @@ -0,0 +1,262 @@ +--- +name: openspec-sync-specs +description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change. +allowed-tools: Bash(openspec:*) +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.8.0" +--- + +Sync delta specs from a change to main specs. + +This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one + + When prompting, show changes that have delta specs (under `specs/` directory). + + Always announce: "Using change: " and how to override (e.g., `$openspec-sync-specs (Codex) or /openspec-sync-specs (other agents) `). + +2. **Resolve change context** + + Run: + ```bash + openspec status --change "" --json + ``` + + The JSON includes `planningHome.root`. Main specs live under `/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository. + +3. **Find delta specs** + + Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the + only source of delta spec paths. If the `specs` entry is missing or + `existingOutputPaths` is empty, report that there are no delta specs to sync, + do not infer them from other artifacts, and stop without requesting artifact + instructions or writing a main spec. + + Sync every path in `existingOutputPaths` unless the caller narrowed the set. + A caller narrows it by naming an explicit list of complete entries from + `existingOutputPaths` — copy those absolute values verbatim. Archive does + this inline, and a user can too (for example, by selecting the entry ending + in `/specs/billing/invoices/spec.md`). + Then sync only the named paths and leave the remaining delta specs untouched: + bulk archive excludes a delta whose implementation it could not find, and + syncing it anyway would write a main spec the caller deliberately withheld. + Carry that narrowed selection through step 4; never widen it back to the full + list. If a named path is not in `existingOutputPaths`, do not sync it — + report it and stop, rather than dropping it silently. If the named list is + empty, report that there is nothing to sync and stop without writing a main + spec. + + Each delta spec file contains sections like: + - `## ADDED Requirements` - New requirements to add + - `## MODIFIED Requirements` - Changes to existing requirements + - `## REMOVED Requirements` - Requirements to remove + - `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format) + + If no delta specs found, inform user and stop. + +4. **For each delta spec, apply changes to main specs** + + Before the first main-spec write, obtain one current specs-rule snapshot: + - If archive invoked this workflow inline and supplied a valid snapshot from + `openspec instructions specs --change "" --json`, reuse it and do not + fetch the same instructions again. + - Otherwise run that command once now with the same selected-root flags. + - If the direct lookup exits non-zero or returns invalid artifact-instruction + JSON, report the error and stop before writing any main spec. Do not treat the + failure as an absent rule set. + - A valid response with omitted `rules` means no artifact rules are configured + and the existing semantic merge continues. + + Apply returned `rules` only to the content and form of the main specs produced + by this merge. Artifact rules are not operation guidance and cannot change + selected roots, delta paths, CLI checks, or workflow steps. Use their text as + constraints without copying it verbatim into a main spec or summary. + + For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo): + + a. **Read the delta spec** to understand the intended changes + + b. **Read the main spec** at `/openspec/specs//spec.md` (may not exist yet) + + c. **Apply changes intelligently**: + + **ADDED Requirements:** + - If requirement doesn't exist in main spec → add it + - If requirement already exists → update it to match (treat as implicit MODIFIED) + + **MODIFIED Requirements:** + - Find the requirement in main spec + - Apply the changes - this can be: + - Adding new scenarios the main spec does not have yet + - Modifying existing scenarios + - Changing the requirement description + - Preserve scenarios/content not mentioned in the delta + + **REMOVED Requirements:** + - Remove the entire requirement block from main spec + - Retiring the capability. Delete the whole `spec.md` - and the directory once + nothing else is left in it - only when ALL of these hold: + 1. removing the requirements *this run* left no requirement blocks; + 2. the rest of the spec is well-formed (it still has a `## Purpose`); + 3. the main spec was not already empty before this sync - if you removed + nothing, change nothing; + 4. every other nonblank line in the whole file is accounted for as the + title, Purpose, Requirements header, or a canonical requirement's + statement, scenarios, or fenced examples; + 5. the change's `.openspec.yaml` declares `retire_capabilities: true`; + 6. the `spec.md` resolves inside the real specs root (do not follow a + capability-directory symlink to delete an external file). + If removing the selected requirements would leave no requirement blocks and + any retirement condition is not satisfied, do not modify the main spec. Stop + the sync for that capability, report the blocking condition, and tell the user + how to resolve it. Never write or leave an empty `## Requirements` section. + When only the marker is missing, say that too - it is the one thing the user + can add to make the retirement go through. + - Deleting the file also deletes its `## Purpose`; any other section blocks + retirement. Name Purpose when you report the retirement. Include a pasteable + `git checkout` only when the spec lived in the caller's checkout; + otherwise give checkout-scoped recovery guidance. + + **RENAMED Requirements:** + - Find the FROM requirement, rename to TO + + **`## Purpose` in the delta:** + - The main spec already has one and it is authoritative - leave it alone + (this is what `openspec archive` does; it warns and moves on) + + d. **Create new main spec** if capability doesn't exist yet: + - Create `/openspec/specs//spec.md` + - Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one + (this is what `openspec archive` does); only write a brief TBD placeholder when it does not + - Add Requirements section with the ADDED requirements + - Follow the **Main Spec Format Reference** below + +5. **Validate updated main specs** + + Run `openspec validate --specs` with the same selected-root flags used earlier. + If validation fails, report the problems and do not claim the sync succeeded. + +6. **Show summary** + + After applying all changes, summarize: + - Which capabilities were updated + - What changes were made (requirements added/modified/removed/renamed) + - Any new main spec left with a TBD Purpose placeholder, so it gets written + now rather than lingering + - Any capability retired, naming the deleted `spec.md`, its Purpose, and + either a pasteable `git checkout` or checkout-scoped recovery guidance + +**Delta Spec Format Reference** + +```markdown +## Purpose + +Only on a delta that introduces a brand-new capability. Seeds the new main spec. + +## ADDED Requirements + +### Requirement: New Feature +The system SHALL do something new. + +#### Scenario: Basic case +- **WHEN** user does X +- **THEN** system does Y + +## MODIFIED Requirements + +### Requirement: Existing Feature +The system SHALL keep doing the existing thing, now also handling A. + +#### Scenario: Scenario the main spec already has +- **WHEN** user does X +- **THEN** system does Y + +#### Scenario: New scenario to add +- **WHEN** user does A +- **THEN** system does B + +## REMOVED Requirements + +### Requirement: Deprecated Feature + +## RENAMED Requirements + +- FROM: `### Requirement: Old Name` +- TO: `### Requirement: New Name` +``` + +**Main Spec Format Reference** + +Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section: + +```markdown +# Specification + +## Purpose +Short description of what this capability does and why it exists. + +## Requirements + +### Requirement: New Feature +The system SHALL do something new. + +#### Scenario: Basic case +- **WHEN** user does X +- **THEN** system does Y +``` + +**Key Principle: Intelligent Merging** + +Unlike programmatic merging, you merge rather than overwrite: +- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. `openspec validate` and `openspec archive` both reject one that drops a scenario the main spec still has. +- Keep anything the delta does not mention, in the main spec's existing order +- Use your judgment to merge changes sensibly + +**Output On Success** + +```markdown +## Specs Synced: + +Updated main specs: + +****: +- Added requirement: "New Feature" +- Modified requirement: "Existing Feature" (added 1 scenario) + +****: +- Created new spec file +- Added requirement: "Another Feature" + +Main specs are now updated. The change remains active - archive when implementation is complete. +``` + +**Guardrails** +- Read both delta and main specs before making changes +- Preserve existing content not mentioned in delta +- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers +- If something is unclear, ask for clarification +- Show what you're changing as you go +- The operation should be idempotent - running twice should give same result +- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts +- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list +- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline +- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response +- Artifact rules constrain only the specs being written and are never copied into output files diff --git a/.agents/skills/openspec-update-change/SKILL.md b/.agents/skills/openspec-update-change/SKILL.md new file mode 100644 index 0000000..b1094e6 --- /dev/null +++ b/.agents/skills/openspec-update-change/SKILL.md @@ -0,0 +1,91 @@ +--- +name: openspec-update-change +description: Update an OpenSpec change by revising its existing planning artifacts and keeping them coherent with one another. Use when the user wants to revise a change's plan, fold new decisions into it, or reconcile its artifacts after an edit. Never edits code. +allowed-tools: Bash(openspec:*) +license: MIT +compatibility: Requires openspec CLI. +metadata: + author: openspec + version: "1.0" + generatedBy: "1.8.0" +--- + +Revise a change's existing planning artifacts and keep them coherent. Never edit code. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +`$openspec-continue-change (Codex) or /openspec-continue-change (other agents)` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "" --json` shows the next artifact and `openspec instructions "" --change "" --json` explains how to create it. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes sorted by most recently modified, and ask the user to select one + + When prompting, present the top 3-4 most recently modified changes as options, showing: + - Change name + - Schema (from `schema` field if present, otherwise "spec-driven") + - Status (e.g., "0/5 tasks", "complete", "no tasks") + - How recently it was modified (from `lastModified` field) + + Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to update. + + Always announce: "Using change: " and how to override (e.g., `$openspec-update-change (Codex) or /openspec-update-change (other agents) `). + +2. **Get the change's artifacts** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to understand current state. The response includes: + - `schemaName`: The workflow schema being used (e.g., "spec-driven") + - `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked") + - `isPlanningComplete`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as `isComplete`. + - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. + + The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged. + + The files to edit are `artifactPaths..existingOutputPaths` - the concrete files that exist on disk, already glob-expanded for glob artifacts (e.g. `specs/**/*.md`). Do NOT write to `resolvedOutputPath`: for a glob artifact it is still the glob pattern, not a real file. + +3. **Understand the request** + - If the user asked for a specific revision ("the design now uses X"), that is the starting edit. + - If they only said "update" / "make this coherent", treat it as a coherence review: read the existing artifacts and check them against each other for contradictions, gaps, and duplication. + +4. **Read and reconcile** + - Read the artifact(s) the request touches and the change's other existing artifacts. + - Apply the requested edit. Then check every other existing artifact against it - in ANY direction: an edit to a later artifact may require revising an earlier one, not only the other way around. Build order is a useful reading order, not a constraint on which artifacts may be revised. + - Note everything that is now inconsistent, missing, or contradictory. + - Revise only files that already exist (`existingOutputPaths`). Do NOT create artifacts that don't exist yet, and do NOT invent new files under a glob artifact - note them and point the user to `$openspec-continue-change (Codex) or /openspec-continue-change (other agents)` to create them. + - If the change is already coherent, say so and make no edits. + +5. **Confirm and apply, one artifact at a time** + - Show each proposed revision and why. Write only after the user confirms. + - If the user rejects a revision, do not write it - leave that artifact unchanged. + - When a substantial rewrite is needed, get that artifact's rules and template first: + ```bash + openspec instructions "" --change "" --json + ``` + +6. **Point to the next step (guidance only - NEVER act on it)** + - Artifacts still missing -> suggest `$openspec-continue-change (Codex) or /openspec-continue-change (other agents)` to create them. + - Change already implemented (tasks checked off / already applied) -> the code may no longer match the revised plan; suggest `$openspec-apply-change (Codex) or /openspec-apply-change (other agents)` to carry the delta into code. + - Everything done and implemented -> suggest `$openspec-archive-change (Codex) or /openspec-archive-change (other agents)`. + +**Output** + +After each invocation, show: +- Which artifacts were revised (and which proposed revisions were rejected) +- Anything deferred to `$openspec-continue-change (Codex) or /openspec-continue-change (other agents)` (not-yet-created artifacts or files) +- Where the change stands and the recommended next command + +**Guardrails** +- Planning artifacts only - NEVER edit implementation code. If the revised plan implies code changes, stop and point to `$openspec-apply-change (Codex) or /openspec-apply-change (other agents)`. +- Use the artifact ids and paths reported by `openspec status`; never branch on hardcoded artifact names. +- Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`. +- Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is `$openspec-continue-change (Codex) or /openspec-continue-change (other agents)`'s job. +- Confirm every edit with the user before writing. +- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile `$openspec-new-change (Codex) or /openspec-new-change (other agents)` workflow is available. If it is, recommend starting fresh with `$openspec-new-change (Codex) or /openspec-new-change (other agents)` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend `openspec new change ""` instead. diff --git a/.claude/commands/opsx/apply.md b/.claude/commands/opsx/apply.md index c6cb9b6..31c4fa2 100644 --- a/.claude/commands/opsx/apply.md +++ b/.claude/commands/opsx/apply.md @@ -1,14 +1,14 @@ --- name: "OPSX: Apply" -description: Implement tasks from an OpenSpec change (Experimental) +description: "Implement tasks from an OpenSpec change (Experimental)" allowed-tools: Bash(openspec:*) -category: Workflow -tags: [workflow, artifacts, experimental] +category: "Workflow" +tags: ["workflow", "artifacts", "experimental"] --- Implement tasks from an OpenSpec change. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. **Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. @@ -19,7 +19,7 @@ Implement tasks from an OpenSpec change. If a name is provided, use it. Otherwise: - Infer from conversation context if the user mentioned a change - Auto-select if only one active change exists - - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). @@ -39,16 +39,33 @@ Implement tasks from an OpenSpec change. ``` This returns: - - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema) + - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs) - Progress (total, complete, remaining) - Task list with status - Dynamic instruction based on current state + - Optional `context`: current required project instruction input from the selected root + - Optional `operationGuidance`: current advisory guidance for apply **Handle states:** - - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` + - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` (if it is not installed, run `openspec status --change "" --json` to see the next artifact and `openspec instructions --change "" --json` for how to create it) - If `state: "all_done"`: congratulate, suggest archive - Otherwise: proceed to implementation + Treat `context` as a required prompt-level input. Read and consider it, and + apply relevant project facts, conventions, and constraints while implementing. + Treat `operationGuidance` as optional additive advice. Read and consider every + entry, and follow entries that are applicable and compatible with the built-in + workflow. + + Keep both fields separate from CLI-returned state, missing artifacts, tasks, + progress, `contextFiles`, and the built-in `instruction`. They are not + evidence of task completion, do not replace the built-in instruction, and do + not permit bypassing a blocked state. If context conflicts with the built-in + instruction, an explicit user choice, or a CLI-controlled value, report the + conflict and preserve the controlling value. If guidance is inapplicable or + conflicts with those controlling inputs, do not follow it and explain why. + These are prompt-level behavior contracts, not enforceable checks. + 4. **Read context files** Read every file path listed under `contextFiles` from the apply instructions output. @@ -56,6 +73,9 @@ Implement tasks from an OpenSpec change. - **spec-driven**: proposal, specs, design, tasks - Other schemas: follow the contextFiles from CLI output + Do not copy `context` or `operationGuidance` verbatim into implementation + files or planning artifacts unless the user separately asks for that content. + 5. **Show current progress** Display: @@ -147,6 +167,11 @@ What would you like to do? - Update task checkbox immediately after completing each task - Pause on errors, blockers, or unclear requirements - don't guess - Use contextFiles from CLI output, don't assume specific file names +- Do not use context or operation guidance as proof that a task is complete +- Apply relevant project context; report conflicts with controlling workflow inputs +- Consider every guidance entry; explain any inapplicable or conflicting advice +- Do not copy runtime context or operation guidance into implementation files or planning artifacts +- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria **Fluid Workflow Integration** diff --git a/.claude/commands/opsx/archive.md b/.claude/commands/opsx/archive.md index df8a2f2..fc04a05 100644 --- a/.claude/commands/opsx/archive.md +++ b/.claude/commands/opsx/archive.md @@ -1,27 +1,59 @@ --- name: "OPSX: Archive" -description: Archive a completed change in the experimental workflow +description: "Archive a completed change in the experimental workflow" allowed-tools: Bash(openspec:*) -category: Workflow -tags: [workflow, archive, experimental] +category: "Workflow" +tags: ["workflow", "archive", "experimental"] --- Archive a completed change in the experimental workflow. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** -1. **If no change name provided, prompt for selection** +1. **Select the change** - Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one - Show only active changes (not already archived). + When prompting, show only active changes (not already archived). Include the schema used for each change if available. - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + Always announce: "Using change: " and how to override (e.g., `/opsx:archive `). + + **Load current archive inputs before the existing archive checks:** + + After resolving the selected change and planning root, run: + ```bash + openspec instructions archive --change "" --json + ``` + Keep the same selected-root flags on this command. This lookup is advisory and + optional: it only supplies extra prompt inputs, so it must never block archiving. + If it exits non-zero or returns invalid JSON — for example on an older CLI that + does not support this command yet — continue the archive workflow with no + context and no operation guidance. Do not report an error and do not stop. + + A successful response may omit both optional fields. Treat `context` as a + required prompt-level input: read and consider it, and apply relevant project + facts, conventions, and constraints. Treat `operationGuidance` as optional + additive advice: read and consider every entry, and follow entries that are + applicable and compatible with the built-in archive workflow. + + Keep both fields separate from built-in steps, explicit user choices, resolved + paths, CLI checks, and command contracts. If context conflicts with one of those + controlling inputs, report the conflict and preserve the controlling value. If + guidance is inapplicable or conflicts with a controlling input, do not follow it + and explain why. Do not infer replacement paths, skipped prompts, or flags from + either field, and do not copy their text verbatim into specs, change artifacts, + or archive summaries unless the user separately asks for it. These are + prompt-level behavior contracts, not enforceable checks. 2. **Check artifact completion status** @@ -30,9 +62,9 @@ Archive a completed change in the experimental workflow. Parse the JSON to understand: - `schemaName`: The workflow being used - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context - - `artifacts`: List of artifacts with their status (`done` or other) + - `artifacts`: List of artifacts with their status (`done`, `skipped`, or other) - **If any artifacts are not `done`:** + **If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs): - Display warning listing incomplete artifacts - Prompt user for confirmation to continue - Proceed if user confirms @@ -52,10 +84,13 @@ Archive a completed change in the experimental workflow. 4. **Assess delta spec sync state** - Use `artifactPaths.specs.existingOutputPaths` from status JSON to check for delta specs. If none exist, proceed without sync prompt. + Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only + delta-spec source. If the `specs` entry is missing or + `existingOutputPaths` is empty, proceed without a sync prompt and do not infer + delta specs from other artifacts. **If delta specs exist:** - - Compare each delta spec with its corresponding main spec at `openspec/specs//spec.md` + - Compare each delta spec with its corresponding main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path) - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting @@ -63,7 +98,30 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. + Route on the answer: + - "Cancel" — stop, do not archive + - "Archive without syncing" or "Archive now" — proceed to archive + - "Sync now" or "Sync anyway" — sync, then verify (below) + - Anything else — ask again rather than archiving + + Before a selected sync writes any main spec, run + `openspec instructions specs --change "" --json` once with the same + selected-root flags. Require a zero exit status and valid artifact-instruction + JSON. If the lookup fails or returns invalid JSON, report the error and stop + before writing any main spec or moving the change. A valid response with omitted + `rules` is the no-rules case. Apply returned `rules` only to the content and + form of main specs produced by this merge; do not use them as archive guidance, + change CLI behavior, or copy the rule text into any output file. + + Then run the `/opsx:sync` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result. + + Then re-run the comparison from the top of this step against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced: + - ADDED requirements present + - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact + - REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match + - RENAMED requirements present under the new name and absent under the old one + + If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again. 5. **Perform the archive** @@ -72,14 +130,14 @@ Archive a completed change in the experimental workflow. mkdir -p "/archive" ``` - Generate target name using current date: `YYYY-MM-DD-` + Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-`. Never stack a second date (same rule as `openspec archive`). **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - If no: Move `changeRoot` to the archive directory ```bash - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" ``` 6. **Display summary** @@ -93,12 +151,12 @@ Archive a completed change in the experimental workflow. **Output On Success** -``` +```markdown ## Archive Complete **Change:** **Schema:** -**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from `planningHome.changesDir`// **Specs:** ✓ Synced to main specs All artifacts complete. All tasks complete. @@ -106,12 +164,12 @@ All artifacts complete. All tasks complete. **Output On Success (No Delta Specs)** -``` +```markdown ## Archive Complete **Change:** **Schema:** -**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from `planningHome.changesDir`// **Specs:** No delta specs All artifacts complete. All tasks complete. @@ -119,12 +177,12 @@ All artifacts complete. All tasks complete. **Output On Success With Warnings** -``` +```markdown ## Archive Complete (with warnings) **Change:** **Schema:** -**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ +**Archived to:** the archive path derived from `planningHome.changesDir`// **Specs:** Sync skipped (user chose to skip) **Warnings:** @@ -137,11 +195,11 @@ Review the archive if this was not intentional. **Output On Error (Archive Exists)** -``` +```markdown ## Archive Failed **Change:** -**Target:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ +**Target:** the archive path derived from `planningHome.changesDir`// Target archive directory already exists. @@ -152,10 +210,16 @@ Target archive directory already exists. ``` **Guardrails** -- Always prompt for change selection if not provided +- Announce the selected change; prompt for selection when it is ambiguous - Use artifact graph (openspec status --json) for completion checking - Don't block archive on warnings - just inform and confirm - Preserve .openspec.yaml when moving to archive (it moves with the directory) - Show clear summary of what happened -- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven) +- If sync is requested, run the `/opsx:sync` workflow inline (agent-driven) +- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot` - If delta specs exist, always run the sync assessment and show the combined summary before prompting +- Apply relevant runtime context and report conflicts; operation guidance remains advisory +- Consider every guidance entry and explain any inapplicable or conflicting advice +- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged +- Artifact rules constrain only the specs being written and are never operation guidance +- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files diff --git a/.claude/commands/opsx/explore.md b/.claude/commands/opsx/explore.md index 7558a63..c286e01 100644 --- a/.claude/commands/opsx/explore.md +++ b/.claude/commands/opsx/explore.md @@ -2,17 +2,17 @@ name: "OPSX: Explore" description: "Enter explore mode - think through ideas, investigate problems, clarify requirements" allowed-tools: Bash(openspec:*) -category: Workflow -tags: [workflow, explore, experimental, thinking] +category: "Workflow" +tags: ["workflow", "explore", "experimental", "thinking"] --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. For a new change, scaffold it first as described below. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. **Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be: - A vague idea: "real-time collaboration" @@ -97,6 +97,12 @@ This tells you: - Their names, schemas, and status - What the user might be working on +Then read the project's own context from the resolved root - `/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists: +- `context`: project background - tech stack, conventions, constraints +- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact + +Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create. + If the user mentioned a specific change name, read its artifacts for context. ### When no change exists @@ -106,6 +112,15 @@ Think freely. When insights crystallize, you might offer: - "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize +If the user asks you to capture the exploration as a new change, transition seamlessly into the requested capture: + +1. Run `openspec new change ""` (with `--store ` when applicable) before creating any artifacts. Never create a new change directory under `openspec/changes/` by hand; the CLI scaffold creates required metadata such as `.openspec.yaml`. Keep the selected `--store ` on every applicable follow-up `status` and `instructions` command. +2. Run `openspec status --change "" --json` (append the confirmed `--store ""` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is `ready`, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own `instruction` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) for that prerequisite whether it is `ready` or `blocked`. If its own `instruction` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves. +3. Follow the returned `template` and `instruction` fields. Read completed dependency files listed in `dependencies`, and apply `context` and `rules` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to `resolvedOutputPath`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists. +4. After creating each artifact, re-run `openspec status --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) and continue until every requested artifact is `done`, `skipped`, or was deliberately skipped because its own `instruction` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still `blocked` only because you deliberately skipped a conditional prerequisite, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture. + +Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. + ### When a change exists If the user mentions a change or you detect one is relevant: @@ -121,14 +136,16 @@ If the user mentions a change or you detect one is relevant: 3. **Offer to capture when decisions are made** - | Insight Type | Where to Capture | - |----------------------------|--------------------------------| - | New requirement discovered | `specs//spec.md` | - | Requirement changed | `specs//spec.md` | - | Design decision made | `design.md` | - | Scope changed | `proposal.md` | - | New work identified | `tasks.md` | - | Assumption invalidated | Relevant artifact | + `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities. + + | Insight Type | Where to Capture | + |----------------------------|-------------------------------------| + | New requirement discovered | `specs//spec.md` | + | Requirement changed | `specs//spec.md` | + | Design decision made | `design.md` | + | Scope changed | `proposal.md` | + | New work identified | `tasks.md` | + | Assumption invalidated | Relevant artifact | Example offers: - "That's a design decision. Capture it in design.md?" @@ -170,6 +187,7 @@ When things crystallize, you might offer a summary - but it's optional. Sometime - **Don't rush** - Discovery is thinking time, not task time - **Don't force structure** - Let patterns emerge naturally - **Don't auto-capture** - Offer to save insights, don't just do it +- **Don't manually scaffold changes** - Never create a new change directory under `openspec/changes/` by hand. Always use `openspec new change ""` (with `--store ` when applicable) so required metadata such as `.openspec.yaml` is created before writing artifacts. - **Do visualize** - A good diagram is worth many paragraphs - **Do explore the codebase** - Ground discussions in reality - **Do question assumptions** - Including the user's and your own diff --git a/.claude/commands/opsx/propose.md b/.claude/commands/opsx/propose.md index 8d99588..46da73e 100644 --- a/.claude/commands/opsx/propose.md +++ b/.claude/commands/opsx/propose.md @@ -1,55 +1,81 @@ --- name: "OPSX: Propose" -description: Propose a new change - create it and generate all artifacts in one step +description: "Propose a new change - create it and generate all artifacts in one step" allowed-tools: Bash(openspec:*) -category: Workflow -tags: [workflow, artifacts, experimental] +category: "Workflow" +tags: ["workflow", "artifacts", "experimental"] --- Propose a new change - create the change and generate all artifacts in one step. -I'll create a change with artifacts: +**Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow. + +I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is: - proposal.md (what & why) +- `specs//spec.md` (what the system must do - a delta, not the main spec) - design.md (how) - tasks.md (implementation steps) -When ready to implement, run /opsx:apply +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities. + +When the user is ready to implement, they must start the apply workflow explicitly. --- -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. **Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build. **Steps** -1. **If no input provided, ask what they want to build** +1. **Understand the request and clarify material ambiguity** - Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: + If no input is provided, ask the user (open-ended, no preset options): > "What change do you want to work on? Describe what you want to build or fix." From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. -2. **Create the change directory** + If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts. + +2. **Determine the workflow schema** + + Use the configured default schema unless the user explicitly requests a different workflow. + + **Use a different schema only if the user:** + - Explicitly requests a specific schema by name → use `--schema ` + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store ""`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; `schemas` does not accept `--store`. If context reports only `no_openspec_root`, run `openspec schemas --json` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + + Otherwise, omit `--schema` to preserve the configured default. + +3. **Create the change directory** + + Choose one schema form below. If a registered store is selected, append `--store ""` to that command and each later OpenSpec command shown below that accepts `--store`. + + Using the configured default: ```bash openspec new change "" ``` + + Using an explicitly requested schema: + ```bash + openspec new change "" --schema "" + ``` This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`. -3. **Get the artifact build order** +4. **Get the artifact build order** ```bash openspec status --change "" --json ``` Parse the JSON to get: - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) - - `artifacts`: list of all artifacts with their status and dependencies + - `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on) - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. -4. **Create artifacts in sequence until apply-ready** +5. **Create every artifact in the required set** - Use the **TodoWrite tool** to track progress through the artifacts. + Use a todo list to track progress through the artifacts. Loop through artifacts in dependency order (artifacts with no pending dependencies first): @@ -63,23 +89,30 @@ When ready to implement, run /opsx:apply - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) - `template`: The structure to use for your output file - `instruction`: Schema-specific guidance for this artifact type + - `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact - `resolvedOutputPath`: Resolved path or pattern to write the artifact - `dependencies`: Completed artifacts to read for context - - Read any completed dependency files for context - - Create the artifact file using `template` as the structure and write it to `resolvedOutputPath` + - Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them) + - If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath` + - Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path - Apply `context` and `rules` as constraints - but do NOT copy them into the file - Show brief progress: "Created " - b. **Continue until all `applyRequires` artifacts are complete** + b. **Continue until every artifact in the required set exists (not just `apply.requires`)** - After creating each artifact, re-run `openspec status --change "" --json` - - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array - - Stop when all `applyRequires` artifacts are done + - The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone + - `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on + - An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one + - Create every artifact in the required set that is missing, then re-check - creating one can unblock others + - Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions --change "" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it + - Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway + - Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped c. **If an artifact requires user input** (unclear context): - - Use **AskUserQuestion tool** to clarify + - Ask the user to clarify - Then continue with creation -5. **Show final status** +6. **Show final status** ```bash openspec status --change "" ``` @@ -88,13 +121,14 @@ When ready to implement, run /opsx:apply After completing all artifacts, summarize: - Change name and location -- List of artifacts created with brief descriptions -- What's ready: "All artifacts created! Ready for implementation." -- Prompt: "Run `/opsx:apply` to start implementing." +- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why +- What's ready: "All artifacts needed for implementation are ready." +- Prompt: "The artifacts are ready for review. When you are ready, run `/opsx:apply`." **Artifact Creation Guidelines** -- Follow the `instruction` field from `openspec instructions` for each artifact type +- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names +- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones - Use `template` as the structure for your output file - fill in its sections @@ -103,8 +137,9 @@ After completing all artifacts, summarize: - These guide what you write, but should never appear in the output **Guardrails** -- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) -- Always read dependency artifacts before creating a new one -- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum +- The request that invoked this workflow authorizes planning only. Any implementation or apply instruction in that request does not carry forward. Do NOT implement the change, start the apply workflow, or edit project code during this workflow. After presenting the artifacts, stop and wait for a new user request to start the apply workflow +- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires` +- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them) +- Ask about ambiguities that would materially change scope, externally observable behavior, compatibility, or acceptance criteria; for minor details, make reasonable assumptions and record them - If a change with that name already exists, ask if user wants to continue it or create a new one - Verify each artifact file exists after writing before proceeding to next diff --git a/.claude/commands/opsx/sync.md b/.claude/commands/opsx/sync.md index a3550cf..b432850 100644 --- a/.claude/commands/opsx/sync.md +++ b/.claude/commands/opsx/sync.md @@ -1,28 +1,33 @@ --- name: "OPSX: Sync" -description: Sync delta specs from a change to main specs +description: "Sync delta specs from a change to main specs" allowed-tools: Bash(openspec:*) -category: Workflow -tags: [workflow, specs, experimental] +category: "Workflow" +tags: ["workflow", "specs", "experimental"] --- Sync delta specs from a change to main specs. This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name after `/opsx:sync` (e.g., `/opsx:sync add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** -1. **If no change name provided, prompt for selection** +1. **Select the change** - Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one - Show changes that have delta specs (under `specs/` directory). + When prompting, show changes that have delta specs (under `specs/` directory). - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + Always announce: "Using change: " and how to override (e.g., `/opsx:sync `). 2. **Resolve change context** @@ -31,9 +36,29 @@ This is an **agent-driven** operation - you will read delta specs and directly e openspec status --change "" --json ``` + The JSON includes `planningHome.root`. Main specs live under `/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository. + 3. **Find delta specs** - Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the list of delta spec files. + Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the + only source of delta spec paths. If the `specs` entry is missing or + `existingOutputPaths` is empty, report that there are no delta specs to sync, + do not infer them from other artifacts, and stop without requesting artifact + instructions or writing a main spec. + + Sync every path in `existingOutputPaths` unless the caller narrowed the set. + A caller narrows it by naming an explicit list of complete entries from + `existingOutputPaths` — copy those absolute values verbatim. Archive does + this inline, and a user can too (for example, by selecting the entry ending + in `/specs/billing/invoices/spec.md`). + Then sync only the named paths and leave the remaining delta specs untouched: + bulk archive excludes a delta whose implementation it could not find, and + syncing it anyway would write a main spec the caller deliberately withheld. + Carry that narrowed selection through step 4; never widen it back to the full + list. If a named path is not in `existingOutputPaths`, do not sync it — + report it and stop, rather than dropping it silently. If the named list is + empty, report that there is nothing to sync and stop without writing a main + spec. Each delta spec file contains sections like: - `## ADDED Requirements` - New requirements to add @@ -45,11 +70,27 @@ This is an **agent-driven** operation - you will read delta specs and directly e 4. **For each delta spec, apply changes to main specs** - For each repo-local capability delta spec path returned by the CLI: + Before the first main-spec write, obtain one current specs-rule snapshot: + - If archive invoked this workflow inline and supplied a valid snapshot from + `openspec instructions specs --change "" --json`, reuse it and do not + fetch the same instructions again. + - Otherwise run that command once now with the same selected-root flags. + - If the direct lookup exits non-zero or returns invalid artifact-instruction + JSON, report the error and stop before writing any main spec. Do not treat the + failure as an absent rule set. + - A valid response with omitted `rules` means no artifact rules are configured + and the existing semantic merge continues. + + Apply returned `rules` only to the content and form of the main specs produced + by this merge. Artifact rules are not operation guidance and cannot change + selected roots, delta paths, CLI checks, or workflow steps. Use their text as + constraints without copying it verbatim into a main spec or summary. + + For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo): a. **Read the delta spec** to understand the intended changes - b. **Read the main spec** at `openspec/specs//spec.md` (may not exist yet) + b. **Read the main spec** at `/openspec/specs//spec.md` (may not exist yet) c. **Apply changes intelligently**: @@ -60,31 +101,72 @@ This is an **agent-driven** operation - you will read delta specs and directly e **MODIFIED Requirements:** - Find the requirement in main spec - Apply the changes - this can be: - - Adding new scenarios (don't need to copy existing ones) + - Adding new scenarios the main spec does not have yet - Modifying existing scenarios - Changing the requirement description - Preserve scenarios/content not mentioned in the delta **REMOVED Requirements:** - Remove the entire requirement block from main spec + - Retiring the capability. Delete the whole `spec.md` - and the directory once + nothing else is left in it - only when ALL of these hold: + 1. removing the requirements *this run* left no requirement blocks; + 2. the rest of the spec is well-formed (it still has a `## Purpose`); + 3. the main spec was not already empty before this sync - if you removed + nothing, change nothing; + 4. every other nonblank line in the whole file is accounted for as the + title, Purpose, Requirements header, or a canonical requirement's + statement, scenarios, or fenced examples; + 5. the change's `.openspec.yaml` declares `retire_capabilities: true`; + 6. the `spec.md` resolves inside the real specs root (do not follow a + capability-directory symlink to delete an external file). + If removing the selected requirements would leave no requirement blocks and + any retirement condition is not satisfied, do not modify the main spec. Stop + the sync for that capability, report the blocking condition, and tell the user + how to resolve it. Never write or leave an empty `## Requirements` section. + When only the marker is missing, say that too - it is the one thing the user + can add to make the retirement go through. + - Deleting the file also deletes its `## Purpose`; any other section blocks + retirement. Name Purpose when you report the retirement. Include a pasteable + `git checkout` only when the spec lived in the caller's checkout; + otherwise give checkout-scoped recovery guidance. **RENAMED Requirements:** - Find the FROM requirement, rename to TO - d. **Create new main spec** if capability doesn't exist yet: - - Create `openspec/specs//spec.md` - - Add Purpose section (can be brief, mark as TBD) - - Add Requirements section with the ADDED requirements + **`## Purpose` in the delta:** + - The main spec already has one and it is authoritative - leave it alone + (this is what `openspec archive` does; it warns and moves on) -5. **Show summary** + d. **Create new main spec** if capability doesn't exist yet: + - Create `/openspec/specs//spec.md` + - Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one + (this is what `openspec archive` does); only write a brief TBD placeholder when it does not + - Add Requirements section with the ADDED requirements + - Follow the **Main Spec Format Reference** below + +5. **Validate updated main specs** + + Run `openspec validate --specs` with the same selected-root flags used earlier. + If validation fails, report the problems and do not claim the sync succeeded. + +6. **Show summary** After applying all changes, summarize: - Which capabilities were updated - What changes were made (requirements added/modified/removed/renamed) + - Any new main spec left with a TBD Purpose placeholder, so it gets written + now rather than lingering + - Any capability retired, naming the deleted `spec.md`, its Purpose, and + either a pasteable `git checkout` or checkout-scoped recovery guidance **Delta Spec Format Reference** ```markdown +## Purpose + +Only on a delta that introduces a brand-new capability. Seeds the new main spec. + ## ADDED Requirements ### Requirement: New Feature @@ -97,6 +179,12 @@ The system SHALL do something new. ## MODIFIED Requirements ### Requirement: Existing Feature +The system SHALL keep doing the existing thing, now also handling A. + +#### Scenario: Scenario the main spec already has +- **WHEN** user does X +- **THEN** system does Y + #### Scenario: New scenario to add - **WHEN** user does A - **THEN** system does B @@ -111,16 +199,36 @@ The system SHALL do something new. - TO: `### Requirement: New Name` ``` +**Main Spec Format Reference** + +Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section: + +```markdown +# Specification + +## Purpose +Short description of what this capability does and why it exists. + +## Requirements + +### Requirement: New Feature +The system SHALL do something new. + +#### Scenario: Basic case +- **WHEN** user does X +- **THEN** system does Y +``` + **Key Principle: Intelligent Merging** -Unlike programmatic merging, you can apply **partial updates**: -- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios -- The delta represents *intent*, not a wholesale replacement +Unlike programmatic merging, you merge rather than overwrite: +- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. `openspec validate` and `openspec archive` both reject one that drops a scenario the main spec still has. +- Keep anything the delta does not mention, in the main spec's existing order - Use your judgment to merge changes sensibly **Output On Success** -``` +```markdown ## Specs Synced: Updated main specs: @@ -139,6 +247,12 @@ Main specs are now updated. The change remains active - archive when implementat **Guardrails** - Read both delta and main specs before making changes - Preserve existing content not mentioned in delta +- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers - If something is unclear, ask for clarification - Show what you're changing as you go - The operation should be idempotent - running twice should give same result +- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts +- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list +- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline +- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response +- Artifact rules constrain only the specs being written and are never copied into output files diff --git a/.claude/commands/opsx/update.md b/.claude/commands/opsx/update.md index 75104f5..725b453 100644 --- a/.claude/commands/opsx/update.md +++ b/.claude/commands/opsx/update.md @@ -1,24 +1,29 @@ --- name: "OPSX: Update" -description: Update a change - revise existing planning artifacts and keep them coherent (Experimental) +description: "Update a change - revise existing planning artifacts and keep them coherent (Experimental)" allowed-tools: Bash(openspec:*) -category: Workflow -tags: [workflow, artifacts, experimental] +category: "Workflow" +tags: ["workflow", "artifacts", "experimental"] --- Revise a change's existing planning artifacts and keep them coherent. Never edit code. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. **Input**: Optionally specify a change name after `/opsx:update` (e.g., `/opsx:update add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +`/opsx:continue` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "" --json` shows the next artifact and `openspec instructions "" --change "" --json` explains how to create it. + **Steps** -1. **If no change name provided, prompt for selection** +1. **Select the change** - Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to update. + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes sorted by most recently modified, and ask the user to select one - Present the top 3-4 most recently modified changes as options, showing: + When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - Schema (from `schema` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") @@ -26,7 +31,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to update. - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + Always announce: "Using change: " and how to override (e.g., `/opsx:update `). 2. **Get the change's artifacts** ```bash @@ -34,8 +39,8 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit ``` Parse the JSON to understand current state. The response includes: - `schemaName`: The workflow schema being used (e.g., "spec-driven") - - `artifacts`: Array of artifacts with their status ("done", "ready", "blocked") - - `isComplete`: Boolean indicating if all artifacts are complete + - `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked") + - `isPlanningComplete`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as `isComplete`. - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged. @@ -58,7 +63,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit - If the user rejects a revision, do not write it - leave that artifact unchanged. - When a substantial rewrite is needed, get that artifact's rules and template first: ```bash - openspec instructions --change "" --json + openspec instructions "" --change "" --json ``` 6. **Point to the next step (guidance only - NEVER act on it)** @@ -79,4 +84,4 @@ After each invocation, show: - Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`. - Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is `/opsx:continue`'s job. - Confirm every edit with the user before writing. -- If the request changes the change's *intent* rather than refining it, recommend starting fresh with `/opsx:new` (the "Update vs. Start Fresh" heuristic). +- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile `/opsx:new` workflow is available. If it is, recommend starting fresh with `/opsx:new` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend `openspec new change ""` instead. diff --git a/.claude/skills/openspec-apply-change/SKILL.md b/.claude/skills/openspec-apply-change/SKILL.md index 1375861..c64461d 100644 --- a/.claude/skills/openspec-apply-change/SKILL.md +++ b/.claude/skills/openspec-apply-change/SKILL.md @@ -7,14 +7,14 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.6.0" + generatedBy: "1.8.0" --- Implement tasks from an OpenSpec change. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. -**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** @@ -23,7 +23,7 @@ Implement tasks from an OpenSpec change. If a name is provided, use it. Otherwise: - Infer from conversation context if the user mentioned a change - Auto-select if only one active change exists - - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). @@ -47,12 +47,29 @@ Implement tasks from an OpenSpec change. - Progress (total, complete, remaining) - Task list with status - Dynamic instruction based on current state + - Optional `context`: current required project instruction input from the selected root + - Optional `operationGuidance`: current advisory guidance for apply **Handle states:** - - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change + - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` (if it is not installed, run `openspec status --change "" --json` to see the next artifact and `openspec instructions --change "" --json` for how to create it) - If `state: "all_done"`: congratulate, suggest archive - Otherwise: proceed to implementation + Treat `context` as a required prompt-level input. Read and consider it, and + apply relevant project facts, conventions, and constraints while implementing. + Treat `operationGuidance` as optional additive advice. Read and consider every + entry, and follow entries that are applicable and compatible with the built-in + workflow. + + Keep both fields separate from CLI-returned state, missing artifacts, tasks, + progress, `contextFiles`, and the built-in `instruction`. They are not + evidence of task completion, do not replace the built-in instruction, and do + not permit bypassing a blocked state. If context conflicts with the built-in + instruction, an explicit user choice, or a CLI-controlled value, report the + conflict and preserve the controlling value. If guidance is inapplicable or + conflicts with those controlling inputs, do not follow it and explain why. + These are prompt-level behavior contracts, not enforceable checks. + 4. **Read context files** Read every file path listed under `contextFiles` from the apply instructions output. @@ -60,6 +77,9 @@ Implement tasks from an OpenSpec change. - **spec-driven**: proposal, specs, design, tasks - Other schemas: follow the contextFiles from CLI output + Do not copy `context` or `operationGuidance` verbatim into implementation + files or planning artifacts unless the user separately asks for that content. + 5. **Show current progress** Display: @@ -119,7 +139,7 @@ Working on task 4/7: - [x] Task 2 ... -All tasks complete! Ready to archive this change. +All tasks complete! You can archive this change with `/opsx:archive`. ``` **Output On Pause (Issue Encountered)** @@ -151,6 +171,11 @@ What would you like to do? - Update task checkbox immediately after completing each task - Pause on errors, blockers, or unclear requirements - don't guess - Use contextFiles from CLI output, don't assume specific file names +- Do not use context or operation guidance as proof that a task is complete +- Apply relevant project context; report conflicts with controlling workflow inputs +- Consider every guidance entry; explain any inapplicable or conflicting advice +- Do not copy runtime context or operation guidance into implementation files or planning artifacts +- Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria **Fluid Workflow Integration** diff --git a/.claude/skills/openspec-archive-change/SKILL.md b/.claude/skills/openspec-archive-change/SKILL.md index c0c169d..aa8e868 100644 --- a/.claude/skills/openspec-archive-change/SKILL.md +++ b/.claude/skills/openspec-archive-change/SKILL.md @@ -7,25 +7,57 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.6.0" + generatedBy: "1.8.0" --- Archive a completed change in the experimental workflow. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** -1. **If no change name provided, prompt for selection** +1. **Select the change** - Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one - Show only active changes (not already archived). + When prompting, show only active changes (not already archived). Include the schema used for each change if available. - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + Always announce: "Using change: " and how to override (e.g., `/opsx:archive `). + + **Load current archive inputs before the existing archive checks:** + + After resolving the selected change and planning root, run: + ```bash + openspec instructions archive --change "" --json + ``` + Keep the same selected-root flags on this command. This lookup is advisory and + optional: it only supplies extra prompt inputs, so it must never block archiving. + If it exits non-zero or returns invalid JSON — for example on an older CLI that + does not support this command yet — continue the archive workflow with no + context and no operation guidance. Do not report an error and do not stop. + + A successful response may omit both optional fields. Treat `context` as a + required prompt-level input: read and consider it, and apply relevant project + facts, conventions, and constraints. Treat `operationGuidance` as optional + additive advice: read and consider every entry, and follow entries that are + applicable and compatible with the built-in archive workflow. + + Keep both fields separate from built-in steps, explicit user choices, resolved + paths, CLI checks, and command contracts. If context conflicts with one of those + controlling inputs, report the conflict and preserve the controlling value. If + guidance is inapplicable or conflicts with a controlling input, do not follow it + and explain why. Do not infer replacement paths, skipped prompts, or flags from + either field, and do not copy their text verbatim into specs, change artifacts, + or archive summaries unless the user separately asks for it. These are + prompt-level behavior contracts, not enforceable checks. 2. **Check artifact completion status** @@ -34,11 +66,11 @@ Archive a completed change in the experimental workflow. Parse the JSON to understand: - `schemaName`: The workflow being used - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context - - `artifacts`: List of artifacts with their status (`done` or other) + - `artifacts`: List of artifacts with their status (`done`, `skipped`, or other) - **If any artifacts are not `done`:** + **If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs): - Display warning listing incomplete artifacts - - Use **AskUserQuestion tool** to confirm user wants to proceed + - Ask the user to confirm they want to proceed - Proceed if user confirms 3. **Check task completion status** @@ -49,17 +81,20 @@ Archive a completed change in the experimental workflow. **If incomplete tasks found:** - Display warning showing count of incomplete tasks - - Use **AskUserQuestion tool** to confirm user wants to proceed + - Ask the user to confirm they want to proceed - Proceed if user confirms **If no tasks file exists:** Proceed without task-related warning. 4. **Assess delta spec sync state** - Use `artifactPaths.specs.existingOutputPaths` from status JSON to check for delta specs. If none exist, proceed without sync prompt. + Use `artifactPaths.specs.existingOutputPaths` from status JSON as the only + delta-spec source. If the `specs` entry is missing or + `existingOutputPaths` is empty, proceed without a sync prompt and do not infer + delta specs from other artifacts. **If delta specs exist:** - - Compare each delta spec with its corresponding main spec at `openspec/specs//spec.md` + - Compare each delta spec with its corresponding main spec at `/openspec/specs//spec.md` (use the store-aware `planningHome.root` from step 2, not a hardcoded repo path) - Determine what changes would be applied (adds, modifications, removals, renames) - Show a combined summary before prompting @@ -67,7 +102,30 @@ Archive a completed change in the experimental workflow. - If changes needed: "Sync now (recommended)", "Archive without syncing" - If already synced: "Archive now", "Sync anyway", "Cancel" - If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. + Route on the answer: + - "Cancel" — stop, do not archive + - "Archive without syncing" or "Archive now" — proceed to archive + - "Sync now" or "Sync anyway" — sync, then verify (below) + - Anything else — ask again rather than archiving + + Before a selected sync writes any main spec, run + `openspec instructions specs --change "" --json` once with the same + selected-root flags. Require a zero exit status and valid artifact-instruction + JSON. If the lookup fails or returns invalid JSON, report the error and stop + before writing any main spec or moving the change. A valid response with omitted + `rules` is the no-rules case. Apply returned `rules` only to the content and + form of main specs produced by this merge; do not use them as archive guidance, + change CLI behavior, or copy the rule text into any output file. + + Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result. + + Then re-run the comparison from the top of this step against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced: + - ADDED requirements present + - MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact + - REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match + - RENAMED requirements present under the new name and absent under the old one + + If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again. 5. **Perform the archive** @@ -76,14 +134,14 @@ Archive a completed change in the experimental workflow. mkdir -p "/archive" ``` - Generate target name using current date: `YYYY-MM-DD-` + Generate the target name: use the change name as-is when it already starts with a `YYYY-MM-DD-` prefix; otherwise prepend the current date as `YYYY-MM-DD-`. Never stack a second date (same rule as `openspec archive`). **Check if target already exists:** - If yes: Fail with error, suggest renaming existing archive or using different date - If no: Move `changeRoot` to the archive directory ```bash - mv "" "/archive/YYYY-MM-DD-" + mv "" "/archive/" ``` 6. **Display summary** @@ -97,22 +155,28 @@ Archive a completed change in the experimental workflow. **Output On Success** -``` +```markdown ## Archive Complete **Change:** **Schema:** -**Archived to:** the archive path derived from `planningHome.changesDir`/YYYY-MM-DD-/ -**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped") +**Archived to:** the archive path derived from `planningHome.changesDir`// +**Specs:** <"✓ Synced to main specs" only if the step 4 verification passed; otherwise "No delta specs" or "Sync skipped"> -All artifacts complete. All tasks complete. +<"All artifacts complete. All tasks complete." — or, if archived with warnings, list them instead (e.g. "Archived with 2 incomplete tasks")> ``` **Guardrails** -- Always prompt for change selection if not provided +- Announce the selected change; prompt for selection when it is ambiguous - Use artifact graph (openspec status --json) for completion checking - Don't block archive on warnings - just inform and confirm - Preserve .openspec.yaml when moving to archive (it moves with the directory) - Show clear summary of what happened -- If sync is requested, use openspec-sync-specs approach (agent-driven) +- If sync is requested, run the `openspec-sync-specs` workflow inline (agent-driven) +- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving `changeRoot` - If delta specs exist, always run the sync assessment and show the combined summary before prompting +- Apply relevant runtime context and report conflicts; operation guidance remains advisory +- Consider every guidance entry and explain any inapplicable or conflicting advice +- Existing CLI checks, resolved paths, prompts, and command contracts are unchanged +- Artifact rules constrain only the specs being written and are never operation guidance +- Never copy runtime context, operation guidance, or artifact-rule text verbatim into output files diff --git a/.claude/skills/openspec-explore/SKILL.md b/.claude/skills/openspec-explore/SKILL.md index 771271a..9b0f5d2 100644 --- a/.claude/skills/openspec-explore/SKILL.md +++ b/.claude/skills/openspec-explore/SKILL.md @@ -7,16 +7,16 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.6.0" + generatedBy: "1.8.0" --- Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. -**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing. For a new change, scaffold it first as described below. **This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. --- @@ -94,6 +94,12 @@ This tells you: - Their names, schemas, and status - What the user might be working on +Then read the project's own context from the resolved root - `/openspec/config.yaml` (or `config.yml`). Use the `root.path` returned above, and skip this if neither file exists: +- `context`: project background - tech stack, conventions, constraints +- `rules`: keyed by artifact id - the entries for an artifact apply only when you write that artifact + +Ground your thinking in these. They are constraints for you to follow, not content to reproduce: do NOT copy them into the conversation or into any artifact you create. + ### When no change exists Think freely. When insights crystallize, you might offer: @@ -101,6 +107,15 @@ Think freely. When insights crystallize, you might offer: - "This feels solid enough to start a change. Want me to create a proposal?" - Or keep exploring - no pressure to formalize +If the user asks you to capture the exploration as a new change, transition seamlessly into the requested capture: + +1. Run `openspec new change ""` (with `--store ` when applicable) before creating any artifacts. Never create a new change directory under `openspec/changes/` by hand; the CLI scaffold creates required metadata such as `.openspec.yaml`. Keep the selected `--store ` on every applicable follow-up `status` and `instructions` command. +2. Run `openspec status --change "" --json` (append the confirmed `--store ""` only for a registered standalone store), then process the requested artifacts in dependency order. For each requested artifact that is `ready`, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store). Before creating a requested artifact, evaluate any condition in its own `instruction` against the explored change; record a deliberate skip instead when the condition does not apply. If a requested artifact is blocked by a direct prerequisite the user did not request, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) for that prerequisite whether it is `ready` or `blocked`. If its own `instruction` states a condition, evaluate that condition against the explored change and record a deliberate skip only when the condition does not apply. If the condition applies, or the prerequisite is not conditional, treat it as a normal prerequisite and ask before expanding the capture. Do not create an unrequested prerequisite unless the user approves. +3. Follow the returned `template` and `instruction` fields. Read completed dependency files listed in `dependencies`, and apply `context` and `rules` as constraints without copying them into the artifact. If the instruction delegates creation to a specific skill or command, invoke it; otherwise write the artifact to `resolvedOutputPath`, using the instruction to choose a concrete path when it is a glob. Verify that the selected concrete output exists. +4. After creating each artifact, re-run `openspec status --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) and continue until every requested artifact is `done`, `skipped`, or was deliberately skipped because its own `instruction` stated a condition that did not apply. Tell the user about a deliberate conditional skip, remember it, and do not reconsider it. Dependencies are enablers, not gates: if a requested artifact is still `blocked` only because you deliberately skipped a conditional prerequisite, run `openspec instructions "" --change "" --json` (append the confirmed `--store ""` only for a registered standalone store) despite the blocked status, then create it using step 3 only when those recorded conditional skips are its sole missing dependencies. If a requested artifact is blocked by a prerequisite the user did not ask to capture and cannot be conditionally skipped, explain that dependency and ask before expanding the capture. + +Capture the artifact(s) the user requested without asking them to invoke another workflow command. If they asked only to start a change, stop after scaffolding and show its status. + ### When a change exists If the user mentions a change or you detect one is relevant: @@ -116,14 +131,16 @@ If the user mentions a change or you detect one is relevant: 3. **Offer to capture when decisions are made** - | Insight Type | Where to Capture | - |----------------------------|--------------------------------| - | New requirement discovered | `specs//spec.md` | - | Requirement changed | `specs//spec.md` | - | Design decision made | `design.md` | - | Scope changed | `proposal.md` | - | New work identified | `tasks.md` | - | Assumption invalidated | Relevant artifact | + `` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities. + + | Insight Type | Where to Capture | + |----------------------------|-------------------------------------| + | New requirement discovered | `specs//spec.md` | + | Requirement changed | `specs//spec.md` | + | Design decision made | `design.md` | + | Scope changed | `proposal.md` | + | New work identified | `tasks.md` | + | Assumption invalidated | Relevant artifact | Example offers: - "That's a design decision. Capture it in design.md?" @@ -285,6 +302,7 @@ But this summary is optional. Sometimes the thinking IS the value. - **Don't rush** - Discovery is thinking time, not task time - **Don't force structure** - Let patterns emerge naturally - **Don't auto-capture** - Offer to save insights, don't just do it +- **Don't manually scaffold changes** - Never create a new change directory under `openspec/changes/` by hand. Always use `openspec new change ""` (with `--store ` when applicable) so required metadata such as `.openspec.yaml` is created before writing artifacts. - **Do visualize** - A good diagram is worth many paragraphs - **Do explore the codebase** - Ground discussions in reality - **Do question assumptions** - Including the user's and your own diff --git a/.claude/skills/openspec-propose/SKILL.md b/.claude/skills/openspec-propose/SKILL.md index 716d2d3..8a758cb 100644 --- a/.claude/skills/openspec-propose/SKILL.md +++ b/.claude/skills/openspec-propose/SKILL.md @@ -7,53 +7,79 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.6.0" + generatedBy: "1.8.0" --- Propose a new change - create the change and generate all artifacts in one step. -I'll create a change with artifacts: +**Planning boundary**: This workflow creates planning artifacts only. The user request that selected or triggered this workflow authorizes planning only, even if it asks to build or fix something. Do not edit project code. After the planning artifacts are complete, stop. Do not start implementation in the same response, even if the initial request asks for it. Wait for a new user request after the artifacts are presented; then start the apply workflow. + +I'll create a change with the artifacts your schema defines. With the default spec-driven schema that is: - proposal.md (what & why) +- `specs//spec.md` (what the system must do - a delta, not the main spec) - design.md (how) - tasks.md (implementation steps) -When ready to implement, run /opsx:apply +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve an existing capability's full path and follow the project's established organization for new capabilities. + +When the user is ready to implement, they must start the apply workflow explicitly. --- -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. **Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. **Steps** -1. **If no clear input provided, ask what they want to build** +1. **Understand the request and clarify material ambiguity** - Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: + If no clear input is provided, ask the user (open-ended, no preset options): > "What change do you want to work on? Describe what you want to build or fix." From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. -2. **Create the change directory** + If the request contains ambiguity that would materially affect scope, externally observable behavior, compatibility, or acceptance criteria, ask the user before creating the change. For minor details, make a reasonable assumption and record it in the planning artifacts. + +2. **Determine the workflow schema** + + Use the configured default schema unless the user explicitly requests a different workflow. + + **Use a different schema only if the user:** + - Explicitly requests a specific schema by name → use `--schema ` + - Asks to "show workflows" or asks "what workflows" exist → resolve the authoritative root by running `openspec context --json` from the current working directory. If the user explicitly selected a registered store, use `openspec context --json --store ""`. Then run `openspec schemas --json` with its working directory set to the returned `root.path` and let them choose. This preserves roots selected by a local `store:` pointer or the global `defaultStore`; `schemas` does not accept `--store`. If context reports only `no_openspec_root`, run `openspec schemas --json` from the current working directory instead. Do not use this fallback for invalid or unavailable stores. + + Otherwise, omit `--schema` to preserve the configured default. + +3. **Create the change directory** + + Choose one schema form below. If a registered store is selected, append `--store ""` to that command and each later OpenSpec command shown below that accepts `--store`. + + Using the configured default: ```bash openspec new change "" ``` + + Using an explicitly requested schema: + ```bash + openspec new change "" --schema "" + ``` This creates a scaffolded change in the planning home resolved by the CLI with `.openspec.yaml`. -3. **Get the artifact build order** +4. **Get the artifact build order** ```bash openspec status --change "" --json ``` Parse the JSON to get: - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) - - `artifacts`: list of all artifacts with their status and dependencies + - `artifacts`: list of all artifacts, each with its `status` and its `requires` edges (the artifact IDs it directly depends on) - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. -4. **Create artifacts in sequence until apply-ready** +5. **Create every artifact in the required set** - Use the **TodoWrite tool** to track progress through the artifacts. + Use a todo list to track progress through the artifacts. Loop through artifacts in dependency order (artifacts with no pending dependencies first): @@ -67,23 +93,30 @@ When ready to implement, run /opsx:apply - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) - `template`: The structure to use for your output file - `instruction`: Schema-specific guidance for this artifact type + - `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact - `resolvedOutputPath`: Resolved path or pattern to write the artifact - `dependencies`: Completed artifacts to read for context - - Read any completed dependency files for context - - Create the artifact file using `template` as the structure and write it to `resolvedOutputPath` + - Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them) + - If the `instruction` field delegates creation to a specific skill or command, invoke it to produce the artifact instead of writing the file yourself, then verify the artifact file exists at `resolvedOutputPath` + - Otherwise create the artifact file using `template` as the structure and write it to `resolvedOutputPath`. If `resolvedOutputPath` is a glob, follow `instruction` to choose the concrete file path - Apply `context` and `rules` as constraints - but do NOT copy them into the file - Show brief progress: "Created " - b. **Continue until all `applyRequires` artifacts are complete** + b. **Continue until every artifact in the required set exists (not just `apply.requires`)** - After creating each artifact, re-run `openspec status --change "" --json` - - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array - - Stop when all `applyRequires` artifacts are done + - The required set is `applyRequires` plus every artifact reachable from those by following the `requires` edges in `status --json` - walk them transitively (spec-driven closes over proposal, specs, design, tasks). Leave artifacts outside that set alone + - `status` is file-existence only, so an `applyRequires` artifact reading `done` does NOT mean its dependencies exist - writing `tasks.md` early marks `tasks` done while `specs` was never written. Use each artifact's `requires` edges, not its `status`, to build the required set: a `done` artifact still lists what it depends on + - An artifact already reading `status: "skipped"` is satisfied: the change declares `skip_specs` in `.openspec.yaml`, so its files must NOT exist. Never try to create one + - Create every artifact in the required set that is missing, then re-check - creating one can unblock others + - Skip one only when `status` already reports it `skipped`, or when its own `instruction` says it is conditional: run `openspec instructions --change "" --json` and skip only if its `instruction` field marks it optional (e.g. "create only if..."). Spec-driven's `design.md` qualifies; `specs` qualifies only via the `skipped` status above, never by your own judgment. Tell the user, and do not reconsider it + - Dependencies are enablers, not gates: if a required artifact is still `blocked` only because you skipped a conditional dependency, write it anyway + - Stop when every artifact in the required set is `done`, `skipped`, or was deliberately skipped c. **If an artifact requires user input** (unclear context): - - Use **AskUserQuestion tool** to clarify + - Ask the user to clarify - Then continue with creation -5. **Show final status** +6. **Show final status** ```bash openspec status --change "" ``` @@ -92,13 +125,14 @@ When ready to implement, run /opsx:apply After completing all artifacts, summarize: - Change name and location -- List of artifacts created with brief descriptions -- What's ready: "All artifacts created! Ready for implementation." -- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks." +- List of artifacts created with brief descriptions, plus any conditional artifact you skipped and why +- What's ready: "All artifacts needed for implementation are ready." +- Prompt: "The artifacts are ready for review. When you are ready, run `/opsx:apply` or ask me to apply this change." **Artifact Creation Guidelines** -- Follow the `instruction` field from `openspec instructions` for each artifact type +- Follow the `instruction` field from `openspec instructions` for each artifact type - it is the authoritative guidance, even for familiar artifact names +- If the `instruction` field directs you to use a specific skill or command to create the artifact, invoke it instead of writing the artifact directly - The schema defines what each artifact should contain - follow it - Read dependency artifacts for context before creating new ones - Use `template` as the structure for your output file - fill in its sections @@ -107,8 +141,9 @@ After completing all artifacts, summarize: - These guide what you write, but should never appear in the output **Guardrails** -- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) -- Always read dependency artifacts before creating a new one -- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum +- The request that invoked this workflow authorizes planning only. Any implementation or apply instruction in that request does not carry forward. Do NOT implement the change, start the apply workflow, or edit project code during this workflow. After presenting the artifacts, stop and wait for a new user request to start the apply workflow +- Create every artifact the apply phase transitively depends on, not just the ids listed in `apply.requires` +- Always read dependency artifacts before creating a new one - re-read from disk, not from conversation memory (files may have changed since you last saw them) +- Ask about ambiguities that would materially change scope, externally observable behavior, compatibility, or acceptance criteria; for minor details, make reasonable assumptions and record them - If a change with that name already exists, ask if user wants to continue it or create a new one - Verify each artifact file exists after writing before proceeding to next diff --git a/.claude/skills/openspec-sync-specs/SKILL.md b/.claude/skills/openspec-sync-specs/SKILL.md index b6aad67..2e6b9ed 100644 --- a/.claude/skills/openspec-sync-specs/SKILL.md +++ b/.claude/skills/openspec-sync-specs/SKILL.md @@ -7,26 +7,31 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.6.0" + generatedBy: "1.8.0" --- Sync delta specs from a change to main specs. This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement). -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +`` is the spec directory relative to `specs/` (for example, `user-auth` or `identity/user-auth`). Preserve the full path from each delta spec when resolving its main spec. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. **Steps** -1. **If no change name provided, prompt for selection** +1. **Select the change** - Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and ask the user to select one - Show changes that have delta specs (under `specs/` directory). + When prompting, show changes that have delta specs (under `specs/` directory). - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + Always announce: "Using change: " and how to override (e.g., `/opsx:sync `). 2. **Resolve change context** @@ -35,9 +40,29 @@ This is an **agent-driven** operation - you will read delta specs and directly e openspec status --change "" --json ``` + The JSON includes `planningHome.root`. Main specs live under `/openspec/specs/` — use that (store-aware) root for every main-spec path below, not a hardcoded repo path. When a store is selected it points at the store, not the current repository. + 3. **Find delta specs** - Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the list of delta spec files. + Use `artifactPaths.specs.existingOutputPaths` from the status JSON as the + only source of delta spec paths. If the `specs` entry is missing or + `existingOutputPaths` is empty, report that there are no delta specs to sync, + do not infer them from other artifacts, and stop without requesting artifact + instructions or writing a main spec. + + Sync every path in `existingOutputPaths` unless the caller narrowed the set. + A caller narrows it by naming an explicit list of complete entries from + `existingOutputPaths` — copy those absolute values verbatim. Archive does + this inline, and a user can too (for example, by selecting the entry ending + in `/specs/billing/invoices/spec.md`). + Then sync only the named paths and leave the remaining delta specs untouched: + bulk archive excludes a delta whose implementation it could not find, and + syncing it anyway would write a main spec the caller deliberately withheld. + Carry that narrowed selection through step 4; never widen it back to the full + list. If a named path is not in `existingOutputPaths`, do not sync it — + report it and stop, rather than dropping it silently. If the named list is + empty, report that there is nothing to sync and stop without writing a main + spec. Each delta spec file contains sections like: - `## ADDED Requirements` - New requirements to add @@ -49,11 +74,27 @@ This is an **agent-driven** operation - you will read delta specs and directly e 4. **For each delta spec, apply changes to main specs** - For each repo-local capability delta spec path returned by the CLI: + Before the first main-spec write, obtain one current specs-rule snapshot: + - If archive invoked this workflow inline and supplied a valid snapshot from + `openspec instructions specs --change "" --json`, reuse it and do not + fetch the same instructions again. + - Otherwise run that command once now with the same selected-root flags. + - If the direct lookup exits non-zero or returns invalid artifact-instruction + JSON, report the error and stop before writing any main spec. Do not treat the + failure as an absent rule set. + - A valid response with omitted `rules` means no artifact rules are configured + and the existing semantic merge continues. + + Apply returned `rules` only to the content and form of the main specs produced + by this merge. Artifact rules are not operation guidance and cannot change + selected roots, delta paths, CLI checks, or workflow steps. Use their text as + constraints without copying it verbatim into a main spec or summary. + + For each capability delta spec path selected in step 3 — the full `existingOutputPaths` list, or the narrowed subset when a caller supplied one (these may belong to a selected store, not the repo): a. **Read the delta spec** to understand the intended changes - b. **Read the main spec** at `openspec/specs//spec.md` (may not exist yet) + b. **Read the main spec** at `/openspec/specs//spec.md` (may not exist yet) c. **Apply changes intelligently**: @@ -64,31 +105,72 @@ This is an **agent-driven** operation - you will read delta specs and directly e **MODIFIED Requirements:** - Find the requirement in main spec - Apply the changes - this can be: - - Adding new scenarios (don't need to copy existing ones) + - Adding new scenarios the main spec does not have yet - Modifying existing scenarios - Changing the requirement description - Preserve scenarios/content not mentioned in the delta **REMOVED Requirements:** - Remove the entire requirement block from main spec + - Retiring the capability. Delete the whole `spec.md` - and the directory once + nothing else is left in it - only when ALL of these hold: + 1. removing the requirements *this run* left no requirement blocks; + 2. the rest of the spec is well-formed (it still has a `## Purpose`); + 3. the main spec was not already empty before this sync - if you removed + nothing, change nothing; + 4. every other nonblank line in the whole file is accounted for as the + title, Purpose, Requirements header, or a canonical requirement's + statement, scenarios, or fenced examples; + 5. the change's `.openspec.yaml` declares `retire_capabilities: true`; + 6. the `spec.md` resolves inside the real specs root (do not follow a + capability-directory symlink to delete an external file). + If removing the selected requirements would leave no requirement blocks and + any retirement condition is not satisfied, do not modify the main spec. Stop + the sync for that capability, report the blocking condition, and tell the user + how to resolve it. Never write or leave an empty `## Requirements` section. + When only the marker is missing, say that too - it is the one thing the user + can add to make the retirement go through. + - Deleting the file also deletes its `## Purpose`; any other section blocks + retirement. Name Purpose when you report the retirement. Include a pasteable + `git checkout` only when the spec lived in the caller's checkout; + otherwise give checkout-scoped recovery guidance. **RENAMED Requirements:** - Find the FROM requirement, rename to TO - d. **Create new main spec** if capability doesn't exist yet: - - Create `openspec/specs//spec.md` - - Add Purpose section (can be brief, mark as TBD) - - Add Requirements section with the ADDED requirements + **`## Purpose` in the delta:** + - The main spec already has one and it is authoritative - leave it alone + (this is what `openspec archive` does; it warns and moves on) -5. **Show summary** + d. **Create new main spec** if capability doesn't exist yet: + - Create `/openspec/specs//spec.md` + - Add Purpose section: copy the delta's `## Purpose` body verbatim when it has one + (this is what `openspec archive` does); only write a brief TBD placeholder when it does not + - Add Requirements section with the ADDED requirements + - Follow the **Main Spec Format Reference** below + +5. **Validate updated main specs** + + Run `openspec validate --specs` with the same selected-root flags used earlier. + If validation fails, report the problems and do not claim the sync succeeded. + +6. **Show summary** After applying all changes, summarize: - Which capabilities were updated - What changes were made (requirements added/modified/removed/renamed) + - Any new main spec left with a TBD Purpose placeholder, so it gets written + now rather than lingering + - Any capability retired, naming the deleted `spec.md`, its Purpose, and + either a pasteable `git checkout` or checkout-scoped recovery guidance **Delta Spec Format Reference** ```markdown +## Purpose + +Only on a delta that introduces a brand-new capability. Seeds the new main spec. + ## ADDED Requirements ### Requirement: New Feature @@ -101,6 +183,12 @@ The system SHALL do something new. ## MODIFIED Requirements ### Requirement: Existing Feature +The system SHALL keep doing the existing thing, now also handling A. + +#### Scenario: Scenario the main spec already has +- **WHEN** user does X +- **THEN** system does Y + #### Scenario: New scenario to add - **WHEN** user does A - **THEN** system does B @@ -115,16 +203,36 @@ The system SHALL do something new. - TO: `### Requirement: New Name` ``` +**Main Spec Format Reference** + +Main specs are what the delta merges INTO. They must never contain delta operation headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) - after syncing, every requirement lives under a single `## Requirements` section: + +```markdown +# Specification + +## Purpose +Short description of what this capability does and why it exists. + +## Requirements + +### Requirement: New Feature +The system SHALL do something new. + +#### Scenario: Basic case +- **WHEN** user does X +- **THEN** system does Y +``` + **Key Principle: Intelligent Merging** -Unlike programmatic merging, you can apply **partial updates**: -- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios -- The delta represents *intent*, not a wholesale replacement +Unlike programmatic merging, you merge rather than overwrite: +- A MODIFIED block carries the whole requirement - body plus every scenario that survives the change. `openspec validate` and `openspec archive` both reject one that drops a scenario the main spec still has. +- Keep anything the delta does not mention, in the main spec's existing order - Use your judgment to merge changes sensibly **Output On Success** -``` +```markdown ## Specs Synced: Updated main specs: @@ -143,6 +251,12 @@ Main specs are now updated. The change remains active - archive when implementat **Guardrails** - Read both delta and main specs before making changes - Preserve existing content not mentioned in delta +- Never copy a delta file into a main spec as-is - merge its content so the main spec keeps the Main Spec Format Reference structure, with no delta operation headers - If something is unclear, ask for clarification - Show what you're changing as you go - The operation should be idempotent - running twice should give same result +- Use only `artifactPaths.specs.existingOutputPaths`; never infer delta specs from unrelated artifacts +- Honor a caller-supplied subset of `existingOutputPaths`; never widen it back to the full list +- Fetch specs instructions once for direct sync, or reuse the archive-supplied snapshot inline +- Stop before every main-spec write on a non-zero or invalid JSON specs-instruction response +- Artifact rules constrain only the specs being written and are never copied into output files diff --git a/.claude/skills/openspec-update-change/SKILL.md b/.claude/skills/openspec-update-change/SKILL.md index 3e1c931..ca3cbcf 100644 --- a/.claude/skills/openspec-update-change/SKILL.md +++ b/.claude/skills/openspec-update-change/SKILL.md @@ -7,22 +7,27 @@ compatibility: Requires openspec CLI. metadata: author: openspec version: "1.0" - generatedBy: "1.6.0" + generatedBy: "1.8.0" --- Revise a change's existing planning artifacts and keep them coherent. Never edit code. -**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`, `view`). Once selected, treat `--store ` as sticky for the rest of the workflow. Every unscoped example of those commands below is shorthand: before running it, append the flag. For example, run `openspec status --change "" --json --store ""`, not the unscoped form shown below. Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. +`/opsx:continue` is an expanded-profile workflow and may not be installed. Before suggesting it anywhere below, verify that it is available. If it is unavailable, `openspec status --change "" --json` shows the next artifact and `openspec instructions "" --change "" --json` explains how to create it. + **Steps** -1. **If no change name provided, prompt for selection** +1. **Select the change** - Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to update. + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes sorted by most recently modified, and ask the user to select one - Present the top 3-4 most recently modified changes as options, showing: + When prompting, present the top 3-4 most recently modified changes as options, showing: - Change name - Schema (from `schema` field if present, otherwise "spec-driven") - Status (e.g., "0/5 tasks", "complete", "no tasks") @@ -30,7 +35,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to update. - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. + Always announce: "Using change: " and how to override (e.g., `/opsx:update `). 2. **Get the change's artifacts** ```bash @@ -38,8 +43,8 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit ``` Parse the JSON to understand current state. The response includes: - `schemaName`: The workflow schema being used (e.g., "spec-driven") - - `artifacts`: Array of artifacts with their status ("done", "ready", "blocked") - - `isComplete`: Boolean indicating if all artifacts are complete + - `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked") + - `isPlanningComplete`: Boolean indicating if all planning artifacts are complete. Older CLI versions expose the same value as `isComplete`. - `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths. The artifact ids and paths come from the active schema - do NOT assume them, and do NOT branch on hardcoded artifact names. Custom schemas must work unchanged. @@ -62,7 +67,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit - If the user rejects a revision, do not write it - leave that artifact unchanged. - When a substantial rewrite is needed, get that artifact's rules and template first: ```bash - openspec instructions --change "" --json + openspec instructions "" --change "" --json ``` 6. **Point to the next step (guidance only - NEVER act on it)** @@ -83,4 +88,4 @@ After each invocation, show: - Edit only the concrete files in `existingOutputPaths`; never write to a glob `resolvedOutputPath`. - Do not advance the build frontier: no new artifacts, no new files under glob artifacts - that is `/opsx:continue`'s job. - Confirm every edit with the user before writing. -- If the request changes the change's *intent* rather than refining it, recommend starting fresh with `/opsx:new` (the "Update vs. Start Fresh" heuristic). +- If the request changes the change's *intent* rather than refining it, first verify whether the expanded-profile `/opsx:new` workflow is available. If it is, recommend starting fresh with `/opsx:new` (the "Update vs. Start Fresh" heuristic). If it is unavailable, ask for a distinct unused change name and recommend `openspec new change ""` instead. diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json deleted file mode 100644 index 86bbf56..0000000 --- a/.opencode/package-lock.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "name": ".opencode", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@opencode-ai/plugin": "1.3.17" - } - }, - "node_modules/@opencode-ai/plugin": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.17.tgz", - "integrity": "sha512-N5lckFtYvEu2R8K1um//MIOTHsJHniF2kHoPIWPCrxKG5Jpismt1ISGzIiU3aKI2ht/9VgcqKPC5oZFLdmpxPw==", - "license": "MIT", - "dependencies": { - "@opencode-ai/sdk": "1.3.17", - "zod": "4.1.8" - }, - "peerDependencies": { - "@opentui/core": ">=0.1.96", - "@opentui/solid": ">=0.1.96" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "node_modules/@opencode-ai/sdk": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.17.tgz", - "integrity": "sha512-2+MGgu7wynqTBwxezR01VAGhILXlpcHDY/pF7SWB87WOgLt3kD55HjKHNj6PWxyY8n575AZolR95VUC3gtwfmA==", - "license": "MIT", - "dependencies": { - "cross-spawn": "7.0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/zod": { - "version": "4.1.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/.opencode/skills/comment-standards/SKILL.md b/.opencode/skills/comment-standards/SKILL.md deleted file mode 100644 index aa10e03..0000000 --- a/.opencode/skills/comment-standards/SKILL.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -name: comment-standards -description: Go 注释规范。编写 Go 代码注释、文档注释时使用。包含包注释、结构体注释、接口注释、函数注释、内联注释的完整规范与示例。 ---- - -# Go 注释规范 - -**基本原则**: -- **所有注释使用中文** -- **导出符号必须有文档注释**(包、函数、方法、类型、接口、常量、变量) -- **复杂逻辑必须有实现注释**(解释"为什么",而不是"做了什么") -- **禁止废话注释**(不要用注释复述代码本身) -- **修改代码时必须同步更新注释** - ---- - -## 包注释 - -每个包的入口文件(通常是主文件或 `doc.go`)必须有包注释: - -```go -// Package account 提供账号管理的业务逻辑服务 -// 包含账号创建、修改、删除、权限分配等功能 -package account -``` - -## 结构体注释 - -所有导出结构体必须有文档注释,说明该结构体代表什么: - -```go -// Service 账号业务服务 -// 负责账号的 CRUD、角色分配、密码管理等业务逻辑 -type Service struct { - store *Store - auditService AuditServiceInterface -} -``` - -## 接口注释 - -导出接口必须注释接口用途,每个方法必须说明契约: - -```go -// PermissionChecker 权限检查器接口 -// 用于查询用户的权限列表 -type PermissionChecker interface { - // CheckPermission 检查用户是否拥有指定权限 - // userID: 用户ID - // permCode: 权限编码(格式: module:action) - // platform: 端口类型 (all/web/h5) - CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error) -} -``` - -## 函数和方法注释 - -**导出函数/方法**必须以函数名开头,说明功能: - -```go -// Create 创建账号 -// POST /api/admin/accounts -func (h *AccountHandler) Create(c *fiber.Ctx) error { -``` - -**复杂方法**(超过 30 行或包含复杂业务逻辑)必须额外说明实现思路: - -```go -// ActivateByRealname 首次实名激活套餐 -// 当用户完成实名认证后,自动激活处于"囤货待实名"状态的套餐: -// 1. 查找该卡所有 status=3(待实名激活)的套餐 -// 2. 按创建时间排序,第一个主套餐立即激活(status=1) -// 3. 其余主套餐进入排队状态(status=4) -// 4. 加油包如果绑定了已激活的主套餐则一并激活 -func (s *UsageService) ActivateByRealname(ctx context.Context, cardID uint) error { -``` - -**未导出函数/方法**: -- 简单逻辑(< 15 行):可以不加注释 -- 复杂逻辑(≥ 15 行)或非显而易见的算法:必须加注释 - -```go -// buildPermissionTree 递归构建权限树 -// 采用 map 索引 + 单次遍历算法,时间复杂度 O(n) -func (s *Service) buildPermissionTree(permissions []*model.Permission) []*dto.PermissionTreeNode { -``` - -## 常量和枚举注释 - -分组常量必须有组注释,每个值必须有行内注释: - -```go -// 用户类型常量 -const ( - UserTypeSuperAdmin = 1 // 超级管理员 - UserTypePlatform = 2 // 平台用户 - UserTypeAgent = 3 // 代理账号 - UserTypeEnterprise = 4 // 企业账号 -) -``` - -## 内联注释规范 - -**必须添加内联注释的场景**: - -| 场景 | 要求 | -|------|------| -| 复杂条件判断 | 解释判断的业务含义 | -| 多步骤业务流程 | 用编号注释标明每一步 | -| 非显而易见的设计决策 | 解释"为什么这样做"而不是"做了什么" | -| 缓存/事务/并发处理 | 说明策略和原因 | -| 临时方案/兼容逻辑 | 标注 TODO 或说明背景 | - -**✅ 好的内联注释(解释为什么)**: - -```go -// 使用 Redis 分布式锁防止并发重复创建,锁超时 10 秒 -if !s.acquireLock(ctx, lockKey, 10*time.Second) { - return errors.New(errors.CodeTooManyRequests, "操作过于频繁,请稍后重试") -} - -// 先冻结佣金再扣款,保证资金安全(失败时佣金自动解冻) -if err := s.freezeCommission(ctx, tx, orderID); err != nil { - return err -} -``` - -**❌ 废话注释(禁止)**: - -```go -// 获取用户ID ← 禁止:代码本身已经很清楚 -userID := middleware.GetUserIDFromContext(ctx) - -// 创建账号 ← 禁止:变量名已说明意图 -account := &model.Account{} - -// 返回错误 ← 禁止:return err 不需要注释 -return err -``` diff --git a/.opencode/skills/hurl-test/SKILL.md b/.opencode/skills/hurl-test/SKILL.md deleted file mode 100644 index 072af15..0000000 --- a/.opencode/skills/hurl-test/SKILL.md +++ /dev/null @@ -1,952 +0,0 @@ ---- -name: hurl-test -description: Hurl 接口测试生成器。用户描述要测试的接口或业务流程,自动探索代码、确认需求、生成完整的 .hurl 测试文件(含 DTO 驱动的字段完整性断言)。触发词:测试、hurl、写测试、接口测试。 ---- - -# Hurl 接口测试生成器 - -**用户描述要测试什么,你来读代码、问确认、生成 .hurl 文件。** - -适用于任何后端项目(Go / Python / Node / Java 等),不预设框架和目录结构。 - ---- - -## 触发条件 - -以下情况必须使用本 Skill: -- 用户说"测试 XX 接口"、"写 hurl 测试"、"给 XX 加测试" -- 用户说"测试 XX 流程"、"测试 XX 的业务逻辑" -- 用户说"验证 XX 接口的字段"、"测试接口契约" -- 用户提到 hurl、.hurl、接口测试、集成测试、冒烟测试 - ---- - -## 五阶段工作流(必须按顺序执行) - -``` -Phase 1: 探索 → Phase 2: 确认 → Phase 3: 生成 → Phase 4: 验证 → Phase 5: 研判 -读代码搞清楚 展示给用户确认 输出 .hurl 文件 语法检查+试跑 分析失败/报告发现 -``` - -> **核心原则:测试是用来发现问题的,不是用来全部通过的。** -> 断言基于代码定义(Handler + DTO)生成,与实际行为的差异是"发现",不是要修掉的"错误"。 - ---- - -### Phase 1: 探索(Explore) - -**目标:读代码,搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。要求准确,不求快。** - -#### 真理源层级(必须遵守) - -断言的字段名和类型基于以下优先级确定,**高优先级推翻低优先级**: - -| 优先级 | 来源 | 说明 | 怎么找 | -|-------|------|------|--------| -| **1(最高)** | Handler 实际实现 | Handler 返回什么类型、调用哪个 response 方法、是否做了 model→DTO 转换 | 读 handler 函数体,看 `return response.Success(c, xxx)` 中 xxx 的类型 | -| **2** | DTO / Response 结构体 | Handler 引用的 DTO 的 json tag、字段类型 | 读 DTO struct 定义 | -| **3(最低)** | 路由注册的 Output 声明 | RouteSpec 中声明的 Output 类型,仅作为文档参考,可能过期 | 读路由注册代码 | - -**关键规则**: -- **不能只读 DTO 就生成断言**。必须追溯到 Handler 确认它确实用了这个 DTO -- 如果 Handler 直接返回 GORM model 而非 DTO,断言用 model 的字段名(可能是大写 `ID` 而非小写 `id`) -- 如果 Handler 用了 `response.SuccessWithPagination()`,分页字段以该函数的结构为准,而非 DTO 定义的分页结构 -- 当 Handler 实现与 DTO 定义不一致时,**以 Handler 为准生成断言,同时在 Phase 5 中将不一致标记为发现** - -#### 1.1 项目画像(首次使用时必须执行,后续复用) - -首次为项目生成 Hurl 测试时,先回答以下问题(通过读代码,不要猜): - -| 问题 | 怎么找 | -|------|--------| -| **语言/框架** | 看 go.mod / package.json / requirements.txt / pom.xml | -| **路由注册在哪** | 搜索 `router`、`app.Get`、`@GetMapping`、`@app.route` 等关键词 | -| **请求/响应 schema 定义在哪** | 搜索 DTO / schema / serializer / model 目录,看 json tag 或装饰器 | -| **统一响应格式是什么** | 找 response helper 文件(如 `response.go`、`response.py`),记录 JSON 结构 | -| **认证方式是什么** | 找 auth middleware,确定是 Bearer Token / Cookie / API Key / Basic Auth | -| **登录接口是什么** | 找登录 handler,记录路径、请求体、响应中 token 的位置 | -| **分页格式是什么** | 找列表接口的响应结构,记录 items/total/page 等字段名 | -| **已有 hurl 测试吗** | 搜索 `*.hurl` 文件,复用已有的约定 | - -将画像结果**写入 `tests/hurl/.project-profile.md` 文件持久化保存**。 - -#### 画像持久化(关键机制) - -**首次使用时**:完成 1.1 探索后,将画像写入 `tests/hurl/.project-profile.md`,格式如下: - -```markdown -# 项目画像(Hurl 测试自动生成用) - - - -## 技术栈 -- 语言: Go 1.25 -- 框架: Fiber v2 -- ORM: GORM - -## 路由定义位置 -- 路由注册入口: internal/routes/routes.go -- 按模块拆分: internal/routes/{module}.go -- 路由注册函数: Register(router, doc, basePath, method, path, handler, spec) - -## Schema 定义位置 -- DTO 目录: internal/model/dto/ -- 命名规则: {module}_dto.go -- 字段标签: json / validate / description - -## 统一响应格式 -{code: int, msg: string, data: any, timestamp: string(RFC3339)} -- 成功: code=0, msg="success" -- 错误: code!=0 - -## 分页格式 -{items: [], total: int, page: int, size: int} -- 包裹在 data 字段内: $.data.items / $.data.total - -## 认证方式 -- 后台: POST /api/auth/admin-login → $.data.access_token → Authorization: Bearer {token} -- C端: JWT → Authorization: Bearer {token} - -## 默认测试账号 -- 用户名: admin -- 密码: Admin@123456 - -## 服务端口 -- 默认: 3000 -``` - -**后续使用时**:检查 `tests/hurl/.project-profile.md` 是否存在: -- **存在** → 直接读取,跳过 1.1 的探索步骤,节省时间 -- **不存在** → 执行 1.1 完整探索,然后生成此文件 -- **用户说"刷新画像"** → 删除旧文件,重新执行 1.1 - -#### 1.2 找接口定义 - -根据用户要测的模块,定位路由注册代码,提取: - -- **HTTP 方法**(GET / POST / PUT / DELETE / PATCH) -- **路由路径**(含路径参数格式,如 `/users/:id` 或 `/users/{id}`) -- **接口说明**(注释、Summary、装饰器描述) -- **是否需要认证** -- **请求 schema 类型名**(Input / Request DTO) -- **响应 schema 类型名**(Output / Response DTO) - -#### 1.3 读 schema 定义(DTO / struct / class / type) - -定位请求和响应的 schema 定义文件,提取每个字段的: - -- **字段名**:JSON 序列化后的名称(json tag / @JsonProperty / serializer field) -- **语言类型**:string / int / bool / 数组 / 嵌套对象 / 可空等 -- **是否必填**:validate tag / required 装饰器 / 非空标注 -- **是否可空**:指针类型 / Optional / nullable -- **是否参与序列化**:`json:"-"` / @JsonIgnore / exclude -- **是否 omitempty**:`json:",omitempty"` / 条件序列化 -- **字段描述**:description tag / docstring / 注释 - -#### 1.4 识别业务依赖 - -读 service / business logic 层,识别: - -- 创建操作需要哪些前置数据(如创建订单需要先有商品和用户) -- 是否有唯一性约束(如用户名不能重复) -- 是否依赖外部服务(支付网关、短信、OAuth 等) -- 业务流转逻辑(状态机、级联操作) - ---- - -### Phase 2: 确认(Clarify) - -**目标:向用户展示发现的内容,确认模糊点。不要闷头生成。** - -#### 2.1 必须展示的内容 - -``` -我梳理了相关代码,发现以下信息: - -📋 涉及接口: - - [方法] [路径] - [说明](认证: 是/否) - - ... - -📦 响应字段(基于 {SchemaName}): - - [字段名]: [类型] - [说明] - - ...(共 N 个字段,将全部生成断言) - -🔗 依赖关系: - - [创建 X 需要先创建 Y] - - ... - -⚠️ 特殊情况: - - [涉及外部服务 / 文件上传 / 特殊认证等] -``` - -#### 2.2 按需确认(只问有歧义的) - -| 场景 | 要问的 | -|------|--------| -| 流程范围不明确 | "要测到哪一步?" | -| 多种用户角色 | "用哪种身份测?" | -| 是否测异常 | "需要包含异常 case 吗?(参数校验失败、权限不足等)" | -| 是否测数据隔离 | "需要验证不同用户间数据不可见吗?" | -| 涉及第三方 | "XX 部分怎么处理?绕过 / 模拟回调 / 跳过?" | -| 前置数据来源 | "XX 依赖数据是通过 API 创建还是假设已存在?" | - -**如果用户说"越完整越好"或"都要"→ 默认全部包含,不再追问。** - ---- - -### Phase 3: 生成(Generate) - -**目标:生成完整的 .hurl 文件,字段断言基于 schema 代码,不能编造。** - -#### 3.1 文件头注释 - -```hurl -# ============================================================ -# 测试:{测试名称} -# 生成时间:{日期} -# 涉及模块:{module1, module2, ...} -# 涉及接口:{N} 个 -# 断言数量:{N} 条 -# 前置条件:{服务运行 + 必要的前置条件} -# ============================================================ -# 流程: -# 1. {步骤描述} -# 2. {步骤描述} -# ... -# ============================================================ -``` - -#### 3.2 请求生成规则 - -**认证**: - -- 根据 Phase 1 画像中的登录接口和 token 位置生成 -- token 必须通过 `[Captures]` 捕获,后续请求引用 -- 如果是 Cookie 认证,用 `[Cookies]` 或 cookie capture - -**CRUD 标准模式**: - -| 操作 | 生成要求 | -|------|---------| -| **创建(POST)** | capture 返回的 ID;**所有**唯一约束字段用 `{{newUuid}}` 防冲突 | -| **查询详情(GET)** | **逐字段断言**(类型 + 值,见 3.3) | -| **查询列表(GET)** | 分页结构断言 + items[0] 逐字段断言 | -| **修改(PUT/PATCH)** | 修改后**紧跟一个 GET 验证修改生效** | -| **删除(DELETE)** | 删除后**紧跟一个 GET 验证已删除** | - -#### 唯一值防冲突规范(强制执行) - -**所有创建类请求(POST)中,凡是有唯一约束的字段,必须使用 `{{newUuid}}` 或包含 UUID 后缀**,确保测试可重复运行。 - -| 场景 | 正确做法 | 错误做法 | -|------|---------|---------| -| 编码/Code 字段 | `"code": "TEST-{{newUuid}}"` | `"code": "TEST-001"` | -| 名称(有唯一约束) | `"name": "测试-{{newUuid}}"` | `"name": "测试角色"` | -| 用户名 | `"username": "hurl_{{newUuid}}"` | `"username": "test_user"` | -| 手机号 | `"phone": "189{{newUuid}}"` 取前11位,或放在 env 文件 | `"phone": "18899990001"` | -| 邮箱 | `"email": "{{newUuid}}@test.com"` | `"email": "test@test.com"` | - -**后续步骤需要引用的值(如登录用的 username/password)**: -- 必须定义在 `env/dev.env` 中作为变量 -- 在创建请求和后续引用处都通过 `{{变量名}}` 引用,保持一致 -- env 文件中的值也应该带有测试标识前缀(如 `hurl_test_`),避免与真实数据冲突 -- **env 中有唯一约束的变量值每次运行前可能需要更新**(文档中注明) - -**业务流程模式**: - -- 按用户描述的流程顺序编排请求 -- 上一步的输出(ID、状态等)通过 `[Captures]` 传给下一步 -- 关键步骤加中间状态验证(如创建订单后验证状态为"待支付") - -#### 3.3 schema 到断言的映射 - -读到 schema 字段后,按以下规则生成 jsonpath 断言: - -**通用类型映射(所有语言)**: - -| Schema 类型特征 | Hurl 断言 | -|----------------|-----------| -| 字符串(string / str / String) | `isString` | -| 整数(int / integer / long / Int) | `isInteger` | -| 浮点(float / double / decimal / Float) | `isNumber` | -| 布尔(bool / boolean / Boolean) | `isBoolean` | -| 数组 / 列表([] / List / Array) | `isList` | -| 嵌套对象(struct / class / dict / object) | `isObject`,并递归检查子字段 | -| 可空类型(指针 / Optional / nullable) | `exists`(不强制类型,因为可能是 null) | -| 不参与序列化(json:"-" / @JsonIgnore / exclude=True) | **跳过,不生成断言** | -| 条件序列化(omitempty / if not None) | `exists` 或不生成(取决于场景) | - -**Go 特定映射**: - -| Go 类型 | Hurl 断言 | -|---------|-----------| -| `string` | `isString` | -| `int`, `int8/16/32/64`, `uint`, `uint8/16/32/64` | `isInteger` | -| `float32`, `float64` | `isNumber` | -| `bool` | `isBoolean` | -| `[]T` | `isList` | -| `*string`, `*int`, `*uint` 等指针 | `exists` | -| `time.Time` | `isString`(通常序列化为字符串) | -| `map[string]any` | `isObject` | - -**Python 特定映射(Pydantic / Django / FastAPI)**: - -| Python 类型 | Hurl 断言 | -|------------|-----------| -| `str` | `isString` | -| `int` | `isInteger` | -| `float`, `Decimal` | `isNumber` | -| `bool` | `isBoolean` | -| `list[T]`, `List[T]` | `isList` | -| `Optional[T]`, `T | None` | `exists` | -| `dict`, `Dict` | `isObject` | -| `datetime`, `date` | `isString` | - -**TypeScript/JavaScript 特定映射**: - -| TS/JS 类型 | Hurl 断言 | -|-----------|-----------| -| `string` | `isString` | -| `number`(整数上下文) | `isInteger` | -| `number`(通用) | `isNumber` | -| `boolean` | `isBoolean` | -| `T[]`, `Array` | `isList` | -| `T \| null`, `T \| undefined` | `exists` | -| `object`, `Record<>` | `isObject` | -| `Date` | `isString` | - -**Java 特定映射**: - -| Java 类型 | Hurl 断言 | -|----------|-----------| -| `String` | `isString` | -| `Integer`, `Long`, `int`, `long` | `isInteger` | -| `Double`, `Float`, `BigDecimal` | `isNumber` | -| `Boolean`, `boolean` | `isBoolean` | -| `List` | `isList` | -| `@Nullable`, `Optional` | `exists` | -| `Map` | `isObject` | -| `LocalDateTime`, `Instant` | `isString` | - -#### 3.4 统一响应格式断言 - -根据 Phase 1 画像中发现的统一响应格式,为**每个成功响应**添加格式断言。 - -示例:如果项目的统一格式是 `{code, msg, data, timestamp}`: - -```hurl -[Asserts] -jsonpath "$.code" == 0 -jsonpath "$.msg" == "success" -jsonpath "$.timestamp" isIsoDate -``` - -示例:如果项目的格式是 `{status, message, result}`: - -```hurl -[Asserts] -jsonpath "$.status" == "ok" -jsonpath "$.message" isString -``` - -示例:如果项目无统一包装,直接返回数据: - -```hurl -[Asserts] -# 直接断言业务字段 -jsonpath "$.id" isInteger -jsonpath "$.name" isString -``` - -**不要假设响应格式,必须从代码中确认。** - -#### 3.5 分页断言 - -根据 Phase 1 画像中发现的分页结构生成。 - -示例:如果是 `{items, total, page, size}` 格式: - -```hurl -jsonpath "$.data.items" isList -jsonpath "$.data.total" isInteger -jsonpath "$.data.total" >= 1 -jsonpath "$.data.page" isInteger -jsonpath "$.data.size" isInteger -# items 内元素逐字段断言 -jsonpath "$.data.items[0].{field}" {type_assert} -``` - -示例:如果是 `{results, count, next, previous}` 格式(Django 风格): - -```hurl -jsonpath "$.results" isList -jsonpath "$.count" isInteger -jsonpath "$.count" >= 1 -# results 内元素逐字段断言 -jsonpath "$.results[0].{field}" {type_assert} -``` - -**根据实际代码调整字段名,不硬编码。** - -#### 3.6 异常 Case 模板 - -**参数校验失败**: - -```hurl -# ── 异常:参数校验失败 ── -POST {{base_url}}/{path} -Authorization: Bearer {{token}} -Content-Type: application/json -{ - "required_field": "" -} -HTTP {expected_error_status} -[Asserts] -# 断言错误响应格式(根据项目约定调整) -``` - -> HTTP 状态码根据项目实际返回确定:有的项目错误也返回 200 + 业务错误码,有的返回 400/422。 - -**未认证访问**: - -```hurl -# ── 异常:未认证访问 ── -GET {{base_url}}/{protected_path} -HTTP {expected_unauth_status} -``` - -**越权访问**(如果用户要求): - -```hurl -# ── 异常:用户 B 不能访问用户 A 的资源 ── -GET {{base_url}}/{path}/{{user_a_resource_id}} -Authorization: Bearer {{user_b_token}} -HTTP {expected_forbidden_status} -``` - -#### 3.7 特殊场景处理 - -| 场景 | 处理策略 | -|------|---------| -| **短信/邮件验证码** | 建议服务端加 test_mode 开关,固定验证码写入 env 文件;注释提醒用户 | -| **第三方支付** | 优先用项目内部支付方式(如钱包支付);如需测回调,直接 POST 回调接口模拟 | -| **OAuth 登录(微信/Google/GitHub)** | 建议服务端加 test_mode 支持直接传 openid/email;注释提醒用户 | -| **文件上传** | 用 Hurl 的 `[Multipart]` 语法 + testdata 目录下的样本文件 | -| **外部 API 依赖** | 只测参数校验和错误响应格式,不断言业务结果;注释说明依赖 | -| **WebSocket** | Hurl 不支持,注释说明跳过 | -| **异步任务结果** | 用 Hurl 的 `retry` + `retry-interval` 轮询直到状态变更 | - -异步轮询示例: - -```hurl -# 等待异步任务完成(最多重试 10 次,间隔 500ms) -GET {{base_url}}/{path}/{{task_id}} -Authorization: Bearer {{token}} -[Options] -retry: 10 -retry-interval: 500ms -HTTP 200 -[Asserts] -jsonpath "$.data.status" == "completed" -``` - -#### 3.8 文件输出 - -**目录结构**(首次使用时创建,如不存在): - -``` -tests/hurl/ -├── env/ -│ └── dev.env # 环境变量 -├── testdata/ # 测试用的样本文件 -├── flows/ # 业务流程测试 -├── modules/ # 按模块的接口测试 -│ └── {module}/ -│ └── 01-crud.hurl -├── negative/ # 异常/边界测试 -├── contract/ # 接口契约验证 -├── reports/ # Phase 5 发现报告(自动生成) -│ └── findings-{name}-{date}.md -└── Makefile # 快捷命令 -``` - -文件放置规则: - -| 用户描述 | 输出路径 | -|---------|---------| -| 测试 XX 流程 / 业务流程 | `tests/hurl/flows/{flow-name}.hurl` | -| 测试 XX 模块的 CRUD / 接口 | `tests/hurl/modules/{module}/01-crud.hurl` | -| 测试异常/边界/权限 | `tests/hurl/negative/{name}.hurl` | -| 测试接口契约/字段对齐 | `tests/hurl/contract/{name}.hurl` | - -**如果项目已有 hurl 测试目录结构,沿用已有约定,不要另起炉灶。** - -#### 3.9 env 和 Makefile - -**env/dev.env**(首次创建时生成,内容基于 Phase 1 画像): - -```properties -# 服务地址 -base_url=http://localhost:{port} - -# 认证信息(根据项目实际填写) -admin_username={默认用户名} -admin_password={默认密码} - -# 测试模式变量(如果有特殊场景) -# test_sms_code=888888 -# test_openid=test_openid_001 -``` - -**Makefile**(首次创建时生成): - -```makefile -SHELL := /bin/bash -ENV ?= dev -HURL_OPTS := --variables-file env/$(ENV).env --test - -.PHONY: test test-flows test-modules test-negative report - -test: ## 运行所有测试 - hurl $(HURL_OPTS) . - -test-flows: ## 运行业务流程测试 - hurl $(HURL_OPTS) flows/ - -test-modules: ## 运行模块接口测试 - hurl $(HURL_OPTS) modules/ - -test-negative: ## 运行异常测试 - hurl $(HURL_OPTS) negative/ - -report: ## 生成 HTML 报告 - hurl $(HURL_OPTS) --report-html build/report/ . -``` - ---- - -### Phase 4: 验证(Verify) - -**目标:确保生成的 .hurl 文件语法正确、可运行。** - -#### 4.1 语法自检 - -- [ ] 每个请求之间有空行分隔 -- [ ] `[Captures]` 和 `[Asserts]` 拼写正确(大小写敏感) -- [ ] 所有 `{{变量}}` 引用都有来源(env 文件定义 或 上游 `[Captures]`) -- [ ] JSON body 无尾逗号、格式正确 -- [ ] `HTTP {status}` 在请求之后、`[Captures]` / `[Asserts]` 之前 -- [ ] 请求和断言之间没有多余空行(`HTTP` 行必须紧跟请求) -- [ ] 文件上传路径相对于 hurl 文件位置正确 - -#### 4.2 运行测试 - -```bash -hurl --variables-file tests/hurl/env/dev.env --test tests/hurl/{生成的文件} -``` - -- 服务在跑 → 执行,如有失败分析修正 -- 服务没跑 → 跳过,告知用户手动验证命令 - ---- - -### Phase 5: 研判(Triage) - -**目标:分析测试失败原因,区分"测试自身问题"和"接口问题",生成发现报告。** - -> **核心原则:不因实际返回与预期不符而盲目修改断言。差异是"发现",需要分类研判。** - -#### 5.1 失败分类(A/B/C 三类) - -每个断言失败必须归入以下三类之一: - -| 类型 | 含义 | 判断标准 | 处置方式 | -|------|------|---------|---------| -| **A 类 — 测试自身错误** | JSON path 写错、请求参数遗漏、env 变量缺失、前置步骤失败导致 ID 为空 | 改正后 DTO/Handler 代码能对应上 | ✅ **自动修正测试**,不打扰用户 | -| **B 类 — 代码一致性问题** | DTO 定义和 Handler 实际返回不一致(如 DTO 写 `json:"id"` 但 Handler 返回 `"ID"`;DTO 定义了分页结构但 Handler 用了另一种) | DTO 说的是 A,Handler 做的是 B,两个都是代码 | ⚠️ **以 Handler 为准修正断言** + **写入发现报告** | -| **C 类 — 接口行为 Bug** | 接口行为不符合合理预期(如详情接口缺少列表接口有的字段;创建接口返回 500;同一个 DTO 在不同接口表现不一致) | Handler 的行为本身不合理,不是 DTO 过期的问题 | 🐛 **保留原始断言(测试会失败)** + **写入发现报告** | - -#### 5.2 分类判断流程 - -``` -断言失败 - │ - ├─ jsonpath 写错 / 变量未定义 / 前置步骤失败? - │ → A 类:修正测试 - │ - ├─ DTO 定义字段名 ≠ 实际返回字段名?(如 id vs ID, list vs items) - │ │ - │ ├─ Handler 代码确实返回了不同的结构? - │ │ → B 类:以 Handler 为准,报告不一致 - │ │ - │ └─ Handler 代码引用了 DTO 但行为不符? - │ → C 类:接口 Bug - │ - ├─ 某个字段在接口 A 有、接口 B 没有?(如列表有但详情没有) - │ → C 类:接口一致性 Bug - │ - ├─ 接口返回 5xx 错误? - │ → C 类:服务端 Bug - │ - └─ 业务逻辑不符预期?(如已传 packages 但响应中 packages 为空) - → C 类:业务逻辑 Bug -``` - -#### 5.3 发现报告格式 - -报告输出到 **`tests/hurl/reports/findings-{测试文件名}-{日期}.md`**,格式如下: - -```markdown -# 🔍 Hurl 测试发现报告 - -- 测试文件:`flows/package-resource-full-flow.hurl` -- 生成时间:2026-03-30 -- 总请求数:25 -- 通过:20 -- 失败:5(A类: 2, B类: 1, C类: 2) - ---- - -## A 类(测试自身错误)— 已自动修正 - -### A-1: 角色名冲突导致创建失败 -- **位置**:第 2 步 POST /api/admin/roles -- **原因**:角色名使用固定值,重复运行时 409 冲突 -- **修正**:角色名改为 `hurl测试角色-{{newUuid}}` - ---- - -## B 类(代码一致性问题)— 已按 Handler 修正断言 - -### B-1: 角色接口返回原始 GORM 模型而非 DTO -- **位置**:POST /api/admin/roles → 响应 -- **DTO 定义**:`RoleResponse` 有 `json:"id"`(小写) -- **Handler 实际**:直接返回 `model.Role`,字段为 `ID`(大写 GORM 默认) -- **影响**:前端按 API 文档对接会取不到 `id` 字段 -- **建议**:Handler 添加 model → DTO 转换 - ---- - -## C 类(接口行为 Bug)— 断言保留,测试会失败 - -### C-1: 套餐详情接口缺少 one_time_commission_amount -- **位置**:GET /api/admin/packages/:id(代理视角) -- **预期**:`PackageResponse` DTO 定义了 `one_time_commission_amount` 字段 -- **实际**:列表接口 GET /packages 返回该字段,详情接口不返回 -- **影响**:代理端查看单个套餐时看不到佣金信息 -- **建议**:Detail handler 增加佣金信息增强逻辑 - -### C-2: 批量分配对已授权店铺返回 500 -- **位置**:POST /api/admin/shop-package-batch-allocations -- **预期**:对已有授权的店铺,应返回业务错误码(4xx),而非 500 -- **实际**:返回 `{code: 2001, msg: "内部服务器错误"}` -- **影响**:前端无法区分是参数错误还是服务异常 -- **建议**:添加"已存在授权"的前置检查,返回明确的业务错误 -``` - -#### 5.4 研判完成后的交付物 - -| 交付物 | 说明 | -|-------|------| -| **修正后的 .hurl 文件** | A 类已自动修正;B 类按 Handler 实际修正;C 类**保留原始断言**(预期失败) | -| **发现报告** | `tests/hurl/reports/findings-{name}-{date}.md` | -| **终端摘要** | 向用户输出发现数量和关键 C 类问题摘要 | - -#### 5.5 用户交互 - -研判完成后,向用户展示摘要并询问: - -``` -## 研判结果 - -- A 类(测试错误):N 个,已自动修正 -- B 类(一致性问题):N 个,已按实际修正并记录 -- C 类(接口 Bug):N 个,断言保留(测试会失败直到代码修复) - -### C 类发现摘要: -1. [C-1] 套餐详情缺少 one_time_commission_amount -2. [C-2] 批量分配对已授权店铺返回 500 - -详细报告已写入: tests/hurl/reports/findings-xxx.md - -是否需要对某个 C 类发现调整处置?(如确认为已知行为,改为 B 类) -``` - ---- - -## 红线规则 - -| 规则 | 说明 | -|------|------| -| **不跳过 Phase 1** | 必须读代码确认接口路径和字段,不能凭记忆或猜测 | -| **不跳过 Phase 2** | 必须向用户展示发现的接口和字段,确认后再生成 | -| **不编造字段** | 所有断言的字段名必须来自实际 schema 代码 | -| **不编造路径** | 所有接口路径必须来自实际路由代码 | -| **不遗漏字段** | schema 中每个参与序列化的字段都必须有对应断言 | -| **不硬编码 ID** | 所有依赖的 ID 通过 `[Captures]` 从上游请求获取 | -| **不假设响应格式** | 统一响应结构必须从代码中确认,不同项目格式不同 | -| **唯一值防冲突** | 创建类请求的**所有**唯一约束字段必须使用 `{{newUuid}}`(详见 3.2 规范) | -| **自给自足** | 每个 .hurl 文件自己创建测试数据,不依赖外部数据准备 | -| **不为通过而改断言** | 断言失败时走 Phase 5 研判流程,C 类 Bug 必须保留断言并写入发现报告 | -| **Handler 追溯** | 断言字段名必须追溯到 Handler 实际返回的类型,不能只看 DTO 定义(详见 Phase 1 真理源层级) | - ---- - -## AI 助手检查清单 - -### 生成阶段自检(Phase 3 完成后) - -1. ✅ 文件头注释包含流程描述和前置条件 -2. ✅ 认证步骤正确 capture 了 token / cookie -3. ✅ 所有依赖数据通过 API 链式创建(自给自足) -4. ✅ 每个成功响应断言了项目的统一响应格式 -5. ✅ 查询详情接口**逐字段断言**(类型 + 值,基于 Handler 实际返回) -6. ✅ 分页接口断言了分页结构 + 第一条记录的字段 -7. ✅ 修改操作后紧跟 GET 验证修改生效 -8. ✅ 删除操作后紧跟 GET 验证已删除 -9. ✅ **所有**创建请求的唯一约束字段使用了 `{{newUuid}}` -10. ✅ `{{变量}}` 引用无悬空(都有 env 或 capture 来源) -11. ✅ 文件放在了正确的目录位置 -12. ✅ 特殊场景有明确的处理策略和注释提醒 -13. ✅ 断言字段名已追溯到 Handler 实际返回类型(非仅依赖 DTO) - -### 研判阶段自检(Phase 5 完成后) - -14. ✅ 每个断言失败都已分类(A/B/C) -15. ✅ A 类错误已自动修正 -16. ✅ B 类不一致已按 Handler 修正断言,并写入发现报告 -17. ✅ C 类 Bug 保留了原始断言(测试预期失败),并写入发现报告 -18. ✅ 发现报告已输出到 `tests/hurl/reports/findings-{name}-{date}.md` -19. ✅ 向用户展示了研判结果摘要 - ---- - -## 附录:Hurl 语法速查 - -**生成 .hurl 文件时必须参照本速查,不可凭记忆编造语法。** - -### 文件结构 - -一个 .hurl 文件由多个 entry 组成,每个 entry = 请求 + 可选响应: - -``` -请求1 -响应1(可选) - -请求2 -响应2(可选) -``` - -entry 之间用空行分隔。 - -### 请求格式 - -```hurl -METHOD URL -Header1: value1 -Header2: value2 -[Options] -key: value -[Query] -param1: value1 -[Form] -field1: value1 -[Multipart] -file1: file,path/to/file; -[BasicAuth] -username: password -[Cookies] -name: value -BODY(JSON / XML / multiline string / file) -``` - -**规则**: -- Method + URL 是第一行,必须 -- Headers 紧跟 URL 之后(无 section 标记) -- Sections(`[Query]`、`[Form]`、`[Options]` 等)顺序任意 -- Body 必须在最后 -- JSON body 直接写 `{ }` 即可,自动设置 Content-Type: application/json - -### 响应格式 - -```hurl -HTTP {status_code} -Header1: expected_value1 -[Captures] -var_name: jsonpath "$.path" -[Asserts] -jsonpath "$.field" == "value" -``` - -**规则**: -- `HTTP {status}` 紧跟请求之后(中间不能有空行) -- `HTTP *` 表示不检查状态码 -- Headers 检查紧跟 HTTP 行之后 -- `[Captures]` 和 `[Asserts]` 顺序任意 - -### 变量和模板 - -```hurl -# 引用变量(从 env 文件、命令行或上游 capture 获取) -GET {{base_url}}/api/users/{{user_id}} - -# 内置函数 -POST {{base_url}}/api/users -{ - "email": "{{newUuid}}@test.com", - "created_at": "{{newDate}}" -} -``` - -可用函数: -- `{{newUuid}}` — 生成 UUID v4 -- `{{newDate}}` — 生成 RFC 3339 UTC 时间戳 - -### Capture 语法 - -```hurl -[Captures] -# JSONPath -token: jsonpath "$.data.access_token" -user_id: jsonpath "$.data.id" -first_item: jsonpath "$.items[0].name" - -# Header -location: header "Location" - -# Cookie -session: cookie "SESSIONID" - -# Body(整个响应体作为字符串) -full_body: body - -# Status code -code: status - -# 正则表达式 -csrf: regex "name=\"csrf\" value=\"([^\"]+)\"" - -# 响应时间(毫秒) -response_time: duration -``` - -### Assert 语法 - -```hurl -[Asserts] -# ── 状态码 ── -status == 200 -status >= 200 -status < 300 - -# ── JSONPath 断言 ── -jsonpath "$.name" == "Alice" # 等于 -jsonpath "$.name" != "Bob" # 不等于 -jsonpath "$.age" > 18 # 大于 -jsonpath "$.age" >= 18 # 大于等于 -jsonpath "$.count" < 100 # 小于 -jsonpath "$.items" count == 5 # 集合长度 -jsonpath "$.name" startsWith "Al" # 前缀 -jsonpath "$.name" endsWith "ce" # 后缀 -jsonpath "$.name" contains "lic" # 包含 -jsonpath "$.date" matches /\\d{4}-\\d{2}-\\d{2}/ # 正则 - -# ── 类型断言 ── -jsonpath "$.name" isString -jsonpath "$.age" isInteger -jsonpath "$.score" isFloat -jsonpath "$.count" isNumber # 整数或浮点 -jsonpath "$.active" isBoolean -jsonpath "$.items" isList -jsonpath "$.meta" isObject -jsonpath "$.id" isUuid -jsonpath "$.created_at" isIsoDate # RFC 3339 格式 -jsonpath "$.field" isEmpty # 空集合 - -# ── 存在性 ── -jsonpath "$.field" exists -jsonpath "$.field" not exists - -# ── 否定 ── -jsonpath "$.name" not contains "Bob" -jsonpath "$.status" not == "deleted" - -# ── Header 断言 ── -header "Content-Type" contains "application/json" -header "X-Request-Id" exists - -# ── 性能 ── -duration < 1000 # 响应时间(毫秒) - -# ── Body 断言 ── -body contains "Hello" -bytes count == 1024 -``` - -### Options(逐请求配置) - -```hurl -GET {{base_url}}/api/task/{{task_id}} -[Options] -retry: 10 # 最大重试次数(-1 = 无限) -retry-interval: 500ms # 重试间隔 -delay: 2s # 请求前等待 -location: true # 跟随重定向 -insecure: true # 允许不安全 SSL -verbose: true # 输出详细日志 -very-verbose: true # 输出更详细日志 -skip: true # 跳过此请求 -variable: key=value # 定义变量 -HTTP 200 -``` - -### Multipart 文件上传 - -```hurl -POST {{base_url}}/api/upload -[Multipart] -file: file,testdata/sample.xlsx; -field1: value1 -# 指定 Content-Type -file2: file,testdata/data.bin; application/octet-stream -``` - -### 运行命令 - -```bash -# 运行单个文件 -hurl --test file.hurl - -# 带变量文件 -hurl --variables-file env/dev.env --test file.hurl - -# 运行目录下所有 .hurl -hurl --test tests/hurl/ - -# 生成 HTML 报告 -hurl --test --report-html build/report/ tests/hurl/ - -# 生成 JUnit 报告(CI 用) -hurl --test --report-junit build/report.xml tests/hurl/ - -# 并行执行(--test 默认并行,同文件内串行) -hurl --test --jobs 4 tests/hurl/ - -# 指定单个变量 -hurl --variable base_url=http://localhost:3000 --test file.hurl - -# 失败后继续执行 -hurl --test --continue-on-error tests/hurl/ -``` - -### 常见错误 - -| 错误 | 原因 | 修正 | -|------|------|------| -| `HTTP 200` 和请求之间有空行 | 空行会被当作 entry 分隔符 | 删除空行,HTTP 行紧跟请求 | -| JSON body 有尾逗号 | Hurl 严格解析 JSON | 删除最后一个逗号 | -| `jsonpath` 写成 `json_path` 或 `JsonPath` | 关键字大小写敏感 | 必须小写 `jsonpath` | -| `[Captures]` 写成 `[Capture]` | 必须是复数 | `[Captures]`、`[Asserts]`、`[Options]` | -| 变量 `{{ var }}` 有空格 | 允许,但建议统一 | `{{var}}` 或 `{{ var }}` 都可以 | -| `isIsoDate` 用在非 RFC 3339 格式 | 只认 `YYYY-MM-DDTHH:mm:ss` 格式 | 如果是其他格式用 `matches` | -| `file,path;` 路径含 `..` | Hurl 禁止相对父目录 | 用 `--file-root` 或调整路径 | diff --git a/.opencode/skills/openspec-api-contract/SKILL.md b/.opencode/skills/openspec-api-contract/SKILL.md deleted file mode 100644 index ba0984e..0000000 --- a/.opencode/skills/openspec-api-contract/SKILL.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -name: openspec-api-contract -description: OpenSpec API 契约规范。创建涉及接口的 OpenSpec 提案时使用。探索阶段提供业务与契约引导清单,提案文档要求 API 契约设计、错误码与完成标准等必填章节。 ---- - -# OpenSpec API 契约规范 - -**适用场景**:创建涉及 API/接口的 OpenSpec 提案时,探索和提案两个阶段均须遵守本规范。 - ---- - -## 一、探索阶段引导清单 - -在 `openspec-explore` 阶段,当内容涉及接口时,讨论必须覆盖以下所有维度。 - -### 业务与契约确认 - -**输入与输出** -- 请求方是谁?用户类型(SuperAdmin/Platform/Agent/Enterprise/Personal)? -- 输入参数:必填/选填字段、格式约束、参数来源(路径/查询/Body)? -- 输出结构:哪些字段必须返回?是否需要分页? - -**业务规则** -- 核心业务规则与边界条件? -- 是否涉及状态流转?状态机的完整定义? -- 依赖外部服务时,外部异常的降级行为? - -**权限与资源所有权** -- 哪些用户类型可以访问? -- 是否涉及跨用户/跨店铺/跨企业的资源访问?(需三层越权防护) -- 资源所有权校验方式:`CanManageShop` / `CanManageEnterprise` / 自有资源? - -**幂等性** -- 操作类型:查询(天然幂等)/ 创建 / 更新 / 删除? -- 写操作幂等策略:状态条件更新 / Redis 业务键防重 + 分布式锁 / 乐观锁(version)? -- 异步任务是否需要任务锁? - -**数据模型变更** -- 是否需要新建表、修改现有表或数据回填? -- 迁移策略:上线顺序、兼容旧数据的方式? -- 是否影响 GORM Callback 自动数据权限过滤? - -**错误码与异常语义** -- 预期错误场景及对应错误码? -- 错误响应是否泄露敏感信息?(参数校验失败统一返回 `CodeInvalidParam`) - ---- - -## 二、提案文档必填章节 - -在 `openspec-propose` 生成的提案(`proposal.md` / `design.md`)中,涉及接口时以下内容**不可缺失**。 - -### API 契约设计 - -**接口定义** - -| 项 | 内容 | -|---|---| -| Endpoint | `METHOD /api/{scope}/{resource}[/:id]` | -| 请求参数 | 字段名、类型、必填/选填、说明 | -| 响应结构 | `data` 字段的完整结构定义 | -| 鉴权要求 | 允许的用户类型 | -| 资源所有权 | 所有权校验方式 | - -**列表接口额外要求** -- 分页:`page` + `page_size`(默认 20,最大 100) -- 排序:默认排序字段与方向 -- 过滤:支持的过滤条件列表 - -**错误码清单** - -| 场景 | 错误码 | 说明 | -|---|---|---| -| 参数校验失败 | `CodeInvalidParam` | 统一返回,不泄露细节 | -| 资源不存在/越权 | `CodeForbidden` | 不区分两者,防止信息泄露 | -| (业务错误场景...) | (对应错误码) | (说明) | - -### 完成标准 - -**最小验证步骤**(按顺序列出可操作的验证步骤) - -1. (例:调用创建接口,验证返回 `code=0`) -2. (例:查询接口确认数据存在且字段正确) -3. (例:PostgreSQL MCP 查询确认数据库记录符合预期) - -**影响范围说明** -- 新增/修改的表: -- 影响的现有接口: -- 影响的权限与数据过滤范围: - ---- - -## 约束(必须遵守) - -- **优先复用现有架构与库**:不引入新依赖,错误码优先复用已有定义 -- **不做顺手重构**:提案范围严格限定在目标功能;发现可优化点,记录到 backlog 但不执行 -- **数据库设计**:禁止外键约束,禁止 GORM 关联标签,关联通过 ID 字段手动维护 diff --git a/.opencode/skills/openspec-archive-change/SKILL.md b/.opencode/skills/openspec-archive-change/SKILL.md deleted file mode 100644 index 9b1f851..0000000 --- a/.opencode/skills/openspec-archive-change/SKILL.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -name: openspec-archive-change -description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. -license: MIT -compatibility: Requires openspec CLI. -metadata: - author: openspec - version: "1.0" - generatedBy: "1.2.0" ---- - -Archive a completed change in the experimental workflow. - -**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. - -**Steps** - -1. **If no change name provided, prompt for selection** - - Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select. - - Show only active changes (not already archived). - Include the schema used for each change if available. - - **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose. - -2. **Check artifact completion status** - - Run `openspec status --change "" --json` to check artifact completion. - - Parse the JSON to understand: - - `schemaName`: The workflow being used - - `artifacts`: List of artifacts with their status (`done` or other) - - **If any artifacts are not `done`:** - - Display warning listing incomplete artifacts - - Use **AskUserQuestion tool** to confirm user wants to proceed - - Proceed if user confirms - -3. **Check task completion status** - - Read the tasks file (typically `tasks.md`) to check for incomplete tasks. - - Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete). - - **If incomplete tasks found:** - - Display warning showing count of incomplete tasks - - Use **AskUserQuestion tool** to confirm user wants to proceed - - Proceed if user confirms - - **If no tasks file exists:** Proceed without task-related warning. - -4. **Assess delta spec sync state** - - Check for delta specs at `openspec/changes//specs/`. If none exist, proceed without sync prompt. - - **If delta specs exist:** - - Compare each delta spec with its corresponding main spec at `openspec/specs//spec.md` - - Determine what changes would be applied (adds, modifications, removals, renames) - - Show a combined summary before prompting - - **Prompt options:** - - If changes needed: "Sync now (recommended)", "Archive without syncing" - - If already synced: "Archive now", "Sync anyway", "Cancel" - - If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice. - -5. **Perform the archive** - - Create the archive directory if it doesn't exist: - ```bash - mkdir -p openspec/changes/archive - ``` - - Generate target name using current date: `YYYY-MM-DD-` - - **Check if target already exists:** - - If yes: Fail with error, suggest renaming existing archive or using different date - - If no: Move the change directory to archive - - ```bash - mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD- - ``` - -6. **Display summary** - - Show archive completion summary including: - - Change name - - Schema that was used - - Archive location - - Whether specs were synced (if applicable) - - Note about any warnings (incomplete artifacts/tasks) - -**Output On Success** - -``` -## Archive Complete - -**Change:** -**Schema:** -**Archived to:** openspec/changes/archive/YYYY-MM-DD-/ -**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped") - -All artifacts complete. All tasks complete. -``` - -**Guardrails** -- Always prompt for change selection if not provided -- Use artifact graph (openspec status --json) for completion checking -- Don't block archive on warnings - just inform and confirm -- Preserve .openspec.yaml when moving to archive (it moves with the directory) -- Show clear summary of what happened -- If sync is requested, use openspec-sync-specs approach (agent-driven) -- If delta specs exist, always run the sync assessment and show the combined summary before prompting diff --git a/.opencode/skills/openspec-lock-consensus/SKILL.md b/.opencode/skills/openspec-lock-consensus/SKILL.md deleted file mode 100644 index a7d532b..0000000 --- a/.opencode/skills/openspec-lock-consensus/SKILL.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -name: openspec-lock-consensus -description: 锁定共识 - 在探索讨论后,将讨论结果锁定为正式共识文档。防止后续提案偏离讨论内容。 -license: MIT -compatibility: Requires openspec CLI. -metadata: - author: junhong - version: "1.1" ---- - -# 共识锁定 Skill - -在 `/opsx:explore` 讨论后,使用此 skill 将讨论结果锁定为正式共识。共识文档是后续所有 artifact 的基础约束。 - -## 触发方式 - -``` -/opsx:lock -``` - -或在探索结束后,AI 主动提议: -> "讨论已经比较清晰了,要锁定共识吗?" - ---- - -## 工作流程 - -### Step 1: 整理讨论要点 - -从对话中提取以下四个维度的共识: - -| 维度 | 说明 | 示例 | -|------|------|------| -| **要做什么** | 明确的功能范围 | "支持批量导入 IoT 卡" | -| **不做什么** | 明确排除的内容 | "不支持实时同步,仅定时批量" | -| **关键约束** | 技术/业务限制 | "必须使用 Asynq 异步任务" | -| **验收标准** | 如何判断完成 | "导入 1000 张卡 < 30s" | - -### Step 2: 使用 Question_tool 逐维度确认 - -**必须使用 Question_tool 进行结构化确认**,每个维度一个问题: - -```typescript -// 示例:确认"要做什么" -Question_tool({ - questions: [{ - header: "确认:要做什么", - question: "以下是整理的功能范围,请确认:\n\n" + - "1. 功能点 A\n" + - "2. 功能点 B\n" + - "3. 功能点 C\n\n" + - "是否准确完整?", - options: [ - { label: "确认无误", description: "以上内容准确完整" }, - { label: "需要补充", description: "有遗漏的功能点" }, - { label: "需要删减", description: "有不应该包含的内容" } - ], - multiple: false - }] -}) -``` - -**如果用户选择"需要补充"或"需要删减"**: -- 用户会通过自定义输入提供修改意见 -- 根据反馈更新列表,再次使用 Question_tool 确认 - -**确认流程**: - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Question_tool: 确认"要做什么" │ -│ ├── 用户选择"确认无误" → 进入下一维度 │ -│ └── 用户选择其他/自定义 → 修改后重新确认 │ -├─────────────────────────────────────────────────────────────────────┤ -│ Question_tool: 确认"不做什么" │ -│ ├── 用户选择"确认无误" → 进入下一维度 │ -│ └── 用户选择其他/自定义 → 修改后重新确认 │ -├─────────────────────────────────────────────────────────────────────┤ -│ Question_tool: 确认"关键约束" │ -│ ├── 用户选择"确认无误" → 进入下一维度 │ -│ └── 用户选择其他/自定义 → 修改后重新确认 │ -├─────────────────────────────────────────────────────────────────────┤ -│ Question_tool: 确认"验收标准" │ -│ ├── 用户选择"确认无误" → 生成 consensus.md │ -│ └── 用户选择其他/自定义 → 修改后重新确认 │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### Step 3: 生成 consensus.md - -所有维度确认后,创建文件: - -```bash -# 检查 change 是否存在 -openspec list --json - -# 如果 change 不存在,先创建 -# openspec new - -# 写入 consensus.md -``` - -**文件路径**: `openspec/changes//consensus.md` - ---- - -## Question_tool 使用规范 - -### 每个维度的问题模板 - -**1. 要做什么** -```typescript -{ - header: "确认:要做什么", - question: "以下是整理的【功能范围】:\n\n" + - items.map((item, i) => `${i+1}. ${item}`).join('\n') + - "\n\n请确认是否准确完整?", - options: [ - { label: "确认无误", description: "功能范围准确完整" }, - { label: "需要补充", description: "有遗漏的功能点" }, - { label: "需要删减", description: "有不应该包含的内容" } - ] -} -``` - -**2. 不做什么** -```typescript -{ - header: "确认:不做什么", - question: "以下是明确【排除的内容】:\n\n" + - items.map((item, i) => `${i+1}. ${item}`).join('\n') + - "\n\n请确认是否正确?", - options: [ - { label: "确认无误", description: "排除范围正确" }, - { label: "需要补充", description: "还有其他需要排除的" }, - { label: "需要删减", description: "有些不应该排除" } - ] -} -``` - -**3. 关键约束** -```typescript -{ - header: "确认:关键约束", - question: "以下是【关键约束】:\n\n" + - items.map((item, i) => `${i+1}. ${item}`).join('\n') + - "\n\n请确认是否正确?", - options: [ - { label: "确认无误", description: "约束条件正确" }, - { label: "需要补充", description: "还有其他约束" }, - { label: "需要修改", description: "约束描述不准确" } - ] -} -``` - -**4. 验收标准** -```typescript -{ - header: "确认:验收标准", - question: "以下是【验收标准】(必须可测量):\n\n" + - items.map((item, i) => `${i+1}. ${item}`).join('\n') + - "\n\n请确认是否正确?", - options: [ - { label: "确认无误", description: "验收标准清晰可测量" }, - { label: "需要补充", description: "还有其他验收标准" }, - { label: "需要修改", description: "标准不够清晰或无法测量" } - ] -} -``` - -### 处理用户反馈 - -当用户选择非"确认无误"选项或提供自定义输入时: - -1. 解析用户的修改意见 -2. 更新对应维度的内容 -3. 再次使用 Question_tool 确认更新后的内容 -4. 重复直到用户选择"确认无误" - ---- - -## consensus.md 模板 - -```markdown -# 共识文档 - -**Change**: -**确认时间**: -**确认人**: 用户 - ---- - -## 1. 要做什么 - -- [x] 功能点 A(已确认) -- [x] 功能点 B(已确认) -- [x] 功能点 C(已确认) - -## 2. 不做什么 - -- [x] 排除项 A(已确认) -- [x] 排除项 B(已确认) - -## 3. 关键约束 - -- [x] 技术约束 A(已确认) -- [x] 业务约束 B(已确认) - -## 4. 验收标准 - -- [x] 验收标准 A(已确认) -- [x] 验收标准 B(已确认) - ---- - -## 讨论背景 - -<简要总结讨论的核心问题和解决方向> - -## 关键决策记录 - -| 决策点 | 选择 | 原因 | -|--------|------|------| -| 决策 1 | 选项 A | 理由... | -| 决策 2 | 选项 B | 理由... | - ---- - -**签字确认**: 用户已通过 Question_tool 逐条确认以上内容 -``` - ---- - -## 后续流程绑定 - -### Proposal 生成时 - -`/opsx:continue` 生成 proposal 时,**必须**: - -1. 读取 `consensus.md` -2. 确保 proposal 的 Capabilities 覆盖"要做什么"中的每一项 -3. 确保 proposal 不包含"不做什么"中的内容 -4. 确保 proposal 遵守"关键约束" - -### 验证机制 - -如果 proposal 与 consensus 不一致,输出警告: - -``` -⚠️ Proposal 验证警告: - -共识中"要做什么"但 Proposal 未提及: -- 功能点 C - -共识中"不做什么"但 Proposal 包含: -- 排除项 A - -建议修正 Proposal 或更新共识。 -``` - ---- - -## Guardrails - -- **必须使用 Question_tool** - 不要用纯文本确认 -- **逐维度确认** - 四个维度分开确认,不要合并 -- **不要跳过确认** - 每个维度都必须让用户明确确认 -- **不要自作主张** - 只整理讨论中明确提到的内容 -- **避免模糊表述** - "尽量"、"可能"、"考虑"等词汇需要明确化 -- **验收标准必须可测量** - 避免"性能要好"这类无法验证的标准 - ---- - -## 与其他 Skills 的关系 - -| Skill | 关系 | -|-------|------| -| `openspec-explore` | 探索结束后触发 lock | -| `openspec-new-change` | lock 后触发 new(如果 change 不存在)| -| `openspec-continue-change` | 生成 proposal 时读取 consensus 验证 | -| `openspec-generate-acceptance-tests` | 从 consensus 的验收标准生成测试骨架 | diff --git a/.opencode/skills/openspec-propose/SKILL.md b/.opencode/skills/openspec-propose/SKILL.md deleted file mode 100644 index b92cb90..0000000 --- a/.opencode/skills/openspec-propose/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: openspec-propose -description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. -license: MIT -compatibility: Requires openspec CLI. -metadata: - author: openspec - version: "1.0" - generatedBy: "1.2.0" ---- - -Propose a new change - create the change and generate all artifacts in one step. - -I'll create a change with artifacts: -- proposal.md (what & why) -- design.md (how) -- tasks.md (implementation steps) - -When ready to implement, run /opsx-apply - ---- - -**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. - -**Steps** - -1. **If no clear input provided, ask what they want to build** - - Use the **AskUserQuestion tool** (open-ended, no preset options) to ask: - > "What change do you want to work on? Describe what you want to build or fix." - - From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). - - **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. - -2. **Create the change directory** - ```bash - openspec new change "" - ``` - This creates a scaffolded change at `openspec/changes//` with `.openspec.yaml`. - -3. **Get the artifact build order** - ```bash - openspec status --change "" --json - ``` - Parse the JSON to get: - - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`) - - `artifacts`: list of all artifacts with their status and dependencies - -4. **Create artifacts in sequence until apply-ready** - - Use the **TodoWrite tool** to track progress through the artifacts. - - Loop through artifacts in dependency order (artifacts with no pending dependencies first): - - a. **For each artifact that is `ready` (dependencies satisfied)**: - - Get instructions: - ```bash - openspec instructions --change "" --json - ``` - - The instructions JSON includes: - - `context`: Project background (constraints for you - do NOT include in output) - - `rules`: Artifact-specific rules (constraints for you - do NOT include in output) - - `template`: The structure to use for your output file - - `instruction`: Schema-specific guidance for this artifact type - - `outputPath`: Where to write the artifact - - `dependencies`: Completed artifacts to read for context - - Read any completed dependency files for context - - Create the artifact file using `template` as the structure - - Apply `context` and `rules` as constraints - but do NOT copy them into the file - - Show brief progress: "Created " - - b. **Continue until all `applyRequires` artifacts are complete** - - After creating each artifact, re-run `openspec status --change "" --json` - - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array - - Stop when all `applyRequires` artifacts are done - - c. **If an artifact requires user input** (unclear context): - - Use **AskUserQuestion tool** to clarify - - Then continue with creation - -5. **Show final status** - ```bash - openspec status --change "" - ``` - -**Output** - -After completing all artifacts, summarize: -- Change name and location -- List of artifacts created with brief descriptions -- What's ready: "All artifacts created! Ready for implementation." -- Prompt: "Run `/opsx-apply` or ask me to implement to start working on the tasks." - -**Artifact Creation Guidelines** - -- Follow the `instruction` field from `openspec instructions` for each artifact type -- The schema defines what each artifact should contain - follow it -- Read dependency artifacts for context before creating new ones -- Use `template` as the structure for your output file - fill in its sections -- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file - - Do NOT copy ``, ``, `` blocks into the artifact - - These guide what you write, but should never appear in the output - -**Guardrails** -- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`) -- Always read dependency artifacts before creating a new one -- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum -- If a change with that name already exists, ask if user wants to continue it or create a new one -- Verify each artifact file exists after writing before proceeding to next diff --git a/.scratch/tech-global-audit/审计覆盖基线.md b/.scratch/tech-global-audit/审计覆盖基线.md index b29136e..6d57f27 100644 --- a/.scratch/tech-global-audit/审计覆盖基线.md +++ b/.scratch/tech-global-audit/审计覆盖基线.md @@ -1,11 +1,11 @@ # 全系统审计覆盖基线 -状态:2026-07-29 当前源码重扫及业务语义、研发边界、安全字段三视角复核已完成;后续入口变更由静态门禁持续校验 +状态:2026-08-06 当前源码最终重扫及业务语义、研发边界、安全字段三视角复核已完成;后续入口变更由静态门禁持续校验 ## 可复核制品 -- 显式逐入口清单:[`审计覆盖清单.json`](审计覆盖清单.json),当前共 647 项;七月旧 490 项只用于遗漏比对,不再作为现行契约。 -- 扫描范围:326 个 HTTP RouteSpec、31 个 Asynq Worker、8 个 Asynq 定时任务、27 个 Application、5 个 Domain、178 个旧 Service 公共业务入口、57 个 Integration Log 调用点和 15 个旧 Writer 调用点。 +- 显式逐入口清单:[`审计覆盖清单.json`](审计覆盖清单.json),当前共 692 项;七月旧 490 项只用于遗漏比对,不再作为现行契约。 +- 扫描范围:331 个 HTTP RouteSpec、39 个 Asynq Worker 注册语句(35 个唯一 TaskType)、12 个 Asynq 定时任务、33 个 Application、5 个 Domain、180 个旧 Service 公共业务入口、78 个 Integration Log 调用点、14 个 Outbox Consumer 注册点和 0 个旧 Writer 调用点。 - 生成入口:`go run ./cmd/audit-coverage`。 - 发布门禁:重新执行 `go run ./cmd/audit-coverage`,并通过清单差异、gopls、构建和数据库数据核对确认入口与决策同步。 - 每项均固定代码入口、业务所有者、中文摘要以及待评审的 Audit Event / Domain Ledger / Integration Log / Outbox 分类候选,并预留动作、风险、资源、操作者来源、事务边界、失败策略、前后数据、敏感策略和确认核对接缝。 @@ -13,12 +13,12 @@ 生成器只负责产生待评审候选,不能自行代表评审通过。更新清单时必须核对业务语义,不能仅运行生成命令后直接提交。本轮复核已按下述证据完成,后续新增入口仍需重新执行同样流程。 -## 2026-07-29 当前入口重扫结论 +## 2026-08-06 当前入口最终重扫结论 ### 现行矩阵与旧清单差异 -- 当前逐入口矩阵固定 `code_entry/kind/owner/action/actor/resource/transaction/visibility`,并分别登记 Audit Event、Domain Ledger、Integration Log、Outbox 或 N/A 理由;647 项不存在空白决策字段。 -- RouteSpec 从旧 270 项增至当前 316 项;Worker 从 24 项增至 31 项;Asynq 定时任务从 4 项增至 8 项。当前另显式登记 60 个 Integration Log 调用点和 21 个旧 Writer 调用点,避免用模块级泛化描述替代真实入口。 +- 当前逐入口矩阵固定 `code_entry/kind/owner/action/actor/resource/transaction/visibility`,并分别登记 Audit Event、Domain Ledger、Integration Log、Outbox 或 N/A 理由;692 项不存在空白决策字段,所有 Audit Event N/A 项均有理由。 +- RouteSpec 从旧 270 项增至当前 331 项;Worker 从 24 项增至 39 个注册语句(35 个唯一 TaskType);Asynq 定时任务从 4 项增至 12 项。当前另显式登记 78 个 Integration Log 调用点、14 个 Outbox Consumer 注册点和 0 个旧 Writer 调用点,避免用模块级泛化描述替代真实入口。 - 扫描器排除无 `context.Context` 的依赖注入 `SetXxx`、构造器、注册/装配函数和纯分发壳;普通 GET、购买检查和资产验证均按只读 N/A 登记。`SetSpeedTier` 因包含真实业务上下文和 Gateway 副作用保留为业务入口。 - Callback 使用 `external_system/callback` actor;Worker 与消费者使用 `system_task/asynq`;Scheduler 只投递任务时为 N/A,真实业务变化归对应 Worker/Application。 @@ -37,8 +37,8 @@ ### Integration Log 与旧 Writer 复核 - Integration Log 唯一 Repository 写入口为 `Start/Complete/RecordInbound/ClaimExpiredInboundPending`,当前已覆盖运营商回调、支付 H5 下单/查单/回调、卡观测、卡限速及企微 token/通讯录/模板/附件/提交/详情/回调;支付宝 WAP URL 本地签名不伪造外部尝试。 -- 当前设备 Gateway、IoT 卡 Gateway、统一资产实时状态等直连调用尚未全部接入 Integration Log;矩阵已逐用例登记为“必须”,由后续 3.3、6.x、8.6~8.8 纵向切片落实,不能用旧 Asset Operation Log 代替。 -- 支付配置的 5 个旧账号日志外层 goroutine 已归零;旧 Writer 仍有少量未迁移资产、卡、设备、导入和轮询调用,旧 Writer 自身的 goroutine 留待最终 contract 任务统一移除。 +- 设备 Gateway、IoT 卡 Gateway、统一资产实时状态、支付、企微与运营商回调均已按真实外部尝试接入 Integration Log;扫描器覆盖 `Start/Complete/RecordInbound/ClaimExpiredInboundPending`,不能用 Audit Event 或旧 Asset Operation Log 代替。 +- 旧账号和旧资产 Writer、直接业务调用及裸/双重 goroutine 均已归零;旧账号表与旧资产表原样保留,旧资产仅保留独立历史只读入口。 ### 已落地代表切片 @@ -149,10 +149,8 @@ ### 旧账号审计 -- Writer 与 Store:`internal/service/account_audit/service.go`、`internal/store/postgres/account_operation_log_store.go`。 -- 生产装配:`internal/bootstrap/services.go`、`internal/bootstrap/stores.go`。 -- 调用模块:`internal/service/account/service.go`、`internal/service/agent_recharge/service.go`、`internal/service/shop_package_batch_allocation/service.go`、`internal/service/wechat_config/service.go`。 -- 已切换入口:账号创建、基础资料更新、独立启停、软删除、管理员改密、本人改密、企微绑定及后台登录/登出已改用统一 Writer;PostgreSQL 账号安全事实与审计同事务,登录/登出审计为提交后 best-effort,失败不撤销 Token 也不改变原认证结果;刷新保持原单次刷新协议,不因审计重写。企微绑定旧账号日志调用已移除,其他借用旧账号日志的调用模块仍按后续任务保留。 +- contract 结果:旧 Writer、Store、生产装配和全部借用旧账号日志的调用已删除;`tb_account_operation_log` 表及存量数据原样保留,不回填、不转换、不接入统一 Query。 +- 已切换入口:账号创建、基础资料更新、独立启停、软删除、管理员改密、本人改密、企微绑定及后台登录/登出已改用统一 Writer;PostgreSQL 账号安全事实与审计同事务,登录/登出审计为提交后 best-effort,失败不撤销 Token 也不改变原认证结果;刷新保持原单次刷新协议,不因审计重写。 - 角色创建/更新/启停/删除/默认信用、权限创建/更新/删除及角色权限分配/移除已切换统一 Writer;角色、权限和关联变化与 Audit Event 同一 GORM 事务,批量配置不再逐项自动提交,权限快照保存 code/name 和资源级 before/after。 - 账号角色分配/移除及店铺默认角色分配/移除已切换统一 Writer;账号或店铺为主要资源,实际变化角色保存 ID/name/type/status 与分配前后值,关系变化与审计共用同一 GORM 事务;权限缓存在提交后 best-effort 失效,Redis 失败不回滚业务。账号两处旧 operation log 写入已移除;店铺侧原本不存在旧账号日志写入。 - 店铺创建与基础资料变化已切换统一 Writer:创建事件保存店铺编码、名称、上级和层级并与店铺、初始账号、默认角色及钱包初始化共用原有事务;更新事件只记录实际变化的基础资料,状态、业务员和 C 端登录限制留给 5.6。父子层级仍仅在创建时按既有七级规则确定,相关入口原本不存在旧账号日志写入。 @@ -198,21 +196,59 @@ - Integration Log 冷归档复用同一对象存储、归档 Service 和 `tb_log_archive_run`:`integration:daily:archive` 按 `created_at` 保存前一自然日的结构化 JSONL+gzip 创建日快照;`integration:monthly:finalize` 在月初逐日按数据库当前内容重新生成并比较记录数与 SHA-256,首次终结、内容变化或对象 metadata 不一致时创建新的不可变 revision,旧对象不覆盖。月度复核仅把无 `pending` 记录且最终对象、manifest 均复核成功的日期标记 `is_final`;`pending`、对象损坏或复核失败会使任务失败并明确阻止后续清理。该切片不修改 Integration Writer、恢复语义或业务状态,也不删除 PostgreSQL 数据、不提供对象存储查询/恢复接口。 - 月度留存清理使用 `audit:monthly:retention` Asynq 任务,在 `Asia/Shanghai` 每月 1 日 06:00 处理上一完整自然月:先补齐最后一日 Audit/Integration 归档并完成 Integration 最终 revision,再逐日核对 ledger、manifest、对象 metadata、压缩对象实际大小/SHA-256 及数据库数量。全月硬门禁通过后仅按 Event Resource → Audit Event → Integration Log 顺序对 `tb_audit_event_resource`、`tb_audit_event`、`tb_integration_log` 以 1000 行有界批次执行 GORM 物理删除,并复用 `tb_log_archive_run.cleanup_started_at/cleaned_at` 断点续跑;对象存储归档和 manifest 长期保留。清理结果以 `retention_worker/system_task` 写当月 `audit.retention_cleanup` Audit Event,资源为 `log_archive_month`;Domain Ledger、Integration Log 新写、Outbox 均为 N/A,因该事实是内部留存执行结果,不是业务状态、外部交互或可靠投递。Access Log、订单/支付/退款/钱包等 Domain Ledger、Outbox、Asynq 运行事实、手动轮询、旧 operation log 及其他业务表明确不删除。 - 在线审计查询以 `tb_log_archive_run.cleaned_at/range_end` 作为真实清理边界:平台 Audit/Integration、request/correlation/finance/risk 及代理/企业活动响应统一返回 `retention{online_from,archived_before,timezone}`;缺省时间范围只查 PostgreSQL 在线窗口,显式早于或跨越边界返回 `CodeAuditDataArchived` 和当前边界。稳定事件或 Integration ID 只在在线库查找,不存在仍返回资源不存在;历史资源快照搜索和 Integration 尝试序列同样受边界限制。该 Query 切片不访问对象存储,不新增归档下载、恢复、冷热联合查询、导出或写路由,普通读取仍为 Audit Event/Domain Ledger/Integration Log/Outbox N/A。 +- 留存灰度默认使用 `worker.audit_retention_cleanup_enabled=false`:月初 `audit:monthly:retention` 只读复核完整月 ledger、manifest、对象 metadata/大小/SHA-256、Audit 计数和 Integration 最终 revision,并记录三类在线行数、manifest 数、校验耗时和预计 1000 行删除批次;不会写 `cleanup_started_at/cleaned_at`,也不会执行 DELETE。完整自然月可在显式确认的隔离测试数据库中按真实日界构造,无需等待现实时间流逝;dry-run 通过后仅在该测试环境启用清理,验证目标月三张日志表归零且月前/月后哨兵、其他业务事实和对象归档不受影响。2026-08-06 使用 `.env.local` 的 `junhong_cmp_test` 完成 `2001-02` 仿真:56/56 归档成功、28/28 Integration final、最大 revision/attempt=2、压缩率 0.5047、dry-run 无清理断点、清理后目标月三表归零且 6 条边界哨兵保留、56 条账本清理断点完整并写入 1 条清理审计。生产开关仍保持关闭;该 Release/Observability 切片不产生 Domain Ledger、Integration Log 或 Outbox。 + +## 2026-08-06 最终 Registry 与覆盖门禁复核 + +- 业务复核:692 个当前源码入口均进入显式清单;状态变更、资金、权限、关键配置、批量、多资源、设备卡槽、换货、自动入口及 5 组敏感读取均已在上方领域矩阵给出 Audit Event 或逐项 N/A 决定。39 个 Worker 注册语句对应 35 个唯一 TaskType,重复注册来自对象存储可用/不可用两条互斥装配分支,不代表重复消费。 +- 研发复核:219 个 `AuditAction` 常量与 219 个 `actionsByCode` 注册项一一对应,16 个兼容 `AuditOperation` 与 `actionsByOperation` 一一对应;生产使用未注册动作、动作字符串字面量均为 0。61 个资源类型常量与 61 个 Resource Registry 条目一一对应,146 个资源角色常量均有生产引用,生产 `ResourceInput` 未使用资源类型或角色字符串字面量。Writer 对未知 action、未知 resource、主要资源不匹配及不完整关系保持 fail-closed。 +- 查询复核:通用资源时间线直接以 Resource Registry 判定支持类型,不再维护独立 13 类白名单;资源搜索仍按第一阶段契约只开放卡、设备、店铺、订单和退款 5 类 resolver。统一 Query 不读取旧账号/资产 operation log。 +- 外部与可靠链路复核:78 个 Integration Log 调用点覆盖 `Start/Complete/RecordInbound/ClaimExpiredInboundPending`,14 个 Outbox Consumer 注册点均显式登记;Consumer 注册本身为技术装配 N/A,实际状态变化沿用对应 Consumer/Application 动作,Outbox 投递事实不伪装成业务成功。 +- 安全复核:普通 GET 继续逐项 N/A;企微明文应用凭据、资产实时状态、后台/个人实名链接、完成态导出任务详情等敏感读取保持返回前 fail-closed 审计。密码、验证码、Token、Secret、私钥、Cookie、签名 URL 和支付密钥不进入 Audit/Integration 或主体投影。 +- contract 复核:旧 Writer 调用为 0,旧账号表与旧资产表原样保留,旧资产历史入口只读,手动轮询 ledger 继续读写。扫描清单 692 项必填分类字段缺失为 0,Audit Event N/A 无理由为 0;静态扫描、gopls、全仓构建与 `git diff --check` 作为本 Change 禁止自动化测试约束下的完成证据。 + +## 2026-08-06 安全与身份发布门禁复核 + +- 身份边界:平台 Audit/Integration Query 仅允许 `SuperAdmin/Platform`,代理与企业即使通过通用后台认证也在 Query 层统一 fail-closed;代理范围只取认证上下文中的自身及下级店铺,企业只取认证上下文企业 ID,并要求卡/设备授权 `revoked_at IS NULL AND deleted_at IS NULL`。 +- 主体投影:不支持资源类型与不存在/越权统一返回“无权限操作该资源或资源不存在”;活动投影返回前再次核验目标资源当前归属或有效授权。`internal_only` 不进入统计、列表和关联资源,`subject_result` 强制返回空 `subject_data`,只有 `subject_detail` 解码持久化白名单数据;主体 DTO 不包含 actor、risk、内部 before/after、Audit Event ID、request/correlation 或 Integration 内容。 +- 平台完整性:平台事件详情继续完整投影已存业务 metadata、资源身份快照及各资源 before/after,不对手机号、IP、ICCID、VirtualNo、金额和交易号执行展示掩码;系统安全凭据仍由 Writer/Sanitizer 在持久化前删除。 +- 凭据门禁:统一 Sanitizer 已覆盖驼峰字段名、裸 token/api/payment key 及凭据文本值;Audit/Integration 标量同样在落库和历史响应投影前清理。`.env.local` 测试 PostgreSQL 只读抽样中,Audit Event JSON、Audit Resource JSON、Audit 标量、Audit Resource 标量、Integration 标量、Integration 禁止键和 Integration 凭据值命中均为 0。历史 Integration 的 129 条初始正则命中全部是允许的 `token_present` 布尔安全事实,不包含 token 值。 +- 能力面:`internal/routes/audit.go` 及生成的 `docs/admin-openapi.yaml` 中 15 条平台/代理/企业审计路径全部只有 GET;未注册审计导出、对象存储归档查询/恢复、Integration 重试/补偿/修改/删除或 Audit Event 业务删除路由。旧资产 operation-logs 继续作为仅平台可读的独立历史白名单入口。 +- 验证约束:本 Change 明确禁止新增、修改或运行自动化测试;本门禁使用敌对身份调用链静态复核、数据库只读抽样、路由/OpenAPI 扫描、全仓构建、gopls 与 `git diff --check` 作为完成证据。 + +## 2026-08-06 事务、幂等与跨链路发布门禁复核 + +- 同事务失败关闭:系统配置更新在同一 GORM 事务内完成业务写和 `WriteConfigChange`;代理主钱包扣款/入账在同一事务内完成条件更新、唯一流水和 Audit Event,Writer 错误直接返回并回滚业务事务。Outbox 恢复、审批终态及其他高风险纵向切片沿用相同 `tx` 接缝,不使用提交后的补写冒充原子性。 +- 失败短事务:统一 `Writer.RecordFailure` 仅在原业务返回后开启独立短事务,`fillFailureInput` 保留原 `AppError` code/message;二次写失败只递增 `secondaryWriteFailures` 并输出含 action、资源、request/correlation 和原错误码的 critical 日志,不替换原业务错误。 +- 批量根子:`AppendBatch` 在落库前拒绝负数、`success+fail>total`、子事件少于成功数或多于已处理数,并为子事件补齐稳定 parent/correlation。该门禁发现并修复 IoT 卡批量轮询开关原先缺少根/子 `EventID` 的生产缺口,现按请求批次和卡 ID 生成稳定 ID,并以实际有效卡数计数。测试库只读核对中,批量计数错误、非法结果、缺少/多个主要资源均为 0;当前在线窗口没有带批次统计的根事件,因此不伪造动态样本结论。 +- Audit 链路:测试库中 `child_without_parent_online`、`child_without_correlation`、`parent_correlation_mismatch` 均为 0。跨事实 Query 分别读取 Audit Event、Integration Log、Outbox,并以独立 `record_source` 投影;Domain Ledger 和 Asynq 仅作为 `reference_only` 稳定引用,金额权威仍由资金业务表提供。 +- Integration 技术序列:只读核对发现存量代理充值查单及卡观测记录存在重复 `trigger_series + attempt`。新写入已前向修复:自动 attempt 在序列级 PostgreSQL advisory lock 的同一短事务内分配;卡观测按 `series_id + sync_type` 区分不同 Gateway operation。历史数据不回填、不猜测,详情保留每条真实 operation,并以 `attempt_sequence_reliable=false` 明确重复、断档或混合 operation 的受限保真度。 +- 幂等边界:Audit Event 继续以稳定 `event_id` 冲突不重复写;资金使用业务唯一流水和状态/版本条件;Outbox 保持独立至少一次投递状态。Audit、Integration、Domain Ledger 和 Outbox 之间只通过 request/correlation/parent、稳定业务 ID 或只读引用关联,没有把外部尝试、投递成功或审计摘要伪装成业务成功。 +- 验证约束:未新增、修改或运行自动化测试;完成证据使用代表性事务源码调用链、测试 PostgreSQL 只读不变量查询、覆盖清单、OpenAPI、全仓构建、gopls 与 `git diff --check`。 + +## 2026-08-06 迁移、性能、OpenAPI、归档留存与回滚发布门禁复核 + +- 迁移现状:`.env.local` 测试 PostgreSQL 的 `schema_migrations` 为 `205/dirty=false`;Audit、Integration、归档账本关键增量索引已存在。共享测试库已有 Audit 和归档事实,未对 `public` 执行 down。 +- 无事实 down/up:在同一测试 PostgreSQL 的隔离 schema 中使用最小前置表结构执行 `000199`~`000205` up,再按 `205→199` 逆序 down;上行后目标表/列/索引存在,回滚后 Audit/归档表移除且 Outbox parent/订单预占列恢复。`000199.down` 自带事务并提交外层事务,隔离 schema 已显式删除且核对不存在;该注意项已写入操作手册。 +- 性能观测:复用已完成 9.4 的分页 ID + 批量投影与索引证据;当前库只读 `EXPLAIN (ANALYZE, BUFFERS)` 中,事件列表、资源时间线、Integration 列表、风险聚合执行时间分别为 `0.315ms/0.116ms/0.186ms/0.123ms`,均低于 50ms。当前 Audit 在线样本很小,PostgreSQL 对部分路径选择顺序扫描,Integration 已命中 provider 索引;未因小样本增加缓存、分区或额外抽象。API P95/P99 沿用 9.4 完成证据及发布后 Access Log 阈值,本门禁不重复运行接口压测。 +- OpenAPI:`go run ./cmd/gendocs` 生成 `docs/admin-openapi.yaml`;运行时文档入口已生成 `logs/openapi.yaml`。两份制品均包含 15 条平台 Audit/Integration 及代理/企业资源活动路径。 +- 归档留存:复用 10.5 的 `2001-02` 完整月证据:56/56 归档成功、28/28 Integration final、最大 revision/attempt=2、压缩率 0.5047、dry-run 零清理断点,物理清理后目标三表为 0、6 条边界哨兵保留、56 条清理断点完整、1 条当月清理审计且对象/manifest 未删除。归档失败、缺日、pending、计数/SHA-256/对象 metadata 不一致均在 DELETE 前 fail-closed;生产清理开关仍关闭。 +- 回滚:现有归档手册已补充 contract 回滚 SOP 与全局监控阈值。业务回滚保留版本 205 结构、当前在线 Audit/Integration、全部 Domain Ledger/Outbox 和对象存储归档;只能回到旧 Writer 已停写的 contract 基线,不恢复旧 Writer,不对已清理月份回填或伪造在线历史。 +- 静态门禁:覆盖清单重新生成 692 项;全仓 `go build ./...`、已改 Go 文件 `gopls check` 和 `git diff --check` 通过。Go 只输出 module stat cache 无权限警告,构建退出码为 0。本 Change 未新增、修改或运行 `_test.go`;当前门禁按用户要求不再执行接口压测。 ### 与审计接入分开保留的独立修复 - 企业、企业卡和企业设备的权限校验、空筛选防全量、批量边界、越权同错及企业设备 TOCTOU/真实命中计数作为独立安全与并发修复保留,不视为审计所需的业务重构。 - 手机号绑定/换绑的行锁和事务内二次复检、账号状态与代理越权校验、管理员账号改密后撤销 Token 作为独立修复保留;企业账号改密不扩展同样的 Token 行为。 - 角色/权限变化和店铺删除后的权限缓存清理能力作为独立修复保留,但只在数据库提交后 best-effort 执行,不让 Redis 失败反向回滚业务事实。 -- 已确认支付配置和员工线下充值的外层裸 goroutine 均已归零;旧 Writer 自身仍另启 goroutine,由最终 contract 任务统一处理。 +- 支付配置和员工线下充值的外层裸 goroutine、旧账号 Writer 及旧资产 Writer 的异步写入均已归零。 - 迁移责任:04、07、08;19 号票验证生产装配和直接旧表写入归零。 ### 旧资产审计 -- Writer、Builder 与 Store:`internal/service/asset_audit/`、`internal/store/postgres/asset_operation_log_store.go`。 -- API/Worker 装配:`internal/bootstrap/services.go`、`internal/bootstrap/worker_services.go`、`internal/bootstrap/stores.go`、`internal/bootstrap/worker_stores.go`。 -- 调用模块:`internal/service/asset/`、`internal/service/device/`、`internal/service/iot_card/`、`internal/service/polling/asset_polling_service.go`;卡/设备导入任务创建已停止调用旧 Writer,但生产组合根的兼容参数留待 11.1 统一清理。 -- 兼容读取:`internal/handler/admin/asset.go`、`internal/routes/asset.go`、`internal/model/dto/asset_operation_log_dto.go`;由 10 号票保留读取契约。 +- contract 结果:旧资产 Create/Writer、全部业务调用、裸/双重 goroutine 和 Worker 装配已删除;卡/设备停用、轮询开关及套餐人工调整已切换统一 Writer,业务事实与成功审计同事务,已识别资源后的失败/拒绝使用独立短事务。 +- 历史只读白名单:`internal/service/asset_audit/`、`internal/store/postgres/asset_operation_log_store.go`、`internal/handler/admin/asset.go`、`internal/routes/asset.go`、`internal/model/dto/asset_operation_log_dto.go` 仅保留旧资产历史查询;主进程只注入只读 Store/Service,Worker 不再装配旧资产 Store。 +- `tb_asset_operation_log` 表及存量数据原样保留,不回填、不转换、不接入统一 Query;生产业务不再新增记录。 - 迁移责任:05、06、09;19 号票验证生产装配和直接旧表写入归零。 ### 旧手动轮询日志 diff --git a/.scratch/tech-global-audit/审计覆盖清单.json b/.scratch/tech-global-audit/审计覆盖清单.json index 4efe40d..d0da82d 100644 --- a/.scratch/tech-global-audit/审计覆盖清单.json +++ b/.scratch/tech-global-audit/审计覆盖清单.json @@ -1,8 +1,8 @@ [ { - "key": "application:internal/application/agentrecharge/approval_decision.go:29:Handle", + "key": "application:internal/application/agentrecharge/approval_decision.go:31:Handle", "kind": "application", - "code_entry": "internal/application/agentrecharge/approval_decision.go:29 Handle", + "code_entry": "internal/application/agentrecharge/approval_decision.go:31 Handle", "owner": "agentrecharge", "summary": "业务方法 Handle", "audit_event": "必须", @@ -21,12 +21,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/agentrecharge/confirm_online_payment.go:75:Execute", + "key": "application:internal/application/agentrecharge/confirm_online_payment.go:76:Execute", "kind": "application", - "code_entry": "internal/application/agentrecharge/confirm_online_payment.go:75 Execute", + "code_entry": "internal/application/agentrecharge/confirm_online_payment.go:76 Execute", "owner": "agentrecharge", "summary": "业务方法 Execute", "audit_event": "必须", @@ -45,12 +45,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/agentrecharge/offline_creation.go:49:Execute", + "key": "application:internal/application/agentrecharge/offline_creation.go:50:Execute", "kind": "application", - "code_entry": "internal/application/agentrecharge/offline_creation.go:49 Execute", + "code_entry": "internal/application/agentrecharge/offline_creation.go:50 Execute", "owner": "agentrecharge", "summary": "业务方法 Execute", "audit_event": "必须", @@ -69,12 +69,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/agentrecharge/online_creation.go:60:Execute", + "key": "application:internal/application/agentrecharge/online_creation.go:61:Execute", "kind": "application", - "code_entry": "internal/application/agentrecharge/online_creation.go:60 Execute", + "code_entry": "internal/application/agentrecharge/online_creation.go:61 Execute", "owner": "agentrecharge", "summary": "业务方法 Execute", "audit_event": "必须", @@ -93,12 +93,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/agentrecharge/recover_online_payment.go:29:ProcessBatch", + "key": "application:internal/application/agentrecharge/recover_online_payment.go:30:ProcessBatch", "kind": "application", - "code_entry": "internal/application/agentrecharge/recover_online_payment.go:29 ProcessBatch", + "code_entry": "internal/application/agentrecharge/recover_online_payment.go:30 ProcessBatch", "owner": "agentrecharge", "summary": "业务方法 ProcessBatch", "audit_event": "必须", @@ -117,12 +117,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/approval/create.go:125:CreateContextInTx", + "key": "application:internal/application/approval/create.go:143:CreateContextInTx", "kind": "application", - "code_entry": "internal/application/approval/create.go:125 CreateContextInTx", + "code_entry": "internal/application/approval/create.go:143 CreateContextInTx", "owner": "approval", "summary": "业务方法 CreateContextInTx", "audit_event": "必须", @@ -141,12 +141,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/approval/create.go:64:CreateInTx", + "key": "application:internal/application/approval/create.go:70:CreateInTx", "kind": "application", - "code_entry": "internal/application/approval/create.go:64 CreateInTx", + "code_entry": "internal/application/approval/create.go:70 CreateInTx", "owner": "approval", "summary": "业务方法 CreateInTx", "audit_event": "必须", @@ -165,12 +165,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/approval/sync_decision.go:83:Execute", + "key": "application:internal/application/approval/sync_decision.go:90:Execute", "kind": "application", - "code_entry": "internal/application/approval/sync_decision.go:83 Execute", + "code_entry": "internal/application/approval/sync_decision.go:90 Execute", "owner": "approval", "summary": "业务方法 Execute", "audit_event": "必须", @@ -189,12 +189,156 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/cardobservation/series.go:124:Trigger", + "key": "application:internal/application/auditarchive/integration.go:50:ArchivePreviousIntegrationDay", "kind": "application", - "code_entry": "internal/application/cardobservation/series.go:124 Trigger", + "code_entry": "internal/application/auditarchive/integration.go:50 ArchivePreviousIntegrationDay", + "owner": "auditarchive", + "summary": "业务方法 ArchivePreviousIntegrationDay", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "auditarchive.archive_previous_integration_day", + "action_name": "执行业务方法(ArchivePreviousIntegrationDay)", + "category": "business", + "risk": "normal", + "primary_resource": "auditarchive", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "application:internal/application/auditarchive/integration.go:56:ArchiveIntegrationDate", + "kind": "application", + "code_entry": "internal/application/auditarchive/integration.go:56 ArchiveIntegrationDate", + "owner": "auditarchive", + "summary": "业务方法 ArchiveIntegrationDate", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "auditarchive.archive_integration_date", + "action_name": "执行业务方法(ArchiveIntegrationDate)", + "category": "business", + "risk": "normal", + "primary_resource": "auditarchive", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "application:internal/application/auditarchive/retention.go:55:CleanupPreviousMonth", + "kind": "application", + "code_entry": "internal/application/auditarchive/retention.go:55 CleanupPreviousMonth", + "owner": "auditarchive", + "summary": "业务方法 CleanupPreviousMonth", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "auditarchive.cleanup_previous_month", + "action_name": "执行业务方法(CleanupPreviousMonth)", + "category": "business", + "risk": "normal", + "primary_resource": "auditarchive", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "application:internal/application/auditarchive/retention.go:91:CleanupMonth", + "kind": "application", + "code_entry": "internal/application/auditarchive/retention.go:91 CleanupMonth", + "owner": "auditarchive", + "summary": "业务方法 CleanupMonth", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "auditarchive.cleanup_month", + "action_name": "执行业务方法(CleanupMonth)", + "category": "business", + "risk": "normal", + "primary_resource": "auditarchive", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "application:internal/application/auditarchive/service.go:104:ArchiveDate", + "kind": "application", + "code_entry": "internal/application/auditarchive/service.go:104 ArchiveDate", + "owner": "auditarchive", + "summary": "业务方法 ArchiveDate", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "auditarchive.archive_date", + "action_name": "执行业务方法(ArchiveDate)", + "category": "business", + "risk": "normal", + "primary_resource": "auditarchive", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "application:internal/application/auditarchive/service.go:98:ArchivePreviousDay", + "kind": "application", + "code_entry": "internal/application/auditarchive/service.go:98 ArchivePreviousDay", + "owner": "auditarchive", + "summary": "业务方法 ArchivePreviousDay", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "auditarchive.archive_previous_day", + "action_name": "执行业务方法(ArchivePreviousDay)", + "category": "business", + "risk": "normal", + "primary_resource": "auditarchive", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "application:internal/application/cardobservation/series.go:127:Trigger", + "kind": "application", + "code_entry": "internal/application/cardobservation/series.go:127 Trigger", "owner": "cardobservation", "summary": "业务方法 Trigger", "audit_event": "必须", @@ -213,12 +357,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/cardobservation/series.go:129:TriggerEvent", + "key": "application:internal/application/cardobservation/series.go:132:TriggerEvent", "kind": "application", - "code_entry": "internal/application/cardobservation/series.go:129 TriggerEvent", + "code_entry": "internal/application/cardobservation/series.go:132 TriggerEvent", "owner": "cardobservation", "summary": "业务方法 TriggerEvent", "audit_event": "必须", @@ -237,12 +381,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/cardobservation/series.go:201:Execute", + "key": "application:internal/application/cardobservation/series.go:205:Execute", "kind": "application", - "code_entry": "internal/application/cardobservation/series.go:201 Execute", + "code_entry": "internal/application/cardobservation/series.go:205 Execute", "owner": "cardobservation", "summary": "业务方法 Execute", "audit_event": "必须", @@ -261,7 +405,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "application:internal/application/outbox/recovery.go:146:ReleaseExpiredLeases", @@ -285,7 +429,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "application:internal/application/outbox/recovery.go:88:Replay", @@ -309,12 +453,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/refundapproval/creation.go:43:Execute", + "key": "application:internal/application/refundapproval/creation.go:58:Execute", "kind": "application", - "code_entry": "internal/application/refundapproval/creation.go:43 Execute", + "code_entry": "internal/application/refundapproval/creation.go:58 Execute", "owner": "refundapproval", "summary": "业务方法 Execute", "audit_event": "必须", @@ -333,20 +477,20 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/role/default_credit.go:31:Update", + "key": "application:internal/application/role/default_credit.go:34:Update", "kind": "application", - "code_entry": "internal/application/role/default_credit.go:31 Update", + "code_entry": "internal/application/role/default_credit.go:34 Update", "owner": "role", "summary": "业务方法 Update", "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.update_default_credit", - "action_name": "更新客户角色的新建代理默认信用模板", + "action_code": "role.update", + "action_name": "执行业务方法(Update)", "category": "security", "risk": "high", "primary_resource": "role", @@ -357,12 +501,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/shop/create.go:29:Create", + "key": "application:internal/application/shop/create.go:32:Create", "kind": "application", - "code_entry": "internal/application/shop/create.go:29 Create", + "code_entry": "internal/application/shop/create.go:32 Create", "owner": "shop", "summary": "业务方法 Create", "audit_event": "必须", @@ -370,7 +514,7 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "shop.create", - "action_name": "创建店铺", + "action_name": "执行业务方法(Create)", "category": "business", "risk": "normal", "primary_resource": "shop", @@ -381,12 +525,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/shop/update.go:27:Update", + "key": "application:internal/application/shop/update.go:29:Update", "kind": "application", - "code_entry": "internal/application/shop/update.go:27 Update", + "code_entry": "internal/application/shop/update.go:29 Update", "owner": "shop", "summary": "业务方法 Update", "audit_event": "必须", @@ -394,7 +538,7 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "shop.update", - "action_name": "更新店铺基础资料", + "action_name": "执行业务方法(Update)", "category": "business", "risk": "normal", "primary_resource": "shop", @@ -405,12 +549,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/systemconfig/update.go:68:Execute", + "key": "application:internal/application/systemconfig/update.go:71:Execute", "kind": "application", - "code_entry": "internal/application/systemconfig/update.go:68 Execute", + "code_entry": "internal/application/systemconfig/update.go:71 Execute", "owner": "systemconfig", "summary": "业务方法 Execute", "audit_event": "必须", @@ -429,12 +573,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/wallet/change_credit.go:26:Execute", + "key": "application:internal/application/wallet/change_credit.go:46:Execute", "kind": "application", - "code_entry": "internal/application/wallet/change_credit.go:26 Execute", + "code_entry": "internal/application/wallet/change_credit.go:46 Execute", "owner": "wallet", "summary": "业务方法 Execute", "audit_event": "必须", @@ -453,7 +597,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "application:internal/application/wallet/debit.go:82:DebitInTx", @@ -477,7 +621,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "application:internal/application/wallet/refund.go:89:RefundInTx", @@ -501,7 +645,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "application:internal/application/wallet/reservation.go:108:ReleaseInTx", @@ -525,7 +669,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "application:internal/application/wallet/reservation.go:70:FreezeInTx", @@ -549,12 +693,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/wecom/connection.go:242:SaveDefaultCreator", + "key": "application:internal/application/wecom/connection.go:253:SaveDefaultCreator", "kind": "application", - "code_entry": "internal/application/wecom/connection.go:242 SaveDefaultCreator", + "code_entry": "internal/application/wecom/connection.go:253 SaveDefaultCreator", "owner": "wecom", "summary": "业务方法 SaveDefaultCreator", "audit_event": "必须", @@ -573,12 +717,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/wecom/connection.go:92:Save", + "key": "application:internal/application/wecom/connection.go:93:Save", "kind": "application", - "code_entry": "internal/application/wecom/connection.go:92 Save", + "code_entry": "internal/application/wecom/connection.go:93 Save", "owner": "wecom", "summary": "业务方法 Save", "audit_event": "必须", @@ -597,12 +741,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/wecom/directory.go:52:Sync", + "key": "application:internal/application/wecom/directory.go:57:Sync", "kind": "application", - "code_entry": "internal/application/wecom/directory.go:52 Sync", + "code_entry": "internal/application/wecom/directory.go:57 Sync", "owner": "wecom", "summary": "业务方法 Sync", "audit_event": "必须", @@ -621,12 +765,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "application:internal/application/wecom/scene.go:110:Save", + "key": "application:internal/application/wecom/scene.go:111:Save", "kind": "application", - "code_entry": "internal/application/wecom/scene.go:110 Save", + "code_entry": "internal/application/wecom/scene.go:111 Save", "owner": "wecom", "summary": "业务方法 Save", "audit_event": "必须", @@ -645,7 +789,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "domain:internal/domain/wallet/wallet.go:128:ChangeCredit", @@ -664,7 +808,7 @@ "failure_strategy": "返回领域错误,由 Application 在回滚后裁决 failed/denied 审计", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口", "na_reason": "Domain 只维护业务不变量和领域事实,不依赖审计基础设施;Audit Event 由 Application 写入" }, { @@ -684,7 +828,7 @@ "failure_strategy": "返回领域错误,由 Application 在回滚后裁决 failed/denied 审计", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口", "na_reason": "Domain 只维护业务不变量和领域事实,不依赖审计基础设施;Audit Event 由 Application 写入" }, { @@ -704,7 +848,7 @@ "failure_strategy": "返回领域错误,由 Application 在回滚后裁决 failed/denied 审计", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口", "na_reason": "Domain 只维护业务不变量和领域事实,不依赖审计基础设施;Audit Event 由 Application 写入" }, { @@ -724,7 +868,7 @@ "failure_strategy": "返回领域错误,由 Application 在回滚后裁决 failed/denied 审计", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口", "na_reason": "Domain 只维护业务不变量和领域事实,不依赖审计基础设施;Audit Event 由 Application 写入" }, { @@ -744,7 +888,7 @@ "failure_strategy": "返回领域错误,由 Application 在回滚后裁决 failed/denied 审计", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口", "na_reason": "Domain 只维护业务不变量和领域事实,不依赖审计基础设施;Audit Event 由 Application 写入" }, { @@ -768,7 +912,7 @@ "actor_source": "登录账号快照/admin_api", "visibility": "按认证账号类型和现有数据权限可见;审计调查另按平台/主体投影隔离", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", - "failure_strategy": "参数错误不写 Audit Event;权限或全项规则拒绝写 denied 短事务;业务回滚后的 failed 使用独立短事务,二次失败保留业务错误并记录 critical", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" @@ -815,7 +959,7 @@ "actor_source": "登录账号快照/admin_api", "visibility": "按认证账号类型和现有数据权限可见;审计调查另按平台/主体投影隔离", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", - "failure_strategy": "参数错误不写 Audit Event;权限、全项规则拒绝或并发零命中写 denied 短事务;业务回滚后的 failed 使用独立短事务,二次失败保留业务错误并记录 critical", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" @@ -1840,9 +1984,114 @@ "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" }, { - "key": "http:internal/routes/audit.go:58:GET:/integrations/overview", + "key": "http:internal/routes/audit.go:57:GET:/requests/:request_id/timeline", "kind": "http", - "code_entry": "internal/routes/audit.go:58 handler.IntegrationOverview", + "code_entry": "internal/routes/audit.go:57 handler.RequestTimeline", + "owner": "audit", + "method": "GET", + "path": "/requests/:request_id/timeline", + "summary": "查询请求关联时间线", + "audit_event": "N/A", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", + "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", + "actor_source": "登录账号快照/admin_api", + "visibility": "仅超级管理员和平台账号可见", + "transaction": "N/A", + "failure_strategy": "Access Log 记录统一错误;普通读取不创建业务审计", + "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", + "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", + "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口", + "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" + }, + { + "key": "http:internal/routes/audit.go:62:GET:/correlations/:correlation_id/timeline", + "kind": "http", + "code_entry": "internal/routes/audit.go:62 handler.CorrelationTimeline", + "owner": "audit", + "method": "GET", + "path": "/correlations/:correlation_id/timeline", + "summary": "查询业务关联时间线", + "audit_event": "N/A", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", + "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", + "actor_source": "登录账号快照/admin_api", + "visibility": "仅超级管理员和平台账号可见", + "transaction": "N/A", + "failure_strategy": "Access Log 记录统一错误;普通读取不创建业务审计", + "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", + "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", + "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口", + "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" + }, + { + "key": "http:internal/routes/audit.go:67:GET:/finance/timeline", + "kind": "http", + "code_entry": "internal/routes/audit.go:67 handler.FinanceTimeline", + "owner": "audit", + "method": "GET", + "path": "/finance/timeline", + "summary": "查询资金调查时间线", + "audit_event": "N/A", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", + "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", + "actor_source": "登录账号快照/admin_api", + "visibility": "仅超级管理员和平台账号可见", + "transaction": "N/A", + "failure_strategy": "Access Log 记录统一错误;普通读取不创建业务审计", + "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", + "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", + "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口", + "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" + }, + { + "key": "http:internal/routes/audit.go:72:GET:/risks/overview", + "kind": "http", + "code_entry": "internal/routes/audit.go:72 handler.RiskOverview", + "owner": "audit", + "method": "GET", + "path": "/risks/overview", + "summary": "查询风险调查总览", + "audit_event": "N/A", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", + "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", + "actor_source": "登录账号快照/admin_api", + "visibility": "仅超级管理员和平台账号可见", + "transaction": "N/A", + "failure_strategy": "Access Log 记录统一错误;普通读取不创建业务审计", + "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", + "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", + "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口", + "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" + }, + { + "key": "http:internal/routes/audit.go:77:GET:/risks/events", + "kind": "http", + "code_entry": "internal/routes/audit.go:77 handler.RiskEvents", + "owner": "audit", + "method": "GET", + "path": "/risks/events", + "summary": "查询风险事件明细", + "audit_event": "N/A", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", + "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", + "actor_source": "登录账号快照/admin_api", + "visibility": "仅超级管理员和平台账号可见", + "transaction": "N/A", + "failure_strategy": "Access Log 记录统一错误;普通读取不创建业务审计", + "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", + "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", + "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口", + "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" + }, + { + "key": "http:internal/routes/audit.go:83:GET:/integrations/overview", + "kind": "http", + "code_entry": "internal/routes/audit.go:83 handler.IntegrationOverview", "owner": "audit", "method": "GET", "path": "/integrations/overview", @@ -1861,9 +2110,9 @@ "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" }, { - "key": "http:internal/routes/audit.go:63:GET:/integrations", + "key": "http:internal/routes/audit.go:88:GET:/integrations", "kind": "http", - "code_entry": "internal/routes/audit.go:63 handler.ListIntegrations", + "code_entry": "internal/routes/audit.go:88 handler.ListIntegrations", "owner": "audit", "method": "GET", "path": "/integrations", @@ -1882,9 +2131,9 @@ "na_reason": "普通只读查询,不改变业务事实且不返回需二次授权的完整敏感值" }, { - "key": "http:internal/routes/audit.go:68:GET:/integrations/:integration_id", + "key": "http:internal/routes/audit.go:93:GET:/integrations/:integration_id", "kind": "http", - "code_entry": "internal/routes/audit.go:68 handler.GetIntegration", + "code_entry": "internal/routes/audit.go:93 handler.GetIntegration", "owner": "audit", "method": "GET", "path": "/integrations/:integration_id", @@ -2863,7 +3112,7 @@ "category": "business", "risk": "normal", "primary_resource": "enterprise", - "affected_resource": "企业初始账号;归属店铺作为 reference", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", "visibility": "企业认证上下文范围内可见;内部审计字段不可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", @@ -2909,7 +3158,7 @@ "category": "business", "risk": "normal", "primary_resource": "enterprise", - "affected_resource": "归属店铺作为 reference;不改变 owner_shop_id", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", "visibility": "企业认证上下文范围内可见;内部审计字段不可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", @@ -2935,7 +3184,7 @@ "category": "business", "risk": "normal", "primary_resource": "enterprise", - "affected_resource": "实际同步状态的企业账号;归属店铺作为 reference", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", "visibility": "企业认证上下文范围内可见;内部审计字段不可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", @@ -2961,12 +3210,12 @@ "category": "business", "risk": "high", "primary_resource": "enterprise", - "affected_resource": "实际改密的企业账号;归属店铺作为 reference", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", "visibility": "企业认证上下文范围内可见;内部审计字段不可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", - "sensitive_policy": "密码明文与散列均禁止进入审计;只保存 credentials_configured/state 安全事实", + "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" }, @@ -3059,12 +3308,12 @@ "action_name": "授权设备给企业", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "实际授权设备、设备授权记录及随设备授权的绑定卡/卡授权记录;owner shop 和卡槽绑定作为 reference", + "primary_resource": "enterprise_device", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", - "visibility": "设备和卡仅保存 subject_result;授权记录与卡槽绑定 internal_only", + "visibility": "企业认证上下文范围内可见;内部审计字段不可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", - "failure_strategy": "参数错误不写 Audit Event;权限或全项规则拒绝写 denied 短事务;业务回滚后的 failed 使用独立短事务,二次失败保留业务错误并记录 critical", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" @@ -3085,12 +3334,12 @@ "action_name": "撤销设备授权", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "实际撤销的设备授权记录及其 device_auth_id 关联卡授权、设备和卡;owner shop 和实际卡槽绑定作为 reference", + "primary_resource": "enterprise_device", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", - "visibility": "设备和卡仅保存 subject_result;授权记录与卡槽绑定 internal_only", + "visibility": "企业认证上下文范围内可见;内部审计字段不可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", - "failure_strategy": "参数错误不写 Audit Event;权限、全项规则拒绝或并发零命中写 denied 短事务;业务回滚后的 failed 使用独立短事务,二次失败保留业务错误并记录 critical", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" @@ -5053,12 +5302,12 @@ "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "personal_customer.update_profile", + "action_code": "personal.update_profile", "action_name": "更新个人资料", - "category": "identity", + "category": "business", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "N/A:资料字段直接属于个人客户主要资源", + "primary_resource": "personal", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "personal_customer/personal_api", "visibility": "当前个人客户本人范围内可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", @@ -5671,19 +5920,19 @@ "method": "POST", "path": "authBasePath+\"/wechat-login\"", "summary": "公众号登录", - "audit_event": "客户资料或微信主体实际变化时必须;仅签发 Token 时 N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "personal_customer.update_wechat_identity / personal_customer.update_profile", - "action_name": "同步个人微信主体 / 同步个人资料", - "category": "identity", + "action_code": "personal.wechat_login", + "action_name": "公众号登录", + "category": "business", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "实际新建的 personal_customer_openid;资料同步只关联个人客户", + "primary_resource": "personal", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "personal_customer/personal_api", "visibility": "当前个人客户本人范围内可见", - "transaction": "客户/OpenID 实际变化与 Audit Event 同一 GORM 事务;重复登录无变化不写资料事件", + "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", @@ -5697,19 +5946,19 @@ "method": "POST", "path": "authBasePath+\"/miniapp-login\"", "summary": "小程序登录", - "audit_event": "客户资料或微信主体实际变化时必须;仅签发 Token 时 N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "personal_customer.update_wechat_identity / personal_customer.update_profile", - "action_name": "同步个人微信主体 / 同步个人资料", - "category": "identity", + "action_code": "personal.miniapp_login", + "action_name": "小程序登录", + "category": "business", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "实际新建的 personal_customer_openid;资料同步只关联个人客户", + "primary_resource": "personal", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "personal_customer/personal_api", "visibility": "当前个人客户本人范围内可见", - "transaction": "客户/OpenID 实际变化与 Audit Event 同一 GORM 事务;重复登录无变化不写资料事件", + "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", @@ -5723,18 +5972,23 @@ "method": "POST", "path": "authBasePath+\"/send-code\"", "summary": "发送验证码", - "audit_event": "N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", + "action_code": "personal.send_code", + "action_name": "发送验证码", + "category": "business", + "risk": "normal", + "primary_resource": "personal", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "personal_customer/personal_api", "visibility": "当前个人客户本人范围内可见", - "transaction": "N/A", - "failure_strategy": "验证码发送和限流进入 Access/Integration/Security 事实,不创建个人资料 Audit Event", + "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;二次失败保留业务错误并记录 critical", "sensitive_policy": "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限", "before_after_policy": "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用", - "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口", - "na_reason": "验证码是短期安全凭据,不属于个人客户资料、手机号绑定事实或微信主体变化,禁止写入审计" + "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" }, { "key": "http:internal/routes/personal.go:77:POST:authBasePath+\"/dev-login\"", @@ -5774,12 +6028,12 @@ "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "personal_customer.bind_phone", + "action_code": "personal.bind_phone", "action_name": "绑定手机号", - "category": "identity", + "category": "business", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "personal_customer_phone", + "primary_resource": "personal", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "personal_customer/personal_api", "visibility": "当前个人客户本人范围内可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", @@ -5800,12 +6054,12 @@ "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "personal_customer.change_phone", + "action_code": "personal.change_phone", "action_name": "更换手机号", - "category": "identity", + "category": "business", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "personal_customer_phone", + "primary_resource": "personal", + "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "personal_customer/personal_api", "visibility": "当前个人客户本人范围内可见", "transaction": "成功事件与关键业务事实同一 GORM 事务;敏感读取在返回前写入", @@ -7461,7 +7715,7 @@ "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "shop.create", + "action_code": "shop.core_shop_management", "action_name": "创建店铺", "category": "business", "risk": "normal", @@ -7508,8 +7762,8 @@ "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "shop.update", - "action_name": "更新店铺基础资料", + "action_code": "shop.core_shop_management", + "action_name": "更新店铺", "category": "business", "risk": "normal", "primary_resource": "shop", @@ -7560,10 +7814,10 @@ "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "无外部交互时 N/A;用例调用 Gateway、支付、企微或运营商时必须", "outbox": "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A", - "action_code": "shop.delete", + "action_code": "shop.core_shop_management", "action_name": "删除店铺", "category": "business", - "risk": "high", + "risk": "normal", "primary_resource": "shop", "affected_resource": "由对应 Application/Service 用例按直接影响资源显式填写,禁止递归扩展", "actor_source": "登录账号快照/admin_api", @@ -8465,9 +8719,47 @@ "test_seam": "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口" }, { - "key": "integration_log:internal/handler/callback/cmcc_realname.go:123:h.integration.RecordInbound", + "key": "integration_log:internal/handler/app/client_wallet_payment_audit.go:57:h.paymentIntegration.Start", "kind": "integration_log", - "code_entry": "internal/handler/callback/cmcc_realname.go:123 h.integration.RecordInbound", + "code_entry": "internal/handler/app/client_wallet_payment_audit.go:57 h.paymentIntegration.Start", + "owner": "app", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/app/client_wallet_payment_audit.go:76:h.paymentIntegration.Complete", + "kind": "integration_log", + "code_entry": "internal/handler/app/client_wallet_payment_audit.go:76 h.paymentIntegration.Complete", + "owner": "app", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/carrier_switch.go:76:repository.ClaimExpiredInboundPending", + "kind": "integration_log", + "code_entry": "internal/handler/callback/carrier_switch.go:76 repository.ClaimExpiredInboundPending", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8484,9 +8776,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/cmcc_realname.go:140:h.integration.Complete", + "key": "integration_log:internal/handler/callback/cmcc_realname.go:125:h.integration.RecordInbound", "kind": "integration_log", - "code_entry": "internal/handler/callback/cmcc_realname.go:140 h.integration.Complete", + "code_entry": "internal/handler/callback/cmcc_realname.go:125 h.integration.RecordInbound", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8503,9 +8795,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/cmcc_realname.go:67:h.integration.RecordInbound", + "key": "integration_log:internal/handler/callback/cmcc_realname.go:133:h.integration.ClaimExpiredInboundPending", "kind": "integration_log", - "code_entry": "internal/handler/callback/cmcc_realname.go:67 h.integration.RecordInbound", + "code_entry": "internal/handler/callback/cmcc_realname.go:133 h.integration.ClaimExpiredInboundPending", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8522,9 +8814,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/ctcc_realname.go:156:h.integration.RecordInbound", + "key": "integration_log:internal/handler/callback/cmcc_realname.go:142:h.integration.Complete", "kind": "integration_log", - "code_entry": "internal/handler/callback/ctcc_realname.go:156 h.integration.RecordInbound", + "code_entry": "internal/handler/callback/cmcc_realname.go:142 h.integration.Complete", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8541,9 +8833,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/ctcc_realname.go:183:h.integration.Complete", + "key": "integration_log:internal/handler/callback/cmcc_realname.go:68:h.integration.RecordInbound", "kind": "integration_log", - "code_entry": "internal/handler/callback/ctcc_realname.go:183 h.integration.Complete", + "code_entry": "internal/handler/callback/cmcc_realname.go:68 h.integration.RecordInbound", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8560,9 +8852,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/ctcc_realname.go:90:h.integration.RecordInbound", + "key": "integration_log:internal/handler/callback/cmcc_realname.go:84:h.integration.ClaimExpiredInboundPending", "kind": "integration_log", - "code_entry": "internal/handler/callback/ctcc_realname.go:90 h.integration.RecordInbound", + "code_entry": "internal/handler/callback/cmcc_realname.go:84 h.integration.ClaimExpiredInboundPending", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8579,9 +8871,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/cucc_realname.go:122:h.integration.RecordInbound", + "key": "integration_log:internal/handler/callback/ctcc_realname.go:158:h.integration.RecordInbound", "kind": "integration_log", - "code_entry": "internal/handler/callback/cucc_realname.go:122 h.integration.RecordInbound", + "code_entry": "internal/handler/callback/ctcc_realname.go:158 h.integration.RecordInbound", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8598,9 +8890,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/cucc_realname.go:155:h.integration.Complete", + "key": "integration_log:internal/handler/callback/ctcc_realname.go:181:h.integration.ClaimExpiredInboundPending", "kind": "integration_log", - "code_entry": "internal/handler/callback/cucc_realname.go:155 h.integration.Complete", + "code_entry": "internal/handler/callback/ctcc_realname.go:181 h.integration.ClaimExpiredInboundPending", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8617,9 +8909,104 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/cucc_realname.go:68:h.integration.RecordInbound", + "key": "integration_log:internal/handler/callback/ctcc_realname.go:185:h.integration.Complete", "kind": "integration_log", - "code_entry": "internal/handler/callback/cucc_realname.go:68 h.integration.RecordInbound", + "code_entry": "internal/handler/callback/ctcc_realname.go:185 h.integration.Complete", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/ctcc_realname.go:91:h.integration.RecordInbound", + "kind": "integration_log", + "code_entry": "internal/handler/callback/ctcc_realname.go:91 h.integration.RecordInbound", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/cucc_realname.go:124:h.integration.RecordInbound", + "kind": "integration_log", + "code_entry": "internal/handler/callback/cucc_realname.go:124 h.integration.RecordInbound", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/cucc_realname.go:148:h.integration.ClaimExpiredInboundPending", + "kind": "integration_log", + "code_entry": "internal/handler/callback/cucc_realname.go:148 h.integration.ClaimExpiredInboundPending", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/cucc_realname.go:157:h.integration.Complete", + "kind": "integration_log", + "code_entry": "internal/handler/callback/cucc_realname.go:157 h.integration.Complete", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/cucc_realname.go:69:h.integration.RecordInbound", + "kind": "integration_log", + "code_entry": "internal/handler/callback/cucc_realname.go:69 h.integration.RecordInbound", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8654,6 +9041,25 @@ "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, + { + "key": "integration_log:internal/handler/callback/cucc_realname_removal.go:123:h.integration.ClaimExpiredInboundPending", + "kind": "integration_log", + "code_entry": "internal/handler/callback/cucc_realname_removal.go:123 h.integration.ClaimExpiredInboundPending", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, { "key": "integration_log:internal/handler/callback/cucc_realname_removal.go:127:h.integration.Complete", "kind": "integration_log", @@ -8693,9 +9099,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/payment.go:230:h.integration.RecordInbound", + "key": "integration_log:internal/handler/callback/payment.go:238:h.integration.RecordInbound", "kind": "integration_log", - "code_entry": "internal/handler/callback/payment.go:230 h.integration.RecordInbound", + "code_entry": "internal/handler/callback/payment.go:238 h.integration.RecordInbound", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8712,9 +9118,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/payment.go:260:h.integration.Complete", + "key": "integration_log:internal/handler/callback/payment.go:306:h.integration.Complete", "kind": "integration_log", - "code_entry": "internal/handler/callback/payment.go:260 h.integration.Complete", + "code_entry": "internal/handler/callback/payment.go:306 h.integration.Complete", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8731,9 +9137,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/handler/callback/payment.go:276:h.integration.Complete", + "key": "integration_log:internal/handler/callback/payment.go:328:h.integration.RecordInbound", "kind": "integration_log", - "code_entry": "internal/handler/callback/payment.go:276 h.integration.Complete", + "code_entry": "internal/handler/callback/payment.go:328 h.integration.RecordInbound", "owner": "callback", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8750,9 +9156,66 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:168:r.integration.Complete", + "key": "integration_log:internal/handler/callback/payment.go:364:h.integration.RecordInbound", "kind": "integration_log", - "code_entry": "internal/infrastructure/cardobservation/series_runner.go:168 r.integration.Complete", + "code_entry": "internal/handler/callback/payment.go:364 h.integration.RecordInbound", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/payment.go:374:h.integration.Complete", + "kind": "integration_log", + "code_entry": "internal/handler/callback/payment.go:374 h.integration.Complete", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/handler/callback/payment.go:385:h.integration.Complete", + "kind": "integration_log", + "code_entry": "internal/handler/callback/payment.go:385 h.integration.Complete", + "owner": "callback", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:174:r.integration.Complete", + "kind": "integration_log", + "code_entry": "internal/infrastructure/cardobservation/series_runner.go:174 r.integration.Complete", "owner": "cardobservation", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8769,9 +9232,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:201:r.integration.Complete", + "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:207:r.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/cardobservation/series_runner.go:201 r.integration.Complete", + "code_entry": "internal/infrastructure/cardobservation/series_runner.go:207 r.integration.Complete", "owner": "cardobservation", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8788,9 +9251,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:214:r.integration.Start", + "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:220:r.integration.Start", "kind": "integration_log", - "code_entry": "internal/infrastructure/cardobservation/series_runner.go:214 r.integration.Start", + "code_entry": "internal/infrastructure/cardobservation/series_runner.go:220 r.integration.Start", "owner": "cardobservation", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -8807,9 +9270,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:353:r.integration.Start", + "key": "integration_log:internal/infrastructure/cardobservation/series_runner.go:530:r.integration.Start", "kind": "integration_log", - "code_entry": "internal/infrastructure/cardobservation/series_runner.go:353 r.integration.Start", + "code_entry": "internal/infrastructure/cardobservation/series_runner.go:530 r.integration.Start", "owner": "cardobservation", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9054,9 +9517,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_detail_client.go:125:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_detail_client.go:126:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_detail_client.go:125 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_detail_client.go:126 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9073,9 +9536,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_detail_client.go:56:c.integration.Start", + "key": "integration_log:internal/infrastructure/wecom/approval_detail_client.go:57:c.integration.Start", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_detail_client.go:56 c.integration.Start", + "code_entry": "internal/infrastructure/wecom/approval_detail_client.go:57 c.integration.Start", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9092,9 +9555,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_detail_client.go:91:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_detail_client.go:92:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_detail_client.go:91 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_detail_client.go:92 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9111,9 +9574,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_detail_task.go:114:h.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_detail_task.go:118:h.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_detail_task.go:114 h.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_detail_task.go:118 h.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9130,9 +9593,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_detail_task.go:81:h.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_detail_task.go:82:h.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_detail_task.go:81 h.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_detail_task.go:82 h.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9149,9 +9612,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_info_client.go:111:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_info_client.go:112:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_info_client.go:111 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_info_client.go:112 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9168,9 +9631,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_info_client.go:171:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_info_client.go:172:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_info_client.go:171 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_info_client.go:172 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9187,9 +9650,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_info_client.go:75:c.integration.Start", + "key": "integration_log:internal/infrastructure/wecom/approval_info_client.go:76:c.integration.Start", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_info_client.go:75 c.integration.Start", + "code_entry": "internal/infrastructure/wecom/approval_info_client.go:76 c.integration.Start", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9206,9 +9669,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:109:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:110:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:109 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:110 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9225,9 +9688,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:138:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:139:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:138 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:139 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9244,9 +9707,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:148:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:149:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:148 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:149 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9263,9 +9726,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:80:c.integration.Start", + "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:81:c.integration.Start", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:80 c.integration.Start", + "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:81 c.integration.Start", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9282,9 +9745,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:95:c.integration.Complete", + "key": "integration_log:internal/infrastructure/wecom/approval_submission_client.go:96:c.integration.Complete", "kind": "integration_log", - "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:95 c.integration.Complete", + "code_entry": "internal/infrastructure/wecom/approval_submission_client.go:96 c.integration.Complete", "owner": "wecom", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9509,6 +9972,120 @@ "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, + { + "key": "integration_log:internal/service/client_order/payment_audit.go:52:s.paymentIntegration.Start", + "kind": "integration_log", + "code_entry": "internal/service/client_order/payment_audit.go:52 s.paymentIntegration.Start", + "owner": "client_order", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/service/client_order/payment_audit.go:72:s.paymentIntegration.Complete", + "kind": "integration_log", + "code_entry": "internal/service/client_order/payment_audit.go:72 s.paymentIntegration.Complete", + "owner": "client_order", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/service/device/gateway_audit.go:109:s.gatewayIntegration.Start", + "kind": "integration_log", + "code_entry": "internal/service/device/gateway_audit.go:109 s.gatewayIntegration.Start", + "owner": "device", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/service/device/gateway_audit.go:142:s.gatewayIntegration.Complete", + "kind": "integration_log", + "code_entry": "internal/service/device/gateway_audit.go:142 s.gatewayIntegration.Complete", + "owner": "device", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/service/iot_card/gateway_integration.go:38:s.speedTierIntegration.Start", + "kind": "integration_log", + "code_entry": "internal/service/iot_card/gateway_integration.go:38 s.speedTierIntegration.Start", + "owner": "iot_card", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, + { + "key": "integration_log:internal/service/iot_card/gateway_integration.go:71:s.speedTierIntegration.Complete", + "kind": "integration_log", + "code_entry": "internal/service/iot_card/gateway_integration.go:71 s.speedTierIntegration.Complete", + "owner": "iot_card", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" + }, { "key": "integration_log:internal/service/iot_card/speed_tier.go:45:s.speedTierIntegration.Start", "kind": "integration_log", @@ -9529,9 +10106,9 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "integration_log:internal/service/iot_card/speed_tier.go:82:s.speedTierIntegration.Complete", + "key": "integration_log:internal/service/iot_card/speed_tier.go:81:s.speedTierIntegration.Complete", "kind": "integration_log", - "code_entry": "internal/service/iot_card/speed_tier.go:82 s.speedTierIntegration.Complete", + "code_entry": "internal/service/iot_card/speed_tier.go:81 s.speedTierIntegration.Complete", "owner": "iot_card", "summary": "记录外部交互尝试或终态", "audit_event": "N/A", @@ -9548,369 +10125,351 @@ "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "legacy_writer:internal/service/agent_recharge/service.go:235:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/agent_recharge/service.go:235 s.auditService.LogOperation", - "owner": "agent_recharge", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "agent_recharge.log_operation", - "action_name": "迁移旧审计写入", - "category": "finance", - "risk": "high", - "primary_resource": "agent_recharge", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" - }, - { - "key": "legacy_writer:internal/service/asset/lifecycle_service.go:50:s.assetAuditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/asset/lifecycle_service.go:50 s.assetAuditService.LogOperation", - "owner": "asset", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "asset.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "asset", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" - }, - { - "key": "legacy_writer:internal/service/asset/service.go:1223:s.assetAuditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/asset/service.go:1223 s.assetAuditService.LogOperation", - "owner": "asset", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "asset.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "asset", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" - }, - { - "key": "legacy_writer:internal/service/device/audit.go:28:s.assetAuditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/device/audit.go:28 s.assetAuditService.LogOperation", - "owner": "device", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "device.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "device", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" - }, - { - "key": "legacy_writer:internal/service/device_import/audit.go:31:s.assetAudit.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/device_import/audit.go:31 s.assetAudit.LogOperation", - "owner": "device_import", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "device_import.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "device_import", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" - }, - { - "key": "legacy_writer:internal/service/iot_card/audit.go:28:s.assetAuditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/iot_card/audit.go:28 s.assetAuditService.LogOperation", + "key": "integration_log:internal/service/iot_card/stop_resume_audit.go:151:s.integration.Start", + "kind": "integration_log", + "code_entry": "internal/service/iot_card/stop_resume_audit.go:151 s.integration.Start", "owner": "iot_card", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "iot_card.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "iot_card", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "legacy_writer:internal/service/iot_card/audit.go:42:s.assetAuditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/iot_card/audit.go:42 s.assetAuditService.LogOperation", + "key": "integration_log:internal/service/iot_card/stop_resume_audit.go:184:s.integration.Complete", + "kind": "integration_log", + "code_entry": "internal/service/iot_card/stop_resume_audit.go:184 s.integration.Complete", "owner": "iot_card", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "iot_card.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "iot_card", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "legacy_writer:internal/service/iot_card_import/audit.go:31:s.assetAudit.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/iot_card_import/audit.go:31 s.assetAudit.LogOperation", - "owner": "iot_card_import", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "iot_card_import.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "iot_card_import", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "integration_log:internal/service/order/payment_audit.go:30:s.paymentIntegration.Start", + "kind": "integration_log", + "code_entry": "internal/service/order/payment_audit.go:30 s.paymentIntegration.Start", + "owner": "order", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "legacy_writer:internal/service/polling/asset_polling_service.go:57:s.assetAuditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/polling/asset_polling_service.go:57 s.assetAuditService.LogOperation", - "owner": "polling", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "polling.log_operation", - "action_name": "迁移旧审计写入", - "category": "asset", - "risk": "normal", - "primary_resource": "polling", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "integration_log:internal/service/order/payment_audit.go:53:s.paymentIntegration.Complete", + "kind": "integration_log", + "code_entry": "internal/service/order/payment_audit.go:53 s.paymentIntegration.Complete", + "owner": "order", + "summary": "记录外部交互尝试或终态", + "audit_event": "N/A", + "domain_ledger": "N/A:Integration Log 只记录外部交互事实,不替代内部业务表", + "integration_log": "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要", + "outbox": "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox", + "actor_source": "external_system 或发起外呼的真实 Application/Worker/Callback", + "visibility": "仅平台内部调查完整可见;代理/企业不得读取外部交互细节", + "transaction": "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event", + "failure_strategy": "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功", + "sensitive_policy": "请求、响应和 metadata 写入前删除凭据,历史读取再次清理", + "before_after_policy": "N/A:保存外部尝试结构化摘要和本地状态是否变化", + "test_seam": "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对", + "na_reason": "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event" }, { - "key": "legacy_writer:internal/service/shop_package_batch_allocation/service.go:94:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/shop_package_batch_allocation/service.go:94 s.auditService.LogOperation", - "owner": "shop_package_batch_allocation", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "shop_package_batch_allocation.log_operation", - "action_name": "迁移旧审计写入", - "category": "business", - "risk": "normal", - "primary_resource": "shop_package_batch_allocation", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "outbox_consumer:cmd/worker/main.go:340:constants.OutboxEventTypeApprovalSubmissionRequested", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:340 consumer", + "owner": "constants._outbox_event_type_approval_submission_requested", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeApprovalSubmissionRequested", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" }, { - "key": "legacy_writer:internal/service/wechat_config/service.go:131:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/wechat_config/service.go:131 s.auditService.LogOperation", - "owner": "wechat_config", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "wechat_config.log_operation", - "action_name": "迁移旧审计写入", - "category": "business", - "risk": "high", - "primary_resource": "wechat_config", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "outbox_consumer:cmd/worker/main.go:384:constants.OutboxEventTypeApprovalTerminalDecision", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:384 decisionConsumer", + "owner": "constants._outbox_event_type_approval_terminal_decision", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeApprovalTerminalDecision", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" }, { - "key": "legacy_writer:internal/service/wechat_config/service.go:274:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/wechat_config/service.go:274 s.auditService.LogOperation", - "owner": "wechat_config", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "wechat_config.log_operation", - "action_name": "迁移旧审计写入", - "category": "business", - "risk": "high", - "primary_resource": "wechat_config", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "outbox_consumer:cmd/worker/main.go:453:constants.OutboxEventTypeCardRealnameChanged", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:453 consumer", + "owner": "constants._outbox_event_type_card_realname_changed", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeCardRealnameChanged", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" }, { - "key": "legacy_writer:internal/service/wechat_config/service.go:331:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/wechat_config/service.go:331 s.auditService.LogOperation", - "owner": "wechat_config", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "wechat_config.log_operation", - "action_name": "迁移旧审计写入", - "category": "business", - "risk": "high", - "primary_resource": "wechat_config", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "outbox_consumer:cmd/worker/main.go:463:constants.OutboxEventTypeCardTrafficIncremented", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:463 trafficConsumer", + "owner": "constants._outbox_event_type_card_traffic_incremented", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeCardTrafficIncremented", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" }, { - "key": "legacy_writer:internal/service/wechat_config/service.go:381:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/wechat_config/service.go:381 s.auditService.LogOperation", - "owner": "wechat_config", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "wechat_config.log_operation", - "action_name": "迁移旧审计写入", - "category": "business", - "risk": "high", - "primary_resource": "wechat_config", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "outbox_consumer:cmd/worker/main.go:468:constants.OutboxEventTypeCardNetworkChanged", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:468 networkConsumer", + "owner": "constants._outbox_event_type_card_network_changed", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeCardNetworkChanged", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" }, { - "key": "legacy_writer:internal/service/wechat_config/service.go:416:s.auditService.LogOperation", - "kind": "legacy_writer", - "code_entry": "internal/service/wechat_config/service.go:416 s.auditService.LogOperation", - "owner": "wechat_config", - "summary": "调用旧 Operation Log Writer", - "audit_event": "必须迁移到统一 Audit Event 后停写旧表", - "domain_ledger": "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger", - "integration_log": "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录", - "outbox": "由原完整用例决定,旧 Writer 不得替代 Outbox", - "action_code": "wechat_config.log_operation", - "action_name": "迁移旧审计写入", - "category": "business", - "risk": "high", - "primary_resource": "wechat_config", - "affected_resource": "按原完整业务用例登记实际资源", - "actor_source": "沿用原调用入口真实操作者", - "visibility": "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影", - "transaction": "迁移后关键成功与业务事实同事务,旧异步 Writer 停写", - "failure_strategy": "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine", - "sensitive_policy": "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据", - "before_after_policy": "按资源保存本次直接变化,不复制旧单体 JSON", - "test_seam": "静态调用归零扫描 + 对应业务入口与数据库抽样核对" + "key": "outbox_consumer:cmd/worker/main.go:478:constants.OutboxEventTypeCardSeriesRequested", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:478 seriesConsumer", + "owner": "constants._outbox_event_type_card_series_requested", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeCardSeriesRequested", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" }, { - "key": "schedule:cmd/worker/main.go:680:constants.TaskTypeAgentRechargeRecovery", + "key": "outbox_consumer:cmd/worker/main.go:488:constants.OutboxEventTypeAgentRechargePaymentConfirmed", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:488 agentRechargeConsumer", + "owner": "constants._outbox_event_type_agent_recharge_payment_confirmed", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAgentRechargePaymentConfirmed", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:493:constants.OutboxEventTypeAgentMainWalletDebited", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:493 debitConsumer", + "owner": "constants._outbox_event_type_agent_main_wallet_debited", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAgentMainWalletDebited", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:498:constants.OutboxEventTypeAgentMainWalletReservationChanged", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:498 reservationConsumer", + "owner": "constants._outbox_event_type_agent_main_wallet_reservation_changed", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAgentMainWalletReservationChanged", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:503:constants.OutboxEventTypeAgentMainWalletCredited", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:503 creditConsumer", + "owner": "constants._outbox_event_type_agent_main_wallet_credited", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAgentMainWalletCredited", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:508:constants.OutboxEventTypeAgentMainWalletRefunded", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:508 refundConsumer", + "owner": "constants._outbox_event_type_agent_main_wallet_refunded", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAgentMainWalletRefunded", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:521:constants.OutboxEventTypeAdminDirectNotification", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:521 consumer", + "owner": "constants._outbox_event_type_admin_direct_notification", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAdminDirectNotification", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:525:constants.OutboxEventTypePersonalCustomerDirectNotification", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:525 consumer", + "owner": "constants._outbox_event_type_personal_customer_direct_notification", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypePersonalCustomerDirectNotification", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "outbox_consumer:cmd/worker/main.go:529:constants.OutboxEventTypeAdminDynamicNotification", + "kind": "outbox_consumer", + "code_entry": "cmd/worker/main.go:529 consumer", + "owner": "constants._outbox_event_type_admin_dynamic_notification", + "summary": "注册 Outbox 消费者 constants.OutboxEventTypeAdminDynamicNotification", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "必须:消费已提交的可靠事件", + "actor_source": "system_task/outbox_consumer", + "visibility": "内部系统装配入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + "sensitive_policy": "注册入口不读取或记录事件载荷与安全凭据", + "before_after_policy": "N/A:注册入口不修改业务事实", + "test_seam": "静态扫描注册点、消费者实现和对应业务动作", + "na_reason": "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责" + }, + { + "key": "schedule:cmd/worker/main.go:696:constants.TaskTypeAgentRechargeRecovery", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:680", + "code_entry": "cmd/worker/main.go:696", "owner": "agent_recharge_recovery", "summary": "按 @every 1m 调度 constants.TaskTypeAgentRechargeRecovery", "audit_event": "N/A", @@ -9927,9 +10486,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:690:constants.TaskTypeOrderExpire", + "key": "schedule:cmd/worker/main.go:706:constants.TaskTypeOrderExpire", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:690", + "code_entry": "cmd/worker/main.go:706", "owner": "order_expire", "summary": "按 @every 1m 调度 constants.TaskTypeOrderExpire", "audit_event": "N/A", @@ -9946,9 +10505,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:697:constants.TaskTypeAlertCheck", + "key": "schedule:cmd/worker/main.go:713:constants.TaskTypeAlertCheck", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:697", + "code_entry": "cmd/worker/main.go:713", "owner": "alert_check", "summary": "按 @every 1m 调度 constants.TaskTypeAlertCheck", "audit_event": "N/A", @@ -9965,9 +10524,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:704:constants.TaskTypeWeComApprovalRecovery", + "key": "schedule:cmd/worker/main.go:720:constants.TaskTypeWeComApprovalRecovery", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:704", + "code_entry": "cmd/worker/main.go:720", "owner": "we_com_approval_recovery", "summary": "按 @every 2m 调度 constants.TaskTypeWeComApprovalRecovery", "audit_event": "N/A", @@ -9984,9 +10543,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:713:constants.TaskTypeDataCleanup", + "key": "schedule:cmd/worker/main.go:729:constants.TaskTypeDataCleanup", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:713", + "code_entry": "cmd/worker/main.go:729", "owner": "data_cleanup", "summary": "按 0 2 * * * 调度 constants.TaskTypeDataCleanup", "audit_event": "N/A", @@ -10003,9 +10562,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:720:constants.TaskTypeNotificationCleanup", + "key": "schedule:cmd/worker/main.go:736:constants.TaskTypeNotificationCleanup", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:720", + "code_entry": "cmd/worker/main.go:736", "owner": "notification_cleanup", "summary": "按 15 2 * * * 调度 constants.TaskTypeNotificationCleanup", "audit_event": "N/A", @@ -10022,9 +10581,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:728:constants.TaskTypePackageExpiryReminder", + "key": "schedule:cmd/worker/main.go:744:constants.TaskTypePackageExpiryReminder", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:728", + "code_entry": "cmd/worker/main.go:744", "owner": "package_expiry_reminder", "summary": "按 CRON_TZ=Asia/Shanghai 0 3 * * * 调度 constants.TaskTypePackageExpiryReminder", "audit_event": "N/A", @@ -10041,9 +10600,9 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "schedule:cmd/worker/main.go:737:constants.TaskTypeDailyTrafficFlush", + "key": "schedule:cmd/worker/main.go:753:constants.TaskTypeDailyTrafficFlush", "kind": "scheduled_job", - "code_entry": "cmd/worker/main.go:737", + "code_entry": "cmd/worker/main.go:753", "owner": "daily_traffic_flush", "summary": "按 0 2 * * * 调度 constants.TaskTypeDailyTrafficFlush", "audit_event": "N/A", @@ -10060,177 +10619,85 @@ "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "service:internal/service/account/service.go:198:BindWeCom", - "kind": "service", - "code_entry": "internal/service/account/service.go:198 BindWeCom", - "owner": "account", - "summary": "业务方法 BindWeCom", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.bind_we_com", - "action_name": "执行业务方法(BindWeCom)", - "category": "security", - "risk": "normal", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "key": "schedule:cmd/worker/main.go:765:constants.TaskTypeAuditDailyArchive", + "kind": "scheduled_job", + "code_entry": "cmd/worker/main.go:765", + "owner": "audit_daily_archive", + "summary": "按 CRON_TZ=Asia/Shanghai 0 4 * * * 调度 constants.TaskTypeAuditDailyArchive", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "N/A", + "actor_source": "system_task/scheduled_job", + "visibility": "内部系统入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "调度注册失败阻止 Worker 启动;执行结果由对应 Worker 入口负责", + "sensitive_policy": "调度日志仅记录任务类型与安全时间信息", + "before_after_policy": "N/A:调度入口不修改业务事实", + "test_seam": "调度注册公开函数 + 覆盖门禁静态比对本入口", + "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "service:internal/service/account/service.go:268:Update", - "kind": "service", - "code_entry": "internal/service/account/service.go:268 Update", - "owner": "account", - "summary": "业务方法 Update", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.update", - "action_name": "执行业务方法(Update)", - "category": "security", - "risk": "normal", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "key": "schedule:cmd/worker/main.go:775:constants.TaskTypeIntegrationDailyArchive", + "kind": "scheduled_job", + "code_entry": "cmd/worker/main.go:775", + "owner": "integration_daily_archive", + "summary": "按 CRON_TZ=Asia/Shanghai 30 4 * * * 调度 constants.TaskTypeIntegrationDailyArchive", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "N/A", + "actor_source": "system_task/scheduled_job", + "visibility": "内部系统入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "调度注册失败阻止 Worker 启动;执行结果由对应 Worker 入口负责", + "sensitive_policy": "调度日志仅记录任务类型与安全时间信息", + "before_after_policy": "N/A:调度入口不修改业务事实", + "test_seam": "调度注册公开函数 + 覆盖门禁静态比对本入口", + "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "service:internal/service/account/service.go:365:Delete", - "kind": "service", - "code_entry": "internal/service/account/service.go:365 Delete", - "owner": "account", - "summary": "业务方法 Delete", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.delete", - "action_name": "执行业务方法(Delete)", - "category": "security", - "risk": "normal", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "key": "schedule:cmd/worker/main.go:785:constants.TaskTypeIntegrationMonthlyFinalize", + "kind": "scheduled_job", + "code_entry": "cmd/worker/main.go:785", + "owner": "integration_monthly_finalize", + "summary": "按 CRON_TZ=Asia/Shanghai 0 5 1 * * 调度 constants.TaskTypeIntegrationMonthlyFinalize", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "N/A", + "actor_source": "system_task/scheduled_job", + "visibility": "内部系统入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "调度注册失败阻止 Worker 启动;执行结果由对应 Worker 入口负责", + "sensitive_policy": "调度日志仅记录任务类型与安全时间信息", + "before_after_policy": "N/A:调度入口不修改业务事实", + "test_seam": "调度注册公开函数 + 覆盖门禁静态比对本入口", + "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "service:internal/service/account/service.go:465:AssignRoles", - "kind": "service", - "code_entry": "internal/service/account/service.go:465 AssignRoles", - "owner": "account", - "summary": "业务方法 AssignRoles", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.assign_roles", - "action_name": "为账号分配角色", - "category": "security", - "risk": "high", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "key": "schedule:cmd/worker/main.go:795:constants.TaskTypeAuditMonthlyRetention", + "kind": "scheduled_job", + "code_entry": "cmd/worker/main.go:795", + "owner": "audit_monthly_retention", + "summary": "按 CRON_TZ=Asia/Shanghai 0 6 1 * * 调度 constants.TaskTypeAuditMonthlyRetention", + "audit_event": "N/A", + "domain_ledger": "N/A", + "integration_log": "N/A", + "outbox": "N/A", + "actor_source": "system_task/scheduled_job", + "visibility": "内部系统入口,不直接对用户展示", + "transaction": "N/A", + "failure_strategy": "调度注册失败阻止 Worker 启动;执行结果由对应 Worker 入口负责", + "sensitive_policy": "调度日志仅记录任务类型与安全时间信息", + "before_after_policy": "N/A:调度入口不修改业务事实", + "test_seam": "调度注册公开函数 + 覆盖门禁静态比对本入口", + "na_reason": "本入口只产生调度信号,不直接读取或修改业务事实;审计责任位于对应 Worker" }, { - "key": "service:internal/service/account/service.go:616:RemoveRole", + "key": "service:internal/service/account/service.go:102:Create", "kind": "service", - "code_entry": "internal/service/account/service.go:616 RemoveRole", - "owner": "account", - "summary": "业务方法 RemoveRole", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.remove_role", - "action_name": "移除账号角色", - "category": "security", - "risk": "high", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/account/service.go:684:UpdatePassword", - "kind": "service", - "code_entry": "internal/service/account/service.go:684 UpdatePassword", - "owner": "account", - "summary": "业务方法 UpdatePassword", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.update_password", - "action_name": "执行业务方法(UpdatePassword)", - "category": "security", - "risk": "high", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/account/service.go:766:UpdateStatus", - "kind": "service", - "code_entry": "internal/service/account/service.go:766 UpdateStatus", - "owner": "account", - "summary": "业务方法 UpdateStatus", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "account.update_status", - "action_name": "执行业务方法(UpdateStatus)", - "category": "security", - "risk": "normal", - "primary_resource": "account", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/account/service.go:87:Create", - "kind": "service", - "code_entry": "internal/service/account/service.go:87 Create", + "code_entry": "internal/service/account/service.go:102 Create", "owner": "account", "summary": "业务方法 Create", "audit_event": "必须", @@ -10249,12 +10716,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/account/service.go:919:CreateSystemAccount", + "key": "service:internal/service/account/service.go:1085:CreateSystemAccount", "kind": "service", - "code_entry": "internal/service/account/service.go:919 CreateSystemAccount", + "code_entry": "internal/service/account/service.go:1085 CreateSystemAccount", "owner": "account", "summary": "业务方法 CreateSystemAccount", "audit_event": "必须", @@ -10273,7 +10740,175 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:212:BindWeCom", + "kind": "service", + "code_entry": "internal/service/account/service.go:212 BindWeCom", + "owner": "account", + "summary": "业务方法 BindWeCom", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.bind_we_com", + "action_name": "执行业务方法(BindWeCom)", + "category": "security", + "risk": "normal", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:284:Update", + "kind": "service", + "code_entry": "internal/service/account/service.go:284 Update", + "owner": "account", + "summary": "业务方法 Update", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.update", + "action_name": "执行业务方法(Update)", + "category": "security", + "risk": "normal", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:384:Delete", + "kind": "service", + "code_entry": "internal/service/account/service.go:384 Delete", + "owner": "account", + "summary": "业务方法 Delete", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.delete", + "action_name": "执行业务方法(Delete)", + "category": "security", + "risk": "normal", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:484:AssignRoles", + "kind": "service", + "code_entry": "internal/service/account/service.go:484 AssignRoles", + "owner": "account", + "summary": "业务方法 AssignRoles", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.assign_roles", + "action_name": "执行业务方法(AssignRoles)", + "category": "security", + "risk": "high", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:553:RemoveRole", + "kind": "service", + "code_entry": "internal/service/account/service.go:553 RemoveRole", + "owner": "account", + "summary": "业务方法 RemoveRole", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.remove_role", + "action_name": "执行业务方法(RemoveRole)", + "category": "security", + "risk": "high", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:840:UpdatePassword", + "kind": "service", + "code_entry": "internal/service/account/service.go:840 UpdatePassword", + "owner": "account", + "summary": "业务方法 UpdatePassword", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.update_password", + "action_name": "执行业务方法(UpdatePassword)", + "category": "security", + "risk": "high", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/account/service.go:932:UpdateStatus", + "kind": "service", + "code_entry": "internal/service/account/service.go:932 UpdateStatus", + "owner": "account", + "summary": "业务方法 UpdateStatus", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "account.update_status", + "action_name": "执行业务方法(UpdateStatus)", + "category": "security", + "risk": "normal", + "primary_resource": "account", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/agent_open_api/service.go:218:ResumeCard", @@ -10297,7 +10932,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/agent_open_api/service.go:348:CreateWalletPackageOrders", @@ -10321,7 +10956,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/agent_open_api/service.go:558:ResetDevice", @@ -10345,12 +10980,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/agent_recharge/service.go:273:HandlePaymentCallback", + "key": "service:internal/service/agent_recharge/service.go:260:HandlePaymentCallback", "kind": "service", - "code_entry": "internal/service/agent_recharge/service.go:273 HandlePaymentCallback", + "code_entry": "internal/service/agent_recharge/service.go:260 HandlePaymentCallback", "owner": "agent_recharge", "summary": "业务方法 HandlePaymentCallback", "audit_event": "必须", @@ -10369,12 +11004,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/agent_recharge/service.go:378:Reject", + "key": "service:internal/service/agent_recharge/service.go:365:Reject", "kind": "service", - "code_entry": "internal/service/agent_recharge/service.go:378 Reject", + "code_entry": "internal/service/agent_recharge/service.go:365 Reject", "owner": "agent_recharge", "summary": "业务方法 Reject", "audit_event": "必须", @@ -10393,12 +11028,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/agent_recharge/service.go:95:Create", + "key": "service:internal/service/agent_recharge/service.go:93:Create", "kind": "service", - "code_entry": "internal/service/agent_recharge/service.go:95 Create", + "code_entry": "internal/service/agent_recharge/service.go:93 Create", "owner": "agent_recharge", "summary": "业务方法 Create", "audit_event": "必须", @@ -10417,36 +11052,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/asset/lifecycle_service.go:162:DeactivateDevice", + "key": "service:internal/service/asset/lifecycle_service.go:35:DeactivateIotCard", "kind": "service", - "code_entry": "internal/service/asset/lifecycle_service.go:162 DeactivateDevice", - "owner": "asset", - "summary": "业务方法 DeactivateDevice", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "asset.deactivate_device", - "action_name": "执行业务方法(DeactivateDevice)", - "category": "asset", - "risk": "normal", - "primary_resource": "asset", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/asset/lifecycle_service.go:54:DeactivateIotCard", - "kind": "service", - "code_entry": "internal/service/asset/lifecycle_service.go:54 DeactivateIotCard", + "code_entry": "internal/service/asset/lifecycle_service.go:35 DeactivateIotCard", "owner": "asset", "summary": "业务方法 DeactivateIotCard", "audit_event": "必须", @@ -10465,12 +11076,36 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/asset/service.go:108:Resolve", + "key": "service:internal/service/asset/lifecycle_service.go:72:DeactivateDevice", "kind": "service", - "code_entry": "internal/service/asset/service.go:108 Resolve", + "code_entry": "internal/service/asset/lifecycle_service.go:72 DeactivateDevice", + "owner": "asset", + "summary": "业务方法 DeactivateDevice", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "asset.deactivate_device", + "action_name": "执行业务方法(DeactivateDevice)", + "category": "asset", + "risk": "normal", + "primary_resource": "asset", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/asset/service.go:111:Resolve", + "kind": "service", + "code_entry": "internal/service/asset/service.go:111 Resolve", "owner": "asset", "summary": "业务方法 Resolve", "audit_event": "必须", @@ -10489,12 +11124,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/asset/service.go:609:Refresh", + "key": "service:internal/service/asset/service.go:612:Refresh", "kind": "service", - "code_entry": "internal/service/asset/service.go:609 Refresh", + "code_entry": "internal/service/asset/service.go:612 Refresh", "owner": "asset", "summary": "业务方法 Refresh", "audit_event": "必须", @@ -10513,12 +11148,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/asset/service.go:903:UpdatePackageExpiresAt", + "key": "service:internal/service/asset/service.go:906:UpdatePackageExpiresAt", "kind": "service", - "code_entry": "internal/service/asset/service.go:903 UpdatePackageExpiresAt", + "code_entry": "internal/service/asset/service.go:906 UpdatePackageExpiresAt", "owner": "asset", "summary": "业务方法 UpdatePackageExpiresAt", "audit_event": "必须", @@ -10537,12 +11172,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/asset/service.go:948:UpdatePackageUsage", + "key": "service:internal/service/asset/service.go:942:UpdatePackageUsage", "kind": "service", - "code_entry": "internal/service/asset/service.go:948 UpdatePackageUsage", + "code_entry": "internal/service/asset/service.go:942 UpdatePackageUsage", "owner": "asset", "summary": "业务方法 UpdatePackageUsage", "audit_event": "必须", @@ -10561,12 +11196,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/asset_package_batch_order/service.go:42:Create", + "key": "service:internal/service/asset_package_batch_order/service.go:50:Create", "kind": "service", - "code_entry": "internal/service/asset_package_batch_order/service.go:42 Create", + "code_entry": "internal/service/asset_package_batch_order/service.go:50 Create", "owner": "asset_package_batch_order", "summary": "业务方法 Create", "audit_event": "必须", @@ -10585,12 +11220,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/auth/service.go:170:Logout", + "key": "service:internal/service/auth/service.go:168:Logout", "kind": "service", - "code_entry": "internal/service/auth/service.go:170 Logout", + "code_entry": "internal/service/auth/service.go:168 Logout", "owner": "auth", "summary": "业务方法 Logout", "audit_event": "必须", @@ -10609,12 +11244,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/auth/service.go:197:RefreshToken", + "key": "service:internal/service/auth/service.go:194:RefreshToken", "kind": "service", - "code_entry": "internal/service/auth/service.go:197 RefreshToken", + "code_entry": "internal/service/auth/service.go:194 RefreshToken", "owner": "auth", "summary": "业务方法 RefreshToken", "audit_event": "必须", @@ -10633,12 +11268,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/auth/service.go:243:ChangePassword", + "key": "service:internal/service/auth/service.go:218:ChangePassword", "kind": "service", - "code_entry": "internal/service/auth/service.go:243 ChangePassword", + "code_entry": "internal/service/auth/service.go:218 ChangePassword", "owner": "auth", "summary": "业务方法 ChangePassword", "audit_event": "必须", @@ -10657,7 +11292,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/auth/service.go:60:Login", @@ -10681,12 +11316,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/carrier/service.go:115:Delete", + "key": "service:internal/service/carrier/service.go:139:Delete", "kind": "service", - "code_entry": "internal/service/carrier/service.go:115 Delete", + "code_entry": "internal/service/carrier/service.go:139 Delete", "owner": "carrier", "summary": "业务方法 Delete", "audit_event": "必须", @@ -10705,12 +11340,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/carrier/service.go:168:UpdateStatus", + "key": "service:internal/service/carrier/service.go:202:UpdateStatus", "kind": "service", - "code_entry": "internal/service/carrier/service.go:168 UpdateStatus", + "code_entry": "internal/service/carrier/service.go:202 UpdateStatus", "owner": "carrier", "summary": "业务方法 UpdateStatus", "audit_event": "必须", @@ -10729,12 +11364,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/carrier/service.go:26:Create", + "key": "service:internal/service/carrier/service.go:30:Create", "kind": "service", - "code_entry": "internal/service/carrier/service.go:26 Create", + "code_entry": "internal/service/carrier/service.go:30 Create", "owner": "carrier", "summary": "业务方法 Create", "audit_event": "必须", @@ -10753,12 +11388,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/carrier/service.go:74:Update", + "key": "service:internal/service/carrier/service.go:89:Update", "kind": "service", - "code_entry": "internal/service/carrier/service.go:74 Update", + "code_entry": "internal/service/carrier/service.go:89 Update", "owner": "carrier", "summary": "业务方法 Update", "audit_event": "必须", @@ -10777,79 +11412,84 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/client_auth/service.go:268:SendCode", + "key": "service:internal/service/client_auth/service.go:274:SendCode", "kind": "service", - "code_entry": "internal/service/client_auth/service.go:268 SendCode", + "code_entry": "internal/service/client_auth/service.go:274 SendCode", "owner": "client_auth", "summary": "业务方法 SendCode", - "audit_event": "N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "actor_source": "未认证个人入口/personal_api", - "visibility": "验证码不进入 Audit Event", - "transaction": "N/A", - "failure_strategy": "验证码发送和限流进入 Access/Integration/Security 事实", + "action_code": "client_auth.send_code", + "action_name": "执行业务方法(SendCode)", + "category": "security", + "risk": "normal", + "primary_resource": "client_auth", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", - "na_reason": "验证码是禁止写入审计的短期安全凭据,不属于个人身份资料变化" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/client_auth/service.go:292:BindPhone", + "key": "service:internal/service/client_auth/service.go:298:BindPhone", "kind": "service", - "code_entry": "internal/service/client_auth/service.go:292 BindPhone", + "code_entry": "internal/service/client_auth/service.go:298 BindPhone", "owner": "client_auth", "summary": "业务方法 BindPhone", "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "personal_customer.bind_phone", - "action_name": "绑定个人手机号", - "category": "identity", + "action_code": "client_auth.bind_phone", + "action_name": "执行业务方法(BindPhone)", + "category": "security", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "personal_customer_phone", - "actor_source": "personal_customer/personal_api", - "visibility": "subject_detail,仅返回 Registry 白名单 phone 字段", + "primary_resource": "client_auth", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/client_auth/service.go:335:ChangePhone", + "key": "service:internal/service/client_auth/service.go:385:ChangePhone", "kind": "service", - "code_entry": "internal/service/client_auth/service.go:335 ChangePhone", + "code_entry": "internal/service/client_auth/service.go:385 ChangePhone", "owner": "client_auth", "summary": "业务方法 ChangePhone", "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "personal_customer.change_phone", - "action_name": "更换个人手机号", - "category": "identity", + "action_code": "client_auth.change_phone", + "action_name": "执行业务方法(ChangePhone)", + "category": "security", "risk": "normal", - "primary_resource": "personal_customer", - "affected_resource": "personal_customer_phone", - "actor_source": "personal_customer/personal_api", - "visibility": "subject_detail,仅返回 Registry 白名单 phone 字段", + "primary_resource": "client_auth", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/client_auth/service.go:383:Logout", + "key": "service:internal/service/client_auth/service.go:528:Logout", "kind": "service", - "code_entry": "internal/service/client_auth/service.go:383 Logout", + "code_entry": "internal/service/client_auth/service.go:528 Logout", "owner": "client_auth", "summary": "业务方法 Logout", "audit_event": "必须", @@ -10868,7 +11508,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/client_order/payment_provider.go:39:CreateJSAPIPayment", @@ -10892,7 +11532,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/client_order/payment_provider.go:53:CreateJSAPIPayment", @@ -10916,7 +11556,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/client_order/payment_provider.go:69:CreateJSAPIPayment", @@ -10940,12 +11580,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/client_order/service.go:1293:PayOrder", + "key": "service:internal/service/client_order/service.go:1352:PayOrder", "kind": "service", - "code_entry": "internal/service/client_order/service.go:1293 PayOrder", + "code_entry": "internal/service/client_order/service.go:1352 PayOrder", "owner": "client_order", "summary": "业务方法 PayOrder", "audit_event": "必须", @@ -10964,12 +11604,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/client_order/service.go:134:CreateOrder", + "key": "service:internal/service/client_order/service.go:146:CreateOrder", "kind": "service", - "code_entry": "internal/service/client_order/service.go:134 CreateOrder", + "code_entry": "internal/service/client_order/service.go:146 CreateOrder", "owner": "client_order", "summary": "业务方法 CreateOrder", "audit_event": "必须", @@ -10988,12 +11628,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/commission_calculation/service.go:334:TriggerOneTimeCommissionForCard", + "key": "service:internal/service/commission_calculation/service.go:347:TriggerOneTimeCommissionForCard", "kind": "service", - "code_entry": "internal/service/commission_calculation/service.go:334 TriggerOneTimeCommissionForCard", + "code_entry": "internal/service/commission_calculation/service.go:347 TriggerOneTimeCommissionForCard", "owner": "commission_calculation", "summary": "业务方法 TriggerOneTimeCommissionForCard", "audit_event": "必须", @@ -11012,12 +11652,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/commission_calculation/service.go:413:TriggerOneTimeCommissionForDevice", + "key": "service:internal/service/commission_calculation/service.go:426:TriggerOneTimeCommissionForDevice", "kind": "service", - "code_entry": "internal/service/commission_calculation/service.go:413 TriggerOneTimeCommissionForDevice", + "code_entry": "internal/service/commission_calculation/service.go:426 TriggerOneTimeCommissionForDevice", "owner": "commission_calculation", "summary": "业务方法 TriggerOneTimeCommissionForDevice", "audit_event": "必须", @@ -11036,12 +11676,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/commission_calculation/service.go:750:CreditCommission", + "key": "service:internal/service/commission_calculation/service.go:763:CreditCommission", "kind": "service", - "code_entry": "internal/service/commission_calculation/service.go:750 CreditCommission", + "code_entry": "internal/service/commission_calculation/service.go:763 CreditCommission", "owner": "commission_calculation", "summary": "业务方法 CreditCommission", "audit_event": "必须", @@ -11060,7 +11700,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/commission_stats/service.go:37:UpdateStats", @@ -11084,7 +11724,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/commission_stats/service.go:64:ArchiveCompletedPeriod", @@ -11108,12 +11748,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/commission_withdrawal/service.go:145:Approve", + "key": "service:internal/service/commission_withdrawal/service.go:152:Approve", "kind": "service", - "code_entry": "internal/service/commission_withdrawal/service.go:145 Approve", + "code_entry": "internal/service/commission_withdrawal/service.go:152 Approve", "owner": "commission_withdrawal", "summary": "业务方法 Approve", "audit_event": "必须", @@ -11132,12 +11772,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/commission_withdrawal/service.go:258:Reject", + "key": "service:internal/service/commission_withdrawal/service.go:272:Reject", "kind": "service", - "code_entry": "internal/service/commission_withdrawal/service.go:258 Reject", + "code_entry": "internal/service/commission_withdrawal/service.go:272 Reject", "owner": "commission_withdrawal", "summary": "业务方法 Reject", "audit_event": "必须", @@ -11156,7 +11796,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/commission_withdrawal_setting/service.go:35:Create", @@ -11180,12 +11820,36 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/customer_binding/service.go:218:Bind", + "key": "service:internal/service/customer_binding/audit.go:152:UnbindByVirtualNo", "kind": "service", - "code_entry": "internal/service/customer_binding/service.go:218 Bind", + "code_entry": "internal/service/customer_binding/audit.go:152 UnbindByVirtualNo", + "owner": "customer_binding", + "summary": "业务方法 UnbindByVirtualNo", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "customer_binding.unbind_by_virtual_no", + "action_name": "执行业务方法(UnbindByVirtualNo)", + "category": "business", + "risk": "normal", + "primary_resource": "customer_binding", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/customer_binding/service.go:220:Bind", + "kind": "service", + "code_entry": "internal/service/customer_binding/service.go:220 Bind", "owner": "customer_binding", "summary": "业务方法 Bind", "audit_event": "必须", @@ -11204,12 +11868,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/customer_binding/service.go:321:Migrate", + "key": "service:internal/service/customer_binding/service.go:354:Migrate", "kind": "service", - "code_entry": "internal/service/customer_binding/service.go:321 Migrate", + "code_entry": "internal/service/customer_binding/service.go:354 Migrate", "owner": "customer_binding", "summary": "业务方法 Migrate", "audit_event": "必须", @@ -11228,12 +11892,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device/binding.go:348:UnbindCard", + "key": "service:internal/service/device/binding.go:186:UnbindCard", "kind": "service", - "code_entry": "internal/service/device/binding.go:348 UnbindCard", + "code_entry": "internal/service/device/binding.go:186 UnbindCard", "owner": "device", "summary": "业务方法 UnbindCard", "audit_event": "必须", @@ -11252,12 +11916,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device/binding.go:79:BindCard", + "key": "service:internal/service/device/binding.go:80:BindCard", "kind": "service", - "code_entry": "internal/service/device/binding.go:79 BindCard", + "code_entry": "internal/service/device/binding.go:80 BindCard", "owner": "device", "summary": "业务方法 BindCard", "audit_event": "必须", @@ -11276,7 +11940,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/device/realname_policy_batch.go:16:BatchUpdateRealnamePolicy", @@ -11300,36 +11964,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device/service.go:1190:BatchSetSeriesBinding", + "key": "service:internal/service/device/service.go:1338:StopDevice", "kind": "service", - "code_entry": "internal/service/device/service.go:1190 BatchSetSeriesBinding", - "owner": "device", - "summary": "业务方法 BatchSetSeriesBinding", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "device.batch_set_series_binding", - "action_name": "执行业务方法(BatchSetSeriesBinding)", - "category": "asset", - "risk": "normal", - "primary_resource": "device", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/device/service.go:1613:StopDevice", - "kind": "service", - "code_entry": "internal/service/device/service.go:1613 StopDevice", + "code_entry": "internal/service/device/service.go:1338 StopDevice", "owner": "device", "summary": "业务方法 StopDevice", "audit_event": "必须", @@ -11348,12 +11988,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device/service.go:2082:UpdateRealnamePolicy", + "key": "service:internal/service/device/service.go:1655:UpdateRealnamePolicy", "kind": "service", - "code_entry": "internal/service/device/service.go:2082 UpdateRealnamePolicy", + "code_entry": "internal/service/device/service.go:1655 UpdateRealnamePolicy", "owner": "device", "summary": "业务方法 UpdateRealnamePolicy", "audit_event": "必须", @@ -11372,7 +12012,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/device/service.go:416:Delete", @@ -11396,12 +12036,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device/service.go:530:AllocateDevices", + "key": "service:internal/service/device/service.go:479:AllocateDevices", "kind": "service", - "code_entry": "internal/service/device/service.go:530 AllocateDevices", + "code_entry": "internal/service/device/service.go:479 AllocateDevices", "owner": "device", "summary": "业务方法 AllocateDevices", "audit_event": "必须", @@ -11420,12 +12060,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device/service.go:739:RecallDevices", + "key": "service:internal/service/device/service.go:614:RecallDevices", "kind": "service", - "code_entry": "internal/service/device/service.go:739 RecallDevices", + "code_entry": "internal/service/device/service.go:614 RecallDevices", "owner": "device", "summary": "业务方法 RecallDevices", "audit_event": "必须", @@ -11444,12 +12084,36 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device_import/service.go:100:CreateBatchAllocationTask", + "key": "service:internal/service/device/service.go:985:BatchSetSeriesBinding", "kind": "service", - "code_entry": "internal/service/device_import/service.go:100 CreateBatchAllocationTask", + "code_entry": "internal/service/device/service.go:985 BatchSetSeriesBinding", + "owner": "device", + "summary": "业务方法 BatchSetSeriesBinding", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "device.batch_set_series_binding", + "action_name": "执行业务方法(BatchSetSeriesBinding)", + "category": "asset", + "risk": "normal", + "primary_resource": "device", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/device_import/service.go:109:CreateBatchAllocationTask", + "kind": "service", + "code_entry": "internal/service/device_import/service.go:109 CreateBatchAllocationTask", "owner": "device_import", "summary": "业务方法 CreateBatchAllocationTask", "audit_event": "必须", @@ -11468,12 +12132,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/device_import/service.go:46:CreateImportTask", + "key": "service:internal/service/device_import/service.go:50:CreateImportTask", "kind": "service", - "code_entry": "internal/service/device_import/service.go:46 CreateImportTask", + "code_entry": "internal/service/device_import/service.go:50 CreateImportTask", "owner": "device_import", "summary": "业务方法 CreateImportTask", "audit_event": "必须", @@ -11492,7 +12156,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/email/service.go:32:SendWelcomeEmail", @@ -11516,7 +12180,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/email/service.go:66:SendPasswordResetEmail", @@ -11540,7 +12204,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/email/service.go:98:SendNotificationEmail", @@ -11564,12 +12228,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/enterprise/service.go:189:Update", + "key": "service:internal/service/enterprise/service.go:182:Update", "kind": "service", - "code_entry": "internal/service/enterprise/service.go:189 Update", + "code_entry": "internal/service/enterprise/service.go:182 Update", "owner": "enterprise", "summary": "业务方法 Update", "audit_event": "必须", @@ -11581,19 +12245,19 @@ "category": "business", "risk": "normal", "primary_resource": "enterprise", - "affected_resource": "归属店铺作为 reference;不改变 owner_shop_id", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/enterprise/service.go:250:UpdateStatus", + "key": "service:internal/service/enterprise/service.go:243:UpdateStatus", "kind": "service", - "code_entry": "internal/service/enterprise/service.go:250 UpdateStatus", + "code_entry": "internal/service/enterprise/service.go:243 UpdateStatus", "owner": "enterprise", "summary": "业务方法 UpdateStatus", "audit_event": "必须", @@ -11605,19 +12269,19 @@ "category": "business", "risk": "normal", "primary_resource": "enterprise", - "affected_resource": "实际同步状态的企业账号;归属店铺作为 reference", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/enterprise/service.go:318:UpdatePassword", + "key": "service:internal/service/enterprise/service.go:311:UpdatePassword", "kind": "service", - "code_entry": "internal/service/enterprise/service.go:318 UpdatePassword", + "code_entry": "internal/service/enterprise/service.go:311 UpdatePassword", "owner": "enterprise", "summary": "业务方法 UpdatePassword", "audit_event": "必须", @@ -11629,19 +12293,19 @@ "category": "business", "risk": "high", "primary_resource": "enterprise", - "affected_resource": "实际改密的企业账号;归属店铺作为 reference", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "密码明文与散列均禁止进入审计;只保存 credentials_configured/state 安全事实", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/enterprise/service.go:52:Create", + "key": "service:internal/service/enterprise/service.go:45:Create", "kind": "service", - "code_entry": "internal/service/enterprise/service.go:52 Create", + "code_entry": "internal/service/enterprise/service.go:45 Create", "owner": "enterprise", "summary": "业务方法 Create", "audit_event": "必须", @@ -11653,14 +12317,14 @@ "category": "business", "risk": "normal", "primary_resource": "enterprise", - "affected_resource": "企业初始账号;归属店铺作为 reference", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_card/authorization_service.go:154:RevokeAuthorizations", @@ -11668,19 +12332,23 @@ "code_entry": "internal/service/enterprise_card/authorization_service.go:154 RevokeAuthorizations", "owner": "enterprise_card", "summary": "业务方法 RevokeAuthorizations", - "audit_event": "N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "N/A:无生产调用方", - "affected_resource": "N/A:遗留未调用方法", - "actor_source": "N/A:无生产调用入口", - "visibility": "N/A:无生产调用入口", - "transaction": "N/A:无生产调用方", - "failure_strategy": "N/A:无生产调用方", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "enterprise_card.revoke_authorizations", + "action_name": "执行业务方法(RevokeAuthorizations)", + "category": "asset", + "risk": "normal", + "primary_resource": "enterprise_card", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "静态扫描生产调用关系", - "na_reason": "无生产调用方,仅为遗留未调用方法;生产回收统一由 Service.RecallCards 承担" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_card/authorization_service.go:409:UpdateRecordRemark", @@ -11693,18 +12361,18 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "enterprise_card.update_record_remark", - "action_name": "更新企业卡授权备注", + "action_name": "执行业务方法(UpdateRecordRemark)", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "企业卡授权记录;IoT 卡与 owner shop 作为 reference", + "primary_resource": "enterprise_card", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "授权记录与备注仅 internal_only", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_card/authorization_service.go:54:BatchAuthorize", @@ -11712,19 +12380,23 @@ "code_entry": "internal/service/enterprise_card/authorization_service.go:54 BatchAuthorize", "owner": "enterprise_card", "summary": "业务方法 BatchAuthorize", - "audit_event": "N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "N/A:无生产调用方", - "affected_resource": "N/A:遗留未调用方法", - "actor_source": "N/A:无生产调用入口", - "visibility": "N/A:无生产调用入口", - "transaction": "N/A:无生产调用方", - "failure_strategy": "N/A:无生产调用方", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "enterprise_card.batch_authorize", + "action_name": "执行业务方法(BatchAuthorize)", + "category": "asset", + "risk": "normal", + "primary_resource": "enterprise_card", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "静态扫描生产调用关系", - "na_reason": "无生产调用方,仅为遗留未调用方法;生产授权统一由 Service.AllocateCards 承担" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_card/service.go:207:AllocateCards", @@ -11737,18 +12409,18 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "enterprise_card.allocate_cards", - "action_name": "向企业授权卡", + "action_name": "执行业务方法(AllocateCards)", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "实际新增授权的 IoT 卡和企业卡授权记录;owner shop 作为 reference", + "primary_resource": "enterprise_card", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "IoT 卡保存 subject_result;授权记录 internal_only", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_card/service.go:344:RecallCards", @@ -11761,18 +12433,18 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "enterprise_card.recall_cards", - "action_name": "回收企业卡授权", + "action_name": "执行业务方法(RecallCards)", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "实际撤销授权的 IoT 卡和企业卡授权记录;owner shop 作为 reference", + "primary_resource": "enterprise_card", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "IoT 卡保存 subject_result;授权记录 internal_only", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_card/service.go:43:AllocateCardsPreview", @@ -11780,24 +12452,28 @@ "code_entry": "internal/service/enterprise_card/service.go:43 AllocateCardsPreview", "owner": "enterprise_card", "summary": "业务方法 AllocateCardsPreview", - "audit_event": "N/A", + "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "N/A:纯读预览不产生可靠副作用", - "affected_resource": "N/A:仅读取企业和卡的当前状态", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "enterprise_card.allocate_cards_preview", + "action_name": "执行业务方法(AllocateCardsPreview)", + "category": "asset", + "risk": "normal", + "primary_resource": "enterprise_card", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "N/A:普通查询", - "failure_strategy": "N/A:查询失败由 Access Log 记录", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "静态覆盖清单与纯读调用链比对", - "na_reason": "授权预览是普通查询,不改变业务事实,且未被 Action Registry 标记为敏感读取" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/enterprise_card/service.go:482:ResumeCard", + "key": "service:internal/service/enterprise_card/service.go:661:ResumeCard", "kind": "service", - "code_entry": "internal/service/enterprise_card/service.go:482 ResumeCard", + "code_entry": "internal/service/enterprise_card/service.go:661 ResumeCard", "owner": "enterprise_card", "summary": "业务方法 ResumeCard", "audit_event": "必须", @@ -11816,7 +12492,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_device/service.go:407:RecallDevices", @@ -11829,18 +12505,18 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "enterprise_device.recall_devices", - "action_name": "回收企业设备授权", + "action_name": "执行业务方法(RecallDevices)", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "实际撤销的设备授权记录及其 device_auth_id 关联卡授权、设备和卡;owner shop 和卡槽绑定作为 reference", + "primary_resource": "enterprise_device", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "设备和卡仅保存 subject_result;授权记录与卡槽绑定 internal_only", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "参数错误不写 Audit Event;权限、全项规则拒绝或并发零命中写 denied 短事务;业务回滚后的 failed 使用独立短事务,审计二次失败记录 critical", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/enterprise_device/service.go:56:AllocateDevices", @@ -11853,23 +12529,23 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "enterprise_device.allocate_devices", - "action_name": "向企业授权设备", + "action_name": "执行业务方法(AllocateDevices)", "category": "asset", "risk": "normal", - "primary_resource": "enterprise", - "affected_resource": "实际授权设备、设备授权记录及随设备授权的绑定卡/卡授权记录;owner shop 和卡槽绑定作为 reference", + "primary_resource": "enterprise_device", + "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "设备和卡仅保存 subject_result;授权记录与卡槽绑定 internal_only", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "参数错误不写 Audit Event;权限、全项规则拒绝或并发零命中写 denied 短事务;业务回滚后的 failed 使用独立短事务,审计二次失败记录 critical", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/exchange/service.go:224:Cancel", + "key": "service:internal/service/exchange/service.go:269:Cancel", "kind": "service", - "code_entry": "internal/service/exchange/service.go:224 Cancel", + "code_entry": "internal/service/exchange/service.go:269 Cancel", "owner": "exchange", "summary": "业务方法 Cancel", "audit_event": "必须", @@ -11888,12 +12564,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/exchange/service.go:386:SubmitShippingInfo", + "key": "service:internal/service/exchange/service.go:529:SubmitShippingInfo", "kind": "service", - "code_entry": "internal/service/exchange/service.go:386 SubmitShippingInfo", + "code_entry": "internal/service/exchange/service.go:529 SubmitShippingInfo", "owner": "exchange", "summary": "业务方法 SubmitShippingInfo", "audit_event": "必须", @@ -11912,12 +12588,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/exchange/service.go:71:Create", + "key": "service:internal/service/exchange/service.go:73:Create", "kind": "service", - "code_entry": "internal/service/exchange/service.go:71 Create", + "code_entry": "internal/service/exchange/service.go:73 Create", "owner": "exchange", "summary": "业务方法 Create", "audit_event": "必须", @@ -11936,12 +12612,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/export_task/service.go:222:CancelTask", + "key": "service:internal/service/export_task/service.go:251:CancelTask", "kind": "service", - "code_entry": "internal/service/export_task/service.go:222 CancelTask", + "code_entry": "internal/service/export_task/service.go:251 CancelTask", "owner": "export_task", "summary": "业务方法 CancelTask", "audit_event": "必须", @@ -11960,12 +12636,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/export_task/service.go:49:CreateTask", + "key": "service:internal/service/export_task/service.go:58:CreateTask", "kind": "service", - "code_entry": "internal/service/export_task/service.go:49 CreateTask", + "code_entry": "internal/service/export_task/service.go:58 CreateTask", "owner": "export_task", "summary": "业务方法 CreateTask", "audit_event": "必须", @@ -11984,12 +12660,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/realname_policy_batch.go:17:BatchUpdateRealnamePolicy", + "key": "service:internal/service/iot_card/realname_policy_batch.go:16:BatchUpdateRealnamePolicy", "kind": "service", - "code_entry": "internal/service/iot_card/realname_policy_batch.go:17 BatchUpdateRealnamePolicy", + "code_entry": "internal/service/iot_card/realname_policy_batch.go:16 BatchUpdateRealnamePolicy", "owner": "iot_card", "summary": "业务方法 BatchUpdateRealnamePolicy", "audit_event": "必须", @@ -12008,12 +12684,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1107:BatchSetSeriesBinding", + "key": "service:internal/service/iot_card/service.go:1012:BatchSetSeriesBinding", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1107 BatchSetSeriesBinding", + "code_entry": "internal/service/iot_card/service.go:1012 BatchSetSeriesBinding", "owner": "iot_card", "summary": "业务方法 BatchSetSeriesBinding", "audit_event": "必须", @@ -12032,12 +12708,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1522:RefreshCardDataByID", + "key": "service:internal/service/iot_card/service.go:1373:RefreshCardDataByID", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1522 RefreshCardDataByID", + "code_entry": "internal/service/iot_card/service.go:1373 RefreshCardDataByID", "owner": "iot_card", "summary": "业务方法 RefreshCardDataByID", "audit_event": "必须", @@ -12056,12 +12732,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1536:RefreshCardDataFromGateway", + "key": "service:internal/service/iot_card/service.go:1387:RefreshCardDataFromGateway", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1536 RefreshCardDataFromGateway", + "code_entry": "internal/service/iot_card/service.go:1387 RefreshCardDataFromGateway", "owner": "iot_card", "summary": "业务方法 RefreshCardDataFromGateway", "audit_event": "必须", @@ -12080,12 +12756,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1677:UpdatePollingStatus", + "key": "service:internal/service/iot_card/service.go:1692:UpdatePollingStatus", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1677 UpdatePollingStatus", + "code_entry": "internal/service/iot_card/service.go:1692 UpdatePollingStatus", "owner": "iot_card", "summary": "业务方法 UpdatePollingStatus", "audit_event": "必须", @@ -12104,12 +12780,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1784:BatchUpdatePollingStatus", + "key": "service:internal/service/iot_card/service.go:1741:BatchUpdatePollingStatus", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1784 BatchUpdatePollingStatus", + "code_entry": "internal/service/iot_card/service.go:1741 BatchUpdatePollingStatus", "owner": "iot_card", "summary": "业务方法 BatchUpdatePollingStatus", "audit_event": "必须", @@ -12128,12 +12804,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1842:DeleteCard", + "key": "service:internal/service/iot_card/service.go:1780:DeleteCard", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1842 DeleteCard", + "code_entry": "internal/service/iot_card/service.go:1780 DeleteCard", "owner": "iot_card", "summary": "业务方法 DeleteCard", "audit_event": "必须", @@ -12152,12 +12828,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1911:BatchDeleteCards", + "key": "service:internal/service/iot_card/service.go:1824:BatchDeleteCards", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1911 BatchDeleteCards", + "code_entry": "internal/service/iot_card/service.go:1824 BatchDeleteCards", "owner": "iot_card", "summary": "业务方法 BatchDeleteCards", "audit_event": "必须", @@ -12176,12 +12852,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:1982:UpdateRealnamePolicy", + "key": "service:internal/service/iot_card/service.go:1866:UpdateRealnamePolicy", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:1982 UpdateRealnamePolicy", + "code_entry": "internal/service/iot_card/service.go:1866 UpdateRealnamePolicy", "owner": "iot_card", "summary": "业务方法 UpdateRealnamePolicy", "audit_event": "必须", @@ -12200,7 +12876,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/iot_card/service.go:514:AllocateCards", @@ -12224,12 +12900,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/service.go:708:RecallCards", + "key": "service:internal/service/iot_card/service.go:666:RecallCards", "kind": "service", - "code_entry": "internal/service/iot_card/service.go:708 RecallCards", + "code_entry": "internal/service/iot_card/service.go:666 RecallCards", "owner": "iot_card", "summary": "业务方法 RecallCards", "audit_event": "必须", @@ -12248,7 +12924,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/iot_card/speed_tier.go:26:SetSpeedTier", @@ -12272,12 +12948,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card/stop_resume_service.go:400:ResumeCardIfStopped", + "key": "service:internal/service/iot_card/stop_resume_service.go:410:ResumeCardIfStopped", "kind": "service", - "code_entry": "internal/service/iot_card/stop_resume_service.go:400 ResumeCardIfStopped", + "code_entry": "internal/service/iot_card/stop_resume_service.go:410 ResumeCardIfStopped", "owner": "iot_card", "summary": "业务方法 ResumeCardIfStopped", "audit_event": "必须", @@ -12296,12 +12972,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/iot_card_import/service.go:67:CreateImportTask", + "key": "service:internal/service/iot_card_import/service.go:72:CreateImportTask", "kind": "service", - "code_entry": "internal/service/iot_card_import/service.go:67 CreateImportTask", + "code_entry": "internal/service/iot_card_import/service.go:72 CreateImportTask", "owner": "iot_card_import", "summary": "业务方法 CreateImportTask", "audit_event": "必须", @@ -12320,7 +12996,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/operation_password/service.go:27:Set", @@ -12344,12 +13020,36 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order/service.go:1387:Cancel", + "key": "service:internal/service/order/service.go:1240:CreatePendingOrder", "kind": "service", - "code_entry": "internal/service/order/service.go:1387 Cancel", + "code_entry": "internal/service/order/service.go:1240 CreatePendingOrder", + "owner": "order", + "summary": "业务方法 CreatePendingOrder", + "audit_event": "必须", + "domain_ledger": "必须:订单、充值、退款、钱包流水等既有业务表是领域权威", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "order.create_pending_order", + "action_name": "执行业务方法(CreatePendingOrder)", + "category": "finance", + "risk": "normal", + "primary_resource": "order", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/order/service.go:1438:Cancel", + "kind": "service", + "code_entry": "internal/service/order/service.go:1438 Cancel", "owner": "order", "summary": "业务方法 Cancel", "audit_event": "必须", @@ -12368,12 +13068,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order/service.go:1409:CancelExpiredOrders", + "key": "service:internal/service/order/service.go:1465:CancelExpiredOrders", "kind": "service", - "code_entry": "internal/service/order/service.go:1409 CancelExpiredOrders", + "code_entry": "internal/service/order/service.go:1465 CancelExpiredOrders", "owner": "order", "summary": "业务方法 CancelExpiredOrders", "audit_event": "必须", @@ -12392,12 +13092,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order/service.go:148:CreateAdminOrder", + "key": "service:internal/service/order/service.go:153:CreateAdminOrder", "kind": "service", - "code_entry": "internal/service/order/service.go:148 CreateAdminOrder", + "code_entry": "internal/service/order/service.go:153 CreateAdminOrder", "owner": "order", "summary": "业务方法 CreateAdminOrder", "audit_event": "必须", @@ -12416,12 +13116,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order/service.go:1753:HandlePaymentCallback", + "key": "service:internal/service/order/service.go:1913:HandlePaymentCallback", "kind": "service", - "code_entry": "internal/service/order/service.go:1753 HandlePaymentCallback", + "code_entry": "internal/service/order/service.go:1913 HandlePaymentCallback", "owner": "order", "summary": "业务方法 HandlePaymentCallback", "audit_event": "必须", @@ -12440,12 +13140,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order/service.go:1820:HandlePaymentRecordCallback", + "key": "service:internal/service/order/service.go:1989:HandlePaymentRecordCallback", "kind": "service", - "code_entry": "internal/service/order/service.go:1820 HandlePaymentRecordCallback", + "code_entry": "internal/service/order/service.go:1989 HandlePaymentRecordCallback", "owner": "order", "summary": "业务方法 HandlePaymentRecordCallback", "audit_event": "必须", @@ -12464,12 +13164,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order/service.go:552:CreateH5Order", + "key": "service:internal/service/order/service.go:565:CreateH5Order", "kind": "service", - "code_entry": "internal/service/order/service.go:552 CreateH5Order", + "code_entry": "internal/service/order/service.go:565 CreateH5Order", "owner": "order", "summary": "业务方法 CreateH5Order", "audit_event": "必须", @@ -12488,12 +13188,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/order_package_invalidate/service.go:43:Create", + "key": "service:internal/service/order_package_invalidate/service.go:53:Create", "kind": "service", - "code_entry": "internal/service/order_package_invalidate/service.go:43 Create", + "code_entry": "internal/service/order_package_invalidate/service.go:53 Create", "owner": "order_package_invalidate", "summary": "业务方法 Create", "audit_event": "必须", @@ -12512,12 +13212,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/activation_service.go:184:ActivateQueuedPackage", + "key": "service:internal/service/package/activation_service.go:211:ActivateQueuedPackage", "kind": "service", - "code_entry": "internal/service/package/activation_service.go:184 ActivateQueuedPackage", + "code_entry": "internal/service/package/activation_service.go:211 ActivateQueuedPackage", "owner": "package", "summary": "业务方法 ActivateQueuedPackage", "audit_event": "必须", @@ -12536,12 +13236,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/activation_service.go:255:ActivateSpecificPackage", + "key": "service:internal/service/package/activation_service.go:298:ActivateSpecificPackage", "kind": "service", - "code_entry": "internal/service/package/activation_service.go:255 ActivateSpecificPackage", + "code_entry": "internal/service/package/activation_service.go:298 ActivateSpecificPackage", "owner": "package", "summary": "业务方法 ActivateSpecificPackage", "audit_event": "必须", @@ -12560,12 +13260,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/activation_service.go:361:ActivateNextPendingMainPackage", + "key": "service:internal/service/package/activation_service.go:405:ActivateNextPendingMainPackage", "kind": "service", - "code_entry": "internal/service/package/activation_service.go:361 ActivateNextPendingMainPackage", + "code_entry": "internal/service/package/activation_service.go:405 ActivateNextPendingMainPackage", "owner": "package", "summary": "业务方法 ActivateNextPendingMainPackage", "audit_event": "必须", @@ -12584,12 +13284,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/activation_service.go:66:ActivateByRealname", + "key": "service:internal/service/package/activation_service.go:74:ActivateByRealname", "kind": "service", - "code_entry": "internal/service/package/activation_service.go:66 ActivateByRealname", + "code_entry": "internal/service/package/activation_service.go:74 ActivateByRealname", "owner": "package", "summary": "业务方法 ActivateByRealname", "audit_event": "必须", @@ -12608,12 +13308,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/activation_service.go:700:InvalidatePackagesForRefund", + "key": "service:internal/service/package/activation_service.go:774:InvalidatePackagesForRefund", "kind": "service", - "code_entry": "internal/service/package/activation_service.go:700 InvalidatePackagesForRefund", + "code_entry": "internal/service/package/activation_service.go:774 InvalidatePackagesForRefund", "owner": "package", "summary": "业务方法 InvalidatePackagesForRefund", "audit_event": "必须", @@ -12632,12 +13332,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/activation_service.go:818:InvalidateAllPackagesByAsset", + "key": "service:internal/service/package/activation_service.go:871:InvalidateAllPackagesByAsset", "kind": "service", - "code_entry": "internal/service/package/activation_service.go:818 InvalidateAllPackagesByAsset", + "code_entry": "internal/service/package/activation_service.go:871 InvalidateAllPackagesByAsset", "owner": "package", "summary": "业务方法 InvalidateAllPackagesByAsset", "audit_event": "必须", @@ -12656,12 +13356,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/reset_service.go:106:ResetMonthlyUsage", + "key": "service:internal/service/package/reset_service.go:134:ResetMonthlyUsage", "kind": "service", - "code_entry": "internal/service/package/reset_service.go:106 ResetMonthlyUsage", + "code_entry": "internal/service/package/reset_service.go:134 ResetMonthlyUsage", "owner": "package", "summary": "业务方法 ResetMonthlyUsage", "audit_event": "必须", @@ -12680,12 +13380,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/reset_service.go:182:ResetYearlyUsage", + "key": "service:internal/service/package/reset_service.go:296:ResetYearlyUsage", "kind": "service", - "code_entry": "internal/service/package/reset_service.go:182 ResetYearlyUsage", + "code_entry": "internal/service/package/reset_service.go:296 ResetYearlyUsage", "owner": "package", "summary": "业务方法 ResetYearlyUsage", "audit_event": "必须", @@ -12704,12 +13404,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/reset_service.go:44:ResetDailyUsage", + "key": "service:internal/service/package/reset_service.go:52:ResetDailyUsage", "kind": "service", - "code_entry": "internal/service/package/reset_service.go:44 ResetDailyUsage", + "code_entry": "internal/service/package/reset_service.go:52 ResetDailyUsage", "owner": "package", "summary": "业务方法 ResetDailyUsage", "audit_event": "必须", @@ -12728,12 +13428,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/service.go:194:Update", + "key": "service:internal/service/package/service.go:208:Update", "kind": "service", - "code_entry": "internal/service/package/service.go:194 Update", + "code_entry": "internal/service/package/service.go:208 Update", "owner": "package", "summary": "业务方法 Update", "audit_event": "必须", @@ -12752,12 +13452,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/service.go:317:Delete", + "key": "service:internal/service/package/service.go:342:Delete", "kind": "service", - "code_entry": "internal/service/package/service.go:317 Delete", + "code_entry": "internal/service/package/service.go:342 Delete", "owner": "package", "summary": "业务方法 Delete", "audit_event": "必须", @@ -12776,12 +13476,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/service.go:41:Create", + "key": "service:internal/service/package/service.go:44:Create", "kind": "service", - "code_entry": "internal/service/package/service.go:41 Create", + "code_entry": "internal/service/package/service.go:44 Create", "owner": "package", "summary": "业务方法 Create", "audit_event": "必须", @@ -12800,12 +13500,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/service.go:447:UpdateStatus", + "key": "service:internal/service/package/service.go:478:UpdateStatus", "kind": "service", - "code_entry": "internal/service/package/service.go:447 UpdateStatus", + "code_entry": "internal/service/package/service.go:478 UpdateStatus", "owner": "package", "summary": "业务方法 UpdateStatus", "audit_event": "必须", @@ -12824,12 +13524,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/service.go:475:UpdateShelfStatus", + "key": "service:internal/service/package/service.go:514:UpdateShelfStatus", "kind": "service", - "code_entry": "internal/service/package/service.go:475 UpdateShelfStatus", + "code_entry": "internal/service/package/service.go:514 UpdateShelfStatus", "owner": "package", "summary": "业务方法 UpdateShelfStatus", "audit_event": "必须", @@ -12848,12 +13548,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/service.go:512:UpdateRetailPrice", + "key": "service:internal/service/package/service.go:559:UpdateRetailPrice", "kind": "service", - "code_entry": "internal/service/package/service.go:512 UpdateRetailPrice", + "code_entry": "internal/service/package/service.go:559 UpdateRetailPrice", "owner": "package", "summary": "业务方法 UpdateRetailPrice", "audit_event": "必须", @@ -12872,12 +13572,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package/usage_service.go:71:DeductDataUsage", + "key": "service:internal/service/package/usage_service.go:78:DeductDataUsage", "kind": "service", - "code_entry": "internal/service/package/usage_service.go:71 DeductDataUsage", + "code_entry": "internal/service/package/usage_service.go:78 DeductDataUsage", "owner": "package", "summary": "业务方法 DeductDataUsage", "audit_event": "必须", @@ -12896,84 +13596,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/package_series/service.go:138:Delete", + "key": "service:internal/service/package_series/service.go:105:Update", "kind": "service", - "code_entry": "internal/service/package_series/service.go:138 Delete", - "owner": "package_series", - "summary": "业务方法 Delete", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "package_series.delete", - "action_name": "执行业务方法(Delete)", - "category": "business", - "risk": "normal", - "primary_resource": "package_series", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/package_series/service.go:226:UpdateStatus", - "kind": "service", - "code_entry": "internal/service/package_series/service.go:226 UpdateStatus", - "owner": "package_series", - "summary": "业务方法 UpdateStatus", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "package_series.update_status", - "action_name": "执行业务方法(UpdateStatus)", - "category": "business", - "risk": "normal", - "primary_resource": "package_series", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/package_series/service.go:35:Create", - "kind": "service", - "code_entry": "internal/service/package_series/service.go:35 Create", - "owner": "package_series", - "summary": "业务方法 Create", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "package_series.create", - "action_name": "执行业务方法(Create)", - "category": "business", - "risk": "normal", - "primary_resource": "package_series", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/package_series/service.go:90:Update", - "kind": "service", - "code_entry": "internal/service/package_series/service.go:90 Update", + "code_entry": "internal/service/package_series/service.go:105 Update", "owner": "package_series", "summary": "业务方法 Update", "audit_event": "必须", @@ -12992,12 +13620,84 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/permission/service.go:119:Update", + "key": "service:internal/service/package_series/service.go:164:Delete", "kind": "service", - "code_entry": "internal/service/permission/service.go:119 Update", + "code_entry": "internal/service/package_series/service.go:164 Delete", + "owner": "package_series", + "summary": "业务方法 Delete", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "package_series.delete", + "action_name": "执行业务方法(Delete)", + "category": "business", + "risk": "normal", + "primary_resource": "package_series", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/package_series/service.go:258:UpdateStatus", + "kind": "service", + "code_entry": "internal/service/package_series/service.go:258 UpdateStatus", + "owner": "package_series", + "summary": "业务方法 UpdateStatus", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "package_series.update_status", + "action_name": "执行业务方法(UpdateStatus)", + "category": "business", + "risk": "normal", + "primary_resource": "package_series", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/package_series/service.go:38:Create", + "kind": "service", + "code_entry": "internal/service/package_series/service.go:38 Create", + "owner": "package_series", + "summary": "业务方法 Create", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "package_series.create", + "action_name": "执行业务方法(Create)", + "category": "business", + "risk": "normal", + "primary_resource": "package_series", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/permission/service.go:153:Update", + "kind": "service", + "code_entry": "internal/service/permission/service.go:153 Update", "owner": "permission", "summary": "业务方法 Update", "audit_event": "必须", @@ -13016,12 +13716,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/permission/service.go:182:Delete", + "key": "service:internal/service/permission/service.go:247:Delete", "kind": "service", - "code_entry": "internal/service/permission/service.go:182 Delete", + "code_entry": "internal/service/permission/service.go:247 Delete", "owner": "permission", "summary": "业务方法 Delete", "audit_event": "必须", @@ -13040,12 +13740,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/permission/service.go:56:Create", + "key": "service:internal/service/permission/service.go:65:Create", "kind": "service", - "code_entry": "internal/service/permission/service.go:56 Create", + "code_entry": "internal/service/permission/service.go:65 Create", "owner": "permission", "summary": "业务方法 Create", "audit_event": "必须", @@ -13064,12 +13764,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/personal_customer/service.go:35:UpdateProfile", + "key": "service:internal/service/personal_customer/service.go:45:UpdateProfile", "kind": "service", - "code_entry": "internal/service/personal_customer/service.go:35 UpdateProfile", + "code_entry": "internal/service/personal_customer/service.go:45 UpdateProfile", "owner": "personal_customer", "summary": "业务方法 UpdateProfile", "audit_event": "必须", @@ -13077,71 +13777,23 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "personal_customer.update_profile", - "action_name": "更新个人资料", - "category": "identity", + "action_name": "执行业务方法(UpdateProfile)", + "category": "business", "risk": "normal", "primary_resource": "personal_customer", "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "personal_customer/personal_api", - "visibility": "subject_detail,仅返回 Registry 白名单 nickname/avatar_url 字段", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/polling/alert_service.go:112:DeleteRule", - "kind": "service", - "code_entry": "internal/service/polling/alert_service.go:112 DeleteRule", - "owner": "polling", - "summary": "业务方法 DeleteRule", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "polling.delete_rule", - "action_name": "执行业务方法(DeleteRule)", - "category": "asset", - "risk": "normal", - "primary_resource": "polling", - "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", "transaction": "关键成功事件与业务事实同一 GORM 事务", "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/alert_service.go:46:CreateRule", + "key": "service:internal/service/polling/alert_service.go:119:UpdateRule", "kind": "service", - "code_entry": "internal/service/polling/alert_service.go:46 CreateRule", - "owner": "polling", - "summary": "业务方法 CreateRule", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "polling.create_rule", - "action_name": "执行业务方法(CreateRule)", - "category": "asset", - "risk": "normal", - "primary_resource": "polling", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/polling/alert_service.go:83:UpdateRule", - "kind": "service", - "code_entry": "internal/service/polling/alert_service.go:83 UpdateRule", + "code_entry": "internal/service/polling/alert_service.go:119 UpdateRule", "owner": "polling", "summary": "业务方法 UpdateRule", "audit_event": "必须", @@ -13160,12 +13812,60 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/asset_polling_service.go:64:UpdatePollingStatus", + "key": "service:internal/service/polling/alert_service.go:173:DeleteRule", "kind": "service", - "code_entry": "internal/service/polling/asset_polling_service.go:64 UpdatePollingStatus", + "code_entry": "internal/service/polling/alert_service.go:173 DeleteRule", + "owner": "polling", + "summary": "业务方法 DeleteRule", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "polling.delete_rule", + "action_name": "执行业务方法(DeleteRule)", + "category": "asset", + "risk": "normal", + "primary_resource": "polling", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/polling/alert_service.go:57:CreateRule", + "kind": "service", + "code_entry": "internal/service/polling/alert_service.go:57 CreateRule", + "owner": "polling", + "summary": "业务方法 CreateRule", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "polling.create_rule", + "action_name": "执行业务方法(CreateRule)", + "category": "asset", + "risk": "normal", + "primary_resource": "polling", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/polling/asset_polling_service.go:60:UpdatePollingStatus", + "kind": "service", + "code_entry": "internal/service/polling/asset_polling_service.go:60 UpdatePollingStatus", "owner": "polling", "summary": "业务方法 UpdatePollingStatus", "audit_event": "必须", @@ -13184,7 +13884,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/polling/cleanup_service.go:192:TriggerCleanup", @@ -13208,7 +13908,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/polling/cleanup_service.go:38:CreateConfig", @@ -13232,7 +13932,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/polling/cleanup_service.go:67:UpdateConfig", @@ -13256,7 +13956,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/polling/cleanup_service.go:98:DeleteConfig", @@ -13280,12 +13980,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/concurrency_service.go:109:UpdateMaxConcurrency", + "key": "service:internal/service/polling/concurrency_service.go:120:UpdateMaxConcurrency", "kind": "service", - "code_entry": "internal/service/polling/concurrency_service.go:109 UpdateMaxConcurrency", + "code_entry": "internal/service/polling/concurrency_service.go:120 UpdateMaxConcurrency", "owner": "polling", "summary": "业务方法 UpdateMaxConcurrency", "audit_event": "必须", @@ -13304,12 +14004,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/concurrency_service.go:136:ResetConcurrency", + "key": "service:internal/service/polling/concurrency_service.go:167:ResetConcurrency", "kind": "service", - "code_entry": "internal/service/polling/concurrency_service.go:136 ResetConcurrency", + "code_entry": "internal/service/polling/concurrency_service.go:167 ResetConcurrency", "owner": "polling", "summary": "业务方法 ResetConcurrency", "audit_event": "必须", @@ -13328,12 +14028,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/concurrency_service.go:171:SyncConfigToRedis", + "key": "service:internal/service/polling/concurrency_service.go:255:SyncConfigToRedis", "kind": "service", - "code_entry": "internal/service/polling/concurrency_service.go:171 SyncConfigToRedis", + "code_entry": "internal/service/polling/concurrency_service.go:255 SyncConfigToRedis", "owner": "polling", "summary": "业务方法 SyncConfigToRedis", "audit_event": "必须", @@ -13352,84 +14052,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/config_service.go:163:Delete", + "key": "service:internal/service/polling/config_service.go:133:Update", "kind": "service", - "code_entry": "internal/service/polling/config_service.go:163 Delete", - "owner": "polling", - "summary": "业务方法 Delete", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "polling.delete", - "action_name": "执行业务方法(Delete)", - "category": "asset", - "risk": "high", - "primary_resource": "polling", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/polling/config_service.go:225:UpdateStatus", - "kind": "service", - "code_entry": "internal/service/polling/config_service.go:225 UpdateStatus", - "owner": "polling", - "summary": "业务方法 UpdateStatus", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "polling.update_status", - "action_name": "执行业务方法(UpdateStatus)", - "category": "asset", - "risk": "high", - "primary_resource": "polling", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/polling/config_service.go:42:Create", - "kind": "service", - "code_entry": "internal/service/polling/config_service.go:42 Create", - "owner": "polling", - "summary": "业务方法 Create", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "polling.create", - "action_name": "执行业务方法(Create)", - "category": "asset", - "risk": "high", - "primary_resource": "polling", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/polling/config_service.go:99:Update", - "kind": "service", - "code_entry": "internal/service/polling/config_service.go:99 Update", + "code_entry": "internal/service/polling/config_service.go:133 Update", "owner": "polling", "summary": "业务方法 Update", "audit_event": "必须", @@ -13448,12 +14076,84 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/manual_trigger_service.go:124:TriggerBatch", + "key": "service:internal/service/polling/config_service.go:226:Delete", "kind": "service", - "code_entry": "internal/service/polling/manual_trigger_service.go:124 TriggerBatch", + "code_entry": "internal/service/polling/config_service.go:226 Delete", + "owner": "polling", + "summary": "业务方法 Delete", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "polling.delete", + "action_name": "执行业务方法(Delete)", + "category": "asset", + "risk": "high", + "primary_resource": "polling", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/polling/config_service.go:310:UpdateStatus", + "kind": "service", + "code_entry": "internal/service/polling/config_service.go:310 UpdateStatus", + "owner": "polling", + "summary": "业务方法 UpdateStatus", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "polling.update_status", + "action_name": "执行业务方法(UpdateStatus)", + "category": "asset", + "risk": "high", + "primary_resource": "polling", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/polling/config_service.go:51:Create", + "kind": "service", + "code_entry": "internal/service/polling/config_service.go:51 Create", + "owner": "polling", + "summary": "业务方法 Create", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "调用外部系统或处理回调时必须;纯本地分支 N/A", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "polling.create", + "action_name": "执行业务方法(Create)", + "category": "asset", + "risk": "high", + "primary_resource": "polling", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/polling/manual_trigger_service.go:175:TriggerBatch", + "kind": "service", + "code_entry": "internal/service/polling/manual_trigger_service.go:175 TriggerBatch", "owner": "polling", "summary": "业务方法 TriggerBatch", "audit_event": "必须", @@ -13472,12 +14172,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/manual_trigger_service.go:233:TriggerByCondition", + "key": "service:internal/service/polling/manual_trigger_service.go:315:TriggerByCondition", "kind": "service", - "code_entry": "internal/service/polling/manual_trigger_service.go:233 TriggerByCondition", + "code_entry": "internal/service/polling/manual_trigger_service.go:315 TriggerByCondition", "owner": "polling", "summary": "业务方法 TriggerByCondition", "audit_event": "必须", @@ -13496,12 +14196,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/manual_trigger_service.go:337:CancelTrigger", + "key": "service:internal/service/polling/manual_trigger_service.go:452:CancelTrigger", "kind": "service", - "code_entry": "internal/service/polling/manual_trigger_service.go:337 CancelTrigger", + "code_entry": "internal/service/polling/manual_trigger_service.go:452 CancelTrigger", "owner": "polling", "summary": "业务方法 CancelTrigger", "audit_event": "必须", @@ -13520,12 +14220,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/polling/manual_trigger_service.go:42:TriggerSingle", + "key": "service:internal/service/polling/manual_trigger_service.go:52:TriggerSingle", "kind": "service", - "code_entry": "internal/service/polling/manual_trigger_service.go:42 TriggerSingle", + "code_entry": "internal/service/polling/manual_trigger_service.go:52 TriggerSingle", "owner": "polling", "summary": "业务方法 TriggerSingle", "audit_event": "必须", @@ -13544,12 +14244,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/recharge_order/service.go:63:HandlePaymentCallback", + "key": "service:internal/service/recharge_order/service.go:72:HandlePaymentCallback", "kind": "service", - "code_entry": "internal/service/recharge_order/service.go:63 HandlePaymentCallback", + "code_entry": "internal/service/recharge_order/service.go:72 HandlePaymentCallback", "owner": "recharge_order", "summary": "业务方法 HandlePaymentCallback", "audit_event": "必须", @@ -13568,12 +14268,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/refund/approval_decision.go:17:Handle", + "key": "service:internal/service/refund/approval_decision.go:19:Handle", "kind": "service", - "code_entry": "internal/service/refund/approval_decision.go:17 Handle", + "code_entry": "internal/service/refund/approval_decision.go:19 Handle", "owner": "refund", "summary": "业务方法 Handle", "audit_event": "必须", @@ -13592,12 +14292,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/refund/service.go:106:Create", + "key": "service:internal/service/refund/service.go:115:Create", "kind": "service", - "code_entry": "internal/service/refund/service.go:106 Create", + "code_entry": "internal/service/refund/service.go:115 Create", "owner": "refund", "summary": "业务方法 Create", "audit_event": "必须", @@ -13616,12 +14316,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/refund/service.go:245:Approve", + "key": "service:internal/service/refund/service.go:258:Approve", "kind": "service", - "code_entry": "internal/service/refund/service.go:245 Approve", + "code_entry": "internal/service/refund/service.go:258 Approve", "owner": "refund", "summary": "业务方法 Approve", "audit_event": "必须", @@ -13640,12 +14340,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/refund/service.go:552:Reject", + "key": "service:internal/service/refund/service.go:588:Reject", "kind": "service", - "code_entry": "internal/service/refund/service.go:552 Reject", + "code_entry": "internal/service/refund/service.go:588 Reject", "owner": "refund", "summary": "业务方法 Reject", "audit_event": "必须", @@ -13664,156 +14364,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/role/service.go:132:Delete", + "key": "service:internal/service/role/service.go:114:Update", "kind": "service", - "code_entry": "internal/service/role/service.go:132 Delete", - "owner": "role", - "summary": "业务方法 Delete", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.delete", - "action_name": "执行业务方法(Delete)", - "category": "security", - "risk": "high", - "primary_resource": "role", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/role/service.go:199:AssignPermissions", - "kind": "service", - "code_entry": "internal/service/role/service.go:199 AssignPermissions", - "owner": "role", - "summary": "业务方法 AssignPermissions", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.assign_permissions", - "action_name": "执行业务方法(AssignPermissions)", - "category": "security", - "risk": "high", - "primary_resource": "role", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/role/service.go:290:RemovePermission", - "kind": "service", - "code_entry": "internal/service/role/service.go:290 RemovePermission", - "owner": "role", - "summary": "业务方法 RemovePermission", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.remove_permission", - "action_name": "执行业务方法(RemovePermission)", - "category": "security", - "risk": "high", - "primary_resource": "role", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/role/service.go:307:BatchRemovePermissions", - "kind": "service", - "code_entry": "internal/service/role/service.go:307 BatchRemovePermissions", - "owner": "role", - "summary": "业务方法 BatchRemovePermissions", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.batch_remove_permissions", - "action_name": "执行业务方法(BatchRemovePermissions)", - "category": "security", - "risk": "high", - "primary_resource": "role", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/role/service.go:325:UpdateStatus", - "kind": "service", - "code_entry": "internal/service/role/service.go:325 UpdateStatus", - "owner": "role", - "summary": "业务方法 UpdateStatus", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.update_status", - "action_name": "执行业务方法(UpdateStatus)", - "category": "security", - "risk": "high", - "primary_resource": "role", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/role/service.go:42:Create", - "kind": "service", - "code_entry": "internal/service/role/service.go:42 Create", - "owner": "role", - "summary": "业务方法 Create", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "role.create", - "action_name": "执行业务方法(Create)", - "category": "security", - "risk": "high", - "primary_resource": "role", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/role/service.go:86:Update", - "kind": "service", - "code_entry": "internal/service/role/service.go:86 Update", + "code_entry": "internal/service/role/service.go:114 Update", "owner": "role", "summary": "业务方法 Update", "audit_event": "必须", @@ -13832,20 +14388,164 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop/service.go:112:Enable", + "key": "service:internal/service/role/service.go:173:Delete", "kind": "service", - "code_entry": "internal/service/shop/service.go:112 Enable", - "owner": "shop", - "summary": "业务方法 Enable", + "code_entry": "internal/service/role/service.go:173 Delete", + "owner": "role", + "summary": "业务方法 Delete", "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "shop.enable", - "action_name": "启用店铺", + "action_code": "role.delete", + "action_name": "执行业务方法(Delete)", + "category": "security", + "risk": "high", + "primary_resource": "role", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/role/service.go:255:AssignPermissions", + "kind": "service", + "code_entry": "internal/service/role/service.go:255 AssignPermissions", + "owner": "role", + "summary": "业务方法 AssignPermissions", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "role.assign_permissions", + "action_name": "执行业务方法(AssignPermissions)", + "category": "security", + "risk": "high", + "primary_resource": "role", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/role/service.go:383:RemovePermission", + "kind": "service", + "code_entry": "internal/service/role/service.go:383 RemovePermission", + "owner": "role", + "summary": "业务方法 RemovePermission", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "role.remove_permission", + "action_name": "执行业务方法(RemovePermission)", + "category": "security", + "risk": "high", + "primary_resource": "role", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/role/service.go:436:BatchRemovePermissions", + "kind": "service", + "code_entry": "internal/service/role/service.go:436 BatchRemovePermissions", + "owner": "role", + "summary": "业务方法 BatchRemovePermissions", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "role.batch_remove_permissions", + "action_name": "执行业务方法(BatchRemovePermissions)", + "category": "security", + "risk": "high", + "primary_resource": "role", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/role/service.go:495:UpdateStatus", + "kind": "service", + "code_entry": "internal/service/role/service.go:495 UpdateStatus", + "owner": "role", + "summary": "业务方法 UpdateStatus", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "role.update_status", + "action_name": "执行业务方法(UpdateStatus)", + "category": "security", + "risk": "high", + "primary_resource": "role", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/role/service.go:54:Create", + "kind": "service", + "code_entry": "internal/service/role/service.go:54 Create", + "owner": "role", + "summary": "业务方法 Create", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "role.create", + "action_name": "执行业务方法(Create)", + "category": "security", + "risk": "high", + "primary_resource": "role", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/shop/service.go:104:Disable", + "kind": "service", + "code_entry": "internal/service/shop/service.go:104 Disable", + "owner": "shop", + "summary": "业务方法 Disable", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "shop.disable", + "action_name": "执行业务方法(Disable)", "category": "business", "risk": "normal", "primary_resource": "shop", @@ -13856,22 +14556,22 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop/service.go:287:Delete", + "key": "service:internal/service/shop/service.go:125:Enable", "kind": "service", - "code_entry": "internal/service/shop/service.go:287 Delete", + "code_entry": "internal/service/shop/service.go:125 Enable", "owner": "shop", - "summary": "业务方法 Delete", + "summary": "业务方法 Enable", "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "shop.delete", - "action_name": "删除店铺", + "action_code": "shop.enable", + "action_name": "执行业务方法(Enable)", "category": "business", - "risk": "high", + "risk": "normal", "primary_resource": "shop", "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", @@ -13880,12 +14580,36 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop/service.go:37:Update", + "key": "service:internal/service/shop/service.go:300:Delete", "kind": "service", - "code_entry": "internal/service/shop/service.go:37 Update", + "code_entry": "internal/service/shop/service.go:300 Delete", + "owner": "shop", + "summary": "业务方法 Delete", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "shop.delete", + "action_name": "执行业务方法(Delete)", + "category": "business", + "risk": "normal", + "primary_resource": "shop", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/shop/service.go:50:Update", + "kind": "service", + "code_entry": "internal/service/shop/service.go:50 Update", "owner": "shop", "summary": "业务方法 Update", "audit_event": "必须", @@ -13904,22 +14628,22 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop/service.go:91:Disable", + "key": "service:internal/service/shop/shop_role.go:19:AssignRolesToShop", "kind": "service", - "code_entry": "internal/service/shop/service.go:91 Disable", + "code_entry": "internal/service/shop/shop_role.go:19 AssignRolesToShop", "owner": "shop", - "summary": "业务方法 Disable", + "summary": "业务方法 AssignRolesToShop", "audit_event": "必须", "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "shop.disable", - "action_name": "禁用店铺", + "action_code": "shop.assign_roles_to_shop", + "action_name": "执行业务方法(AssignRolesToShop)", "category": "business", - "risk": "normal", + "risk": "high", "primary_resource": "shop", "affected_resource": "完整用例直接修改或引用的资源", "actor_source": "由调用入口传入操作者与来源快照", @@ -13928,12 +14652,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop/shop_role.go:130:DeleteShopRole", + "key": "service:internal/service/shop/shop_role.go:95:DeleteShopRole", "kind": "service", - "code_entry": "internal/service/shop/shop_role.go:130 DeleteShopRole", + "code_entry": "internal/service/shop/shop_role.go:95 DeleteShopRole", "owner": "shop", "summary": "业务方法 DeleteShopRole", "audit_event": "必须", @@ -13941,7 +14665,7 @@ "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", "action_code": "shop.delete_shop_role", - "action_name": "删除店铺默认角色", + "action_name": "执行业务方法(DeleteShopRole)", "category": "business", "risk": "high", "primary_resource": "shop", @@ -13952,36 +14676,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop/shop_role.go:13:AssignRolesToShop", + "key": "service:internal/service/shop_commission/service.go:412:CreateWithdrawalRequest", "kind": "service", - "code_entry": "internal/service/shop/shop_role.go:13 AssignRolesToShop", - "owner": "shop", - "summary": "业务方法 AssignRolesToShop", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "shop.assign_shop_roles", - "action_name": "分配店铺默认角色", - "category": "business", - "risk": "high", - "primary_resource": "shop", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/shop_commission/service.go:405:CreateWithdrawalRequest", - "kind": "service", - "code_entry": "internal/service/shop_commission/service.go:405 CreateWithdrawalRequest", + "code_entry": "internal/service/shop_commission/service.go:412 CreateWithdrawalRequest", "owner": "shop_commission", "summary": "业务方法 CreateWithdrawalRequest", "audit_event": "必须", @@ -14000,12 +14700,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop_commission/service.go:621:ResolveCommissionRecord", + "key": "service:internal/service/shop_commission/service.go:627:ResolveCommissionRecord", "kind": "service", - "code_entry": "internal/service/shop_commission/service.go:621 ResolveCommissionRecord", + "code_entry": "internal/service/shop_commission/service.go:627 ResolveCommissionRecord", "owner": "shop_commission", "summary": "业务方法 ResolveCommissionRecord", "audit_event": "必须", @@ -14024,36 +14724,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop_package_batch_allocation/service.go:104:BatchAllocate", + "key": "service:internal/service/shop_package_batch_allocation/service.go:47:UpdateExpiryBase", "kind": "service", - "code_entry": "internal/service/shop_package_batch_allocation/service.go:104 BatchAllocate", - "owner": "shop_package_batch_allocation", - "summary": "业务方法 BatchAllocate", - "audit_event": "必须", - "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", - "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", - "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", - "action_code": "shop_package_batch_allocation.batch_allocate", - "action_name": "执行业务方法(BatchAllocate)", - "category": "business", - "risk": "normal", - "primary_resource": "shop_package_batch_allocation", - "affected_resource": "完整用例直接修改或引用的资源", - "actor_source": "由调用入口传入操作者与来源快照", - "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", - "transaction": "关键成功事件与业务事实同一 GORM 事务", - "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", - "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", - "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" - }, - { - "key": "service:internal/service/shop_package_batch_allocation/service.go:50:UpdateExpiryBase", - "kind": "service", - "code_entry": "internal/service/shop_package_batch_allocation/service.go:50 UpdateExpiryBase", + "code_entry": "internal/service/shop_package_batch_allocation/service.go:47 UpdateExpiryBase", "owner": "shop_package_batch_allocation", "summary": "业务方法 UpdateExpiryBase", "audit_event": "必须", @@ -14072,12 +14748,36 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop_package_batch_pricing/service.go:37:BatchUpdatePricing", + "key": "service:internal/service/shop_package_batch_allocation/service.go:97:BatchAllocate", "kind": "service", - "code_entry": "internal/service/shop_package_batch_pricing/service.go:37 BatchUpdatePricing", + "code_entry": "internal/service/shop_package_batch_allocation/service.go:97 BatchAllocate", + "owner": "shop_package_batch_allocation", + "summary": "业务方法 BatchAllocate", + "audit_event": "必须", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "N/A:当前方法按代码位置属于本地业务用例;后续新增外部调用必须重新分类", + "outbox": "存在提交后可靠副作用时必须在同一事务追加;否则 N/A", + "action_code": "shop_package_batch_allocation.batch_allocate", + "action_name": "执行业务方法(BatchAllocate)", + "category": "business", + "risk": "normal", + "primary_resource": "shop_package_batch_allocation", + "affected_resource": "完整用例直接修改或引用的资源", + "actor_source": "由调用入口传入操作者与来源快照", + "visibility": "由完整用例决定平台完整视图、主体安全投影或 internal_only", + "transaction": "关键成功事件与业务事实同一 GORM 事务", + "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", + "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", + "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "service:internal/service/shop_package_batch_pricing/service.go:44:BatchUpdatePricing", + "kind": "service", + "code_entry": "internal/service/shop_package_batch_pricing/service.go:44 BatchUpdatePricing", "owner": "shop_package_batch_pricing", "summary": "业务方法 BatchUpdatePricing", "audit_event": "必须", @@ -14096,12 +14796,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop_series_grant/service.go:302:Create", + "key": "service:internal/service/shop_series_grant/service.go:306:Create", "kind": "service", - "code_entry": "internal/service/shop_series_grant/service.go:302 Create", + "code_entry": "internal/service/shop_series_grant/service.go:306 Create", "owner": "shop_series_grant", "summary": "业务方法 Create", "audit_event": "必须", @@ -14120,12 +14820,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop_series_grant/service.go:641:Update", + "key": "service:internal/service/shop_series_grant/service.go:657:Update", "kind": "service", - "code_entry": "internal/service/shop_series_grant/service.go:641 Update", + "code_entry": "internal/service/shop_series_grant/service.go:657 Update", "owner": "shop_series_grant", "summary": "业务方法 Update", "audit_event": "必须", @@ -14144,12 +14844,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/shop_series_grant/service.go:858:Delete", + "key": "service:internal/service/shop_series_grant/service.go:932:Delete", "kind": "service", - "code_entry": "internal/service/shop_series_grant/service.go:858 Delete", + "code_entry": "internal/service/shop_series_grant/service.go:932 Delete", "owner": "shop_series_grant", "summary": "业务方法 Delete", "audit_event": "必须", @@ -14168,7 +14868,7 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { "key": "service:internal/service/verification/service.go:36:SendCode", @@ -14192,12 +14892,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/wechat_config/service.go:195:Update", + "key": "service:internal/service/wechat_config/service.go:191:Update", "kind": "service", - "code_entry": "internal/service/wechat_config/service.go:195 Update", + "code_entry": "internal/service/wechat_config/service.go:191 Update", "owner": "wechat_config", "summary": "业务方法 Update", "audit_event": "必须", @@ -14216,12 +14916,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/wechat_config/service.go:291:Delete", + "key": "service:internal/service/wechat_config/service.go:280:Delete", "kind": "service", - "code_entry": "internal/service/wechat_config/service.go:291 Delete", + "code_entry": "internal/service/wechat_config/service.go:280 Delete", "owner": "wechat_config", "summary": "业务方法 Delete", "audit_event": "必须", @@ -14240,12 +14940,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/wechat_config/service.go:348:Activate", + "key": "service:internal/service/wechat_config/service.go:332:Activate", "kind": "service", - "code_entry": "internal/service/wechat_config/service.go:348 Activate", + "code_entry": "internal/service/wechat_config/service.go:332 Activate", "owner": "wechat_config", "summary": "业务方法 Activate", "audit_event": "必须", @@ -14264,12 +14964,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/wechat_config/service.go:398:Deactivate", + "key": "service:internal/service/wechat_config/service.go:379:Deactivate", "kind": "service", - "code_entry": "internal/service/wechat_config/service.go:398 Deactivate", + "code_entry": "internal/service/wechat_config/service.go:379 Deactivate", "owner": "wechat_config", "summary": "业务方法 Deactivate", "audit_event": "必须", @@ -14288,12 +14988,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "service:internal/service/wechat_config/service.go:69:Create", + "key": "service:internal/service/wechat_config/service.go:67:Create", "kind": "service", - "code_entry": "internal/service/wechat_config/service.go:69 Create", + "code_entry": "internal/service/wechat_config/service.go:67 Create", "owner": "wechat_config", "summary": "业务方法 Create", "audit_event": "必须", @@ -14312,12 +15012,12 @@ "failure_strategy": "业务回滚后的 failed/denied 使用独立短事务;审计二次失败记录 critical", "sensitive_policy": "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", "before_after_policy": "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口" + "test_seam": "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:cmd/worker/main.go:149:constants.TaskTypeOutboxDeliver", + "key": "worker:cmd/worker/main.go:152:constants.TaskTypeOutboxDeliver", "kind": "worker", - "code_entry": "cmd/worker/main.go:149 outboxHandler.Handle", + "code_entry": "cmd/worker/main.go:152 outboxHandler.Handle", "owner": "outbox_deliver", "summary": "处理异步任务 constants.TaskTypeOutboxDeliver", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14336,12 +15036,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:cmd/worker/main.go:405:constants.TaskTypeWeComApprovalSync", + "key": "worker:cmd/worker/main.go:418:constants.TaskTypeWeComApprovalSync", "kind": "worker", - "code_entry": "cmd/worker/main.go:405 detailHandler.Handle", + "code_entry": "cmd/worker/main.go:418 detailHandler.Handle", "owner": "we_com_approval_sync", "summary": "处理异步任务 constants.TaskTypeWeComApprovalSync", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14360,12 +15060,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:cmd/worker/main.go:406:constants.TaskTypeWeComApprovalRecovery", + "key": "worker:cmd/worker/main.go:419:constants.TaskTypeWeComApprovalRecovery", "kind": "worker", - "code_entry": "cmd/worker/main.go:406 recoveryHandler.Handle", + "code_entry": "cmd/worker/main.go:419 recoveryHandler.Handle", "owner": "we_com_approval_recovery", "summary": "处理异步任务 constants.TaskTypeWeComApprovalRecovery", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14384,12 +15084,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:cmd/worker/main.go:425:constants.TaskTypeAgentRechargeRecovery", + "key": "worker:cmd/worker/main.go:440:constants.TaskTypeAgentRechargeRecovery", "kind": "worker", - "code_entry": "cmd/worker/main.go:425 handler.Handle", + "code_entry": "cmd/worker/main.go:440 handler.Handle", "owner": "agent_recharge_recovery", "summary": "处理异步任务 constants.TaskTypeAgentRechargeRecovery", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14408,12 +15108,204 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:112:constants.TaskTypeIotCardImport", + "key": "worker:cmd/worker/main.go:812:constants.TaskTypeAuditDailyArchive", "kind": "worker", - "code_entry": "pkg/queue/handler.go:112 iotCardImportHandler.HandleIotCardImport", + "code_entry": "cmd/worker/main.go:812 task.NewAuditDailyArchiveHandler.Handle", + "owner": "audit_daily_archive", + "summary": "处理异步任务 constants.TaskTypeAuditDailyArchive", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "audit_daily_archive.handle", + "action_name": "处理异步任务(constants.TaskTypeAuditDailyArchive)", + "category": "business", + "risk": "normal", + "primary_resource": "audit_daily_archive", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:814:constants.TaskTypeIntegrationDailyArchive", + "kind": "worker", + "code_entry": "cmd/worker/main.go:814 integrationHandler.HandleDaily", + "owner": "integration_daily_archive", + "summary": "处理异步任务 constants.TaskTypeIntegrationDailyArchive", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "integration_daily_archive.handle_daily", + "action_name": "处理异步任务(constants.TaskTypeIntegrationDailyArchive)", + "category": "business", + "risk": "normal", + "primary_resource": "integration_daily_archive", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:815:constants.TaskTypeIntegrationMonthlyFinalize", + "kind": "worker", + "code_entry": "cmd/worker/main.go:815 integrationHandler.HandleMonthlyFinalize", + "owner": "integration_monthly_finalize", + "summary": "处理异步任务 constants.TaskTypeIntegrationMonthlyFinalize", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "integration_monthly_finalize.handle_monthly_finalize", + "action_name": "处理异步任务(constants.TaskTypeIntegrationMonthlyFinalize)", + "category": "business", + "risk": "normal", + "primary_resource": "integration_monthly_finalize", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:816:constants.TaskTypeAuditMonthlyRetention", + "kind": "worker", + "code_entry": "cmd/worker/main.go:816 task.NewAuditMonthlyRetentionHandler.Handle", + "owner": "audit_monthly_retention", + "summary": "处理异步任务 constants.TaskTypeAuditMonthlyRetention", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "audit_monthly_retention.handle", + "action_name": "处理异步任务(constants.TaskTypeAuditMonthlyRetention)", + "category": "business", + "risk": "normal", + "primary_resource": "audit_monthly_retention", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:827:constants.TaskTypeAuditDailyArchive", + "kind": "worker", + "code_entry": "cmd/worker/main.go:827 task.NewAuditDailyArchiveHandler.Handle", + "owner": "audit_daily_archive", + "summary": "处理异步任务 constants.TaskTypeAuditDailyArchive", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "audit_daily_archive.handle", + "action_name": "处理异步任务(constants.TaskTypeAuditDailyArchive)", + "category": "business", + "risk": "normal", + "primary_resource": "audit_daily_archive", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:829:constants.TaskTypeIntegrationDailyArchive", + "kind": "worker", + "code_entry": "cmd/worker/main.go:829 integrationHandler.HandleDaily", + "owner": "integration_daily_archive", + "summary": "处理异步任务 constants.TaskTypeIntegrationDailyArchive", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "integration_daily_archive.handle_daily", + "action_name": "处理异步任务(constants.TaskTypeIntegrationDailyArchive)", + "category": "business", + "risk": "normal", + "primary_resource": "integration_daily_archive", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:830:constants.TaskTypeIntegrationMonthlyFinalize", + "kind": "worker", + "code_entry": "cmd/worker/main.go:830 integrationHandler.HandleMonthlyFinalize", + "owner": "integration_monthly_finalize", + "summary": "处理异步任务 constants.TaskTypeIntegrationMonthlyFinalize", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "integration_monthly_finalize.handle_monthly_finalize", + "action_name": "处理异步任务(constants.TaskTypeIntegrationMonthlyFinalize)", + "category": "business", + "risk": "normal", + "primary_resource": "integration_monthly_finalize", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:cmd/worker/main.go:831:constants.TaskTypeAuditMonthlyRetention", + "kind": "worker", + "code_entry": "cmd/worker/main.go:831 task.NewAuditMonthlyRetentionHandler.Handle", + "owner": "audit_monthly_retention", + "summary": "处理异步任务 constants.TaskTypeAuditMonthlyRetention", + "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", + "domain_ledger": "既有业务表是状态事实;Audit Event 不替代业务模型", + "integration_log": "Worker 调用外部系统时必须;纯本地处理 N/A", + "outbox": "任务来源 Outbox/业务任务事实;消费端按稳定事件或任务 ID 幂等", + "action_code": "audit_monthly_retention.handle", + "action_name": "处理异步任务(constants.TaskTypeAuditMonthlyRetention)", + "category": "business", + "risk": "normal", + "primary_resource": "audit_monthly_retention", + "affected_resource": "任务载荷定位的直接业务资源", + "actor_source": "system_task/asynq", + "visibility": "内部系统入口;外部主体只读取对应业务安全投影", + "transaction": "业务状态变化、领域流水和 Audit Event 按用例原子提交", + "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", + "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", + "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" + }, + { + "key": "worker:pkg/queue/handler.go:114:constants.TaskTypeIotCardImport", + "kind": "worker", + "code_entry": "pkg/queue/handler.go:114 iotCardImportHandler.HandleIotCardImport", "owner": "iot_card_import", "summary": "处理异步任务 constants.TaskTypeIotCardImport", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14432,12 +15324,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:124:constants.TaskTypeOrderPackageInvalidate", + "key": "worker:pkg/queue/handler.go:127:constants.TaskTypeOrderPackageInvalidate", "kind": "worker", - "code_entry": "pkg/queue/handler.go:124 orderPkgHandler.Handle", + "code_entry": "pkg/queue/handler.go:127 orderPkgHandler.Handle", "owner": "order_package_invalidate", "summary": "处理异步任务 constants.TaskTypeOrderPackageInvalidate", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14456,12 +15348,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:136:constants.TaskTypeAssetPackageBatchOrder", + "key": "worker:pkg/queue/handler.go:140:constants.TaskTypeAssetPackageBatchOrder", "kind": "worker", - "code_entry": "pkg/queue/handler.go:136 handler.Handle", + "code_entry": "pkg/queue/handler.go:140 handler.Handle", "owner": "asset_package_batch_order", "summary": "处理异步任务 constants.TaskTypeAssetPackageBatchOrder", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14480,12 +15372,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:155:constants.TaskTypeDeviceImport", + "key": "worker:pkg/queue/handler.go:160:constants.TaskTypeDeviceImport", "kind": "worker", - "code_entry": "pkg/queue/handler.go:155 deviceImportHandler.HandleDeviceImport", + "code_entry": "pkg/queue/handler.go:160 deviceImportHandler.HandleDeviceImport", "owner": "device_import", "summary": "处理异步任务 constants.TaskTypeDeviceImport", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14504,12 +15396,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:189:constants.TaskTypeExportDispatch", + "key": "worker:pkg/queue/handler.go:194:constants.TaskTypeExportDispatch", "kind": "worker", - "code_entry": "pkg/queue/handler.go:189 dispatchHandler.HandleExportDispatch", + "code_entry": "pkg/queue/handler.go:194 dispatchHandler.HandleExportDispatch", "owner": "export_dispatch", "summary": "处理异步任务 constants.TaskTypeExportDispatch", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14528,12 +15420,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:192:constants.TaskTypeExportShard", + "key": "worker:pkg/queue/handler.go:197:constants.TaskTypeExportShard", "kind": "worker", - "code_entry": "pkg/queue/handler.go:192 shardHandler.HandleExportShard", + "code_entry": "pkg/queue/handler.go:197 shardHandler.HandleExportShard", "owner": "export_shard", "summary": "处理异步任务 constants.TaskTypeExportShard", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14552,12 +15444,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:195:constants.TaskTypeExportFinalize", + "key": "worker:pkg/queue/handler.go:200:constants.TaskTypeExportFinalize", "kind": "worker", - "code_entry": "pkg/queue/handler.go:195 finalizeHandler.HandleExportFinalize", + "code_entry": "pkg/queue/handler.go:200 finalizeHandler.HandleExportFinalize", "owner": "export_finalize", "summary": "处理异步任务 constants.TaskTypeExportFinalize", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14576,12 +15468,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:219:constants.TaskTypeCommissionStatsUpdate", + "key": "worker:pkg/queue/handler.go:224:constants.TaskTypeCommissionStatsUpdate", "kind": "worker", - "code_entry": "pkg/queue/handler.go:219 updateHandler.HandleCommissionStatsUpdate", + "code_entry": "pkg/queue/handler.go:224 updateHandler.HandleCommissionStatsUpdate", "owner": "commission_stats_update", "summary": "处理异步任务 constants.TaskTypeCommissionStatsUpdate", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14600,12 +15492,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:222:constants.TaskTypeCommissionStatsSync", + "key": "worker:pkg/queue/handler.go:227:constants.TaskTypeCommissionStatsSync", "kind": "worker", - "code_entry": "pkg/queue/handler.go:222 syncHandler.HandleCommissionStatsSync", + "code_entry": "pkg/queue/handler.go:227 syncHandler.HandleCommissionStatsSync", "owner": "commission_stats_sync", "summary": "处理异步任务 constants.TaskTypeCommissionStatsSync", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14624,12 +15516,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:225:constants.TaskTypeCommissionStatsArchive", + "key": "worker:pkg/queue/handler.go:230:constants.TaskTypeCommissionStatsArchive", "kind": "worker", - "code_entry": "pkg/queue/handler.go:225 archiveHandler.HandleCommissionStatsArchive", + "code_entry": "pkg/queue/handler.go:230 archiveHandler.HandleCommissionStatsArchive", "owner": "commission_stats_archive", "summary": "处理异步任务 constants.TaskTypeCommissionStatsArchive", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14648,12 +15540,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:235:constants.TaskTypeCommission", + "key": "worker:pkg/queue/handler.go:240:constants.TaskTypeCommission", "kind": "worker", - "code_entry": "pkg/queue/handler.go:235 commissionCalculationHandler.HandleCommissionCalculation", + "code_entry": "pkg/queue/handler.go:240 commissionCalculationHandler.HandleCommissionCalculation", "owner": "commission", "summary": "处理异步任务 constants.TaskTypeCommission", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14672,12 +15564,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:256:constants.TaskTypePollingRealname", + "key": "worker:pkg/queue/handler.go:261:constants.TaskTypePollingRealname", "kind": "worker", - "code_entry": "pkg/queue/handler.go:256 realnameHandler.Handle", + "code_entry": "pkg/queue/handler.go:261 realnameHandler.Handle", "owner": "polling_realname", "summary": "处理异步任务 constants.TaskTypePollingRealname", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14696,12 +15588,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:257:constants.TaskTypePollingCarddata", + "key": "worker:pkg/queue/handler.go:262:constants.TaskTypePollingCarddata", "kind": "worker", - "code_entry": "pkg/queue/handler.go:257 carddataHandler.Handle", + "code_entry": "pkg/queue/handler.go:262 carddataHandler.Handle", "owner": "polling_carddata", "summary": "处理异步任务 constants.TaskTypePollingCarddata", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14720,12 +15612,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:258:constants.TaskTypePollingPackage", + "key": "worker:pkg/queue/handler.go:263:constants.TaskTypePollingPackage", "kind": "worker", - "code_entry": "pkg/queue/handler.go:258 packageHandler.Handle", + "code_entry": "pkg/queue/handler.go:263 packageHandler.Handle", "owner": "polling_package", "summary": "处理异步任务 constants.TaskTypePollingPackage", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14744,12 +15636,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:259:constants.TaskTypePollingProtect", + "key": "worker:pkg/queue/handler.go:264:constants.TaskTypePollingProtect", "kind": "worker", - "code_entry": "pkg/queue/handler.go:259 protectHandler.Handle", + "code_entry": "pkg/queue/handler.go:264 protectHandler.Handle", "owner": "polling_protect", "summary": "处理异步任务 constants.TaskTypePollingProtect", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14768,12 +15660,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:260:constants.TaskTypePollingCardStatus", + "key": "worker:pkg/queue/handler.go:265:constants.TaskTypePollingCardStatus", "kind": "worker", - "code_entry": "pkg/queue/handler.go:260 cardStatusHandler.Handle", + "code_entry": "pkg/queue/handler.go:265 cardStatusHandler.Handle", "owner": "polling_card_status", "summary": "处理异步任务 constants.TaskTypePollingCardStatus", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14792,12 +15684,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:274:constants.TaskTypePackageFirstActivation", + "key": "worker:pkg/queue/handler.go:279:constants.TaskTypePackageFirstActivation", "kind": "worker", - "code_entry": "pkg/queue/handler.go:274 packageActivationHandler.HandlePackageFirstActivation", + "code_entry": "pkg/queue/handler.go:279 packageActivationHandler.HandlePackageFirstActivation", "owner": "package_first_activation", "summary": "处理异步任务 constants.TaskTypePackageFirstActivation", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14816,12 +15708,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:277:constants.TaskTypePackageQueueActivation", + "key": "worker:pkg/queue/handler.go:282:constants.TaskTypePackageQueueActivation", "kind": "worker", - "code_entry": "pkg/queue/handler.go:277 packageActivationHandler.HandlePackageQueueActivation", + "code_entry": "pkg/queue/handler.go:282 packageActivationHandler.HandlePackageQueueActivation", "owner": "package_queue_activation", "summary": "处理异步任务 constants.TaskTypePackageQueueActivation", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14840,12 +15732,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:283:constants.TaskTypeOrderExpire", + "key": "worker:pkg/queue/handler.go:288:constants.TaskTypeOrderExpire", "kind": "worker", - "code_entry": "pkg/queue/handler.go:283 orderExpireHandler.HandleOrderExpire", + "code_entry": "pkg/queue/handler.go:288 orderExpireHandler.HandleOrderExpire", "owner": "order_expire", "summary": "处理异步任务 constants.TaskTypeOrderExpire", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14864,12 +15756,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:289:constants.TaskTypeAlertCheck", + "key": "worker:pkg/queue/handler.go:294:constants.TaskTypeAlertCheck", "kind": "worker", - "code_entry": "pkg/queue/handler.go:289 alertCheckHandler.HandleAlertCheck", + "code_entry": "pkg/queue/handler.go:294 alertCheckHandler.HandleAlertCheck", "owner": "alert_check", "summary": "处理异步任务 constants.TaskTypeAlertCheck", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14888,12 +15780,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:295:constants.TaskTypeDataCleanup", + "key": "worker:pkg/queue/handler.go:300:constants.TaskTypeDataCleanup", "kind": "worker", - "code_entry": "pkg/queue/handler.go:295 dataCleanupHandler.HandleDataCleanup", + "code_entry": "pkg/queue/handler.go:300 dataCleanupHandler.HandleDataCleanup", "owner": "data_cleanup", "summary": "处理异步任务 constants.TaskTypeDataCleanup", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14912,12 +15804,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:302:constants.TaskTypeNotificationCleanup", + "key": "worker:pkg/queue/handler.go:307:constants.TaskTypeNotificationCleanup", "kind": "worker", - "code_entry": "pkg/queue/handler.go:302 cleanupHandler.Handle", + "code_entry": "pkg/queue/handler.go:307 cleanupHandler.Handle", "owner": "notification_cleanup", "summary": "处理异步任务 constants.TaskTypeNotificationCleanup", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14936,12 +15828,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:311:constants.TaskTypePackageExpiryReminder", + "key": "worker:pkg/queue/handler.go:316:constants.TaskTypePackageExpiryReminder", "kind": "worker", - "code_entry": "pkg/queue/handler.go:311 handler.Handle", + "code_entry": "pkg/queue/handler.go:316 handler.Handle", "owner": "package_expiry_reminder", "summary": "处理异步任务 constants.TaskTypePackageExpiryReminder", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14960,12 +15852,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:329:constants.TaskTypeAutoPurchaseAfterRecharge", + "key": "worker:pkg/queue/handler.go:335:constants.TaskTypeAutoPurchaseAfterRecharge", "kind": "worker", - "code_entry": "pkg/queue/handler.go:329 autoPurchaseHandler.ProcessTask", + "code_entry": "pkg/queue/handler.go:335 autoPurchaseHandler.ProcessTask", "owner": "auto_purchase_after_recharge", "summary": "处理异步任务 constants.TaskTypeAutoPurchaseAfterRecharge", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -14984,12 +15876,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:336:constants.TaskTypeDailyTrafficFlush", + "key": "worker:pkg/queue/handler.go:342:constants.TaskTypeDailyTrafficFlush", "kind": "worker", - "code_entry": "pkg/queue/handler.go:336 dailyTrafficFlushHandler.HandleDailyTrafficFlush", + "code_entry": "pkg/queue/handler.go:342 dailyTrafficFlushHandler.HandleDailyTrafficFlush", "owner": "daily_traffic_flush", "summary": "处理异步任务 constants.TaskTypeDailyTrafficFlush", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -15008,12 +15900,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:69:constants.TaskTypeEmailSend", + "key": "worker:pkg/queue/handler.go:70:constants.TaskTypeEmailSend", "kind": "worker", - "code_entry": "pkg/queue/handler.go:69 emailHandler.HandleEmailSend", + "code_entry": "pkg/queue/handler.go:70 emailHandler.HandleEmailSend", "owner": "email_send", "summary": "处理异步任务 constants.TaskTypeEmailSend", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -15032,12 +15924,12 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" }, { - "key": "worker:pkg/queue/handler.go:96:constants.TaskTypeCardObservationSeries", + "key": "worker:pkg/queue/handler.go:97:constants.TaskTypeCardObservationSeries", "kind": "worker", - "code_entry": "pkg/queue/handler.go:96 handler.Handle", + "code_entry": "pkg/queue/handler.go:97 handler.Handle", "owner": "card_observation_series", "summary": "处理异步任务 constants.TaskTypeCardObservationSeries", "audit_event": "按状态变化、人工触发、连续失败或高风险异常决定", @@ -15056,6 +15948,6 @@ "failure_strategy": "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", "sensitive_policy": "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", "before_after_policy": "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口" + "test_seam": "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口" } ] diff --git a/AI系统级规划提示词规范.md b/AI系统级规划提示词规范.md deleted file mode 100644 index 6515fb6..0000000 --- a/AI系统级规划提示词规范.md +++ /dev/null @@ -1,522 +0,0 @@ -# AI 系统级规划提示词规范 - -用途:在需要 AI 生成大功能、复杂系统或跨模块改造计划时,先粘贴本文的提示词,再补充业务背景。它的目标是让 AI 先收敛目标、边界、事实源、状态机、失败路径和验收标准,再生成不容易漂移的系统级 plan。 - -建议使用方式: - -1. 先把下面完整提示词复制给 AI。 -2. 再贴业务背景、已有代码约束、用户旅程或参考材料。 -3. 第一轮只让 AI 提澄清问题和共识摘要,不要直接进入最终 plan。 -4. 共识确认后,再要求 AI 生成完整系统级 plan。 -5. 最后要求 AI 进行反向审查,并把缺口合并回最终版。 - -## 完整提示词 - -````text -你现在不是代码执行者,而是“资深系统架构师 + 产品技术负责人 + 交付负责人”。 - -请帮我为一个功能/项目生成系统级实现计划。你的目标不是简单列任务,而是产出一份能约束后续 AI 执行、不容易漂移、能覆盖业务闭环的完整 plan。 - -在我明确说“开始实现”之前,不要写代码,不要改文件,不要进入实现。 - -# 一、工作方式 - -你必须按以下顺序工作: - -1. 先理解我提供的背景。 -2. 如果信息不足,不要直接生成最终 plan,先提出“关键澄清问题”。 -3. 澄清问题必须按优先级排序,最多分三组: - - 必须确认,否则 plan 会错 - - 建议确认,否则执行中可能漂移 - - 可以暂时由 AI 假设 -4. 当信息足够后,先输出“共识摘要”,让我确认。 -5. 共识确认后,再生成完整系统级 plan。 -6. 生成 plan 后,必须进行一次“反向审查”,找缺口、矛盾、漂移风险。 -7. 最后把审查发现合并进最终版 plan。 - -# 二、计划的核心原则 - -生成计划时必须遵守: - -- 不要只按 Model / Store / Service / Handler 拆任务。 -- 必须按“业务闭环”组织阶段。 -- 每个阶段都要回答:这个阶段让哪个用户、哪个业务流程真正可用? -- 所有计划必须明确: - - 做什么 - - 不做什么 - - 为什么现在做 - - 为什么不是以后做 - - 依赖什么 - - 如何验证完成 -- 对不确定的内容必须显式标记为“假设”或“待确认”,不能悄悄编造。 -- 不允许 scope creep。任何超出 MVP 的想法必须放到 V2 / 后置增强。 -- 必须区分: - - 事实源 - - 缓存 - - 派生数据 - - 日志/对象存储/审计材料 -- 涉及资金、权限、状态流转、异步任务、外部接口时,必须单独成节。 - -# 三、计划必须包含的章节 - -最终 plan 必须包含以下章节,不得省略。 - -## 0. 目标与边界 - -说明: - -- 项目/功能目标 -- 当前版本 MVP 范围 -- 明确不做什么 -- V2 / 后置增强 -- 关键原则 -- 核心业务闭环 - -必须写成清晰约束,例如: - -- “A 是事实源,B 只是缓存” -- “系统不保存 XXX 明文” -- “本阶段不做 XXX” -- “失败时必须 XXX” -- “用户可见结果必须 XXX” - -## 1. 角色与用户旅程 - -列出所有角色,例如: - -- 普通用户 -- 运营人员 -- 管理员 -- 第三方系统 -- 异步 worker -- 外部服务 - -每个角色都要有完整旅程: - -```text -角色 - -> 入口 - -> 操作 - -> 系统处理 - -> 状态变化 - -> 用户看到的结果 - -> 异常时如何处理 -``` - -如果某个角色没有闭环,要指出缺口。 - -## 2. 系统边界与模块职责 - -说明每个模块负责什么、不负责什么。 - -必须包含: - -- 前端职责 -- Handler/API 职责 -- Service 职责 -- Store/DB 职责 -- Worker/异步任务职责 -- 外部服务职责 -- Admin/运营后台职责 - -禁止把业务逻辑模糊地写成“后端处理”。 - -## 3. 当前代码与现状依据 - -如果你能访问代码库,必须先阅读相关文件,再写本节。 - -本节要列出: - -- 已有能力 -- 已有表/模型 -- 已有 API -- 已有服务/Store/Worker -- 可以复用的实现 -- 已知坑点 -- 与现有规范冲突的地方 - -每条重要判断都要带文件路径或明确依据。 - -如果不能访问代码库,必须说明“以下为基于用户描述的假设”。 - -## 4. 数据模型与事实源 - -必须写清楚: - -- 新增表 -- 修改表 -- 字段含义 -- 枚举值 -- 唯一约束 -- 索引 -- 软删除策略 -- 迁移策略 -- 回滚策略 -- 历史数据兼容 -- 哪些数据是事实源 -- 哪些数据只是缓存/快照/投影 - -涉及金额时必须写: - -- 金额单位 -- 精度策略 -- 是否允许负数 -- 四舍五入/截断规则 -- 资金流水是否可追溯 -- 是否允许直接改余额缓存 - -## 5. API 契约 - -列出所有接口: - -```text -METHOD /path -权限: -入参: -出参: -错误码: -状态变化: -幂等规则: -审计/日志: -``` - -必须包含: - -- 用户侧 API -- Admin API -- Webhook/API callback -- Worker 触发入口 -- 内部接口或复用点 - -新增 Handler 时必须说明文档生成器/路由注册同步要求。 - -## 6. 状态机 - -凡是有状态字段,必须写状态机。 - -格式: - -```text -状态: -- pending -- processing -- success -- failed -- cancelled - -允许流转: -pending -> processing -processing -> success -processing -> failed - -禁止流转: -success -> pending -failed -> processing - -并发规则: -- 同一资源同时只能有一个 pending -- 状态更新必须使用 WHERE status = expected -``` - -必须覆盖: - -- 正常路径 -- 失败路径 -- 取消路径 -- 重试路径 -- 人工处理路径 -- 状态不可逆规则 - -## 7. 权限与越权防护 - -必须说明: - -- 谁能访问 -- 如何识别身份 -- 如何判断资源归属 -- Handler 层做什么 -- Service 层做什么 -- Store 层是否需要过滤 -- Admin 是否有二次校验 -- 错误信息是否防止泄露资源存在性 - -要求: - -- 不得信任前端传入的用户身份、角色、权限字段。 -- 无权限和资源不存在是否统一返回,需要明确。 -- 所有敏感操作必须有审计。 - -## 8. 幂等、并发与事务 - -必须单独说明: - -- 创建类操作如何防重复 -- 状态流转如何防重复 -- 金额/库存/余额如何防并发错误 -- 异步任务是否可重复消费 -- Webhook 是否幂等 -- 请求重试会发生什么 -- 事务边界在哪里 -- 事务内禁止做什么 -- 事务提交后才做什么 - -必须写出关键策略,例如: - -```text -状态流转使用 WHERE id = ? AND status = ? -余额变更必须在同一事务内完成 -异步任务在事务提交后入队 -Webhook event_id 必须幂等 -``` - -## 9. 异步任务与外部服务 - -如果涉及外部服务或 Worker,必须写: - -- 任务类型 -- payload 结构 -- 入队时机 -- 消费逻辑 -- 重试策略 -- 幂等键 -- 失败后状态 -- 日志关键字 -- 外部服务超时策略 -- 外部服务返回未知状态时如何处理 - -禁止只写“调用第三方接口”。 - -## 10. 失败路径与异常处理矩阵 - -必须提供失败矩阵: - -| 场景 | 系统行为 | 状态变化 | 是否重试 | 用户可见结果 | Admin 如何处理 | -|---|---|---|---|---|---| - -至少覆盖: - -- 参数错误 -- 无权限 -- 资源不存在 -- 并发冲突 -- 重复请求 -- 外部接口失败 -- 外部接口超时 -- 结果未知 -- 事务失败 -- 异步任务失败 -- 数据不一致 -- 人工介入 - -## 11. Admin / 运营闭环 - -必须回答: - -- Admin 在哪里看到这件事? -- Admin 能筛选什么? -- Admin 能处理什么? -- Admin 处理后状态如何变化? -- 是否需要备注、原因、凭证? -- 是否写审计日志? -- 用户能否看到处理结果? - -如果没有 Admin 入口,必须说明为什么不需要。 - -## 12. 日志、审计与可观测性 - -必须列出: - -- 关键日志 -- 审计事件 -- 操作人 -- request_id / trace_id -- 重要状态变化 -- 外部请求/响应是否保存 -- 敏感字段如何脱敏 -- 排查问题时查哪些表、哪些日志 - -## 13. 前端 / 页面 / 交互契约 - -如果涉及前端,必须说明: - -- 页面入口 -- Tab / 弹窗 / 表单 -- 空态 -- 加载态 -- 错误态 -- 提交前确认 -- 成功后刷新哪些数据 -- 权限不足时如何展示 -- 移动端是否需要特殊处理 - -不要只写“新增页面”。 - -## 14. 分阶段实现计划 - -阶段必须按业务闭环拆,不要只按技术层拆。 - -每个 Phase 必须包含: - -```text -Phase N - 名称 - -目标: -本阶段完成后,哪个业务闭环可用: - -包含需求: -不包含: -依赖: -涉及模块: -关键表: -关键接口: -关键状态机: -关键风险: -验证方式: -完成标准: -``` - -阶段顺序必须解释为什么这样排。 - -如果有破坏性变更,必须单独阶段、单独部署、单独回滚。 - -## 15. 任务拆分 - -在系统级 plan 完成后,再拆 tasks。 - -每个 task 必须包含: - -- 任务目标 -- 修改文件范围 -- 输入依赖 -- 输出产物 -- 验证方式 -- 不允许做什么 -- 完成标准 - -不要把多个无关业务目标塞进同一个 task。 - -## 16. 验收标准 - -验收必须是可观察、可执行、可判定的。 - -格式: - -```text -1. 当用户执行 XXX 时,系统应 XXX -2. DB 中应出现 XXX -3. 日志中应出现 XXX -4. Admin 页面应能看到 XXX -5. 重复提交时不会 XXX -6. 外部服务失败时状态为 XXX -``` - -验收必须覆盖: - -- 用户主链路 -- Admin 处理链路 -- 权限拒绝 -- 幂等重复 -- 并发边界 -- 外部失败 -- 数据一致性 -- 文档/路由注册 - -如果项目不写自动化测试,则验收方式使用: - -- `go build` -- `rg` / 静态搜索 -- 数据库查询 -- curl / Postman 手工接口验证 -- Worker/API 日志 -- OpenAPI 文档生成检查 - -不要生成 `*_test.go`,除非我明确要求。 - -## 17. 风险与后置增强 - -必须分成: - -- 当前必须解决的风险 -- 可以接受但要记录的风险 -- V2 后置增强 -- 不再讨论的 rejected 方案 - -每个 rejected 方案都要写拒绝原因,防止后续 AI 反复重新考虑。 - -## 18. 反向审查 - -生成初稿后,必须审查以下问题: - -1. 是否有用户旅程断点? -2. 是否有数据事实源不清? -3. 是否有状态机缺口? -4. 是否有权限/越权风险? -5. 是否有幂等和并发风险? -6. 是否有外部服务失败路径? -7. 是否有 Admin/运营处理缺口? -8. 是否有资金/余额/库存类一致性风险? -9. 是否有接口文档/路由注册遗漏? -10. 是否有验收标准不可执行? -11. 是否有和项目规范冲突? -12. 是否有 scope creep? -13. 是否有“写入完成但读取侧没规划”的问题? -14. 是否有“后端完成但前端/运营入口缺失”的问题? - -审查后输出: - -```text -发现的问题: -影响: -修正方式: -是否已合并进最终 plan: -``` - -# 四、输出要求 - -输出必须使用中文。 - -最终输出结构: - -1. 关键澄清问题(如需要) -2. 共识摘要 -3. 完整系统级 plan -4. 分阶段计划 -5. 任务拆分 -6. MVP 验收清单 -7. 风险与后置增强 -8. 反向审查结果 - -不要空泛,不要只写原则。必须具体到业务状态、接口、数据、失败路径和验收证据。 - -# 五、我的项目特殊规范 - -以下规范必须遵守: - -- 使用中文交互、中文文档、中文注释、中文日志、中文用户错误消息。 -- 代码命名使用英文。 -- 严格遵守项目现有技术栈,不主动引入新框架或新依赖。 -- 遵守 Handler → Service → Store → Model 分层。 -- Handler 不写业务逻辑。 -- Service 承载业务规则。 -- Store 负责数据访问和事务。 -- 所有错误使用项目统一错误码。 -- 新增 Handler 必须同步路由注册和文档生成器。 -- 状态类字段用 int,类型/方式类字段用 string。 -- 所有枚举描述必须与 constants 保持一致。 -- 数据库不使用外键约束,不使用 GORM 关联标签。 -- 默认不写自动化测试,除非我明确要求。 -- 验证默认使用:go build、静态搜索、数据库查询、curl/Postman、日志、OpenAPI 文档生成检查。 - -# 六、现在请先做第一步 - -我接下来会提供功能背景。 - -你收到背景后,先不要生成最终 plan。 -请先输出: - -1. 你理解的目标 -2. 你理解的非目标 -3. 你认为必须确认的问题 -4. 你建议我补充的上下文 -5. 哪些地方可以先用假设推进 -```` diff --git a/CONTEXT_RESET_PLAN.md b/CONTEXT_RESET_PLAN.md new file mode 100644 index 0000000..7bfa46c --- /dev/null +++ b/CONTEXT_RESET_PLAN.md @@ -0,0 +1,477 @@ +# 项目 Context 全量重置实施方案 + +> 状态:待用户评审,未批准前禁止执行删除、迁移、初始化或全局卸载操作。 +> +> 性质:本文件是本次重置期间唯一执行契约,不是长期项目 Context。全部任务通过验证后删除,由 Git 历史保留。 + +## 1. 目标 + +在不修改业务代码、数据库迁移、测试、构建与部署配置的前提下: + +1. 移除旧 GSD、`.planning`、旧 OpenSpec、过期文档、临时研究和规范型 Skills。 +2. 保留外部系统接入资料,并统一收口到 `docs/integrations/`。 +3. 使用 OpenSpec 1.8.0 当前默认结构重新初始化 Codex 与 Claude 集成。 +4. 重建短小、稳定、渐进披露的 Agent Context。 +5. 重建一页系统架构地图和可判定的工程约束,不再把规范包装成 Skill。 +6. 从可执行代码、迁移、路由、配置和测试生成系统当前 **As-Is** 行为 Specs。 +7. 当前 Bug、不一致和不合理行为照实进入基线,不在本任务中修复。 +8. 后续所有行为修改通过独立 OpenSpec Change 完成。 + +## 2. 已确认决策 + +### 2.1 事实源 + +重置完成后的优先级: + +```text +可复现运行结果 / 自动化测试 + > 实际可达代码与生效配置 + > 数据库迁移与 Schema + > 经归档同步后的 openspec/specs + > ARCHITECTURE.md 系统地图与依赖边界 + > docs/engineering/工程约束.md 长期工程规则 + > docs/integrations 外部接口资料 + > AGENTS.md 导航 +``` + +- `openspec/specs/` 是已建立基线后的当前行为契约。 +- 基线生成期间若 Spec 与代码冲突,以当前实际行为修正 Spec,不修改代码。 +- 旧文档、旧 OpenSpec 和聊天记录不得作为新基线的规范来源。 +- 外部接入资料只说明第三方契约,不直接证明本系统已经实现对应行为。 + +### 2.2 Bug 处理 + +- 当前系统确实存在的 Bug、不合理行为和历史兼容行为,均按现状写入 Spec。 +- 不创建并行的 `Known deviations` 事实源,不要求用户逐条审查基线。 +- AI 可以在后续 Explore 中指出疑似问题,但必须通过新 Change 才能修改行为。 +- 建立基线时禁止顺手修复、重构、优化或理想化现有实现。 + +### 2.3 Context 分层 + +- 根 `AGENTS.md`:全局硬约束、常用验证命令、事实源优先级和导航,目标 80~120 行,150 行为软上限。 +- `openspec/specs/`:可观察业务行为、状态流转、权限、金额、失败与幂等语义。 +- `openspec/changes/`:未来单次变更的 proposal、delta specs、design、tasks。 +- `ARCHITECTURE.md`:系统职责、运行单元、模块地图、依赖方向、关键数据流和验证入口;只作地图,不重复业务 Spec。 +- `docs/integrations/`:按统一契约模板保存第三方版本、字段、签名、错误、重试、幂等、安全和验收证据。 +- `docs/engineering/工程约束.md`:每条规则使用稳定 ID、适用范围、正反例、机械检查、例外、Owner 和最后验证日期。 +- `docs/engineering/从零构建Agent友好项目最佳实践.md`:通用 Harness 方法和上述文档的生成/评审标准,不承载本项目业务事实。 +- Skills:只保留有明确输入、执行步骤、产物和验证的工作流;DTO、Model、路由、迁移、注释等声明式规则不做 Skill。 +- `.lh-harness/`:仅保存本次执行状态和证据,不是需求或行为事实源,不纳入 Git。 + +### 2.4 工作流边界 + +- 不使用 OMX;OMX 已完成本机卸载,本方案只做残留验证。 +- 不使用 GSD;本方案负责删除项目 `.planning` 及本机 GSD Skills/Agents/引擎文件。 +- 不使用旧 OpenSpec Change 管理本次 OpenSpec 自身重置。 +- 本文件是重置期间唯一任务契约;LongHorizon-Harness 只能镜像任务状态和审计证据,不得生成第二份计划。 +- 使用 Codex 原生 Subagent 处理可独立、只读或边界明确的任务;最终删除和验收由主 Agent 负责。 + +## 3. 当前盘点基线 + +以下数据仅用于评审,执行时必须重新盘点: + +| 对象 | 当前状态 | +|---|---| +| OpenSpec CLI | 1.8.0,与 npm 最新版一致 | +| OpenSpec Schema | 官方 `spec-driven`:proposal → specs → design → tasks | +| 主 Specs | 154 个 | +| 活跃 Changes | 10 个 | +| 归档 Changes | 130 个 | +| `.planning` | 51 个文件,约 600 KB | +| `.scratch` | 196 个文件,约 2.3 MB | +| `.sisyphus` | 22 个文件,约 384 KB | +| `docs` | 272 个文件,约 5.7 MB | +| `openspec` | 1106 个文件,约 8.1 MB | +| 当前工作树 | 已存在约 78 项未提交变更,必须原样保护 | +| 全局 GSD Skills | 57 个 `~/.codex/skills/gsd-*` 目录 | +| 全局 GSD Agents | 18 个角色,共 36 个 `.md/.toml` 文件 | +| GSD 引擎 | `~/.codex/get-shit-done/`,约 1.6 MB | +| GSD Manifest | `~/.codex/gsd-file-manifest.json` | + +## 4. 范围 + +### 4.1 必须删除或重建 + +#### 项目内 + +- `.planning/` +- `.scratch/` +- `.sisyphus/` +- `CONTEXT.md` +- 旧 `openspec/` 全部内容 +- 除外部接入资料外的旧 `docs/` 内容 +- 旧 OpenSpec 生成的 `.codex/prompts/opsx-*`、`.codex/skills/openspec-*` +- 旧 OpenSpec 生成的 `.claude/commands/opsx/`、`.claude/skills/openspec-*` +- 项目本地 DTO、Model、路由、迁移、DB 验证、注释、文档管理、API 契约等规范型 Skills +- 项目本地其他没有明确工作流价值的 Skills;默认删除,后续按真实需要重建 +- 现有 `AGENTS.md` 正文,替换为最小导航 +- `CLAUDE.md` 中重复项目规则,替换为对 `AGENTS.md` 的薄引用 + +#### 用户级 GSD + +- `~/.codex/skills/gsd-*` +- `~/.codex/agents/gsd-*.md` +- `~/.codex/agents/gsd-*.toml` +- `~/.codex/get-shit-done/` +- `~/.codex/gsd-file-manifest.json` +- `~/.codex/config.toml`、Hooks、Shell 配置中实际存在的 GSD 专属引用 + +### 4.2 必须保留 + +- `cmd/`、`internal/`、`pkg/` 等业务代码 +- `migrations/` 中数据库迁移 +- 所有 `_test.go`、`tests/` 和测试基础设施 +- `go.mod`、`go.sum`、Makefile、脚本、Docker、CI/CD 与部署配置 +- 业务运行需要的配置模板和环境变量说明 +- 外部系统接入资料,迁移后统一位于 `docs/integrations/` +- `docs/engineering/从零构建Agent友好项目最佳实践.md` +- `.lh-harness/` 运行能力,但其运行数据必须保持 Git 未跟踪 +- 用户级非 GSD Skills、插件和 Codex 配置 +- 当前所有未提交业务修改 + +### 4.3 明确不做 + +- 不修改任何业务行为。 +- 不修复已发现 Bug。 +- 不重构目录或迁移 DDD 用例。 +- 不新增领域对象、接口、抽象或依赖。 +- 不调用生产服务、真实支付渠道或外部审批系统。 +- 不把旧文档移动到仓库内 `legacy/`、`archive/` 或其他可搜索目录。 +- 不创建第二套 roadmap、PRD、issue、todo、status 或任务台账。 +- 不自动提交 Git Commit。 + +## 5. 目标结构 + +```text +AGENTS.md # 最小全局导航 +CLAUDE.md # 仅保留 Claude 工具差异和 AGENTS 引用 +README.md # 项目启动入口,不承载详细领域规则 +ARCHITECTURE.md # 一页系统地图、依赖方向和验证入口 + +openspec/ +├── config.yaml # 最小 schema 配置 +├── specs/ # As-Is 当前行为 +└── changes/ # 后续变更 + +docs/ +├── engineering/ +│ ├── 从零构建Agent友好项目最佳实践.md +│ └── 工程约束.md # 带规则 ID 和检查方式的项目约束 +└── integrations/ + ├── alipay/ + ├── wechat/ + ├── wecom/ + ├── fuiou/ + ├── gateway/ + ├── object-storage/ + └── _unclassified/ # 无法安全归类时的保守落点 + +.agents/skills/ +└── openspec-* # OpenSpec 1.8.0 官方生成 + +.claude/ +├── commands/opsx/ # OpenSpec 1.8.0 官方生成 +└── skills/openspec-* # OpenSpec 1.8.0 官方生成 +``` + +## 6. 外部接入资料判定规则 + +### 6.1 保留内容 + +- 第三方官方 API、字段、签名、回调、错误码和协议说明。 +- 本项目调用第三方所需的配置字段、环境变量和凭证名称说明,但不得包含真实密钥。 +- 沙箱/生产地址差异、限流、超时、重试和验签说明。 +- 必须人工完成的真实环境验收步骤。 + +### 6.2 不作为外部资料保留 + +- 某个功能的完成总结、测试总结或实施报告。 +- 重复描述本系统业务行为的 API 文档。 +- 已过期的内部设计方案、任务清单和前端联调总结。 +- 仅因文件名包含“微信/支付宝/企微”等字样,但正文实际属于业务需求的文档。 + +### 6.3 迁移策略 + +1. 从代码依赖、配置键、适配器和现有文档反向识别全部第三方系统。 +2. 建立原文件到 `docs/integrations//` 的迁移清单。 +3. 对保留文件计算 SHA-256;优先原样移动,不在本任务中重写内容。 +4. 内容混合时只提取第三方契约部分;提取前保留原文件于仓库外备份。 +5. 无法判断时移入 `_unclassified/`,不得直接删除。 +6. 迁移后检查所有仓库内引用并只修正文档链接;禁止修改业务代码引用。 + +## 7. 执行任务 + +所有任务必须按顺序完成,不得合并或跳过。每项只有通过对应验证后才能进入下一项。 + +### 0. 建立安全基线 + +**架构通道:Infrastructure / 工程治理。** 业务边界:仅保护工作树和 Context 文件;不迁移任何旧业务用例。 + +- [ ] 0.1 重新记录 `git status --porcelain=v1 -z`,区分已有修改与本任务修改。 +- [ ] 0.2 对受保护业务路径生成文件清单与 SHA-256 基线。 +- [ ] 0.3 将所有待删除 Context、GSD 文件和未跟踪 Context 打包到仓库外临时目录。 +- [ ] 0.4 生成备份 SHA-256 和可运行恢复脚本,并实际在临时目录解包验证。 +- [ ] 0.5 检查是否存在并发 Agent 正在写入待处理目录;存在时停止对应进程后再继续。 + +**验证:** + +- 备份归档可列出、可解包,恢复脚本通过临时目录演练。 +- 当前未提交业务文件数量、路径与哈希已记录。 +- 受保护路径没有因备份发生变化。 + +### 1. 卸载用户级 GSD + +**架构通道:Infrastructure / 工具链。** 业务边界:仅删除 GSD 用户级安装;不删除其他 Codex Skills、插件和配置。 + +- [ ] 1.1 依据 `gsd-file-manifest.json` 与实际文件系统取交集,形成精确删除集合。 +- [ ] 1.2 备份 GSD Skills、Agents、引擎、Manifest 及包含 GSD 引用的配置片段。 +- [ ] 1.3 删除 57 个 `gsd-*` Skill 目录及实际发现的新增 GSD Skill。 +- [ ] 1.4 删除 `gsd-*` Agent `.md/.toml` 文件。 +- [ ] 1.5 删除 `~/.codex/get-shit-done/` 与 `~/.codex/gsd-file-manifest.json`。 +- [ ] 1.6 只移除配置、Hooks、Shell 中确认属于 GSD 的引用。 + +**验证:** + +```bash +find ~/.codex/skills -maxdepth 1 -name 'gsd-*' +find ~/.codex/agents -maxdepth 1 -name 'gsd-*' +find ~/.codex -maxdepth 2 \( -name 'get-shit-done' -o -name 'gsd-file-manifest.json' \) +``` + +以上命令均应无输出;`codex --version` 必须成功。重新启动 Codex 后,Skill 列表不得再出现 GSD。 + +### 2. 验证 OMX 已完全退出 + +**架构通道:Infrastructure / 工具链。** 业务边界:只读验证,不重复执行卸载。 + +- [ ] 2.1 验证 `omx` 命令、两套 npm 包、用户 Hooks 与 `.omx` 目录均不存在。 +- [ ] 2.2 验证项目与用户级 Agent 指令中没有 `oh-my-codex` 或 `OMX:` 引用。 + +**验证:** `command -v omx` 无输出;Homebrew/NVM npm 列表无 `oh-my-codex`;Codex 可启动。 + +### 3. 收口外部接入资料 + +**架构通道:Infrastructure / Adapter 文档。** 业务边界:只移动第三方契约资料;不修改 Adapter、配置加载或业务调用逻辑。 + +- [ ] 3.1 从依赖、配置、Adapter 和现有文档识别第三方提供商全集。 +- [ ] 3.2 按第 6 节规则分类现有候选资料。 +- [ ] 3.3 创建 `docs/integrations//` 并移动保留资料。 +- [ ] 3.4 混合文档只提取第三方契约内容,原文进入仓库外备份。 +- [ ] 3.5 无法分类的资料进入 `_unclassified/`。 +- [ ] 3.6 修复保留文档之间的相对链接。 +- [ ] 3.7 为每个已分类 Provider 建立标准 `README.md`:Owner、官方来源与版本、适用环境、接入范围、端点/认证、请求响应、签名/回调、错误处置、幂等、超时重试、限流、安全、沙箱验证、人工验收、最后核验日期和更新触发条件。 + +**验证:** + +- 迁移前后保留内容哈希一致;发生提取时有逐文件差异记录。 +- `docs/integrations/` 之外不存在第三方接入资料副本。 +- 仓库中没有指向已删除旧文档的有效入口链接。 +- 项目实际使用的字段、签名、金额单位、错误和重试规则均能追溯到官方来源或脱敏真实样例。 + +### 4. 删除旧项目 Context + +**架构通道:Infrastructure / 工程治理。** 业务边界:删除说明与运行状态,不修改业务实现。 + +- [ ] 4.1 删除 `.planning/`、`.scratch/`、`.sisyphus/`、`CONTEXT.md`。 +- [ ] 4.2 删除旧 `openspec/`。 +- [ ] 4.3 删除 `docs/integrations/` 和已确认保留的 `docs/engineering/从零构建Agent友好项目最佳实践.md` 之外的旧 `docs/` 内容;`工程约束.md` 在任务 6 从零重建,不沿用旧正文。 +- [ ] 4.4 删除项目本地旧 OpenSpec 指令、重复集成和规范型 Skills。 +- [ ] 4.5 删除其余无明确工作流价值的项目本地 Skills。 +- [ ] 4.6 清空旧 `AGENTS.md` 和 `CLAUDE.md` 内容,立即进入任务 5 重建,避免仓库长期无入口。 + +**验证:** 删除目标不存在;旧业务文档与旧工程规范无残留;受保护业务路径哈希与任务 0 基线一致。 + +### 5. 使用 OpenSpec 1.8.0 重新初始化 + +**架构通道:Infrastructure / 工具链。** 业务边界:只生成官方 OpenSpec Harness,不创建业务 Change。 + +- [ ] 5.1 再次确认 `openspec --version` 为 1.8.0 或执行时 npm 最新稳定版。 +- [ ] 5.2 执行: + +```bash +openspec init . --tools codex,claude --force --no-animation +``` + +- [ ] 5.3 将 `openspec/config.yaml` 保持为最小配置: + +```yaml +schema: spec-driven +``` + +- [ ] 5.4 不恢复旧 context、rules、consensus、测试比例或工程规范块。 + +**验证:** + +```bash +openspec doctor --json +openspec schemas --json +openspec context --json +``` + +- Doctor healthy。 +- Schema 仅使用官方 `spec-driven`。 +- 生成的 Skills/Commands 标记来自当前 OpenSpec 版本。 +- 不存在旧 `.codex` OpenSpec 重复入口。 + +### 6. 重建最小 Agent 入口、系统地图和工程约束 + +**架构通道:Infrastructure / 工程治理。** 业务边界:只建立 Agent 导航,不定义或修改业务行为。 + +- [ ] 6.1 从代码和有效构建命令重新生成 80~120 行的 `AGENTS.md`。 +- [ ] 6.2 只保留:语言约束、不可替代技术栈、事实源优先级、受保护边界、常用验证命令、OpenSpec 入口和渐进披露规则。 +- [ ] 6.3 不复制 DTO、Model、路由、迁移、审计、测试、DDD 的长规范与示例。 +- [ ] 6.4 `CLAUDE.md` 只引用 `AGENTS.md` 并保留确有必要的 Claude 工具差异。 +- [ ] 6.5 确保 `.lh-harness/` 被忽略且不成为事实源。 +- [ ] 6.6 从可达入口和真实依赖生成 `ARCHITECTURE.md`:系统职责/非职责、运行单元、模块职责与入口、允许/禁止依赖、关键同步/异步数据流、信任/事务边界、结构验证命令和详细资料链接。 +- [ ] 6.7 从旧规范候选、代码惯例、构建配置和真实故障约束中重新建立 `docs/engineering/工程约束.md`;旧文档只用于发现候选,规则必须由当前代码、测试或配置重新证明。 +- [ ] 6.8 每条工程规则使用 `ENG--NNN`,包含状态、适用范围、单一 MUST/MUST NOT、理由、最小正反例、机械检查或人工原因、例外条件、Owner、最后验证日期和更新触发条件。 +- [ ] 6.9 DTO、Model、路由、迁移、注释等规则全部进入工程约束;不建立对应 Skill。能机械判断的规则链接现有检查器,不能判断的明确标记人工审查,不在本任务中为它们新造 Linter。 + +**验证:** + +- `AGENTS.md` 不超过 150 行,所有链接有效。 +- 不包含 GSD、OMX、旧 `.planning`、旧 OpenSpec 路径。 +- 新 Codex 会话能够发现官方 OpenSpec Skills。 +- `ARCHITECTURE.md` 中每个运行单元和模块入口都能在仓库定位,禁止依赖有验证命令或明确人工门禁。 +- 每条工程约束都满足统一字段要求,且没有与 AGENTS、Specs 或外部契约重复的权威正文。 + +### 7. 从代码建立 As-Is Specs + +**主通道:Query / 只读分析;辅助通道:Infrastructure。** 业务边界:描述现有完整用例,不迁移、不修改任何旧 Service、Domain、Application、Query 或 Handler。 + +- [ ] 7.1 从真实路由、Handler、Application/Service、Domain、Query、常量、迁移、配置和测试生成业务能力清单。 +- [ ] 7.2 按可独立理解的业务能力分批创建 `openspec/specs//spec.md`,避免按 Handler、Model、表或技术组件机械拆分。 +- [ ] 7.3 每个 Requirement 使用可观察 `SHALL`,Scenario 使用 GIVEN/WHEN/THEN。 +- [ ] 7.4 状态、权限、金额、失败、回调、重试和幂等行为按当前实现记录,包括 Bug。 +- [ ] 7.5 队列、包名、函数名、GORM 标签等实现细节不得进入行为 Requirement。 +- [ ] 7.6 死代码、不可达分支和仅存在于旧文档的行为不得进入 Spec。 +- [ ] 7.7 每个能力由独立 Auditor 对照代码证据检查;无需用户逐条批准。 +- [ ] 7.8 每批执行 OpenSpec 校验,通过后继续下一批。 + +建议批次,仅作为发现起点,最终以代码边界为准: + +1. 认证、账号、组织与权限 +2. 店铺、角色和数据范围 +3. 卡、设备、资产与企业授权 +4. 套餐、订购、激活与流量 +5. 订单、支付、充值、钱包与退款 +6. 佣金、提现与资金流水 +7. 轮询、异步任务、通知与审计 +8. 第三方回调与外部集成可观察行为 + +**验证:** + +```bash +openspec validate --all +``` + +- 所有 Specs 格式有效。 +- 每个 Capability 至少有一个执行其 Requirement 的 Scenario。 +- 抽样从路由到持久化端到端追踪,Spec 与现有行为一致。 +- Spec 不引用已删除旧文档。 +- 受保护业务路径哈希未因生成 Spec 改变。 + +### 8. 建立最小机械门禁 + +**架构通道:Infrastructure / 工程治理。** 业务边界:只验证 Context 健康,不改变业务实现。 + +- [ ] 8.1 复用现有命令建立一个最小 Context 健康检查入口;不引入新依赖。 +- [ ] 8.2 检查 OpenSpec、失效链接、禁止目录、GSD/OMX 残留和 `.lh-harness` Git 跟踪状态。 +- [ ] 8.3 不在本任务中实现 DTO、Model、路由等新的定制 Linter;后续出现真实遗漏时独立立项。 + +**验证:** 健康检查在当前仓库通过;故意构造一个临时失效链接或禁止目录时能够失败,随后恢复。 + +### 9. 最终审计与收尾 + +**架构通道:Infrastructure / 验证。** 业务边界:只核对结果和证据。 + +- [ ] 9.1 Auditor 重新检查全部删除、保留和目标结构。 +- [ ] 9.2 核对业务代码、迁移、测试、构建和部署配置未被本任务修改。 +- [ ] 9.3 核对 GSD、OMX、`.planning`、旧 OpenSpec 和旧文档无残留。 +- [ ] 9.4 核对所有外部接入资料已位于 `docs/integrations/`。 +- [ ] 9.5 核对 OpenSpec Doctor、Validate 和 Context 健康检查通过。 +- [ ] 9.6 输出删除清单、保留清单、移动映射、验证命令与字面结果。 +- [ ] 9.7 重新打开并验证备份和恢复脚本。 +- [ ] 9.8 删除本文件 `CONTEXT_RESET_PLAN.md`,确认 LongHorizon 临时状态未被 Git 跟踪。 + +## 8. As-Is 基线生成纪律 + +### 8.1 AI 自动决策 + +- 不要求用户逐条检查 Requirement。 +- 同一请求在不同角色、状态或配置下结果不同,应记录为多个 Scenario,而不是选择“更合理”的一个。 +- 测试与代码不一致时,先实际运行最小验证;能够复现的行为进入 Spec。 +- 无法运行时,以实际可达代码和生效配置为准,并由 Auditor 检查调用链。 +- AI 认为行为可能是 Bug 时仍照实记录,不增加价值判断。 + +### 8.2 只在以下情况请求用户输入 + +- 必须访问生产环境或真实第三方账户才能确认行为。 +- 两条实际可达路径对同一输入产生冲突,且无法通过本地配置判断生效路径。 +- 需要修改业务代码才能完成验证。 +- 外部接入资料可能包含真实凭证或合规敏感内容。 + +除上述情况外,按最保守、最接近当前实现的判断继续,不逐文件询问。 + +## 9. 验收标准 + +全部条件同时满足才算完成: + +1. `command -v omx` 无结果,系统和项目无 OMX 残留。 +2. 用户级 GSD Skills、Agents、引擎与 Manifest 全部删除,重启 Codex 后不再显示 GSD。 +3. `.planning`、`.scratch`、`.sisyphus`、旧 `CONTEXT.md` 和旧 OpenSpec 不存在。 +4. `docs/` 仅保留标准化的 `integrations/`、通用 Harness 最佳实践和从当前事实重建的 `工程约束.md`,没有旧业务资料。 +5. 外部接入资料内容已保留、集中且无有效断链。 +6. OpenSpec 使用执行时最新稳定版官方 `spec-driven` 初始化结果。 +7. OpenSpec 配置不包含旧架构、测试比例、无效 Artifact Rules 或大段全局注入。 +8. `AGENTS.md` 不超过 150 行,只承担导航和硬约束。 +9. `ARCHITECTURE.md` 能从系统职责导航到运行单元、模块入口、依赖方向、关键数据流和验证命令。 +10. 工程约束均有稳定 ID、范围、规则、证据/检查、例外和维护信息;规范型 Skills 已删除。 +11. As-Is Specs 覆盖代码中发现的全部对外业务能力,并通过 `openspec validate --all`。 +12. 当前 Bug 按现状记录,没有在本任务中被修正。 +13. 业务代码、迁移、测试、构建和部署配置没有因本任务发生变化。 +14. `.lh-harness/` 未被 Git 跟踪,仓库不存在第二套计划或状态事实源。 +15. 备份可解包,恢复脚本可运行。 +16. 本文件在最终验证后删除。 + +## 10. 回滚 + +执行前在仓库外生成: + +```text +/tmp/context-reset-/ +├── project-context.tar.gz +├── user-gsd.tar.gz +├── protected-files.sha256 +├── git-status-before.bin +├── move-map.tsv +└── rollback.sh +``` + +`rollback.sh` 必须做到: + +1. 验证备份 SHA-256。 +2. 只恢复本任务删除或移动的 Context 和 GSD 文件。 +3. 不覆盖任务开始前已存在的未提交业务修改。 +4. 恢复外部资料原路径与 OpenSpec 旧结构。 +5. 输出逐项恢复结果和退出码。 + +若任一受保护业务文件被本任务改动、备份无法恢复、外部资料丢失或 OpenSpec 初始化失败,立即停止后续任务并执行回滚。 + +## 11. 审计与公共能力决定 + +本任务只修改仓库 Context 与本机 Agent 工具链,不执行运行时业务用例: + +- Audit Event:N/A,不产生系统业务操作。 +- Domain Ledger:N/A,不修改金额、订单或状态事实。 +- Integration Log:N/A,不调用外部服务。 +- Outbox:N/A,不产生提交后可靠副作用。 +- 数据库迁移:N/A。 +- 业务架构迁移:N/A;明确不迁移任何旧 MVC/DDD 用例。 + +## 12. 决策来源 + +- OpenAI Harness Engineering:短 AGENTS 作为目录、仓库作为事实源、机械反馈闭环和持续清理熵。 +- OpenSpec Overview / Writing Specs / Explore / Reviewing Changes:主 Specs 表达当前行为,Changes 表达拟议差异,Requirement 与 Scenario 必须可观察、可判定。 +- AGENTS.md 实践资料:渐进披露,根文件只保存 Agent 无法自行发现且普遍适用的约束。 +- 长任务资料:执行状态与独立审计可以持久化,但不得复制或改写唯一任务契约。 diff --git a/README.md b/README.md index c1e5843..40e1eb8 100644 --- a/README.md +++ b/README.md @@ -943,10 +943,12 @@ rdb.Set(ctx, key, status, time.Hour) ## 文档 +- [多视角审计中心功能总结](docs/feature-504-multi-view-audit-center/多视角审计中心功能总结.md) - [平台基础审计调查接口](docs/feature-504-multi-view-audit-center/平台基础审计调查接口.md) -- [跨视角调查与前端导航契约](docs/feature-504-multi-view-audit-center/跨视角调查与前端导航契约.md) +- [审计链路与前端接入指南](docs/feature-504-multi-view-audit-center/跨视角调查与前端导航契约.md) - [外部集成调查接口](docs/feature-504-multi-view-audit-center/外部集成调查接口.md) - [代理和企业主体资源活动接口](docs/feature-504-multi-view-audit-center/主体资源活动接口.md) +- [审计归档灰度操作手册](docs/feature-504-multi-view-audit-center/归档灰度操作手册.md) ### 开发规范 diff --git a/cmd/audit-retention-simulate/main.go b/cmd/audit-retention-simulate/main.go new file mode 100644 index 0000000..b641f62 --- /dev/null +++ b/cmd/audit-retention-simulate/main.go @@ -0,0 +1,320 @@ +// Command audit-retention-simulate 在测试环境演练完整自然月归档与清理边界。 +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "os" + "strings" + "time" + + "github.com/bytedance/sonic" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "gorm.io/datatypes" + "gorm.io/gorm" + + auditarchive "github.com/break/junhong_cmp_fiber/internal/application/auditarchive" + auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" + "github.com/break/junhong_cmp_fiber/internal/model" + taskapp "github.com/break/junhong_cmp_fiber/internal/task" + "github.com/break/junhong_cmp_fiber/pkg/config" + "github.com/break/junhong_cmp_fiber/pkg/constants" + "github.com/break/junhong_cmp_fiber/pkg/database" + "github.com/break/junhong_cmp_fiber/pkg/storage" +) + +const ( + simulationMonth = "2001-02" + simulationInstance = "retention-simulation-2001-02" + simulationPrefix = "retention-simulation-2001-02" +) + +type simulationSummary struct { + Month string `json:"month"` + Days int `json:"days"` + ArchiveRuns int64 `json:"archive_runs"` + SuccessfulRuns int64 `json:"successful_runs"` + FinalIntegrationRuns int64 `json:"final_integration_runs"` + MaxRevision int `json:"max_revision"` + MaxAttemptCount int `json:"max_attempt_count"` + CompressionRatio float64 `json:"compression_ratio"` + DryRunEventCount int64 `json:"dry_run_event_count"` + DryRunResourceCount int64 `json:"dry_run_resource_count"` + DryRunIntegrationCount int64 `json:"dry_run_integration_count"` + EstimatedCleanupBatches int64 `json:"estimated_cleanup_batches"` + CleanupMarkersBefore int64 `json:"cleanup_markers_before"` + TargetRowsAfterCleanup int64 `json:"target_rows_after_cleanup"` + BoundaryRowsAfterCleanup int64 `json:"boundary_rows_after_cleanup"` + CleanupMarkersAfter int64 `json:"cleanup_markers_after"` + RetentionAuditRecorded bool `json:"retention_audit_recorded"` +} + +func main() { + if err := run(context.Background()); err != nil { + fmt.Fprintln(os.Stderr, "归档留存仿真失败:", err) + os.Exit(1) + } +} + +func run(ctx context.Context) error { + cfg, err := config.Load() + if err != nil { + return err + } + if !strings.Contains(strings.ToLower(cfg.Database.DBName), "test") || os.Getenv("JUNHONG_AUDIT_RETENTION_SIMULATION_CONFIRM") != cfg.Database.DBName { + return fmt.Errorf("仅允许显式确认的测试数据库,当前数据库为 %q", cfg.Database.DBName) + } + logger := zap.NewNop() + db, err := database.InitPostgreSQL(&cfg.Database, logger) + if err != nil { + return err + } + sqlDB, err := db.DB() + if err != nil { + return err + } + defer sqlDB.Close() + + provider, err := storage.NewS3Provider(&cfg.Storage) + if err != nil { + return err + } + auditWriter := auditinfra.NewWriter(auditinfra.NewRegistry(), nil) + service, err := auditarchive.NewService(db, provider, simulationInstance, auditWriter) + if err != nil { + return err + } + location, err := time.LoadLocation(constants.AuditArchiveTimezone) + if err != nil { + return err + } + monthStart, _ := time.ParseInLocation("2006-01", simulationMonth, location) + monthEnd := monthStart.AddDate(0, 1, 0) + if os.Getenv("JUNHONG_AUDIT_RETENTION_SIMULATION_RESUME") == "true" { + if err := assertReadyToResume(ctx, db, monthStart, monthEnd); err != nil { + return err + } + } else { + if err := assertEmptyTarget(ctx, db, monthStart, monthEnd); err != nil { + return err + } + if err := seedSimulation(ctx, db, monthStart, monthEnd); err != nil { + return err + } + if err := archiveMonth(ctx, db, service, monthStart, monthEnd); err != nil { + return err + } + } + + payload, _ := sonic.Marshal(taskapp.AuditMonthlyRetentionPayload{ArchiveMonth: simulationMonth}) + dryRunTask := asynq.NewTask(constants.TaskTypeAuditMonthlyRetention, payload) + if err := taskapp.NewAuditMonthlyRetentionHandler(service, logger, false).Handle(ctx, dryRunTask); err != nil { + return fmt.Errorf("月度只读演练失败: %w", err) + } + dryRun, err := service.ValidateMonth(ctx, monthStart) + if err != nil { + return err + } + summary, err := collectBeforeCleanup(ctx, db, monthStart, monthEnd, dryRun) + if err != nil { + return err + } + if summary.CleanupMarkersBefore != 0 { + return fmt.Errorf("只归档模式写入了 %d 个清理断点", summary.CleanupMarkersBefore) + } + + cleanupTask := asynq.NewTask(constants.TaskTypeAuditMonthlyRetention, payload) + if err := taskapp.NewAuditMonthlyRetentionHandler(service, logger, true).Handle(ctx, cleanupTask); err != nil { + return fmt.Errorf("隔离测试库物理清理演练失败: %w", err) + } + if err := collectAfterCleanup(ctx, db, monthStart, monthEnd, &summary); err != nil { + return err + } + if summary.TargetRowsAfterCleanup != 0 || summary.BoundaryRowsAfterCleanup != 6 || summary.CleanupMarkersAfter != int64(summary.Days*2) || !summary.RetentionAuditRecorded { + return fmt.Errorf("清理范围复核失败: target=%d boundary=%d markers=%d audit=%t", + summary.TargetRowsAfterCleanup, summary.BoundaryRowsAfterCleanup, summary.CleanupMarkersAfter, summary.RetentionAuditRecorded) + } + encoded, _ := sonic.MarshalIndent(summary, "", " ") + fmt.Println(string(encoded)) + return nil +} + +func assertReadyToResume(ctx context.Context, db *gorm.DB, start, end time.Time) error { + var events, integrations, runs, successful, final int64 + if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&events).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&integrations).Error; err != nil { + return err + } + query := db.WithContext(ctx).Model(&model.LogArchiveRun{}). + Where("archive_date >= ? AND archive_date < ? AND instance_id = ?", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance) + if err := query.Count(&runs).Error; err != nil { + return err + } + if err := query.Where("status = ?", constants.ArchiveStatusSuccess).Count(&successful).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}). + Where("archive_date >= ? AND archive_date < ? AND instance_id = ? AND source = ? AND is_final", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance, constants.IntegrationArchiveSource). + Count(&final).Error; err != nil { + return err + } + days := int64(end.Sub(start).Hours() / 24) + if events != days || integrations != days || runs != days*2 || successful != runs || final != days { + return fmt.Errorf("现有仿真状态不可安全续跑: events=%d integrations=%d runs=%d successful=%d final=%d", events, integrations, runs, successful, final) + } + return nil +} + +func assertEmptyTarget(ctx context.Context, db *gorm.DB, start, end time.Time) error { + var events, integrations, runs int64 + if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&events).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&integrations).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}).Where("archive_date >= ? AND archive_date < ?", start.Format(time.DateOnly), end.Format(time.DateOnly)).Count(&runs).Error; err != nil { + return err + } + if events != 0 || integrations != 0 || runs != 0 { + return fmt.Errorf("仿真目标月已有数据,拒绝清理: events=%d integrations=%d runs=%d", events, integrations, runs) + } + return nil +} + +func seedSimulation(ctx context.Context, db *gorm.DB, start, end time.Time) error { + return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + for date := start; date.Before(end); date = date.AddDate(0, 0, 1) { + result := constants.IntegrationResultSuccess + if date.Equal(start) { + result = constants.IntegrationResultPending + } + if err := seedFact(tx, date.Add(12*time.Hour), date.Format("20060102"), result); err != nil { + return err + } + } + if err := seedFact(tx, start.Add(-time.Second), "before", constants.IntegrationResultSuccess); err != nil { + return err + } + return seedFact(tx, end, "after", constants.IntegrationResultSuccess) + }) +} + +func seedFact(tx *gorm.DB, at time.Time, suffix, integrationResult string) error { + marker := simulationPrefix + "-" + suffix + hash := fmt.Sprintf("%x", sha256.Sum256([]byte(marker))) + event := model.AuditEvent{ + EventID: "evt_" + marker, OccurredAt: at, Category: constants.AuditCategoryReliability, + ActionCode: constants.AuditActionLogRetentionCleanup, ActionName: "归档留存仿真", Summary: "归档留存边界仿真数据", + ActorKind: constants.AuditActorSystemTask, ActorID: simulationInstance, ActorName: "归档留存仿真", + Source: constants.AuditSourceWorker, ScopeType: constants.AuditScopePlatform, + Result: constants.AuditResultSuccess, RiskLevel: constants.AuditRiskLow, + Metadata: datatypes.JSON([]byte(`{"simulation":true}`)), ContentHash: hash, CreatedAt: at, + } + if err := tx.Create(&event).Error; err != nil { + return err + } + resourceID := marker + resource := model.AuditEventResource{ + AuditEventID: event.ID, ResourceType: constants.AuditResourceLogArchiveMonth, ResourceID: &resourceID, + ResourceKey: marker, DisplayName: marker, Relation: constants.AuditResourceRelationPrimary, + Role: constants.AuditResourceRoleRetentionMonth, IdentitySnapshot: datatypes.JSON([]byte(`{"simulation":true}`)), + BeforeData: datatypes.JSON([]byte(`{}`)), AfterData: datatypes.JSON([]byte(`{}`)), + SubjectVisibility: constants.AuditSubjectInternalOnly, SubjectData: datatypes.JSON([]byte(`{}`)), CreatedAt: at, + } + if err := tx.Create(&resource).Error; err != nil { + return err + } + resourceType, resourceKey := constants.AuditResourceLogArchiveMonth, marker + integration := model.IntegrationLog{ + IntegrationID: "int_" + marker, Provider: "retention_simulation", Direction: "outbound", + Operation: "archive_boundary", ResourceType: &resourceType, ResourceID: &resourceID, ResourceKey: &resourceKey, + Attempt: 1, Result: integrationResult, ContentHash: hash, RequestSummary: datatypes.JSON([]byte(`{"simulation":true}`)), + ResponseSummary: datatypes.JSON([]byte(`{}`)), Metadata: datatypes.JSON([]byte(`{"simulation":true}`)), CreatedAt: at, UpdatedAt: at, + } + return tx.Create(&integration).Error +} + +func archiveMonth(ctx context.Context, db *gorm.DB, service *auditarchive.Service, start, end time.Time) error { + for date := start; date.Before(end); date = date.AddDate(0, 0, 1) { + if err := service.ArchiveDate(ctx, date); err != nil { + return err + } + if err := service.ArchiveIntegrationDate(ctx, date); err != nil { + return err + } + } + if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}). + Where("source = ? AND archive_date = ? AND instance_id = ?", constants.AuditArchiveSource, start.Format(time.DateOnly), simulationInstance). + Update("sha256", strings.Repeat("0", 64)).Error; err != nil { + return err + } + if err := service.ArchiveDate(ctx, start); err != nil { + return fmt.Errorf("Audit 故障重试演练失败: %w", err) + } + if err := db.WithContext(ctx).Model(&model.IntegrationLog{}). + Where("integration_id = ?", "int_"+simulationPrefix+"-"+start.Format("20060102")). + Updates(map[string]any{"result": constants.IntegrationResultSuccess, "updated_at": time.Now()}).Error; err != nil { + return err + } + return service.FinalizeIntegrationMonth(ctx, start) +} + +func collectBeforeCleanup(ctx context.Context, db *gorm.DB, start, end time.Time, dryRun auditarchive.RetentionResult) (simulationSummary, error) { + summary := simulationSummary{ + Month: simulationMonth, Days: int(end.Sub(start).Hours() / 24), + DryRunEventCount: dryRun.EventCount, DryRunResourceCount: dryRun.ResourceCount, + DryRunIntegrationCount: dryRun.IntegrationCount, EstimatedCleanupBatches: dryRun.EstimatedBatches, + } + var compressed, uncompressed int64 + err := db.WithContext(ctx).Model(&model.LogArchiveRun{}). + Where("archive_date >= ? AND archive_date < ? AND instance_id = ?", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance). + Select("COUNT(*) AS archive_runs, COUNT(*) FILTER (WHERE status = 'success') AS successful_runs, COUNT(*) FILTER (WHERE source = 'integration' AND is_final) AS final_integration_runs, COALESCE(MAX(revision), 0) AS max_revision, COALESCE(MAX(attempt_count), 0) AS max_attempt_count, COALESCE(SUM(compressed_bytes), 0) AS compressed, COALESCE(SUM(uncompressed_bytes), 0) AS uncompressed, COUNT(*) FILTER (WHERE cleanup_started_at IS NOT NULL OR cleaned_at IS NOT NULL) AS cleanup_markers_before"). + Row().Scan(&summary.ArchiveRuns, &summary.SuccessfulRuns, &summary.FinalIntegrationRuns, &summary.MaxRevision, &summary.MaxAttemptCount, &compressed, &uncompressed, &summary.CleanupMarkersBefore) + if err != nil { + return summary, err + } + if uncompressed > 0 { + summary.CompressionRatio = float64(compressed) / float64(uncompressed) + } + return summary, nil +} + +func collectAfterCleanup(ctx context.Context, db *gorm.DB, start, end time.Time, summary *simulationSummary) error { + var events, resources, integrations int64 + if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&events).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.AuditEventResource{}).Joins("JOIN tb_audit_event e ON e.id = tb_audit_event_resource.audit_event_id").Where("e.created_at >= ? AND e.created_at < ?", start, end).Count(&resources).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("created_at >= ? AND created_at < ?", start, end).Count(&integrations).Error; err != nil { + return err + } + summary.TargetRowsAfterCleanup = events + resources + integrations + if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("event_id IN ?", []string{"evt_" + simulationPrefix + "-before", "evt_" + simulationPrefix + "-after"}).Count(&events).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.AuditEventResource{}).Where("resource_key IN ?", []string{simulationPrefix + "-before", simulationPrefix + "-after"}).Count(&resources).Error; err != nil { + return err + } + if err := db.WithContext(ctx).Model(&model.IntegrationLog{}).Where("integration_id IN ?", []string{"int_" + simulationPrefix + "-before", "int_" + simulationPrefix + "-after"}).Count(&integrations).Error; err != nil { + return err + } + summary.BoundaryRowsAfterCleanup = events + resources + integrations + if err := db.WithContext(ctx).Model(&model.LogArchiveRun{}). + Where("archive_date >= ? AND archive_date < ? AND instance_id = ? AND cleanup_started_at IS NOT NULL AND cleaned_at IS NOT NULL", start.Format(time.DateOnly), end.Format(time.DateOnly), simulationInstance). + Count(&summary.CleanupMarkersAfter).Error; err != nil { + return err + } + var auditCount int64 + if err := db.WithContext(ctx).Model(&model.AuditEvent{}).Where("event_id = ?", "evt_retention_"+strings.ReplaceAll(simulationMonth, "-", "_")).Count(&auditCount).Error; err != nil { + return err + } + summary.RetentionAuditRecorded = auditCount == 1 + return nil +} diff --git a/cmd/worker/main.go b/cmd/worker/main.go index c5a3234..8966f67 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -147,7 +147,7 @@ func runWorker(cfg *config.Config) { taskHandler.RegisterHandlers() registerWeComApprovalTasks(taskHandler.GetMux(), runtime, cfg, appLogger) registerAgentRechargeRecoveryTask(taskHandler.GetMux(), runtime, appLogger) - registerAuditArchiveTask(taskHandler.GetMux(), runtime, appLogger) + registerAuditArchiveTask(taskHandler.GetMux(), runtime, cfg.Worker.AuditRetentionCleanupEnabled, appLogger) outboxHandler := outbox.NewHandler(runtime.outboxConsumers) taskHandler.GetMux().HandleFunc(constants.TaskTypeOutboxDeliver, outboxHandler.Handle) startOutboxRelay(ctx, runtime, cfg.Worker.InstanceName, appLogger) @@ -640,7 +640,6 @@ func startPollingScheduler( nil, nil, nil, nil, nil, runtime.gatewayClient, appLogger, - nil, ) trafficSyncer.SetRedisClient(runtime.redisClient) trafficSyncer.SetCardObservationService(runtime.workerResult.Services.CardObservation) @@ -687,7 +686,8 @@ func startAsynqScheduler(cfg *config.Config, redisAddr string, appLogger *zap.Lo } }() - appLogger.Info("Asynq Scheduler 已启动(企微审批恢复: 每 2 分钟,套餐临期提醒: 上海时区每日 03:00,Audit 归档: 每日 04:00,Integration 归档: 每日 04:30/月初 05:00,月度留存清理: 月初 06:00)") + appLogger.Info("Asynq Scheduler 已启动", + zap.Bool("audit_retention_cleanup_enabled", cfg.Worker.AuditRetentionCleanupEnabled)) return asynqScheduler } @@ -800,20 +800,20 @@ func registerAsynqScheduleTasks(asynqScheduler *asynq.Scheduler) error { asynq.Unique(27*24*time.Hour), asynq.Queue(constants.QueueForTaskType(constants.TaskTypeAuditMonthlyRetention)), )); err != nil { - return fmt.Errorf("注册月度日志留存清理任务失败: %w", err) + return fmt.Errorf("注册月度日志留存演练或清理任务失败: %w", err) } return nil } // registerAuditArchiveTask 注册 Audit 与 Integration 冷归档任务处理器。 -func registerAuditArchiveTask(mux *asynq.ServeMux, runtime *workerRuntime, appLogger *zap.Logger) { +func registerAuditArchiveTask(mux *asynq.ServeMux, runtime *workerRuntime, cleanupEnabled bool, appLogger *zap.Logger) { if runtime.storageSvc == nil { appLogger.Warn("对象存储未配置,审计归档任务将在执行时重试") mux.HandleFunc(constants.TaskTypeAuditDailyArchive, task.NewAuditDailyArchiveHandler(nil, appLogger).Handle) integrationHandler := task.NewIntegrationArchiveHandler(nil, appLogger) mux.HandleFunc(constants.TaskTypeIntegrationDailyArchive, integrationHandler.HandleDaily) mux.HandleFunc(constants.TaskTypeIntegrationMonthlyFinalize, integrationHandler.HandleMonthlyFinalize) - mux.HandleFunc(constants.TaskTypeAuditMonthlyRetention, task.NewAuditMonthlyRetentionHandler(nil, appLogger).Handle) + mux.HandleFunc(constants.TaskTypeAuditMonthlyRetention, task.NewAuditMonthlyRetentionHandler(nil, appLogger, cleanupEnabled).Handle) return } auditWriter, ok := runtime.workerResult.Services.PaymentAudit.(*auditInfra.Writer) @@ -828,12 +828,13 @@ func registerAuditArchiveTask(mux *asynq.ServeMux, runtime *workerRuntime, appLo integrationHandler := task.NewIntegrationArchiveHandler(service, appLogger) mux.HandleFunc(constants.TaskTypeIntegrationDailyArchive, integrationHandler.HandleDaily) mux.HandleFunc(constants.TaskTypeIntegrationMonthlyFinalize, integrationHandler.HandleMonthlyFinalize) - mux.HandleFunc(constants.TaskTypeAuditMonthlyRetention, task.NewAuditMonthlyRetentionHandler(service, appLogger).Handle) + mux.HandleFunc(constants.TaskTypeAuditMonthlyRetention, task.NewAuditMonthlyRetentionHandler(service, appLogger, cleanupEnabled).Handle) appLogger.Info("注册审计归档任务处理器", zap.String("audit_task_type", constants.TaskTypeAuditDailyArchive), zap.String("integration_daily_task_type", constants.TaskTypeIntegrationDailyArchive), zap.String("integration_monthly_task_type", constants.TaskTypeIntegrationMonthlyFinalize), - zap.String("retention_task_type", constants.TaskTypeAuditMonthlyRetention)) + zap.String("retention_task_type", constants.TaskTypeAuditMonthlyRetention), + zap.Bool("retention_cleanup_enabled", cleanupEnabled)) } // createTaskHandler 创建并返回包含全部任务处理器的 Asynq Handler。 diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 18431b7..2148497 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -26,160 +26,436 @@ components: AuditActorRef: properties: id: + description: 操作者稳定ID type: string kind: + description: 操作者类型 + enum: + - account + - personal_customer + - openapi + - system_task + - scheduled_job + - external_system type: string type: object - AuditEventPage: - properties: - items: - items: - $ref: '#/components/schemas/AuditEventView' - nullable: true - type: array - page: - type: integer - page_size: - type: integer - total: - type: integer - type: object - AuditEventView: + AuditEventDetail: properties: action_code: + description: 稳定动作编码;筛选和跳转必须使用该值 type: string action_name: + description: action_code对应的中文展示名称 type: string actor_enterprise_id: + description: 操作者所属企业ID快照 minimum: 0 nullable: true type: integer actor_enterprise_name: + description: 操作者所属企业名称快照 type: string actor_id: + description: 操作者稳定ID,与actor_kind共同定位操作者时间线 type: string actor_kind: + description: 操作者类型稳定编码 + enum: + - account + - personal_customer + - openapi + - system_task + - scheduled_job + - external_system type: string actor_name: + description: 事件发生时的操作者名称快照 type: string actor_shop_id: + description: 操作者所属店铺ID快照 minimum: 0 nullable: true type: integer actor_shop_name: + description: 操作者所属店铺名称快照 type: string batch_total: + description: 批次声明处理总数,非批次为0 type: integer category: + description: 动作类别稳定编码 + enum: + - configuration + - reliability + - asset + - security + - identity + - business type: string content_hash: + description: 事件不可变内容摘要 type: string correlation_id: + description: 跨请求业务链路ID,可传给关联时间线接口 type: string created_at: + description: 审计记录写入时间 format: date-time type: string error_code: + description: 失败或拒绝时的稳定错误码 type: string error_summary: + description: 已脱敏的失败原因摘要 type: string event_id: + description: 稳定审计事件ID,可传给事件详情接口 type: string fail_count: + description: 批次失败数,非批次为0 type: integer investigation_refs: $ref: '#/components/schemas/AuditInvestigationRefs' ip_address: + description: 触发请求的IP地址;非HTTP入口可为空 type: string metadata: additionalProperties: {} + description: 已脱敏的动作扩展元数据,字段由action_code定义 nullable: true type: object occurred_at: + description: 业务事实发生时间 format: date-time type: string parent_event_id: + description: 批量或异步链路的父审计事件ID type: string request_id: + description: HTTP请求关联ID,可传给请求时间线接口 type: string request_method: + description: 触发操作的HTTP方法;非HTTP入口可为空 type: string request_path: + description: 触发操作的HTTP路径;非HTTP入口可为空 type: string resources: + description: 事件涉及的全部资源及各自前后快照 items: $ref: '#/components/schemas/AuditResourceView' nullable: true type: array result: + description: 事件结果稳定编码 + enum: + - success + - failed + - denied + - partial + - unknown type: string + retention: + $ref: '#/components/schemas/RetentionInfo' risk_level: + description: 风险等级稳定编码 + enum: + - low + - normal + - high + - critical type: string scope_id: + description: 业务范围稳定ID,与scope_type共同使用 type: string scope_name: + description: 业务范围名称快照 type: string scope_type: + description: 业务范围类型稳定编码 + enum: + - platform + - shop + - personal_customer type: string source: + description: 操作入口来源稳定编码 + enum: + - admin_api + - personal_api + - openapi + - worker + - scheduler + - callback type: string success_count: + description: 批次成功数,非批次为0 type: integer summary: + description: 事件中文摘要 type: string user_agent: + description: 触发请求的User-Agent;非HTTP入口可为空 + type: string + type: object + AuditEventPage: + properties: + items: + description: 审计事件列表,按发生时间和主键稳定倒序 + items: + $ref: '#/components/schemas/AuditEventView' + nullable: true + type: array + page: + description: 当前页码 + type: integer + page_size: + description: 每页数量 + type: integer + retention: + $ref: '#/components/schemas/RetentionInfo' + total: + description: 符合条件的事件总数 + type: integer + type: object + AuditEventView: + properties: + action_code: + description: 稳定动作编码;筛选和跳转必须使用该值 + type: string + action_name: + description: action_code对应的中文展示名称 + type: string + actor_enterprise_id: + description: 操作者所属企业ID快照 + minimum: 0 + nullable: true + type: integer + actor_enterprise_name: + description: 操作者所属企业名称快照 + type: string + actor_id: + description: 操作者稳定ID,与actor_kind共同定位操作者时间线 + type: string + actor_kind: + description: 操作者类型稳定编码 + enum: + - account + - personal_customer + - openapi + - system_task + - scheduled_job + - external_system + type: string + actor_name: + description: 事件发生时的操作者名称快照 + type: string + actor_shop_id: + description: 操作者所属店铺ID快照 + minimum: 0 + nullable: true + type: integer + actor_shop_name: + description: 操作者所属店铺名称快照 + type: string + batch_total: + description: 批次声明处理总数,非批次为0 + type: integer + category: + description: 动作类别稳定编码 + enum: + - configuration + - reliability + - asset + - security + - identity + - business + type: string + content_hash: + description: 事件不可变内容摘要 + type: string + correlation_id: + description: 跨请求业务链路ID,可传给关联时间线接口 + type: string + created_at: + description: 审计记录写入时间 + format: date-time + type: string + error_code: + description: 失败或拒绝时的稳定错误码 + type: string + error_summary: + description: 已脱敏的失败原因摘要 + type: string + event_id: + description: 稳定审计事件ID,可传给事件详情接口 + type: string + fail_count: + description: 批次失败数,非批次为0 + type: integer + investigation_refs: + $ref: '#/components/schemas/AuditInvestigationRefs' + ip_address: + description: 触发请求的IP地址;非HTTP入口可为空 + type: string + metadata: + additionalProperties: {} + description: 已脱敏的动作扩展元数据,字段由action_code定义 + nullable: true + type: object + occurred_at: + description: 业务事实发生时间 + format: date-time + type: string + parent_event_id: + description: 批量或异步链路的父审计事件ID + type: string + request_id: + description: HTTP请求关联ID,可传给请求时间线接口 + type: string + request_method: + description: 触发操作的HTTP方法;非HTTP入口可为空 + type: string + request_path: + description: 触发操作的HTTP路径;非HTTP入口可为空 + type: string + resources: + description: 事件涉及的全部资源及各自前后快照 + items: + $ref: '#/components/schemas/AuditResourceView' + nullable: true + type: array + result: + description: 事件结果稳定编码 + enum: + - success + - failed + - denied + - partial + - unknown + type: string + risk_level: + description: 风险等级稳定编码 + enum: + - low + - normal + - high + - critical + type: string + scope_id: + description: 业务范围稳定ID,与scope_type共同使用 + type: string + scope_name: + description: 业务范围名称快照 + type: string + scope_type: + description: 业务范围类型稳定编码 + enum: + - platform + - shop + - personal_customer + type: string + source: + description: 操作入口来源稳定编码 + enum: + - admin_api + - personal_api + - openapi + - worker + - scheduler + - callback + type: string + success_count: + description: 批次成功数,非批次为0 + type: integer + summary: + description: 事件中文摘要 + type: string + user_agent: + description: 触发请求的User-Agent;非HTTP入口可为空 type: string type: object AuditFinanceAmountAuthority: properties: authoritative: + description: 当前amount或余额是否来自业务权威表 type: boolean conflict_rule: + description: 多来源冲突时的取值规则说明 type: string field: + description: 权威金额所在字段;非权威节点可为空 type: string table: + description: 权威金额所在业务表;非权威节点可为空 type: string type: object AuditFinanceTimelineNode: properties: amount: + description: 本节点金额,单位分;为空表示该节点不承载金额 nullable: true type: integer amount_authority: $ref: '#/components/schemas/AuditFinanceAmountAuthority' balance_after: + description: 变更后余额,单位分 nullable: true type: integer balance_before: + description: 变更前余额,单位分 nullable: true type: integer code: + description: 来源内稳定业务动作或状态编码 type: string currency: + description: 币种编码,人民币为CNY type: string facts: additionalProperties: {} + description: 该事实来源的安全结构化业务字段 nullable: true type: object investigation_refs: $ref: '#/components/schemas/AuditInvestigationRefs' node_id: + description: 该事实来源内的稳定节点ID type: string occurred_at: + description: 资金事实发生时间 format: date-time type: string record_source: + description: 资金事实来源稳定编码 + enum: + - audit_event + - domain_ledger_ref + - agent_wallet_transaction + - asset_wallet_transaction + - agent_wallet_reservation + - order + - payment + - refund + - agent_recharge + - recharge_order + - commission_record + - commission_withdrawal + - approval_instance type: string result: + description: 来源内原始结果或状态编码 type: string result_name: + description: result对应的中文展示名称 type: string shop_id: + description: 关联店铺ID minimum: 0 nullable: true type: integer title: + description: code对应的中文展示名称 type: string wallet: $ref: '#/components/schemas/AuditFinanceWalletRef' @@ -187,28 +463,40 @@ components: AuditFinanceTimelinePage: properties: items: + description: 按发生时间稳定倒序的资金事实节点 items: $ref: '#/components/schemas/AuditFinanceTimelineNode' nullable: true type: array page: + description: 当前页码 type: integer page_size: + description: 每页数量 type: integer + retention: + $ref: '#/components/schemas/RetentionInfo' total: + description: 关联资金事实总数 type: integer type: object AuditFinanceWalletRef: properties: resource_type: + description: 钱包资源类型 + enum: + - agent_wallet + - asset_wallet type: string wallet_id: + description: 钱包内部稳定ID,可作为finance/timeline的wallet_id minimum: 0 type: integer type: object AuditIntegrationRef: properties: integration_id: + description: 稳定外部集成记录ID type: string type: object AuditInvestigationRefs: @@ -216,20 +504,25 @@ components: actor_ref: $ref: '#/components/schemas/AuditActorRef' correlation_id: + description: 传给GET /audit/correlations/{correlation_id}/timeline nullable: true type: string event_id: + description: 传给GET /audit/events/{event_id} nullable: true type: string integration_refs: + description: integration_id传给GET /audit/integrations/{integration_id} items: $ref: '#/components/schemas/AuditIntegrationRef' nullable: true type: array request_id: + description: 传给GET /audit/requests/{request_id}/timeline nullable: true type: string resource_refs: + description: resource_type/resource_id传给GET /audit/resources/{resource_type}/{resource_id}/timeline;resource_id为空时不可跳转 items: $ref: '#/components/schemas/AuditInvestigationResourceRef' nullable: true @@ -238,37 +531,49 @@ components: AuditInvestigationResourceRef: properties: display_name: + description: 事件发生时的资源展示名称 type: string resource_id: + description: 资源内部稳定ID;为空时不展示平台资源时间线入口 nullable: true type: string resource_key: + description: 资源业务稳定Key,用于展示或精确搜索 type: string resource_type: + description: Resource Registry注册类型 type: string type: object AuditLinkTimeline: properties: access_log_lookup_request_id: + description: 可复制到Access Log检索的request_id;本接口自身不扫描Access Log nullable: true type: string correlation_id: + description: 本次按业务关联查询的稳定ID nullable: true type: string nodes: + description: 跨事实来源按发生时间稳定排序的节点 items: $ref: '#/components/schemas/AuditLinkTimelineNode' nullable: true type: array request_id: + description: 本次按请求查询的稳定ID nullable: true type: string + retention: + $ref: '#/components/schemas/RetentionInfo' type: object AuditLinkTimelineNode: properties: code: + description: 来源内稳定动作、操作或事件编码 type: string correlation_id: + description: 跨请求业务链路ID nullable: true type: string fidelity: @@ -276,174 +581,258 @@ components: investigation_refs: $ref: '#/components/schemas/AuditInvestigationRefs' node_id: + description: 该事实来源内的稳定节点ID type: string occurred_at: + description: 节点发生时间 format: date-time type: string parent_event_id: + description: 父审计事件ID nullable: true type: string record_source: + description: 事实来源 (audit_event:审计事件, integration_log:外部交互, outbox_event:可靠事件引用, asynq_task:异步任务引用, domain_ledger_ref:业务账本引用) + enum: + - audit_event + - integration_log + - outbox_event + - asynq_task + - domain_ledger_ref type: string reference_only: + description: true表示仅保存其他事实的引用,不代表该来源独立完成业务状态变更 type: boolean request_id: + description: HTTP请求关联ID nullable: true type: string resources: + description: 节点可稳定定位的资源引用 items: $ref: '#/components/schemas/AuditInvestigationResourceRef' nullable: true type: array result: + description: 来源内原始结果稳定编码 type: string result_name: + description: result对应的中文展示名称 type: string summary: + description: 已脱敏节点摘要 type: string title: + description: code对应的中文展示名称 type: string type: object AuditLinkageFidelity: properties: correlation_available: + description: 是否有稳定correlation_id type: boolean direct_audit_link_available: + description: 是否可直接跳转审计事件详情 type: boolean parent_event_available: + description: 是否有稳定parent_event_id type: boolean request_available: + description: 是否有稳定request_id type: boolean stable_resource_available: + description: 是否至少有一个含resource_id的稳定资源引用 type: boolean type: object AuditResourceCandidate: properties: display_name: + description: 资源展示名称 type: string historical: + description: 是否仅由历史事件快照解析,true不代表资源当前仍存在 type: boolean identity_snapshot: additionalProperties: {} + description: 当前业务表或历史事件保存的资源身份快照 nullable: true type: object resource_id: + description: 资源内部稳定ID,可传给通用资源时间线接口 type: string resource_key: + description: 资源业务稳定Key type: string resource_type: + description: 资源类型稳定编码 + enum: + - iot_card + - device + - shop + - order + - refund type: string type: object AuditResourceSearchPage: properties: items: + description: 当前资源或历史快照解析出的候选资源 items: $ref: '#/components/schemas/AuditResourceCandidate' nullable: true type: array page: + description: 当前页码 type: integer page_size: + description: 每页数量 type: integer + retention: + $ref: '#/components/schemas/RetentionInfo' total: + description: 符合精确标识的资源总数 type: integer type: object AuditResourceView: properties: after_data: additionalProperties: {} + description: 该资源变更后的完整平台审计数据 nullable: true type: object before_data: additionalProperties: {} + description: 该资源变更前的完整平台审计数据 nullable: true type: object created_at: + description: 资源关联记录写入时间 format: date-time type: string display_name: + description: 事件发生时的资源展示名称 type: string identity_snapshot: additionalProperties: {} + description: 事件发生时的资源身份快照 nullable: true type: object relation: + description: 资源关系 (primary:主要资源, affected:受影响资源, reference:引用资源) + enum: + - primary + - affected + - reference type: string resource_id: + description: 资源内部稳定ID nullable: true type: string resource_key: + description: 资源业务稳定Key type: string resource_type: + description: Resource Registry注册类型 type: string role: + description: Resource Registry定义的资源业务角色编码 type: string sort_order: + description: 资源在事件内的稳定展示顺序 type: integer subject_data: additionalProperties: {} + description: 写入时生成的主体安全字段,不等同于before_data或after_data nullable: true type: object subject_summary: + description: 允许代理或企业查看的安全摘要 type: string subject_visibility: + description: 主体可见性 (internal_only:仅平台, subject_result:主体可见结论, subject_detail:主体可见安全详情) + enum: + - internal_only + - subject_result + - subject_detail type: string type: object AuditRiskEventPage: properties: items: + description: 风险事件及稳定调查引用 items: $ref: '#/components/schemas/AuditEventView' nullable: true type: array page: + description: 当前页码 type: integer page_size: + description: 每页数量 type: integer + retention: + $ref: '#/components/schemas/RetentionInfo' total: + description: 符合条件的风险事件总数 type: integer type: object AuditRiskNamedCount: properties: code: + description: 当前聚合维度的稳定编码,具体枚举域由所属数组字段说明 type: string count: + description: 该编码的事件数量 type: integer name: + description: code对应的中文展示名称 type: string type: object AuditRiskOverview: properties: actions: + description: 动作分布,code为稳定action_code,name为中文action_name items: $ref: '#/components/schemas/AuditRiskNamedCount' nullable: true type: array bucket: + description: 服务端选择的趋势时间粒度 + enum: + - hour + - day type: string results: + description: 结果分布,code为success、failed、denied、partial或unknown,name为中文展示名 items: $ref: '#/components/schemas/AuditRiskNamedCount' nullable: true type: array + retention: + $ref: '#/components/schemas/RetentionInfo' risks: + description: 风险等级分布,code为low、normal、high或critical,name为中文展示名 items: $ref: '#/components/schemas/AuditRiskNamedCount' nullable: true type: array signals: + description: 固定信号分布,code为high_risk、finance、security、failed、denied、partial或unknown,name为中文展示名 items: $ref: '#/components/schemas/AuditRiskNamedCount' nullable: true type: array sources: + description: 来源分布,code为admin_api、personal_api、openapi、worker、scheduler或callback,name为中文展示名 items: $ref: '#/components/schemas/AuditRiskNamedCount' nullable: true type: array total: + description: 固定风险集合内的事件总数 type: integer trend: + description: 固定风险信号的时间趋势 items: $ref: '#/components/schemas/AuditRiskTrendPoint' nullable: true @@ -452,73 +841,105 @@ components: AuditRiskTrendPoint: properties: bucket_at: + description: 时间桶起点 format: date-time type: string denied: + description: 桶内denied事件数 type: integer failed: + description: 桶内failed事件数 type: integer finance: + description: 桶内资金类风险事件数 type: integer high_risk: + description: 桶内high或critical风险事件数 type: integer partial: + description: 桶内partial事件数 type: integer security: + description: 桶内安全类风险事件数 type: integer total: + description: 桶内固定风险集合事件数 type: integer unknown: + description: 桶内unknown事件数 type: integer type: object AuditSubjectActivity: properties: action_code: + description: 稳定动作编码 type: string action_name: + description: action_code对应的中文展示名称 type: string occurred_at: + description: 业务事实发生时间 format: date-time type: string related_resources: + description: 当前主体授权范围内的相关资源摘要 items: $ref: '#/components/schemas/AuditSubjectResourceSummary' nullable: true type: array result: + description: 活动结果稳定编码 + enum: + - success + - failed + - denied + - partial + - unknown type: string subject_data: additionalProperties: {} + description: 写入时生成的主体安全业务字段,不包含平台before/after或内部原因 nullable: true type: object subject_summary: + description: 写入时生成的主体安全摘要 type: string type: object AuditSubjectActivityPage: properties: items: + description: 不包含平台内部调查字段的安全活动列表 items: $ref: '#/components/schemas/AuditSubjectActivity' nullable: true type: array page: + description: 当前页码 type: integer page_size: + description: 每页数量 type: integer resource: $ref: '#/components/schemas/AuditSubjectResourceSummary' + retention: + $ref: '#/components/schemas/RetentionInfo' total: + description: 主体可见活动总数 type: integer type: object AuditSubjectResourceSummary: properties: display_name: + description: 资源安全展示名称 type: string resource_id: + description: 资源内部稳定ID;主体前端不据此调用平台审计接口 type: string resource_key: + description: 资源业务稳定Key type: string resource_type: + description: 资源类型稳定编码 type: string type: object DtoAccountPageResult: @@ -12057,45 +12478,111 @@ components: IntegrationAttemptView: properties: attempt: + description: 同一显式序列内从1开始的尝试序号 type: integer created_at: + description: 本次尝试记录创建时间 format: date-time type: string duration_ms: + description: 本次尝试耗时,单位毫秒 type: integer integration_id: + description: 本次尝试的稳定外部集成记录ID + type: string + operation: + description: 外部操作稳定编码 + enum: + - realname_callback + - realname_removal_callback + - payment_precreate + - payment_query + - payment_callback + - get_access_token + - list_visible_members + - list_visible_departments + - get_template_detail + - upload_approval_attachment + - submit_approval + - approval_callback + - get_approval_detail + - get_approval_info + - query_realname_status + - query_flow + - query_card_status + - query_device_info + - set_speed_tier + - stop_card + - start_card + - set_device_wifi + - set_device_switch_mode + - switch_device_card + - reboot_device + - reset_device + type: string + operation_name: + description: operation对应的中文展示名称 type: string result: + description: 本次尝试原始结果 + enum: + - pending + - success + - failed + - unknown + - not_found + - invalid_payload + - conflict + - ignored + - merged + - rate_limited + - completed + - cancelled type: string result_category: + description: 本次尝试的派生结果类别 + enum: + - processing + - succeeded + - indeterminate + - failed + - not_sent type: string result_name: + description: result对应的中文展示名称 type: string sent: + description: 是否实际向外部系统发送请求 type: boolean state_changed: + description: 本次尝试是否改变本地业务状态 type: boolean type: object IntegrationContentView: properties: content_hash: + description: 持久化内容摘要 type: string metadata: additionalProperties: {} + description: 按白名单重新清理的扩展元数据 nullable: true type: object request_summary: additionalProperties: {} + description: 按白名单重新清理的请求摘要 nullable: true type: object response_summary: additionalProperties: {} + description: 按白名单重新清理的响应摘要 nullable: true type: object type: object - IntegrationDetail: + IntegrationDetailResponse: properties: attempts: + description: 同一trigger_series下按attempt排序的技术尝试 items: $ref: '#/components/schemas/IntegrationAttemptView' nullable: true @@ -12112,6 +12599,8 @@ components: $ref: '#/components/schemas/IntegrationResourceView' result: $ref: '#/components/schemas/IntegrationResultView' + retention: + $ref: '#/components/schemas/RetentionInfo' timestamps: $ref: '#/components/schemas/IntegrationTimestampView' trigger: @@ -12119,240 +12608,461 @@ components: type: object IntegrationFidelityView: properties: + attempt_sequence_reliable: + description: attempt是否连续且operation一致 + type: boolean correlation_available: + description: 是否存在稳定correlation_id type: boolean provider_message_fidelity: + description: 外部消息保真等级;受限时只展示已脱敏摘要 type: string resource_id_available: + description: 是否存在稳定本地resource.id type: boolean trigger_series_available: + description: 是否存在显式trigger_series type: boolean type: object IntegrationIdentityView: properties: direction: + description: 交互方向稳定编码 + enum: + - inbound + - outbound type: string direction_name: + description: direction对应的中文展示名称 type: string external_id: + description: 外部系统业务或请求标识 nullable: true type: string integration_id: + description: 稳定外部集成记录ID type: string operation: + description: 外部操作稳定编码 + enum: + - realname_callback + - realname_removal_callback + - payment_precreate + - payment_query + - payment_callback + - get_access_token + - list_visible_members + - list_visible_departments + - get_template_detail + - upload_approval_attachment + - submit_approval + - approval_callback + - get_approval_detail + - get_approval_info + - query_realname_status + - query_flow + - query_card_status + - query_device_info + - set_speed_tier + - stop_card + - start_card + - set_device_wifi + - set_device_switch_mode + - switch_device_card + - reboot_device + - reset_device type: string operation_name: + description: operation对应的中文展示名称 type: string provider: + description: 外部服务提供方稳定编码 + enum: + - ctcc + - cmcc + - cucc + - wechat_pay + - alipay + - fuiou + - wecom + - gateway type: string provider_name: + description: provider对应的中文展示名称 type: string type: object IntegrationLinkageView: properties: audit_event_id: + description: 内部审计事件数据库引用;前端优先使用调查接口返回的稳定event_id minimum: 0 nullable: true type: integer correlation_id: + description: 传给GET /audit/correlations/{correlation_id}/timeline nullable: true type: string request_id: + description: 传给GET /audit/requests/{request_id}/timeline nullable: true type: string type: object IntegrationListItem: properties: correlation_id: + description: 跨步骤业务链路ID,可跳转关联时间线 nullable: true type: string created_at: + description: 外部交互记录创建时间 format: date-time type: string direction: + description: 交互方向稳定编码 + enum: + - inbound + - outbound type: string direction_name: + description: direction对应的中文展示名称 type: string duration_ms: + description: 交互耗时,单位毫秒 type: integer integration_id: + description: 稳定外部集成记录ID,可传给详情接口 type: string operation: + description: 外部操作稳定编码,可直接用于列表筛选 + enum: + - realname_callback + - realname_removal_callback + - payment_precreate + - payment_query + - payment_callback + - get_access_token + - list_visible_members + - list_visible_departments + - get_template_detail + - upload_approval_attachment + - submit_approval + - approval_callback + - get_approval_detail + - get_approval_info + - query_realname_status + - query_flow + - query_card_status + - query_device_info + - set_speed_tier + - stop_card + - start_card + - set_device_wifi + - set_device_switch_mode + - switch_device_card + - reboot_device + - reset_device type: string operation_name: + description: operation对应的中文展示名称 type: string provider: + description: 外部服务提供方稳定编码 + enum: + - ctcc + - cmcc + - cucc + - wechat_pay + - alipay + - fuiou + - wecom + - gateway type: string provider_name: + description: provider对应的中文展示名称 type: string request_id: + description: 来源HTTP请求ID,可跳转请求时间线 nullable: true type: string resource: $ref: '#/components/schemas/IntegrationResourceView' result: + description: 外部交互原始结果稳定编码 + enum: + - pending + - success + - failed + - unknown + - not_found + - invalid_payload + - conflict + - ignored + - merged + - rate_limited + - completed + - cancelled type: string result_category: + description: 由result派生的固定结果类别 + enum: + - processing + - succeeded + - indeterminate + - failed + - not_sent type: string result_name: + description: result对应的中文展示名称 type: string state_changed: + description: 本次交互是否改变本地业务状态 type: boolean type: object IntegrationListPage: properties: items: + description: 按创建时间和主键稳定倒序的外部交互 items: $ref: '#/components/schemas/IntegrationListItem' nullable: true type: array page: + description: 当前页码 type: integer page_size: + description: 每页数量 type: integer + retention: + $ref: '#/components/schemas/RetentionInfo' total: + description: 符合条件的外部交互总数 type: integer type: object IntegrationNamedCount: properties: code: + description: 当前聚合维度的稳定编码,枚举域由所属数组字段说明 type: string count: + description: 该编码的交互数量 type: integer name: + description: code对应的中文展示名称 type: string type: object IntegrationOverview: properties: anomaly_count: + description: failed或indeterminate类别的异常交互数 type: integer average_duration_ms: + description: 平均交互耗时,单位毫秒 type: number directions: + description: 方向分布,code为inbound或outbound,name为中文展示名 items: $ref: '#/components/schemas/IntegrationNamedCount' nullable: true type: array p95_duration_ms: + description: P95交互耗时,单位毫秒 type: number providers: + description: 提供方分布,code为provider枚举,name为中文展示名 items: $ref: '#/components/schemas/IntegrationNamedCount' nullable: true type: array results: + description: 原始结果分布,code/name/category分别为稳定编码、中文名和派生类别 items: $ref: '#/components/schemas/IntegrationResultCount' nullable: true type: array + retention: + $ref: '#/components/schemas/RetentionInfo' stale_pending_count: + description: 超过既定阈值仍为pending的交互数 type: integer state_changed_count: + description: 改变本地业务状态的交互数 type: integer total: + description: 外部交互总数 type: integer trend: + description: 五类派生结果的时间趋势 items: $ref: '#/components/schemas/IntegrationTrendPoint' nullable: true type: array unknown_count: + description: 原始结果为unknown的交互数 type: integer type: object IntegrationResourceView: properties: id: + description: 资源内部稳定ID;type和id均有值时可跳转资源时间线 nullable: true type: string key: + description: 资源业务稳定Key nullable: true type: string type: + description: Resource Registry注册类型 nullable: true type: string type: object IntegrationResultCount: properties: category: + description: 由code派生的固定结果类别 + enum: + - processing + - succeeded + - indeterminate + - failed + - not_sent type: string code: + description: 外部交互原始结果稳定编码 + enum: + - pending + - success + - failed + - unknown + - not_found + - invalid_payload + - conflict + - ignored + - merged + - rate_limited + - completed + - cancelled type: string count: + description: 该原始结果的交互数量 type: integer name: + description: code对应的中文展示名称 type: string type: object IntegrationResultView: properties: category: + description: 由code派生的固定结果类别 + enum: + - processing + - succeeded + - indeterminate + - failed + - not_sent type: string code: + description: 原始结果稳定编码 + enum: + - pending + - success + - failed + - unknown + - not_found + - invalid_payload + - conflict + - ignored + - merged + - rate_limited + - completed + - cancelled type: string duration_ms: + description: 交互耗时,单位毫秒 type: integer http_status: + description: 外部HTTP响应状态码 nullable: true type: integer name: + description: code对应的中文展示名称 type: string provider_code: + description: 外部服务稳定结果码 nullable: true type: string provider_message: + description: 已脱敏的外部结果摘要 nullable: true type: string recovery_strategy: + description: 已脱敏的既有恢复策略说明;本接口不执行恢复 nullable: true type: string state_changed: + description: 是否改变本地业务状态 type: boolean type: object IntegrationTimestampView: properties: created_at: + description: 记录创建时间 format: date-time type: string scheduled_at: + description: 计划发送时间 format: date-time nullable: true type: string started_at: + description: 实际开始时间 format: date-time nullable: true type: string updated_at: + description: 记录最后更新时间 format: date-time type: string type: object IntegrationTrendPoint: properties: bucket_at: + description: 时间桶起点 format: date-time type: string failed: + description: 桶内failed类别数量 type: integer indeterminate: + description: 桶内indeterminate类别数量 type: integer not_sent: + description: 桶内not_sent类别数量 type: integer processing: + description: 桶内processing类别数量 type: integer succeeded: + description: 桶内succeeded类别数量 type: integer total: + description: 桶内外部交互总数 type: integer type: object IntegrationTriggerView: properties: attempt: + description: 显式序列内的尝试序号 type: integer scene: + description: 触发业务场景 nullable: true type: string series: + description: 显式技术尝试序列ID;为空时禁止按时间或资源猜测重试关系 nullable: true type: string source: + description: 触发来源稳定编码 nullable: true type: string type: object @@ -12385,6 +13095,21 @@ components: url: type: string type: object + RetentionInfo: + properties: + archived_before: + description: 早于该时间的数据已归档;尚未清理时为空 + format: date-time + nullable: true + type: string + online_from: + description: 当前可在线查询的最早时间 + format: date-time + type: string + timezone: + description: 留存自然日时区 + type: string + type: object RoutesHealthResponse: properties: service: @@ -13627,29 +14352,50 @@ paths: - 代理预充值 /api/admin/agent/resource-activities/{resource_type}/{identifier}: get: - description: resource_type/identifier 来自代理当前业务页面稳定字段;店铺范围只读取认证上下文。仅返回写入时生成的安全业务结论和白名单详情,越权与不存在同错。 + description: 代理业务页映射:卡详情使用 `resource_type=iot_card`、`identifier=response.data.iccid`;设备详情使用 `device`、`response.data.virtual_no`;分配详情使用 `asset_allocation_record` 和分配单号;换货详情使用 `exchange_order` 和换货单号;店铺详情使用 `shop` 和店铺编号;企业详情使用 `enterprise` 和企业编号。身份与店铺范围只读取认证上下文,前端不得传入或推断。缺少稳定 identifier 时隐藏入口。仅查询 `retention` 标明的在线窗口。 parameters: - - description: 页码,默认1 + - description: 开始时间(RFC3339,含时区);默认从在线窗口开始 + in: query + name: created_from + schema: + description: 开始时间(RFC3339,含时区);默认从在线窗口开始 + type: string + - description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 + in: query + name: created_to + schema: + description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 + type: string + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer - - description: 资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业) + - description: 代理资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业) in: path name: resource_type required: true schema: - description: 资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业) + description: 代理资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业) + enum: + - iot_card + - device + - asset_allocation_record + - exchange_order + - shop + - enterprise type: string - description: 业务稳定标识;卡使用ICCID,设备使用VirtualNo,其他资源使用对应业务编号 in: path @@ -15356,31 +16102,42 @@ paths: - 资产管理 /api/admin/audit/actors/{kind}/{id}/events: get: - description: kind/id 来自事件 actor_ref 或平台账号选择器;历史名称直接使用事件快照,不查询当前账号名称覆盖历史。 + description: '`kind/id` 来自事件 `investigation_refs.actor_ref`;人工账号页也可使用 `kind=account`、`id=response.data.id`。历史名称使用响应 `actor_name` 快照,不以当前账号名称覆盖。action 使用事件 `action_code`,resource_type/resource_id 使用事件资源引用。' parameters: - - description: 稳定动作编码 + - description: 稳定动作编码;直接使用事件响应的action_code in: query name: action schema: - description: 稳定动作编码 + description: 稳定动作编码;直接使用事件响应的action_code type: string - - description: 事件结果 + - description: 事件结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) in: query name: result schema: - description: 事件结果 + description: 事件结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) + enum: + - success + - failed + - denied + - partial + - unknown type: string - - description: 风险等级 + - description: 风险等级 (low:低, normal:普通, high:高, critical:严重) in: query name: risk schema: - description: 风险等级 + description: 风险等级 (low:低, normal:普通, high:高, critical:严重) + enum: + - low + - normal + - high + - critical type: string - - description: 资源类型 + - description: Resource Registry注册类型;直接使用事件resources或investigation_refs返回的resource_type in: query name: resource_type schema: - description: 资源类型 + description: Resource Registry注册类型;直接使用事件resources或investigation_refs返回的resource_type type: string - description: 资源内部稳定ID in: query @@ -15400,27 +16157,36 @@ paths: schema: description: 结束时间(RFC3339,含时区,不包含该时刻) type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer - - description: 操作者类型 (account:人工账号, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) + - description: 操作者类型 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) in: path name: kind required: true schema: - description: 操作者类型 (account:人工账号, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) + description: 操作者类型 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) + enum: + - account + - personal_customer + - openapi + - system_task + - scheduled_job + - external_system type: string - description: 操作者稳定ID in: path @@ -15487,7 +16253,7 @@ paths: - 审计调查 /api/admin/audit/correlations/{correlation_id}/timeline: get: - description: correlation_id 来自审计、外部集成、可靠事件或业务详情的稳定引用。相同 correlation 只说明属于同一业务链路,不用于猜测技术重试。 + description: '`correlation_id` 来自事件 `investigation_refs.correlation_id` 或 Integration `correlation_id/linkage.correlation_id`。响应节点的 `investigation_refs` 可继续跳转其他视角。只组合在线持久化事实;相同 correlation 不等于技术重试,重试序列只认 Integration 的 `trigger.series`。' parameters: - description: 跨请求、异步任务和外部交互的稳定业务链路ID in: path @@ -15554,37 +16320,53 @@ paths: - 审计调查 /api/admin/audit/events: get: - description: 筛选值来自调查人员输入或其他调查节点的稳定引用;身份范围只读取认证上下文。固定按发生时间和事件ID倒序,不提供导出、修改或删除。 + description: 平台业务页按内部 ID 进入:卡 `resource_type=iot_card&resource_id=response.data.id`,设备 `device/id`,账号 `account/id`,店铺 `shop/id`,企业 `enterprise/id`,订单 `order/id`,退款 `refund/id`,充值 `agent_recharge/id`。筛选 action 必须使用响应 `action_code`,不可用中文名称反推。缺少稳定 ID 时隐藏入口。固定倒序分页,只查 `retention` 在线窗口,不提供导出、修改或删除。 parameters: - - description: 开始时间(RFC3339,含时区) + - description: 开始时间,RFC3339且含时区 in: query name: created_from schema: - description: 开始时间(RFC3339,含时区) + description: 开始时间,RFC3339且含时区 + example: "2026-08-01T00:00:00+08:00" type: string - - description: 结束时间(RFC3339,含时区,不包含该时刻) + - description: 结束时间,RFC3339且含时区,不包含该时刻 in: query name: created_to schema: - description: 结束时间(RFC3339,含时区,不包含该时刻) + description: 结束时间,RFC3339且含时区,不包含该时刻 + example: "2026-08-08T00:00:00+08:00" type: string - - description: 稳定动作编码 + - description: 稳定动作编码;直接使用事件响应的action_code,不按中文名称猜测 in: query name: action schema: - description: 稳定动作编码 + description: 稳定动作编码;直接使用事件响应的action_code,不按中文名称猜测 type: string - - description: 动作类别 + - description: 动作类别 (configuration:配置, reliability:可靠性, asset:资产, security:安全, identity:身份, business:业务) in: query name: category schema: - description: 动作类别 + description: 动作类别 (configuration:配置, reliability:可靠性, asset:资产, security:安全, identity:身份, business:业务) + enum: + - configuration + - reliability + - asset + - security + - identity + - business type: string - - description: 操作者类型 + - description: 操作者类型 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) in: query name: actor_kind schema: - description: 操作者类型 + description: 操作者类型 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) + enum: + - account + - personal_customer + - openapi + - system_task + - scheduled_job + - external_system type: string - description: 操作者稳定ID in: query @@ -15592,29 +16374,51 @@ paths: schema: description: 操作者稳定ID type: string - - description: 操作入口来源 + - description: 操作入口来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调) in: query name: source schema: - description: 操作入口来源 + description: 操作入口来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调) + enum: + - admin_api + - personal_api + - openapi + - worker + - scheduler + - callback type: string - description: 结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) in: query name: result schema: description: 结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) + enum: + - success + - failed + - denied + - partial + - unknown type: string - description: 风险等级 (low:低, normal:普通, high:高, critical:严重) in: query name: risk schema: description: 风险等级 (low:低, normal:普通, high:高, critical:严重) + enum: + - low + - normal + - high + - critical type: string - - description: 业务范围类型 + - description: 业务范围类型 (platform:平台, shop:店铺, personal_customer:个人客户) in: query name: scope_type schema: - description: 业务范围类型 + description: 业务范围类型 (platform:平台, shop:店铺, personal_customer:个人客户) + enum: + - platform + - shop + - personal_customer type: string - description: 业务范围稳定ID in: query @@ -15652,18 +16456,20 @@ paths: schema: description: 跨步骤业务链路ID type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer @@ -15725,7 +16531,7 @@ paths: - 审计调查 /api/admin/audit/events/{event_id}: get: - description: event_id 来自事件、资源、操作者或链路节点的 investigation_refs;返回全部资源快照和各资源 before/after。 + description: '`event_id` 来自列表的 `event_id` 或 `investigation_refs.event_id`。响应 `investigation_refs` 映射:`actor_ref.kind/id` → 操作者时间线;`resource_refs[].resource_type/resource_id` → 资源时间线;`request_id` → 请求时间线;`correlation_id` → 关联时间线;`integration_refs[].integration_id` → 外部集成详情。引用字段为空时隐藏对应入口,不按名称、时间或摘要猜测。' parameters: - description: 稳定审计事件ID in: path @@ -15745,7 +16551,7 @@ paths: example: 0 type: integer data: - $ref: '#/components/schemas/AuditEventView' + $ref: '#/components/schemas/AuditEventDetail' msg: description: 响应消息 example: success @@ -15792,7 +16598,7 @@ paths: - 审计调查 /api/admin/audit/finance/timeline: get: - description: 可使用店铺、钱包、订单、支付、退款、充值、审批、交易号、操作者、时间或 correlation 中任一稳定条件进入;关联事实由服务端解析,金额以钱包流水及对应业务表为权威。 + description: 任一稳定条件即可进入,关联事实由服务端补全:订单页 `order_id=response.data.id`,退款页 `refund_id=response.data.id`,充值页 `recharge_id=response.data.id`,钱包页 `wallet_id=response.data.id`,店铺页 `shop_id=response.data.id`;也支持各业务编号、第三方交易号、actor 或 correlation。金额单位为分,以 `amount_authority.authoritative=true` 指向的业务表字段为权威。 parameters: - description: 店铺ID in: query @@ -15873,11 +16679,18 @@ paths: schema: description: 第三方交易号 type: string - - description: 操作者类型;与actor_id同时提供 + - description: 操作者类型;与actor_id同时提供 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) in: query name: actor_kind schema: - description: 操作者类型;与actor_id同时提供 + description: 操作者类型;与actor_id同时提供 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统) + enum: + - account + - personal_customer + - openapi + - system_task + - scheduled_job + - external_system type: string - description: 操作者稳定ID;与actor_kind同时提供 in: query @@ -15903,18 +16716,20 @@ paths: schema: description: 结束时间(RFC3339,含时区,不包含该时刻) type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer @@ -15976,21 +16791,19 @@ paths: - 审计调查 /api/admin/audit/integrations: get: - description: 组合筛选来自调查输入或关联视角稳定引用,固定按创建时间和记录ID倒序分页,不提供任意摘要搜索。 + description: 组合筛选来自总览 code、事件调查引用或业务页稳定资源字段;operation 必须使用列表/详情返回的稳定编码。列表 `integration_id` 原样传给详情;`request_id/correlation_id` 可跳转链路时间线;`resource.type/resource.id` 均存在时可跳转资源时间线。固定倒序分页,不提供任意摘要搜索。 parameters: - - description: 开始时间(RFC3339,含时区,必填) + - description: 开始时间(RFC3339,含时区);默认从在线窗口开始 in: query name: created_from - required: true schema: - description: 开始时间(RFC3339,含时区,必填) + description: 开始时间(RFC3339,含时区);默认从在线窗口开始 type: string - - description: 结束时间(RFC3339,含时区,不包含该时刻,必填) + - description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 in: query name: created_to - required: true schema: - description: 结束时间(RFC3339,含时区,不包含该时刻,必填) + description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 type: string - description: 稳定外部集成记录ID in: query @@ -15998,35 +16811,93 @@ paths: schema: description: 稳定外部集成记录ID type: string - - description: 外部服务提供方稳定编码 + - description: 外部服务提供方 (ctcc:中国电信, cmcc:中国移动, cucc:中国联通, wechat_pay:微信支付, alipay:支付宝, fuiou:富友, wecom:企业微信, gateway:设备网关) in: query name: provider schema: - description: 外部服务提供方稳定编码 + description: 外部服务提供方 (ctcc:中国电信, cmcc:中国移动, cucc:中国联通, wechat_pay:微信支付, alipay:支付宝, fuiou:富友, wecom:企业微信, gateway:设备网关) + enum: + - ctcc + - cmcc + - cucc + - wechat_pay + - alipay + - fuiou + - wecom + - gateway type: string - description: 交互方向 (inbound:入站, outbound:出站) in: query name: direction schema: description: 交互方向 (inbound:入站, outbound:出站) + enum: + - inbound + - outbound type: string - - description: 外部操作稳定编码 + - description: 外部操作稳定编码;直接使用列表或详情响应的operation,不按中文名称猜测 in: query name: operation schema: - description: 外部操作稳定编码 + description: 外部操作稳定编码;直接使用列表或详情响应的operation,不按中文名称猜测 + enum: + - realname_callback + - realname_removal_callback + - payment_precreate + - payment_query + - payment_callback + - get_access_token + - list_visible_members + - list_visible_departments + - get_template_detail + - upload_approval_attachment + - submit_approval + - approval_callback + - get_approval_detail + - get_approval_info + - query_realname_status + - query_flow + - query_card_status + - query_device_info + - set_speed_tier + - stop_card + - start_card + - set_device_wifi + - set_device_switch_mode + - switch_device_card + - reboot_device + - reset_device type: string - description: 原始结果 (pending:待处理, success:成功, failed:失败, unknown:结果未知, not_found:未找到, invalid_payload:无效载荷, conflict:冲突, ignored:已忽略, merged:已合并, rate_limited:已限频, completed:已提前完成, cancelled:已取消) in: query name: result schema: description: 原始结果 (pending:待处理, success:成功, failed:失败, unknown:结果未知, not_found:未找到, invalid_payload:无效载荷, conflict:冲突, ignored:已忽略, merged:已合并, rate_limited:已限频, completed:已提前完成, cancelled:已取消) + enum: + - pending + - success + - failed + - unknown + - not_found + - invalid_payload + - conflict + - ignored + - merged + - rate_limited + - completed + - cancelled type: string - description: 派生结果类别 (processing:处理中, succeeded:成功, indeterminate:结果不确定, failed:失败, not_sent:未发送) in: query name: result_category schema: description: 派生结果类别 (processing:处理中, succeeded:成功, indeterminate:结果不确定, failed:失败, not_sent:未发送) + enum: + - processing + - succeeded + - indeterminate + - failed + - not_sent type: string - description: 外部系统业务或请求标识 in: query @@ -16034,11 +16905,11 @@ paths: schema: description: 外部系统业务或请求标识 type: string - - description: 本地主要资源类型 + - description: 本地主要资源类型;直接使用列表resource.type或调查引用的resource_type in: query name: resource_type schema: - description: 本地主要资源类型 + description: 本地主要资源类型;直接使用列表resource.type或调查引用的resource_type type: string - description: 本地主要资源稳定ID in: query @@ -16104,18 +16975,20 @@ paths: schema: description: 跨步骤业务链路ID type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer @@ -16177,7 +17050,7 @@ paths: - 审计调查 /api/admin/audit/integrations/{integration_id}: get: - description: integration_id 来自列表、通知目标 target_key 或调查节点稳定引用;只展示结构化详情和显式尝试序列,不提供重试、补偿、确认、绑定、恢复、修改、删除或导出。 + description: '`integration_id` 来自列表、事件 `investigation_refs.integration_refs[]`,或通知 `GET /notifications/{id}/target`:仅当 `available=true` 且 `target_type=integration_log` 时,将 `target_key` 原样作为 integration_id;否则隐藏入口。`linkage.request_id/correlation_id` 可跳转链路时间线;`fidelity` 为 false 时禁止按时间、资源或摘要猜测缺失关系。只读,不提供恢复、修改、删除或导出。' parameters: - description: 稳定外部集成记录ID,来自列表、通知target_key或调查节点 in: path @@ -16197,7 +17070,7 @@ paths: example: 0 type: integer data: - $ref: '#/components/schemas/IntegrationDetail' + $ref: '#/components/schemas/IntegrationDetailResponse' msg: description: 响应消息 example: success @@ -16244,21 +17117,19 @@ paths: - 审计调查 /api/admin/audit/integrations/overview: get: - description: 筛选和时间范围来自调查输入或关联视角跳转,身份只来自认证上下文。总览区分成功、处理中、结果不确定、失败和未发送终态。 + description: 筛选来自调查输入或其他视角稳定字段。`results[].code→result`,`results[].category→result_category`,`providers[].code→provider`,`directions[].code→direction`;所有 name 仅用于中文展示。趋势严格分为 processing、succeeded、indeterminate、failed、not_sent 五类,bucket 为 hour 或 day。 parameters: - - description: 开始时间(RFC3339,含时区,必填) + - description: 开始时间(RFC3339,含时区);默认从在线窗口开始 in: query name: created_from - required: true schema: - description: 开始时间(RFC3339,含时区,必填) + description: 开始时间(RFC3339,含时区);默认从在线窗口开始 type: string - - description: 结束时间(RFC3339,含时区,不包含该时刻,必填) + - description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 in: query name: created_to - required: true schema: - description: 结束时间(RFC3339,含时区,不包含该时刻,必填) + description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 type: string - description: 稳定外部集成记录ID in: query @@ -16266,35 +17137,93 @@ paths: schema: description: 稳定外部集成记录ID type: string - - description: 外部服务提供方稳定编码 + - description: 外部服务提供方 (ctcc:中国电信, cmcc:中国移动, cucc:中国联通, wechat_pay:微信支付, alipay:支付宝, fuiou:富友, wecom:企业微信, gateway:设备网关) in: query name: provider schema: - description: 外部服务提供方稳定编码 + description: 外部服务提供方 (ctcc:中国电信, cmcc:中国移动, cucc:中国联通, wechat_pay:微信支付, alipay:支付宝, fuiou:富友, wecom:企业微信, gateway:设备网关) + enum: + - ctcc + - cmcc + - cucc + - wechat_pay + - alipay + - fuiou + - wecom + - gateway type: string - description: 交互方向 (inbound:入站, outbound:出站) in: query name: direction schema: description: 交互方向 (inbound:入站, outbound:出站) + enum: + - inbound + - outbound type: string - - description: 外部操作稳定编码 + - description: 外部操作稳定编码;直接使用列表或详情响应的operation,不按中文名称猜测 in: query name: operation schema: - description: 外部操作稳定编码 + description: 外部操作稳定编码;直接使用列表或详情响应的operation,不按中文名称猜测 + enum: + - realname_callback + - realname_removal_callback + - payment_precreate + - payment_query + - payment_callback + - get_access_token + - list_visible_members + - list_visible_departments + - get_template_detail + - upload_approval_attachment + - submit_approval + - approval_callback + - get_approval_detail + - get_approval_info + - query_realname_status + - query_flow + - query_card_status + - query_device_info + - set_speed_tier + - stop_card + - start_card + - set_device_wifi + - set_device_switch_mode + - switch_device_card + - reboot_device + - reset_device type: string - description: 原始结果 (pending:待处理, success:成功, failed:失败, unknown:结果未知, not_found:未找到, invalid_payload:无效载荷, conflict:冲突, ignored:已忽略, merged:已合并, rate_limited:已限频, completed:已提前完成, cancelled:已取消) in: query name: result schema: description: 原始结果 (pending:待处理, success:成功, failed:失败, unknown:结果未知, not_found:未找到, invalid_payload:无效载荷, conflict:冲突, ignored:已忽略, merged:已合并, rate_limited:已限频, completed:已提前完成, cancelled:已取消) + enum: + - pending + - success + - failed + - unknown + - not_found + - invalid_payload + - conflict + - ignored + - merged + - rate_limited + - completed + - cancelled type: string - description: 派生结果类别 (processing:处理中, succeeded:成功, indeterminate:结果不确定, failed:失败, not_sent:未发送) in: query name: result_category schema: description: 派生结果类别 (processing:处理中, succeeded:成功, indeterminate:结果不确定, failed:失败, not_sent:未发送) + enum: + - processing + - succeeded + - indeterminate + - failed + - not_sent type: string - description: 外部系统业务或请求标识 in: query @@ -16302,11 +17231,11 @@ paths: schema: description: 外部系统业务或请求标识 type: string - - description: 本地主要资源类型 + - description: 本地主要资源类型;直接使用列表resource.type或调查引用的resource_type in: query name: resource_type schema: - description: 本地主要资源类型 + description: 本地主要资源类型;直接使用列表resource.type或调查引用的resource_type type: string - description: 本地主要资源稳定ID in: query @@ -16372,11 +17301,15 @@ paths: schema: description: 跨步骤业务链路ID type: string - - description: 趋势时间粒度 (hour:小时, day:自然日),默认hour + - description: 趋势时间粒度 (hour:小时, day:自然日) in: query name: bucket schema: - description: 趋势时间粒度 (hour:小时, day:自然日),默认hour + default: hour + description: 趋势时间粒度 (hour:小时, day:自然日) + enum: + - hour + - day type: string responses: "200": @@ -16436,7 +17369,7 @@ paths: - 审计调查 /api/admin/audit/requests/{request_id}/timeline: get: - description: request_id 来自审计或外部集成节点,也可由开发人员从 Access Log 粘贴。响应组合审计事件、外部交互、可靠事件和业务账本引用,但不扫描 Access Log 文件。 + description: '`request_id` 来自事件 `investigation_refs.request_id`、Integration `request_id/linkage.request_id`,也可从 Access Log 粘贴。响应节点的 `investigation_refs` 可继续跳转事件、资源、操作者或 Integration 详情。只组合在线持久化事实,不扫描 Access Log 或对象存储。' parameters: - description: HTTP请求关联ID,来自审计或外部集成节点,也可从Access Log粘贴 in: path @@ -16503,7 +17436,7 @@ paths: - 审计调查 /api/admin/audit/resources/{resource_type}/{resource_id}/timeline: get: - description: resource_type/resource_id 必须来自业务页面稳定字段、资源搜索结果或 investigation_refs。事件在资源作为 primary、affected 或 reference 时均会返回。 + description: '`resource_type/resource_id` 必须来自平台业务页的内部 `response.data.id`、资源搜索 `items[]` 或 `investigation_refs.resource_refs[]`。设备卡槽可使用资源引用返回的卡槽类型和 ID,不自行拼接。事件在资源作为 `primary/affected/reference` 时均返回;缺少 resource_id 时隐藏入口。' parameters: - description: 开始时间(RFC3339,含时区) in: query @@ -16517,30 +17450,38 @@ paths: schema: description: 结束时间(RFC3339,含时区,不包含该时刻) type: string - - description: 稳定动作编码 + - description: 稳定动作编码;直接使用事件响应的action_code in: query name: action schema: - description: 稳定动作编码 + description: 稳定动作编码;直接使用事件响应的action_code type: string - - description: 事件结果 + - description: 事件结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) in: query name: result schema: - description: 事件结果 + description: 事件结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) + enum: + - success + - failed + - denied + - partial + - unknown type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer @@ -16616,7 +17557,7 @@ paths: - 审计调查 /api/admin/audit/resources/search: get: - description: 卡支持 ICCID/VirtualNo,设备支持 VirtualNo/IMEI/SN,店铺、订单、退款使用各自稳定编号。当前资源不存在时仅按 Registry 白名单快照字段精确查找历史,不做任意 JSON 模糊搜索。 + description: 用于平台调查选择器:卡 keyword 使用 ICCID/VirtualNo,设备使用 VirtualNo/IMEI/SN,店铺使用店铺编号,订单使用订单号,退款使用退款单号。选择结果后将 `items[].resource_type/resource_id` 原样传给资源时间线;`historical=true` 表示仅由历史快照命中。仅精确搜索,不做任意 JSON 模糊搜索。 parameters: - description: 资源类型 (iot_card:IoT卡, device:设备, shop:店铺, order:订单, refund:退款单) in: query @@ -16624,6 +17565,12 @@ paths: required: true schema: description: 资源类型 (iot_card:IoT卡, device:设备, shop:店铺, order:订单, refund:退款单) + enum: + - iot_card + - device + - shop + - order + - refund type: string - description: 精确业务标识;卡支持ICCID/VirtualNo,设备支持VirtualNo/IMEI/SN in: query @@ -16632,18 +17579,20 @@ paths: schema: description: 精确业务标识;卡支持ICCID/VirtualNo,设备支持VirtualNo/IMEI/SN type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer @@ -16705,58 +17654,76 @@ paths: - 审计调查 /api/admin/audit/risks/events: get: - description: 筛选条件来自风险总览分桶或调查人员输入,明细返回统一 investigation_refs,可继续进入事件、资源、操作者和 correlation 视角;不提供处置或封禁能力。 + description: 筛选值来自风险总览:`risks[].code→risk`、`results[].code→result`、`actions[].code→action`、`sources[].code→source`。明细 `investigation_refs` 按事件详情相同规则跳转。缺省只查在线窗口,不提供处置或封禁能力。 parameters: - - description: 开始时间(RFC3339,含时区,必填) + - description: 开始时间(RFC3339,含时区);默认从在线窗口开始 in: query name: created_from - required: true schema: - description: 开始时间(RFC3339,含时区,必填) + description: 开始时间(RFC3339,含时区);默认从在线窗口开始 type: string - - description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天,必填) + - description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天);默认当前时间 in: query name: created_to - required: true schema: - description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天,必填) + description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天);默认当前时间 type: string - description: 风险等级 (low:低, normal:普通, high:高, critical:严重) in: query name: risk schema: description: 风险等级 (low:低, normal:普通, high:高, critical:严重) + enum: + - low + - normal + - high + - critical type: string - description: 结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) in: query name: result schema: description: 结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) + enum: + - success + - failed + - denied + - partial + - unknown type: string - - description: 稳定动作编码 + - description: 稳定动作编码;直接使用事件响应的action_code in: query name: action schema: - description: 稳定动作编码 + description: 稳定动作编码;直接使用事件响应的action_code type: string - description: 来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调) in: query name: source schema: description: 来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调) + enum: + - admin_api + - personal_api + - openapi + - worker + - scheduler + - callback type: string - - description: 页码,默认1 + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer @@ -16818,45 +17785,61 @@ paths: - 审计调查 /api/admin/audit/risks/overview: get: - description: 必须提供最长31天的时间范围;只聚合高风险、资金、安全、失败、拒绝、部分成功和结果未知事件,普通低风险成功事件不计入。 + description: 时间范围最长31天,缺省使用当前在线窗口。`signals[].code` 固定为 high_risk、finance、security、failed、denied、partial、unknown;`risks/results/sources[].code` 可原样回填同名筛选参数,`actions[].code` 回填 action。name 字段只用于中文展示。 parameters: - - description: 开始时间(RFC3339,含时区,必填) + - description: 开始时间(RFC3339,含时区);默认从在线窗口开始 in: query name: created_from - required: true schema: - description: 开始时间(RFC3339,含时区,必填) + description: 开始时间(RFC3339,含时区);默认从在线窗口开始 type: string - - description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天,必填) + - description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天);默认当前时间 in: query name: created_to - required: true schema: - description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天,必填) + description: 结束时间(RFC3339,含时区,不包含该时刻,最长31天);默认当前时间 type: string - description: 风险等级 (low:低, normal:普通, high:高, critical:严重) in: query name: risk schema: description: 风险等级 (low:低, normal:普通, high:高, critical:严重) + enum: + - low + - normal + - high + - critical type: string - description: 结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) in: query name: result schema: description: 结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知) + enum: + - success + - failed + - denied + - partial + - unknown type: string - - description: 稳定动作编码 + - description: 稳定动作编码;直接使用事件响应的action_code in: query name: action schema: - description: 稳定动作编码 + description: 稳定动作编码;直接使用事件响应的action_code type: string - description: 来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调) in: query name: source schema: description: 来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调) + enum: + - admin_api + - personal_api + - openapi + - worker + - scheduler + - callback type: string responses: "200": @@ -20022,36 +21005,53 @@ paths: - 设备管理 /api/admin/enterprise/resource-activities/{resource_type}/{identifier}: get: - description: 仅支持企业当前有效授权的卡和设备;企业身份只读取认证上下文,授权撤销后立即不可读取。响应不包含平台操作者、风险、内部前后值或外部交互内容。 + description: 企业仅支持当前有效授权资产:卡列表/详情使用 `resource_type=iot_card`、`identifier=response.data.iccid`;设备列表/详情使用 `resource_type=device`、`identifier=response.data.virtual_no`。企业身份与授权范围只读取认证上下文,前端不得传入或推断。缺少稳定 identifier 时隐藏入口。响应只含主体安全投影,不含平台操作者、风险、内部原因或 before/after。 parameters: - - description: 页码,默认1 + - description: 开始时间(RFC3339,含时区);默认从在线窗口开始 + in: query + name: created_from + schema: + description: 开始时间(RFC3339,含时区);默认从在线窗口开始 + type: string + - description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 + in: query + name: created_to + schema: + description: 结束时间(RFC3339,含时区,不包含该时刻);默认当前时间 + type: string + - description: 页码 in: query name: page schema: - description: 页码,默认1 + default: 1 + description: 页码 minimum: 1 type: integer - - description: 每页数量,默认20,最大100 + - description: 每页数量,最大100 in: query name: page_size schema: - description: 每页数量,默认20,最大100 + default: 20 + description: 每页数量,最大100 maximum: 100 minimum: 1 type: integer - - description: 资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业) + - description: 企业资源类型 (iot_card:IoT卡, device:设备) in: path name: resource_type required: true schema: - description: 资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业) + description: 企业资源类型 (iot_card:IoT卡, device:设备) + enum: + - iot_card + - device type: string - - description: 业务稳定标识;卡使用ICCID,设备使用VirtualNo,其他资源使用对应业务编号 + - description: 业务稳定标识;卡使用ICCID,设备使用VirtualNo in: path name: identifier required: true schema: - description: 业务稳定标识;卡使用ICCID,设备使用VirtualNo,其他资源使用对应业务编号 + description: 业务稳定标识;卡使用ICCID,设备使用VirtualNo type: string responses: "200": diff --git a/docs/engineering/从零构建Agent友好项目最佳实践.md b/docs/engineering/从零构建Agent友好项目最佳实践.md new file mode 100644 index 0000000..dde238a --- /dev/null +++ b/docs/engineering/从零构建Agent友好项目最佳实践.md @@ -0,0 +1,556 @@ +# 从零构建 Agent 友好项目:Harness 工程实践与文档标准 + +> 用途:用于新项目初始化和长期治理。它定义“哪些知识需要存在、放在哪里、按什么格式写、如何验证”。 +> +> 核心结论:OpenSpec 只负责当前行为与行为变更;完整 Harness 还必须让 Agent 能找到事实、运行系统、观察结果、遵守边界、独立验证并持续清理熵。 + +## 1. 先澄清:ADR 不是 OpenSpec + +- OpenSpec 的标准 Artifact 是 `proposal`、增量 `specs`、`design` 和 `tasks`。 +- `design.md` 记录**一次 Change** 的实现设计、取舍、迁移和回滚。 +- ADR 是业界通用但独立的“长期架构决策记录”,不属于 OpenSpec,也不是新项目必需品。 +- 默认不建 `adr/`。只有一个跨 Change、长期生效、存在真实竞争方案且无法从代码理解原因的决策,才考虑单独记录;否则留在对应 Change 的 `design.md`。 + +## 2. Harness 的完整范围 + +OpenAI 的 Harness Engineering 不只是“短 AGENTS.md + docs”。一个完整闭环包含: + +1. **仓库可读**:事实进入仓库,并有清晰索引和单一权威位置。 +2. **环境可运行**:每个工作区能独立启动、测试和复现。 +3. **结果可观察**:Agent 能读取日志、指标、追踪、页面和 DOM,而不是靠猜。 +4. **架构可约束**:依赖方向、边界数据解析和关键不变量可机械检查。 +5. **任务可判定**:需求被写成可观察、可验证的行为契约。 +6. **执行可闭环**:Agent 能复现、修改、验证、比较前后结果并处理反馈。 +7. **知识可维护**:链接、Owner、新鲜度和生成来源可检查,过期内容持续清除。 +8. **人类掌舵**:人负责目标、优先级和判断点;Agent 负责读取、执行、验证和维护。 + +因此,Context 收敛不是“少写文档”,而是让默认 Context 很小、按需 Context 有标准、运行反馈足够强。 + +## 3. 从最小集合开始 + +### 3.1 所有项目 Day 0 必需 + +```text +AGENTS.md # 短导航与真正的全局硬约束 +README.md # 人和 Agent 都可执行的启动入口 +ARCHITECTURE.md # 一页系统地图和依赖边界 +openspec/ # 当前行为与行为变更 +CI / scripts # 格式化、检查、测试、构建命令 +``` + +### 3.2 有真实内容时才创建 + +```text +docs/integrations/ # 存在第三方契约 +docs/engineering/ # 存在跨模块、长期工程约束 +docs/generated/ # 能由命令稳定再生的参考资料 +RELIABILITY.md # 存在队列、重试、容灾、SLO 等系统性要求 +SECURITY.md # 存在统一信任边界、敏感数据或威胁模型 +FRONTEND.md # 前端规模足以需要统一交互/状态/设计规则 +PRODUCT_SENSE.md # 多团队或 Agent 经常误判产品取舍 +QUALITY_SCORE.md # 已有明确评分维度和维护机制 +``` + +不要预建空目录、空模板或“以后也许有用”的文档。 + +### 3.3 如何理解 OpenAI 文章中的示例目录 + +文章展示的是一套成熟仓库的实际结构,不是要求每个项目照抄。与本指南的映射如下: + +| 文章中的示例 | 解决的问题 | 本指南的默认选择 | +|---|---|---| +| `ARCHITECTURE.md` | 系统地图与边界 | Day 0 创建一页版本 | +| `docs/design-docs/`、`DESIGN.md` | 跨范围设计知识 | 单次变更放 OpenSpec `design.md`;只有长期跨 Change 设计才另建 | +| `docs/exec-plans/{active,completed}`、`PLANS.md` | 长任务持续执行 | 默认用 OpenSpec `tasks.md`;非行为型长期治理才建唯一执行计划 | +| `docs/product-specs/` | 当前产品行为 | 使用 `openspec/specs/`,不再复制一套 | +| `docs/generated/db-schema.md` | Agent 可检索的生成事实 | 有稳定生成命令时放 `docs/generated/` | +| `docs/references/*-llms.txt` | 将仓库外依赖资料变成 Agent 可读输入 | 仅保存任务高频需要且无法稳定在线获取的官方资料;第三方协议归 `docs/integrations/` | +| `FRONTEND.md` | 前端统一边界 | 前端复杂度触发后创建 | +| `PRODUCT_SENSE.md` | 产品判断原则 | 重复误判触发后创建 | +| `QUALITY_SCORE.md` | 领域/架构质量评分 | 有固定评分和治理动作后创建 | +| `RELIABILITY.md`、`SECURITY.md` | 跨系统可靠性和安全约束 | 对应风险出现后创建 | + +关键不是目录名一致,而是:每类知识有唯一权威位置、Agent 可发现、能被验证、不会和 OpenSpec 重复。 + +### 3.4 不重复的事实分工 + +| 知识 | 权威位置 | 不应放入 | +|---|---|---| +| 当前可观察业务行为 | `openspec/specs/` | AGENTS、工程规范 | +| 拟议行为变化 | `openspec/changes//` | 主 Specs、路线图副本 | +| 系统地图和依赖方向 | `ARCHITECTURE.md` | 每个 Change 重复描述 | +| 第三方提供的契约 | `docs/integrations/` | 业务 Spec 原文复制 | +| 长期工程约束 | `docs/engineering/` + 检查器 | 任务型 Skill | +| 实现事实 | 代码、配置、迁移 | 叙述文档副本 | +| 可再生参考资料 | `docs/generated/` | 手工维护文档 | +| 任务过程 | OpenSpec Change 或唯一执行计划 | 第二套 TODO/状态台账 | +| 完成证据 | 测试、CI、运行输出 | 长期“完成总结” | + +## 4. 每类文档的统一质量契约 + +除生成文档外,长期文档至少要让读者回答: + +```yaml +purpose: 这份文档解决什么问题 +scope: 适用与不适用范围 +source_of_truth: 哪些事实由本文权威维护,哪些只引用 +owner: 负责判断内容是否仍成立的角色或团队 +last_verified: 最后通过代码、运行结果或官方来源核验的日期 +update_triggers: 哪些变更发生时必须同步检查本文 +verification: 用什么命令或证据判断内容仍有效 +``` + +这不要求所有文件都使用 YAML;要求这些信息明确可找到。 + +### 合格标准 + +- 写具体规则和可判定结果,不写“注意质量”“合理处理”等口号。 +- 关键声明有代码路径、命令、测试、Schema 或官方来源支撑。 +- 复制外部内容时标明来源版本和核验时间。 +- 一项事实只有一个权威正文,其他位置只链接。 +- 每个“必须/禁止”都对应机械检查,或明确说明当前为何只能人工审查。 +- 示例只用于消除歧义;示例不得成为第二份规则。 + +### 不合格信号 + +- 没有适用范围、Owner 或更新触发条件。 +- 只有目录介绍,没有依赖方向、入口或验证方式。 +- 把当前 Bug 写成理想行为,或把未来设计冒充当前事实。 +- 从代码能直接搜索得到的列表被手工复制并长期维护。 +- 同一个状态、错误码或字段定义在多份文档中各写一遍。 + +## 5. AGENTS.md 标准 + +### 必填内容 + +1. 项目一句话目标。 +2. Agent 从代码无法自然发现、且几乎所有任务都适用的硬约束。 +3. build、test、lint、spec 验证的真实命令。 +4. 事实源优先级和按需文档入口。 +5. 禁止直接修改的生成物、敏感路径或外部生产边界。 + +### 禁止内容 + +- 完整 DTO、Model、路由或迁移操作步骤。 +- 领域状态机、接口字段表和第三方协议正文。 +- 已完成任务、历史方案、长 Review 清单。 +- Agent 能从代码、包管理器或 `--help` 获取的通用知识。 + +### 尺寸与验证 + +- 80~120 行是目标,150 行是软预算。 +- 每一行应对大多数任务有价值。 +- CI 检查链接有效;验证命令必须在干净环境可运行。 + +```md +# 项目 Agent 指南 + +## 项目目标 +<一句话> + +## 全局硬约束 +- <规则;对应检查命令或规则文档链接> + +## 验证 +- Build: `` +- Test: `` +- Lint: `` +- Spec: `openspec validate --all` + +## 事实源 +- 当前行为:`openspec/specs/` +- 系统边界:`ARCHITECTURE.md` +- 外部契约:`docs/integrations/` +- 工程约束:`docs/engineering/` +``` + +## 6. ARCHITECTURE.md 标准 + +它是系统地图,不是完整设计书。目标是让新 Agent 在几分钟内知道从哪里进入、允许依赖谁、数据如何流动。 + +### 必填章节 + +1. **系统职责与非职责**:系统解决什么,不解决什么。 +2. **运行单元**:API、Worker、定时任务、前端、数据库、缓存、外部系统。 +3. **领域/模块地图**:模块职责、Owner(如有)和主要入口路径。 +4. **依赖方向**:允许和禁止的跨层、跨模块依赖。 +5. **关键数据流**:请求、异步事件、回调、读写路径。 +6. **信任与事务边界**:外部输入在哪里解析,事务在哪里开始/结束。 +7. **验证方法**:结构测试、依赖检查、启动或 smoke 命令。 +8. **详细资料索引**:链接 Specs、工程约束和外部契约,不复制正文。 + +### 写法标准 + +```md +## 模块:订单 +- 职责:创建订单并管理支付前后的状态流转 +- 入口:`internal/...` +- 可依赖:共享错误、支付 Port +- 禁止依赖:HTTP Handler、具体支付 SDK +- 行为契约:`openspec/specs/orders/spec.md` +- 结构验证:`` +``` + +### 机械检查 + +- 用 import/dependency 测试验证禁止依赖。 +- 用 smoke 命令验证列出的运行单元确实可启动。 +- 用链接检查验证每个引用存在。 +- 模块增删、入口迁移或依赖方向改变时触发更新。 + +## 7. 外部契约文档标准 + +外部契约记录“第三方实际要求我们怎样交互”。它不是本系统的产品需求,也不是 SDK 使用教程。 + +### 7.1 目录规则 + +```text +docs/integrations// +├── README.md # 当前接入契约与导航 +├── examples/ # 经脱敏、可复现的请求响应样例(需要时) +└── source/ # 无稳定链接的官方原文快照(需要时) +``` + +官方内容有稳定 URL 时只保存链接和项目所需摘要;不要复制整站文档。 + +### 7.2 README 必填字段 + +```md +# 接入契约 + +## 元数据 +- Owner: <角色/团队> +- 官方来源: +- 官方版本/发布日期: <值或 unknown> +- 项目适用环境: +- 最后核验: YYYY-MM-DD +- 更新触发: SDK升级、官方版本变化、字段/签名/错误码变化 + +## 接入范围 +- 使用能力:<项目实际使用的 API/事件> +- 不使用能力:<容易误用但明确不接入的能力> + +## 端点与认证 +| 场景 | Method | URL/Topic | 认证 | 超时 | + +## 请求契约 +| 字段 | 类型 | 必填 | 约束 | 来源章节 | 项目映射 | + +## 响应与错误 +| 外部状态/错误码 | 含义 | 可重试 | 项目处理 | 告警 | + +## 签名与回调 +- 签名原文构造:<精确定义> +- 验签步骤:<精确定义> +- 时间窗/重放保护:<规则> +- 回调确认语义:<响应内容和重试条件> + +## 可靠性 +- 幂等键:<字段与作用域> +- 超时:<连接/请求> +- 重试:<次数、退避、仅哪些错误> +- 限流:<规则> +- 对账/补偿:<触发与入口> + +## 安全与数据 +- 凭证名称及托管位置:<只写名称,不写密钥> +- 敏感字段:<日志脱敏规则> +- 来源校验:<证书/IP/签名等> + +## 验证 +- 本地/沙箱命令:`` +- 固定输入:`` +- 预期结果:`` +- 生产人工验收:<只有真实环境才能完成的最小步骤> +``` + +### 7.3 质量门禁 + +- 所有项目使用的字段都能追溯到官方来源或经确认的真实样例。 +- 签名、金额单位、时间格式、编码、回调确认内容必须写成精确规则。 +- 每种外部错误明确:重试、失败、忽略、人工处理中的一种。 +- 重试必须同时定义上限、退避和幂等保护。 +- 示例必须脱敏,并可被测试或脚本读取;真实密钥不得进入仓库。 +- 至少有一个沙箱/契约测试,或明确记录为何只能人工验收。 + +### 7.4 不合格示例 + +```md +调用失败时适当重试。 +``` + +问题:没有错误范围、次数、退避、幂等和最终失败动作,无法实现或验证。 + +合格写法: + +```md +仅对连接超时和外部错误 E_TEMP 重试;最多 3 次,间隔 1s/2s/4s; +每次复用同一幂等键。三次失败后记录 integration failure 并进入人工对账队列。 +``` + +## 8. 工程约束文档标准 + +工程约束描述“所有相关代码长期必须保持的性质”。DTO、Model、路由、迁移、注释、依赖边界属于此类,而不是 Skill。 + +### 8.1 一条约束的标准格式 + +```md +### ENG--NNN:<可判定标题> + +- 状态:active | deprecated +- 适用范围:<路径、语言、组件或操作> +- 规则:MUST/MUST NOT <单一可判定约束> +- 理由:<错误成本或架构原因;不复述规则> +- 正例:`<最小示例或现有代码链接>` +- 反例:`<最小示例>` +- 机械检查:`<测试/Lint/脚本命令>` 或 `暂为人工:<原因>` +- 例外:<允许条件、批准者、记录位置;无则写“无”> +- Owner:<角色/团队> +- 最后验证:YYYY-MM-DD +- 更新触发:<框架升级、目录调整、事故等> +``` + +### 8.2 规则拆分标准 + +- 一个 ID 只表达一个义务,避免“正确、安全、高性能地处理”。 +- 适用范围必须能映射到路径或组件。 +- 正例优先链接仓库内稳定实现,不复制大段代码。 +- 反例只展示最容易犯且有实际代价的错误。 +- 规则能由机器判断时,文档必须链接检查器;检查器才是执行门禁。 +- 例外必须有边界和到期/复审条件,不能写“特殊情况除外”。 + +### 8.3 分类 + +| 分类 | 应记录 | 首选验证 | +|---|---|---| +| 架构 | 依赖方向、分层、边界解析 | 结构测试、import Lint | +| 数据 | 迁移、主键、金额单位、事务 | Schema/Lint/集成测试 | +| API | 路由注册、错误形状、兼容性 | OpenAPI diff、契约测试 | +| 可靠性 | 幂等、重试、锁、Outbox | 故障注入、集成测试 | +| 安全 | 权限、敏感字段、信任边界 | 安全测试、静态检查 | +| 代码品味 | 禁止无价值抽象、复杂度上限 | Lint + Review | + +### 8.4 合格与不合格示例 + +不合格: + +```md +数据库迁移要谨慎,注意兼容旧数据。 +``` + +合格: + +```md +### ENG-DB-003:新增非空列必须可在线回填 +- 适用范围:`migrations/` +- 规则:已有数据表新增非空列时,MUST 先增加可空列或带兼容默认值,完成回填后再收紧约束。 +- 理由:直接增加无默认值的非空列会使现有数据迁移失败。 +- 机械检查:`` +- 例外:仅空表;必须在 Change design 中提供查询证据。 +``` + +## 9. OpenSpec 生成提案的标准 + +用户不需要手工填写长表。AI 生成 Change,但生成器和 Reviewer 必须遵守质量门禁。 + +### 9.1 Explore 后才能确定的内容 + +- 当前行为和证据路径。 +- 目标、原因、范围和非目标。 +- 角色、资源、前置状态和数据范围。 +- 状态、金额、权限、事务、并发、幂等和外部失败边界。 +- 已知事实、推断和真正需要人决策的问题。 + +### 9.2 Proposal + +只回答为什么、做什么、不做什么、影响哪些 Capability。不得塞入实现步骤和通用工程规范。 + +### 9.3 Requirement / Scenario + +```md +### Requirement: <一个可观察义务> +系统 SHALL <测试者无需阅读实现即可判定的结果>。 + +#### Scenario: <场景> +- **GIVEN** <角色、数据、前置状态> +- **WHEN** <一个动作> +- **THEN** <可观察结果> +- **AND** <副作用或必须保持的不变量> +``` + +门禁: + +- 一个 Requirement 一个主要义务。 +- Scenario 有具体前置条件、触发和结果。 +- 覆盖主流程以及代价最高的权限、失败、边界或重复场景。 +- 不使用“体验良好”“正确处理”“高性能”等不可判定词。 +- 不写函数名、表名、ORM、缓存或队列选型。 +- Brownfield 的错误现状也按 As-Is 写入;Spec 描述事实不等于认可设计。 + +### 9.4 Design + +记录本 Change 的架构通道、数据与事务、并发、外部失败、兼容、迁移、发布、回滚和真实取舍。简单且实现显然时保持简短。 + +### 9.5 Tasks + +- 按可独立验证的纵向切片拆分,不按 Model/Service/Handler 水平拆分。 +- 每项映射 Requirement/Scenario,并包含最小验证命令和预期结果。 +- 适用时同时验证响应、持久化、副作用和失败不变量。 +- 实施发现 Artifact 与事实冲突时同步修订,不静默跳过或扩大任务。 + +## 10. Generated docs 标准 + +仅当内容能稳定再生且 Agent 经常需要查询时创建,例如数据库 Schema、OpenAPI 摘要或配置清单。 + +每份生成文件顶部必须包含: + +```md + +- Source: <代码/Schema 路径> +- Command: `<生成命令>` +- Generator version: <版本> +- Generated at: <时间或来源 commit> +``` + +CI 重新生成并检查 diff。生成结果没有稳定命令时,它就不是 generated doc,而是会腐烂的手工副本。 + +## 11. 唯一执行计划标准 + +一般功能直接使用 OpenSpec `tasks.md`,不要再建 `docs/exec-plans/`。只有不改变产品行为、跨多天且 OpenSpec 不适配的迁移/治理任务,才使用单独执行计划。 + +必填内容: + +- 目标和完成定义。 +- 范围、非目标和受保护路径。 +- 前置依赖和顺序。 +- 可恢复的任务清单,每项含验证。 +- 当前进度,只在这一处更新。 +- 关键决策与新发现。 +- 回滚条件和命令。 +- 最终验收命令及预期结果。 + +Manager、Executor、Auditor 可以分工,但只能共享这一份任务契约;Harness state 和报告只是可丢弃证据。 + +## 12. Agent 可操作环境标准 + +### 12.1 独立运行 + +- 每个 worktree/工作区可使用不同端口和隔离的临时数据启动。 +- README 提供一条启动命令、一条测试命令和一条重置命令。 +- 依赖、种子数据和环境变量有可复制的本地默认值;真实凭证不入库。 + +### 12.2 浏览器与 UI + +有 UI 时,Agent 应能: + +- 启动应用并通过浏览器/CDP 操作关键流程。 +- 保存失败前后的截图或视频。 +- 读取 DOM、Console error 和失败网络请求。 +- 用稳定测试定位器,而非脆弱坐标。 + +### 12.3 可观测性 + +本地至少能按 request/correlation ID 串起: + +- 结构化日志。 +- 关键指标。 +- 跨进程或异步链路追踪(系统存在此类链路时)。 + +提供最小查询命令。临时本地可观测栈应可一键启动和销毁,运行数据不作为长期事实源。 + +### 12.4 自主验证闭环 + +```text +复现问题 → 捕获基线 → 修改 → 运行最小测试 → 启动系统 +→ 检查日志/指标/页面 → 对比前后 → 完整门禁 → 提交审查 +``` + +只有产品取舍、不可逆操作、真实生产权限或相互冲突的事实需要升级给人。 + +## 13. Reliability、Security、Product Sense、Quality Score + +这些不是所有项目的固定作业。 + +### RELIABILITY.md:何时创建 + +当系统出现 SLO、重试、队列、定时任务、降级、容灾或数据修复策略时创建。最低结构:关键用户旅程、SLO/错误预算、故障模式、超时重试、幂等与补偿、观测和告警、恢复/演练、Owner。 + +### SECURITY.md:何时创建 + +当存在统一信任边界、敏感数据、权限模型或外部暴露面时创建。最低结构:资产和角色、信任边界、认证授权、数据分类、密钥、审计、主要威胁与控制、验证命令、事件入口、Owner。 + +### PRODUCT_SENSE.md:何时创建 + +当多个功能反复需要相同产品判断、Agent 经常做出局部正确但产品错误的选择时创建。最低结构:目标用户、核心任务、优先级原则、明确非目标、取舍示例、正反例。具体行为仍归 OpenSpec。 + +### QUALITY_SCORE.md:何时创建 + +只有团队真的按固定维度定期评分并采取行动时创建。每项必须有定义、证据来源、当前分数、阈值、Owner、改进动作和复评日期;没有维护机制就不要创建。 + +## 14. 文档检查与持续清熵 + +### 每次变更 + +- 检查内部链接和引用路径。 +- OpenSpec 校验全部通过。 +- 生成文档可无差异再生。 +- 架构边界检查通过。 +- Change 完成后同步/归档 Specs,删除被替代的同义说明。 + +### 周期性 doc-gardening + +Agent 定期生成候选清单,人只处理真正的判断点: + +- 失效链接、孤儿文件和长期 TBD。 +- `last_verified` 过期且影响仍高的文档。 +- 与代码、测试或 Specs 冲突的声明。 +- 重复规则、重复事实和已被检查器取代的提醒。 +- 已完成的临时计划、报告和可丢弃运行证据。 + +清理优先级:删除 > 合并到权威位置 > 更新 > 新建索引。 + +### 黄金原则 + +把反复出现的 Review 意见和事故教训变成最靠近问题的自动化约束:测试、类型、Lint、Schema、结构检查或运行时保护。不要持续加长 AGENTS.md。 + +## 15. Day 0 到稳定期的落地顺序 + +1. Agent 生成最小可运行仓库、格式化、包管理、CI 和测试骨架。 +2. 建立短 `AGENTS.md`、可执行 `README.md` 和一页 `ARCHITECTURE.md`。 +3. 初始化 OpenSpec;第一个真实功能走 Explore → Propose → Review → Apply → Verify → Archive。 +4. 打通独立工作区启动、日志读取和最小 smoke 测试。 +5. 出现真实第三方时按外部契约模板创建资料。 +6. 出现重复工程失误时先加检查器,再补对应约束 ID。 +7. 只有触发条件成立时增加 Reliability、Security 等专项文档。 +8. 持续删除完成报告、重复事实和过期 Context,避免周期性大扫除。 + +## 16. 最低验收清单 + +- [ ] 新 Agent 只读 AGENTS.md 就能找到行为、架构、外部契约和验证入口。 +- [ ] README 的启动、测试、重置命令在干净工作区可执行。 +- [ ] ARCHITECTURE.md 的依赖方向有检查或明确人工门禁。 +- [ ] OpenSpec Requirement/Scenario 可观察、可判定。 +- [ ] 外部契约的版本、字段、签名、重试、幂等和验证证据齐全。 +- [ ] 每条工程约束有范围、规则、理由、检查、例外和 Owner。 +- [ ] Agent 能自行读取错误日志;有 UI 时能读取页面和网络失败。 +- [ ] 同一事实没有多个权威正文。 +- [ ] 文档链接、生成检查、Spec 校验和测试进入 CI。 +- [ ] 临时计划、状态和运行证据不会成为第二事实源。 + +## 17. 参考资料 + +### 第一方 + +- [OpenAI Harness Engineering](https://openai.com/index/harness-engineering/) +- [OpenSpec Overview](https://openspec.dev/docs/overview) +- [OpenSpec Writing Specs](https://openspec.dev/docs/writing-specs) +- [OpenSpec Getting Started](https://openspec.dev/docs/getting-started) +- [LongHorizon-Harness](https://github.com/AMAP-ML/LongHorizon-Harness) + +### 补充实践 + +- [Augment:How to write good AGENTS.md files](https://www.augmentcode.com/blog/how-to-write-good-agents-dot-md-files) +- [BetterClaw:AGENTS.md best practices](https://www.betterclaw.io/blog/agents-md-best-practices) +- [What goes in AGENTS.md?](https://ro14nd.de/what-goes-in-agents-md/) +- [Domain Expertise Is the New Agentic Coding Moat](https://www.developersdigest.tech/blog/domain-expertise-agentic-coding-moat) +- [O’Reilly:How to write a good spec for AI agents](https://www.oreilly.com/radar/how-to-write-a-good-spec-for-ai-agents/) diff --git a/docs/feature-504-multi-view-audit-center/多视角审计中心功能总结.md b/docs/feature-504-multi-view-audit-center/多视角审计中心功能总结.md new file mode 100644 index 0000000..0d4c073 --- /dev/null +++ b/docs/feature-504-multi-view-audit-center/多视角审计中心功能总结.md @@ -0,0 +1,267 @@ +# 多视角审计中心功能总结 + +本文汇总 `build-multi-view-audit-center` 第一阶段已经交付的能力、边界和验收依据。实现以当前 Action/Resource Registry 和覆盖基线为准;旧账号、旧资产 operation log 只保留独立历史查询,不回填、不转换、不进入新审计中心。 + +## 1. 交付范围 + +- 统一不可变 `Audit Event + Event Resource`,记录真实操作者、入口、动作、结果、风险、多资源关系、资源快照、前后变化和跨步骤链路。 +- 平台提供全局事件、操作者、资源、request、correlation、资金、风险和 Integration Log 调查视角。 +- 代理和企业使用独立安全资源活动投影,只查看当前有权资源的允许业务结论。 +- Audit Event 与 Integration Log 按 `Asia/Shanghai` 完整自然日归档;整月完整性门禁通过后,可受控物理清理 PostgreSQL 上月在线数据。 +- 现行覆盖门禁已登记 692 个源码入口、219 个 Action Registry 动作、61 个 Resource Registry 类型、146 个已使用资源角色和 5 组敏感读取。 + +实现入口:`internal/infrastructure/audit/`、`internal/query/audit/`、`internal/query/integration/`、`internal/application/auditarchive/`、`internal/handler/admin/audit.go`、`internal/routes/audit.go`。完整逐入口决定见 [审计覆盖基线](../../.scratch/tech-global-audit/审计覆盖基线.md)。 + +## 2. 四类事实边界 + +| 事实 | 回答的问题 | 权威内容 | 明确不承担 | +|---|---|---|---| +| Access Log | 这次 HTTP 请求收发了什么、耗时多久 | method、path、query、status、duration、request/response body、request_id 等调试信息 | 不证明业务事实成功,不进入审计数据库查询,不上传本次冷归档 | +| Audit Event | 谁从什么入口,对哪些资源做了什么,结果如何 | 操作者、动作、结果、风险、多资源关系、资源快照、before/after、request/correlation/parent | 不替代订单、钱包流水等业务账本,不替代外部交互或可靠投递事实 | +| Integration Log | 系统与外部系统实际发生或未发生了哪次交互 | provider、direction、operation、result、attempt、外部状态、受控请求/响应摘要 | 不把投递成功或外呼成功伪装成内部业务成功,不承担人工恢复 | +| Domain Ledger | 金额、状态、权益最终以什么为准 | 订单、支付、退款、充值、钱包流水、审批、套餐权益、佣金等业务表 | 不负责解释完整操作者上下文,不被 Audit Event 重算或覆盖 | + +Outbox/Asynq/手动轮询是可靠副作用和任务运行事实:跨视角时间线可展示其摘要或引用,但仍保持独立生命周期。Outbox `delivered` 只表示投递事实,不表示消费者业务成功;手动轮询表继续保存进度和结果。 + +链路字段固定语义:`request_id` 关联一次 HTTP 请求;`correlation_id` 关联跨请求、Outbox、Asynq、回调和后续业务步骤;`parent_event_id` 表示直接因果;Integration 的 `trigger_series + attempt` 才表示技术尝试序列。 + +## 3. 核心字段字典 + +### 3.1 Audit Event + +| 字段组 | 字段 | 语义 | +|---|---|---| +| 身份 | `event_id`、`occurred_at`、`created_at` | 稳定公开事件 ID、真实发生时间、持久化时间 | +| 动作 | `category`、`action_code`、`action_name`、`summary` | 稳定动作类别/编码、中文名称快照和内部摘要 | +| 操作者 | `actor_kind/id/name`、`actor_shop_id/name`、`actor_enterprise_id/name` | 事件发生时的真实人工、OpenAPI、系统任务、计划任务或外部系统身份快照 | +| 入口 | `source`、`request_path/method`、`ip_address`、`user_agent` | admin/personal/openapi/worker/scheduler/callback 等来源及 HTTP 摘要 | +| 范围 | `scope_type/id/name` | 平台、店铺、企业、个人等本次主要业务范围快照 | +| 结果 | `result`、`risk_level`、`error_code`、`error_summary` | `success/failed/denied/partial/unknown`、风险和安全错误摘要 | +| 链路 | `request_id`、`correlation_id`、`parent_event_id` | 请求、业务链路和直接父事件 | +| 批量 | `batch_total`、`success_count`、`fail_count` | 批次根统计;子事件通过 parent/correlation 关联 | +| 完整性 | `metadata`、`content_hash` | 有界安全业务参数和标准化内容 SHA-256 | + +### 3.2 Event Resource + +| 字段 | 语义 | +|---|---| +| `resource_type/id/key/display_name` | Registry 类型、可空内部 ID、事件时稳定业务 Key 和展示名 | +| `relation` | `primary`、`affected`、`reference`;每个事件必须且只能按动作契约确定主要资源 | +| `role` | `old_card/new_card/bound_card/entry_device/order/wallet_transaction` 等稳定业务角色 | +| `identity_snapshot` | 事件发生时资源身份,不依赖当前业务表是否删除、更名、换号 | +| `before_data/after_data` | 该资源本次真实涉及的字段,不使用事件级万能 JSON | +| `subject_visibility` | `internal_only`、`subject_result`、`subject_detail` | +| `subject_summary/subject_data` | 写入时生成的主体安全结论及 Registry 白名单字段 | +| `sort_order/created_at` | 稳定展示顺序和落库时间 | + +### 3.3 Integration Log + +| 字段组 | 字段 | 语义 | +|---|---|---| +| 身份 | `integration_id`、`provider`、`direction`、`operation`、`external_id` | 稳定交互身份、提供方、方向、动作和外部业务标识 | +| 资源 | `resource_type/id/key` | 本次外部交互的直接主资源;其他资源通过关联 Audit Event 展开 | +| 触发 | `trigger_source/scene/series`、`attempt` | 来源、业务场景和显式技术尝试序列 | +| 结果 | `result`、`http_status`、`provider_code/message`、`duration_ms`、`state_changed` | 原始结果、安全可读摘要、耗时和是否改变本地事实 | +| 内容 | `request_summary`、`response_summary`、`metadata`、`content_hash` | 写入和返回前均经过白名单及凭据删除的结构化摘要 | +| 链路 | `request_id`、`correlation_id`、`audit_event_id` | 与请求、业务链路和内部状态变化事件的稳定关联 | + +派生结果类别固定为:`pending→processing`、`success→succeeded`、`unknown→indeterminate`、明确失败结果→`failed`、`ignored/merged/rate_limited/completed/cancelled→not_sent`。`completed` 不计外部请求成功。 + +### 3.4 留存与调查引用 + +| 结构 | 字段 | 语义 | +|---|---|---| +| `retention` | `online_from`、`archived_before`、`timezone` | 当前 PostgreSQL 在线窗口、已归档边界、`Asia/Shanghai` 时区 | +| `investigation_refs` | `event_id`、`actor_ref`、`resource_refs[]`、`request_id`、`correlation_id`、`integration_refs[]` | 平台各调查视角唯一允许使用的稳定跳转引用 | +| `tb_log_archive_run` | source/date/instance/schema/revision/status/is_final、range、object/manifest key、数量、字节、SHA-256、attempt、cleanup 时间 | 归档与清理轻量 ledger,不保存日志正文 | + +## 4. 当前领域、资源与动作矩阵 + +下表按领域归并当前 Registry。动作列使用稳定编码前缀及代表动作;219 个动作的唯一权威清单是 `pkg/constants/audit.go` 与 `internal/infrastructure/audit/registry.go`,逐入口事务和 N/A 决定以覆盖基线为准。 + +| 领域 | 主要资源 | 已接入动作范围 | 权威事实/边界 | +|---|---|---|---| +| 账号与认证 | account、authentication、role、permission | `account.*`、`auth.*`、`role.*`、`permission.*` | 账号、角色权限和认证状态表;密码、Token、Cookie 不入审计 | +| 店铺与企业 | shop、enterprise、企业卡/设备授权 | `shop.*`、`enterprise.*`、`enterprise_card.*`、`enterprise_device.*` | 店铺层级和当前有效授权关系 | +| 个人客户 | personal_customer、手机号、OpenID、资产绑定 | `personal_customer.*` | 客户及绑定表;外部主体标识按 Registry 快照 | +| IoT 卡 | iot_card、批次、分配记录、Integration | `iot_card.*`,含创建、删除、分配/回收、系列、实名、停复机、刷新、限速、Worker 同步 | 卡表保存本地事实;Gateway 尝试进入 Integration Log;限速审计记录谁操作、谁被限速、档位及外部结果 | +| 设备与卡槽 | device、iot_card、device_sim_binding、分配记录 | `device.*`,含分配/回收、绑解绑、切当前卡、Wi-Fi、模式、重启、重置、Worker 观测 | 设备、卡及卡槽均为一等资源,多卡逐张关联 | +| 换货 | exchange_order、旧新卡/设备、绑定、客户、钱包、权益 | `exchange.card.*`、`exchange.device.*` | 换货状态机、资产/钱包/权益等业务表;不压缩为单一资产标识 | +| 套餐配置 | package_series、package、店铺授权、价格历史、批次 | `package_series.*`、`package.*`、`shop_series_grant.*`、`shop_package.*` | 套餐、授权和价格历史表 | +| 套餐权益 | package_usage、订单、套餐、资产、退款 | `package_usage.*`,含激活、到期、扣减/重置、退款/资产失效、队列及迁移 | `tb_package_usage` 和流量业务表为权威 | +| 订单与支付 | order、payment、资产、套餐、钱包/流水 | `order.*`、`payment.*` | 订单/支付状态、渠道流水;每次渠道尝试另写 Integration Log | +| 退款 | refund、订单、审批、钱包/流水、佣金、权益、通知 | `refund.*` | 退款、钱包流水、佣金和权益事实同事务收口 | +| 充值与钱包 | agent_recharge、recharge_order、agent/asset wallet、流水、预占 | `agent_recharge.*`、`asset_recharge.*`、`agent_wallet.*` | 金额和余额以钱包流水/业务单为权威;审计只解释操作者和动作 | +| 佣金与提现 | commission_record、commission_withdrawal、钱包/流水 | `commission.*`、`commission_withdrawal.*` | 佣金、提现、钱包和流水状态机 | +| 审批与企微 | approval_instance、业务单、Integration、Outbox | `approval.*`、`wecom.*` | 本地审批事实、企微交互和可靠终态分发各自保留 | +| 系统与连接配置 | system_config、payment_config、carrier、wecom_application/scene | `system_config.*`、`payment_config.*`、`carrier.*`、`wecom.*` | 配置表权威;只记录 `credentials_configured` 等安全事实 | +| 导入、批量与导出任务 | 各 task、批次资源、实际业务资源 | `*_import_task.*`、`asset_package_batch_order_task.*`、`order_package_invalidate_task.*`、`export_task.*` | 任务表保存进度;实际变化资源另有子事件;审计中心本身不导出 | +| 通知 | notification、read/cleanup batch、业务目标 | `notification.deliver/read/read_all/cleanup/cleanup_item` | 通知正文和已读状态以通知表为准 | +| 轮询与监控 | polling_config/concurrency/alert/manual_trigger、卡 | `polling_config.*`、`polling_concurrency.*`、`polling_alert.*`、`polling_manual_trigger.*` | 人工触发写 Audit;每次 Gateway 尝试写 Integration;手动任务表继续保存运行事实 | +| 可靠事件与留存 | outbox_event、log_archive_month | `outbox.*`、`audit.retention_cleanup` | Outbox 投递、归档 ledger、对象存储事实保持独立 | + +普通列表、详情、统计和装配入口为 N/A;Registry 标记的敏感读取在返回结果前写审计,写入失败则不返回敏感结果。 + +## 5. 各资源身份快照 + +| 资源 | 最小可读快照 | +|---|---| +| 账号/角色/权限 | ID、用户名、手机号、账号类型、店铺/企业、企微身份;角色 ID/名称/类型;权限 code/name | +| 店铺/企业 | ID、编码、名称、上级或 owner shop、层级 | +| 个人客户 | ID、昵称、手机号、微信主体标识及资产绑定标识 | +| IoT 卡 | ID、ICCID、VirtualNo、MSISDN、运营商、店铺、系列、generation | +| 设备 | ID、VirtualNo、IMEI、SN、名称/型号、店铺、系列、generation | +| 设备卡槽 | binding ID、设备标识、slot_position、卡 ICCID/VirtualNo、is_current | +| 分配/换货 | 分配单号或换货单号、资产类型/ID/完整标识、来源/目标主体、旧新资产、店铺、状态 | +| 套餐/权益 | 套餐/系列 ID、编码、名称、期限、价格、状态;权益 ID、订单、资产、generation、激活/到期时间 | +| 订单/支付 | 订单/支付 ID 和单号、买家、资产、套餐、金额、支付方式/状态、渠道交易号、配置 ID | +| 退款/充值 | 业务单号、订单/资产/店铺、申请/批准金额、审批、支付方式、状态 | +| 钱包/流水/预占 | 钱包 ID/类型、店铺或资产、币种、流水/预占 ID、reference、amount、balance before/after | +| 佣金/提现 | 记录或申请 ID、店铺、订单、系列、金额、状态、结算周期 | +| 审批 | 实例 ID、业务类型/ID、提交人、provider、external ref、correlation、状态 | +| 配置 | 配置 Key/模块或 ID/name/provider、状态、`credentials_configured`;不保存密钥正文 | +| 任务/通知/轮询 | 任务 ID/单号、文件名、目标、操作者、统计;通知 event/接收人/类别/ref;轮询配置/触发 ID、资产、任务类型 | +| Integration/Outbox | integration ID/provider/operation/resource/external ID;event ID/type/aggregate | + +手机号、IP、ICCID、VirtualNo、金额和交易号对平台按已存业务值完整展示;密码、验证码、Token、Secret、私钥、支付密钥、回调凭据、Authorization、Cookie、签名 URL 和完整第三方原始正文从持久化前删除。 + +## 6. 查询视角与 DTO + +| 视角/API | 请求 DTO 重点 | 响应 DTO 重点 | +|---|---|---| +| `GET /audit/events` | 时间、action/category、actor、source/result/risk、scope、resource、request/correlation、分页 | `EventPage{total,page,page_size,items[],retention}` | +| `GET /audit/events/{event_id}` | 稳定 event ID | `EventDetail`:完整 `EventView`、全部 `ResourceView`、retention | +| `GET /audit/actors/{kind}/{id}/events` | actor、action/result/risk/resource、时间、分页 | 与事件列表相同的稳定分页投影 | +| `GET /audit/resources/search` | 首批 `iot_card/device/shop/order/refund`、精确 keyword、分页 | `ResourceCandidate{type,id,key,display_name,identity_snapshot,historical}` | +| `GET /audit/resources/{type}/{id}/timeline` | Registry type、内部稳定 ID、时间、action/result、分页 | 资源以任意 relation 参与的 `EventPage` | +| `GET /audit/requests/{request_id}/timeline` | 稳定 request ID | `LinkTimeline`、Access Log 检索 ID、按 `record_source` 分组节点 | +| `GET /audit/correlations/{correlation_id}/timeline` | 稳定 correlation ID | Audit、Integration、Outbox、任务摘要和 Domain Ledger 引用节点 | +| `GET /audit/finance/timeline` | shop/wallet/order/payment/refund/recharge/approval/trade/actor/correlation/time 任一稳定条件 | `FinanceTimelineNode`、amount/balance、权威表字段、调查引用 | +| `GET /audit/risks/overview` | 最长 31 天时间范围、risk/result/action/source | signals、risks、results、actions、sources、trend、retention | +| `GET /audit/risks/events` | 总览同源筛选及分页 | 风险范围内的 `EventView` 分页 | +| `GET /audit/integrations/overview` | 时间、bucket 及 Integration 组合筛选 | 总量、异常/unknown/陈旧 pending/状态变化、平均/P95、分布、趋势、retention | +| `GET /audit/integrations` | provider/direction/operation/result/category/resource/series/linkage 等组合筛选、分页 | `ListItem` 稳定编码/中文名、资源、结果、耗时、链路 | +| `GET /audit/integrations/{integration_id}` | 稳定 integration ID | identity/resource/trigger/result/content/linkage/timestamps/attempts/fidelity/retention | +| `GET /agent/resource-activities/{type}/{identifier}` | 业务稳定 identifier、时间、分页;范围来自认证上下文 | 资源摘要、`SubjectActivity[]`、retention | +| `GET /enterprise/resource-activities/{type}/{identifier}` | 企业仅支持当前有效授权卡/设备 | 与代理相同安全 DTO,不含内部调查引用 | + +所有完整路径均以 `/api/admin` 开头。`request_id/correlation_id` 生命周期、15 条接口入参与返回、页面字段来源和降级规则统一见[审计链路与前端接入指南](跨视角调查与前端导航契约.md);专题说明见[平台基础审计调查接口](平台基础审计调查接口.md)、[外部集成调查接口](外部集成调查接口.md)和[主体资源活动接口](主体资源活动接口.md)。OpenAPI 权威制品为 `docs/admin-openapi.yaml` 和运行时生成的 `logs/openapi.yaml`。 + +平台调查节点统一使用 `investigation_refs`。字段不存在时隐藏跳转;只有稳定 Key、没有内部 ID 时先精确资源搜索,零命中或多命中均不自动选择,禁止按名称、时间、中文描述或编号前缀猜测关系。 + +## 7. 平台与主体可见性 + +| 身份 | 可见能力 | 不可见能力 | +|---|---|---| +| 超级管理员/平台账号 | 全部 `/api/admin/audit/*` 在线数据;完整已存业务字段、操作者、风险、before/after、Integration 安全摘要 | 系统安全凭据、归档对象读取、修改/删除/导出/恢复/风险处置 | +| 代理 | 自己及下级店铺当前有权资源的 `subject_result/subject_detail` | 平台 actor、内部原因/备注/风险/before/after、event ID、request/correlation、Integration 内容、`internal_only` | +| 企业 | 当前有效授权卡/设备的安全活动;授权撤销后立即不可读 | 平台接口、企业 ID 伪造范围、分配/换货独立内部调查、所有内部字段 | +| 个人客户 | 不开放本审计中心调查接口 | 平台及主体资源活动接口 | + +代理/企业查询先验证当前资源归属,再读主体投影;不存在、不支持、越权或授权撤销统一返回“无权限操作该资源或资源不存在”,不泄露资源或事件是否存在。`subject_data` 必须在写入时按 Registry 白名单生成,Query 不从内部 before/after 临时删字段拼装。 + +## 8. 代表性业务样例 + +### 多卡设备 + +操作设备第二卡槽的卡时,目标卡是 primary/affected,入口设备是 reference,绑定关系保存 `slot_position=2` 和当时 `is_current`。切换当前卡同时关联设备、旧卡、新卡和两个 binding,分别写各自前后状态,因此设备、两张卡和绑定关系时间线都能定位该事件。 + +### 换货 + +卡换货按角色关联换货单、旧卡、新卡、旧新钱包、流水、套餐权益和客户绑定,并分别保存旧新卡 ICCID/VirtualNo。设备换货保存旧新设备 VirtualNo/IMEI/SN,并逐张关联实际涉及的绑定卡和卡槽;不会用一个 `asset_identifier` 掩盖多资源关系。 + +### 资金 + +订单钱包扣款、退款回充、充值入账、预占/释放、佣金和提现的成功审计与钱包、唯一流水、业务单及必要 Outbox 同一 GORM 事务;审计失败则业务回滚。资金时间线返回 `amount_authority`,金额与余额冲突时以钱包流水及业务表为准,不修改历史 Audit Event。 + +### 批量 + +批量分配、批量配置、导入购包等写一条根事件,记录 total/success/fail 和 `success/partial/failed`;每个实际变化或已识别失败资源写子事件,共享 correlation 并以根 event 为 parent。未命中、未处理和幂等无变化项不伪造成功子事件。 + +## 9. 异常闭环与事务策略 + +| 场景 | 处理 | +|---|---| +| 关键成功或 partial 且改变业务事实 | Audit 与 Domain Ledger/必要 Outbox 同事务;审计失败整体回滚 | +| 已定位主要资源的业务拒绝 | 业务不落地,随后用独立短事务写 `denied` | +| 已定位主要资源后的执行失败 | 保留原业务错误,独立短事务写 `failed` | +| 失败审计二次失败 | 不覆盖原错误;写 critical 日志并递增监控计数 | +| 参数解析前失败或无法定位主要资源 | 不造无资源 Audit Event,只进入 Access/Security Log | +| 外部交互未改变内部事实 | 只写 Integration Log | +| Callback/Worker 改变内部事实 | 保留 Integration/任务事实,并以真实 external/system actor 写 Audit Event | +| 普通查询 | N/A,不写 Audit Event | +| Registry 敏感读取 | 返回敏感结果前写 Audit;失败关闭,不返回结果 | + +Writer 对未知 action、未知 resource/role、缺少主要资源、资源关系不完整、安全凭据未清理和不符合事务要求的动作保持 fail-closed。业务修正通过新的受控动作产生新事件,不更新旧事件。 + +## 10. 每日归档、月度清理与在线窗口 + +1. 每日任务按 `Asia/Shanghai` 前一完整自然日半开区间归档。 +2. Audit 对象使用 UTF-8 JSONL+gzip,每行一个事件及完整 `resources[]`;Integration 每行一条结构化持久化事实。 +3. 每个对象有 manifest、SHA-256、对象 metadata 和 `tb_log_archive_run` ledger;内容变化创建新 revision,不覆盖旧对象。 +4. 月初先完成上月最后一天归档,并用数据库当前内容形成 Integration 最终 revision;任何 pending、缺日、数量/hash/对象不一致都阻止整月清理。 +5. 门禁通过后,Retention Worker 以 1000 行有界批次按 Event Resource → Audit Event → Integration Log 顺序物理删除目标月 PostgreSQL 数据,并通过 cleanup 时间断点续跑。 +6. 清理任务在当前月写 `audit.retention_cleanup`,不写回被清理月份;对象和 manifest 长期保留,不被该任务删除。 + +默认生产开关为 `JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=false`。灰度、仿真、SQL 观测、告警和启停步骤见[审计归档灰度操作手册](归档灰度操作手册.md)。 + +所有在线调查 DTO 返回 `retention`。显式范围早于在线边界或跨越边界时返回 `CodeAuditDataArchived`;ID-only 详情在线库不存在仍按资源不存在处理。接口不扫描对象存储,也不返回误导性的成功空结果或部分历史。 + +## 11. 发布、回滚与监控 + +发布前要求迁移版本 `205`、`dirty=false`,旧 Writer 生产调用为 0,Registry/覆盖门禁通过,两份 OpenAPI 可生成,生产物理清理开关保持关闭。 + +回滚只影响后续流量:保留当前在线 Audit、Integration、Domain Ledger、Outbox、归档 ledger 和已校验对象;不得恢复旧账号/资产 Writer,不得用 migration down 删除已有事实,不得从对象存储回填 PostgreSQL 伪造在线历史。错误事实通过前向业务动作修正。 + +| 监控项 | 阈值 | +|---|---| +| 关键成功审计写入失败 | 任一失败即 critical | +| failed/denied 短事务二次失败 | 增量必须为 0 | +| 未注册 action/resource/role | 必须为 0 | +| 安全凭据落库或响应命中 | 必须为 0 | +| 批量根子计数、parent/correlation 差异 | 必须为 0 | +| 旧 Writer 生产写调用 | 必须为 0 | +| 数据库受控查询 | < 50ms | +| 审计读接口 | P95 < 200ms,P99 < 500ms;发布后用既有 Access Log 观测 | +| 每日归档缺失/失败、对象或 manifest 不一致、月度非 final | critical,阻止清理 | + +## 12. 明确未实现项 + +- 细粒度平台审计权限码和平台数据行过滤。 +- 面向用户的审计导出、归档下载、对象存储历史查询或恢复。 +- PostgreSQL 冷热联合查询、归档回填和月分区。 +- 审计事件修改、删除、专用纠错 API;Retention Worker 的受控整月清理是唯一删除例外。 +- Integration Log 重试、补偿、结果确认、外部单号绑定、人工恢复、修改或删除。 +- 风险处置工单、自动封禁和自动恢复。 +- 任意关系图、任意 JSONB/JSONPath 搜索、JSONB GIN 和 Redis 查询缓存。 +- 将旧 operation log 回填、转换或拼入新审计中心。 +- 从 Access Log、相近时间、相似资源或中文描述猜测业务链路和技术重试。 + +## 13. 验收依据 + +- 实现:`internal/model/audit_event.go`、`internal/model/integration_log.go`、`internal/model/log_archive_run.go`、`internal/infrastructure/audit/`、`internal/query/audit/`、`internal/query/integration/`、`internal/application/auditarchive/`。 +- API:`internal/handler/admin/audit.go`、`internal/routes/audit.go`、`internal/model/dto/audit_dto.go`、`docs/admin-openapi.yaml`、`logs/openapi.yaml`。 +- 覆盖:`.scratch/tech-global-audit/审计覆盖基线.md`、`.scratch/tech-global-audit/审计覆盖清单.json`、`cmd/audit-coverage`。 +- 发布与留存:[审计归档灰度操作手册](归档灰度操作手册.md)。测试环境完整月仿真为 `2001-02`:56/56 归档成功,Integration 28/28 final,清理后三张目标日志表归零,6 条边界哨兵和对象归档全部保留。 +- 性能只读观测:事件列表 0.315ms、资源时间线 0.116ms、Integration 列表 0.186ms、风险聚合 0.123ms;API P95/P99 使用既有 9.4 证据及发布后 Access Log 阈值,不执行接口压测。 +- 本 Change 按明确约束不新增、修改或运行自动化测试;交付验证使用 OpenAPI 生成、覆盖静态门禁、编译、LSP、迁移/数据核对、只读性能观测和文档一致性检查。 + +## 14. 最终验收结论 + +2026-08-07 按 proposal、design、8 份 delta spec 和 77 项 tasks 完成最终核对: + +| 门禁 | 最终证据 | 结论 | +|---|---|---| +| 静态覆盖 | `cmd/audit-coverage` 重新生成 692 条当前入口;Action/Resource Registry、敏感读取、N/A 和旧 Writer 白名单无未登记项 | PASS | +| LSP 与构建 | 变更 Go 文件 `gopls check` 无诊断;`go build ./...` 退出码 0 | PASS | +| 迁移 | `.env.local` 测试库只读核对版本 205、未标 dirty;无事实隔离 schema 已完成 `000199`~`000205` up/down,现有事实库未执行破坏性 down | PASS | +| OpenAPI | `cmd/gendocs` 与运行时生成成功;两份制品各含 15 条平台调查及代理/企业活动路径且契约一致;固定请求/响应枚举已生成真实 `enum`,审计相关公开 schema 无缺失字段说明,RouteSpec 已写明业务页字段、调查引用和通知目标映射 | PASS | +| 前端接入文档 | [审计链路与前端接入指南](跨视角调查与前端导航契约.md) 已覆盖 request/correlation/parent/integration/series 生命周期、数据库落点、自动与人工轮询边界、15 条接口全部入参与主要返回字段、现有业务页面入口矩阵和完整调用链;OpenAPI 48 个请求参数名无遗漏 | PASS | +| 数据库与 API 性能 | 四条代表性数据库查询均小于 1ms且低于 50ms;API P95/P99 复用 9.4 既有证据,发布后以 Access Log 按 200ms/500ms 阈值持续观测 | PASS | +| 身份与凭据 | 平台、代理店铺层级、企业有效授权和 `internal_only` 边界已核对;数据库/响应凭据抽样为 0 命中,无审计导出、归档读取/恢复或业务删除路由 | PASS | +| 事务与业务链路 | 关键成功同事务、失败短事务保留原错、批量根子计数、真实 actor、request/correlation/parent/series 及四类事实边界证据已在 11.4 收口 | PASS | +| 旧写切换 | 旧账号/资产 Writer 生产写为 0;旧表、旧资产历史查询和手动轮询运行 ledger 按契约保留 | PASS | +| 归档与清理 | `2001-02` 共 56/56 归档成功、Integration 28/28 final;dry-run 后测试库目标月三表归零,6 条边界哨兵、56 条断点及全部对象/manifest 保留,生产清理开关关闭 | PASS | +| 制品一致性 | `git diff --check` 无错误;`openspec validate build-multi-view-audit-center --strict` PASS | PASS | + +验收范围内已知错误为零,77 项任务全部完成,Change 可归档。未实现项仍严格保持第 12 节边界,不因验收扩展。 diff --git a/docs/feature-504-multi-view-audit-center/平台基础审计调查接口.md b/docs/feature-504-multi-view-audit-center/平台基础审计调查接口.md index 4e1d92b..29240df 100644 --- a/docs/feature-504-multi-view-audit-center/平台基础审计调查接口.md +++ b/docs/feature-504-multi-view-audit-center/平台基础审计调查接口.md @@ -47,7 +47,7 @@ | 店铺主钱包流水 | path `shop_id`、资产类型与 ID | 资金视角使用 `shop_id`;资产 ID 存在时使用对应资源时间线 | | 资产钱包流水 | 上层 `wallet_id` | 资金视角使用 `wallet_id`;不解析业务编号前缀猜测资源 | -资金视角已交付;缺失的支付、退款、钱包等关联由服务端 Query 解析。完整逐页面映射和调用链见[跨视角调查与前端导航契约](跨视角调查与前端导航契约.md)。 +资金视角已交付;缺失的支付、退款、钱包等关联由服务端 Query 解析。完整 ID 生命周期、逐接口字段、页面映射和调用链见[审计链路与前端接入指南](跨视角调查与前端导航契约.md)。 ## 统一调查引用 diff --git a/docs/feature-504-multi-view-audit-center/归档灰度操作手册.md b/docs/feature-504-multi-view-audit-center/归档灰度操作手册.md new file mode 100644 index 0000000..e0e08ba --- /dev/null +++ b/docs/feature-504-multi-view-audit-center/归档灰度操作手册.md @@ -0,0 +1,166 @@ +# 审计归档灰度操作手册 + +## 灰度开关 + +灰度期必须保持: + +```bash +JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=false +``` + +该配置使每月任务和人工投递的 `audit:monthly:retention` 只执行只读门禁演练,不写清理断点、不删除在线数据。每日 Audit/Integration 归档和 Integration 月度最终复核不受影响。 + +完整自然月可以在显式确认的隔离测试数据库中按真实日界构造,无需等待现实时间流逝。测试环境的完整月 dry-run 全部通过后,才可临时启用清理验证删除范围;生产环境仍须由发布决策将开关改为 `true`。关闭开关并重启 Worker 即可回滚;不得手工删除在线日志或对象存储归档。 + +## 隔离测试环境完整月仿真 + +仓库提供 `cmd/audit-retention-simulate`,固定使用 `2001-02` 和专属实例标识。命令仅允许数据库名包含 `test`,且确认值必须与当前数据库名完全一致;目标月已有任何 Audit、Integration 或归档账本时立即拒绝,避免误清理已有数据。 + +```bash +source .env.local +JUNHONG_AUDIT_RETENTION_SIMULATION_CONFIRM="$JUNHONG_DATABASE_DBNAME" \ + go run ./cmd/audit-retention-simulate +``` + +命令为目标月每日构造一条 Audit Event、Event Resource 和 Integration Log,并在月初前一秒与下月零点各放置一组边界哨兵。流程依次执行每日归档、对象和 manifest 回读、故障重试、Integration 最终 revision、清理关闭 dry-run、清理断点核对、测试库物理清理及边界复核。Redis 不参与该留存流程。 + +## 每日观测 + +以下查询只读取归档运行账本。将日期替换为待验收自然月的半开区间: + +```sql +SELECT + archive_date, + source, + status, + attempt_count, + revision, + is_final, + event_count, + resource_count, + record_count, + uncompressed_bytes, + compressed_bytes, + CASE + WHEN uncompressed_bytes = 0 THEN 1 + ELSE ROUND(compressed_bytes::numeric / uncompressed_bytes, 4) + END AS compression_ratio, + sha256, + object_key, + manifest_key, + error_summary, + completed_at, + cleanup_started_at, + cleaned_at +FROM tb_log_archive_run +WHERE archive_date >= DATE '2026-07-01' + AND archive_date < DATE '2026-08-01' + AND source IN ('audit', 'integration') +ORDER BY archive_date, source; +``` + +每日必须记录:两类归档是否成功、重试次数、完成时间、对象大小、压缩率、SHA-256、Audit 事件/资源数量、Integration 记录数量及 revision。对象 metadata、manifest 和 gzip 实际 SHA-256/计数差异以归档任务自身复核结果及日志为准,不能只看账本字段。 + +## 月度汇总与积压 + +```sql +WITH expected AS ( + SELECT day::date AS archive_date, source + FROM generate_series(DATE '2026-07-01', DATE '2026-07-31', INTERVAL '1 day') AS day + CROSS JOIN (VALUES ('audit'), ('integration')) AS sources(source) +) +SELECT + COUNT(*) AS expected_runs, + COUNT(r.id) FILTER (WHERE r.status = 'success') AS successful_runs, + ROUND(COUNT(r.id) FILTER (WHERE r.status = 'success')::numeric / COUNT(*), 4) AS success_rate, + COUNT(*) FILTER (WHERE r.id IS NULL OR r.status <> 'success') AS backlog_runs, + COALESCE(SUM(r.compressed_bytes), 0) AS object_bytes, + COALESCE(SUM(r.uncompressed_bytes), 0) AS source_bytes, + CASE + WHEN COALESCE(SUM(r.uncompressed_bytes), 0) = 0 THEN 1 + ELSE ROUND(SUM(r.compressed_bytes)::numeric / SUM(r.uncompressed_bytes), 4) + END AS compression_ratio, + COALESCE(MAX(r.revision), 0) AS max_revision, + COALESCE(SUM(r.attempt_count), 0) AS attempts +FROM expected e +LEFT JOIN tb_log_archive_run r + ON r.archive_date = e.archive_date + AND r.source = e.source + AND r.instance_id = 'primary'; +``` + +Integration 月度最终复核后,目标月所有 `source='integration'` 记录必须同时满足 `status='success'`、`is_final=true`,且无 `pending` 阻断日志。灰度期开关关闭时,目标月所有 `cleanup_started_at/cleaned_at` 必须为 `NULL`。 + +## 清理 dry-run 与耗时估算 + +dry-run 只执行现有月度门禁的只读核对:完整月每日 ledger/manifest、对象 metadata、对象大小、SHA-256、Audit 事件与资源计数、Integration 最终 revision。生产和共享灰度环境禁止调用物理清理方法验证 dry-run;显式确认的隔离测试数据库必须在 dry-run 通过后执行一次物理清理,用于证明删除范围。 + +先统计预计删除行数和 1000 行批次数: + +```sql +WITH counts AS ( + SELECT + (SELECT COUNT(*) FROM tb_audit_event WHERE created_at >= TIMESTAMPTZ '2026-07-01 00:00:00+08' AND created_at < TIMESTAMPTZ '2026-08-01 00:00:00+08') AS audit_events, + (SELECT COUNT(*) FROM tb_audit_event_resource r JOIN tb_audit_event e ON e.id = r.audit_event_id WHERE e.created_at >= TIMESTAMPTZ '2026-07-01 00:00:00+08' AND e.created_at < TIMESTAMPTZ '2026-08-01 00:00:00+08') AS audit_resources, + (SELECT COUNT(*) FROM tb_integration_log WHERE created_at >= TIMESTAMPTZ '2026-07-01 00:00:00+08' AND created_at < TIMESTAMPTZ '2026-08-01 00:00:00+08') AS integration_logs +) +SELECT + *, + CEIL(audit_events / 1000.0) + CEIL(audit_resources / 1000.0) + CEIL(integration_logs / 1000.0) AS estimated_batches +FROM counts; +``` + +清理预估耗时为 `estimated_batches × 灰度环境单批删除 P95`,并额外预留 30%。未取得真实单批 P95 前不得启用清理。 + +## 告警与启用门禁 + +- 每日 08:00 前任一前日 Audit/Integration 记录缺失、失败或仍为 running:critical。 +- running 持续超过 3 小时、`attempt_count > 1` 或对象/manifest 复核失败:critical。 +- 有数据时压缩率不在 `(0, 1]`、SHA-256 为空或对象大小为零:critical。 +- 月度最终复核后任一 Integration 记录 `is_final=false` 或存在 pending:critical。 +- 只归档模式出现任一 `cleanup_started_at` 或 `cleaned_at`:critical,立即停 Worker 并调查。 +- 完整月成功率必须为 100%,积压、hash 差异、计数差异和未终结 Integration 数必须均为 0。 +- 故障重试、月度只读 dry-run、启停与关闭回滚均须保留时间、环境、操作者、日志位置和结果证据。 + +任何一项未通过时保持生产开关关闭,修复后重新执行完整自然月仿真;不得以补写验收记录替代真实归档、对象复核和删除范围核对。 + +## 全局审计切换监控阈值 + +contract 切换期按以下阈值观测;任一 critical 条件命中时停止扩大发布范围,保留已提交事实并前向修复。 + +| 观测项 | 阈值 | 证据来源与处置 | +|---|---|---| +| 关键成功审计写入 | 任一失败即 critical | 业务与 Audit Event 同事务回滚;按 action/request/correlation 定位异常生产者,不得补写成功事件。 | +| 失败/拒绝审计二次写入 | `secondary_write_failure_count` 增量必须为 0 | 监控“失败或拒绝审计二次写入失败” critical 日志;保留原业务错误。 | +| 未注册 action/resource/role | 数量必须为 0 | 运行 `go run ./cmd/audit-coverage`;任一未注册项阻断发布。 | +| 安全凭据落库或响应命中 | 数量必须为 0 | 使用禁止键/凭据值受控抽样;发现后停止相关生产者并前向修复,不修改已提交审计事实。 | +| 批量根子计数差异 | 非法统计、缺少父事件或 correlation 数必须为 0 | 根事件 `success_count/fail_count` 与可查子事件不一致即 critical,暂停该批量用例。 | +| 旧 Writer 调用 | 生产写调用必须为 0 | 覆盖门禁只允许旧资产历史查询和手动轮询运行 ledger 白名单;命中 Create/Update/裸 goroutine 立即阻断发布。 | +| 数据库审计查询 | 单条受控查询 < 50ms | 使用在线数据的 `EXPLAIN (ANALYZE, BUFFERS)` 观测;超阈值先检查索引和无界时间范围,不直接增加 Redis 缓存或分区。 | +| 审计读接口 | P95 < 200ms,P99 < 500ms | 使用现有 Access Log 统计真实流量;本 Change 不新增或运行自动化测试。 | + +## contract 发布与回滚 + +1. 发布前确认数据库迁移版本为 `205` 且 `dirty=false`,清理开关保持关闭,覆盖门禁中的旧 Writer 生产调用为 0。 +2. 发布异常时先停止扩大流量,暂停明确异常的生产者或 Worker;不删除当前在线 Audit Event、Integration Log、Domain Ledger、Outbox 或已校验对象存储归档。 +3. 应用回滚只能回到“旧 Writer 已停写”的 contract 基线版本。不得回到恢复 `tb_account_operation_log`/`tb_asset_operation_log` 生产写入或裸 goroutine 审计的版本;无合法基线时只做前向修复。 +4. 有任何 Audit Event 或归档 ledger 事实的环境不执行 `000199`/`000205` down;有 Outbox parent、长 correlation 或订单预占事实的环境不执行 `000200`/`000201`/`000203` down。业务回滚保留版本 `205` 结构,不以 migration down 代替应用回滚。 +5. 需要暂停物理清理时,将 `JUNHONG_WORKER_AUDIT_RETENTION_CLEANUP_ENABLED=false` 并重启 Worker;每日归档和新业务审计继续运行。 +6. 已清理月份不从对象存储回填 PostgreSQL,不伪造在线历史;查询仍按 retention 边界返回已归档错误。错误业务结论通过新的受控业务动作和新 Audit Event 前向修正,不改写原事实。 +7. 回滚后重新运行覆盖门禁、路由/OpenAPI 扫描、数据库凭据抽样和归档账本核对;任一旧 Writer 新增、在线事实减少或归档对象丢失都阻断结案。 + +## 迁移演练约束 + +- 本 Change 的增量迁移范围为 `000199`~`000205`。共享 `.env.local` 测试库已有 Audit 和归档事实,不允许直接 down。 +- down 只能在无事实的隔离 schema/数据库执行;`000200` 和 `000203` 会删除业务快照列,不得对有事实环境演练。 +- `000199_create_audit_event.down.sql` 自带事务边界,不应依赖外层事务自动清理隔离 schema;演练结束必须显式核对并删除精确的隔离对象。 +- 2026-08-06 在 `.env.local` 测试 PostgreSQL 中对无事实隔离 schema 完成 `000199`~`000205` up/down:上行后 Audit/归档表存在,逆序 down 后表和增量列均恢复;隔离 schema 已显式删除,`public` 保持版本 `205` 且 `dirty=false`。 + +## 2026-08-06 仿真记录 + +- 环境:`.env.local` 的 `junhong_cmp_test` 测试数据库及其对象存储。 +- 月份:`2001-02`,共 28 个完整自然日;归档账本 56 条,成功 56 条。 +- Integration 最终 revision:28/28;故障重试后最大 revision=2、最大 attempt_count=2。 +- 压缩率:0.5047;dry-run 时目标月 Audit Event/Event Resource/Integration Log 各 28 条,预计 3 个 1000 行删除批次,且清理断点为 0。 +- 物理清理后目标月三表均为 0;月前一秒和下月零点的 Audit Event、Event Resource、Integration Log 共 6 条全部保留。 +- 目标月 56 条账本均具有 `cleanup_started_at` 和 `cleaned_at`;当月生成 1 条 `evt_retention_2001_02` 清理审计;对象归档及 manifest 未删除。 diff --git a/docs/feature-504-multi-view-audit-center/跨视角调查与前端导航契约.md b/docs/feature-504-multi-view-audit-center/跨视角调查与前端导航契约.md index 7bb0240..3685d7a 100644 --- a/docs/feature-504-multi-view-audit-center/跨视角调查与前端导航契约.md +++ b/docs/feature-504-multi-view-audit-center/跨视角调查与前端导航契约.md @@ -1,8 +1,326 @@ -# 跨视角调查与前端导航契约 +# 审计链路与前端接入指南 -本文对应 `build-multi-view-audit-center` 任务 9.5,冻结 request、correlation、资金和风险调查接口,以及现有业务页面进入审计中心的第一跳。所有字段路径均位于统一响应的 `response.data` 下;列表字段位于 `items[]`。本文只定义前端调用契约,不包含前端页面实现。 +本文对应 `build-multi-view-audit-center` 任务 9.5,作为审计链路和前端接入的单一说明入口。它回答以下问题:链路 ID 从哪里产生、存到哪里、不同入口为什么有的没有 `request_id`、15 条审计接口如何调用、每个入参与返回字段表示什么,以及前端应在哪些现有业务页面增加入口。 -## 跨视角只读接口 +所有接口都使用统一响应 `{code,msg,data,timestamp}`;本文中的返回字段默认位于 `response.data`,列表项默认位于 `response.data.items[]`。本文定义调用契约,不包含前端页面实现;机器可读类型、枚举和约束以 `docs/admin-openapi.yaml` 为准。 + +## 一、先理解五个稳定 ID + +| 字段 | 表示什么 | 谁产生 | 前端能否产生 | 主要用途 | +|---|---|---|---|---| +| `request_id` | 一次 HTTP 请求 | 请求携带 `X-Request-ID` 时沿用;否则 Fiber 中间件生成 UUID | 业务页面不应为了审计查询临时生成;HTTP 客户端可统一传入自己的全局唯一 ID | 把 Access Log、同一次 HTTP 内的 Audit Event、Integration Log 和 Outbox 串起来 | +| `correlation_id` | 一条可跨 HTTP、Outbox、Asynq、Callback 和外部交互的业务链路 | HTTP 起点默认等于 `request_id`;进入异步或具体业务后可改为订单号、支付号、Integration ID、任务 ID 等稳定链路值 | 不得根据时间、资源或中文描述推断 | 查询完整业务链路;它不表示技术重试 | +| `parent_event_id` | 一个审计事件的直接父事件 | 批量根事件、异步派生或消费者在创建子事件时传播 | 不生成 | 表示直接因果和批量根子关系,不替代 correlation | +| `integration_id` | 一次外部交互尝试的稳定 ID | Integration Log Writer 生成或调用方提供 | 不生成 | 查询单次外部交互详情,也是轮询链路的重要入口 | +| `trigger_series` + `attempt` | 同一外部操作的显式技术尝试序列和序号 | 可重试的 Integration 调用方 | 不生成 | 判断第几次重试;没有 series 时只展示单次记录,禁止猜测重试 | + +### `request_id` 的实际生命周期 + +1. 浏览器、开放接口调用方或外部回调发起 HTTP 请求。调用方如已提供 `X-Request-ID`,服务端沿用;否则服务端生成 UUID。 +2. 服务端把同一个值写入响应头 `X-Request-ID`,浏览器可读取该响应头用于问题反馈。 +3. Access Log 把它记录为 `request_id`,同时审计上下文在 HTTP 起点设置 `request_id`,并默认设置 `correlation_id=request_id`。 +4. 当前请求内发生业务变更时,Audit Writer 将其写入 `tb_audit_event.request_id`;发生外部交互时可写入 `tb_integration_log.request_id`;创建可靠事件时可写入 `tb_outbox_event.request_id`。 +5. 前端只有在事件或 Integration 返回了该值,或运维人员从响应头/Access Log 得到该值时,才调用请求时间线。普通资源详情页不需要先取得 `request_id`。 + +不是每个 HTTP 请求都会产生 Audit Event。例如普通列表查询只有 Access Log,没有业务审计事件;此时即使响应头有 `X-Request-ID`,请求时间线也可能没有业务节点。 + +### `request_id` 在哪里落库 + +| 事实 | 存储位置 | `request_id` 字段 | `correlation_id` 字段 | 说明 | +|---|---|---|---|---| +| HTTP 调试事实 | Access Log 文件 | JSON 字段 `request_id` | 当前访问日志不承担业务 correlation 查询 | 用于按请求排查原始 HTTP;审计时间线不会扫描日志文件 | +| Audit Event | `tb_audit_event` | `request_id`,非空列但允许保存空字符串 | `correlation_id`,非空列但允许保存空字符串 | 解释谁对什么资源做了什么 | +| Integration Log | `tb_integration_log` | `request_id`,可空 | `correlation_id`,可空 | 解释调用了哪个外部系统、结果和尝试序列 | +| Outbox | `tb_outbox_event` | `request_id`,允许空字符串 | `correlation_id`,允许空字符串 | 可靠投递事实;投递成功不等于业务成功 | +| Asynq | 任务 payload 或 Audit Event 的任务资源引用 | 按任务类型选择性携带 | 按任务类型选择性携带 | Redis 队列不是审计查询数据源;查询只展示已持久化引用,不扫描历史队列 | +| 手动轮询运行记录 | `tb_polling_manual_trigger_log` | 没有该字段 | 没有该字段 | 只承担进度、结果、触发人和卡列表;人工触发命令另写 Audit Event | + +## 二、轮询为什么通常没有 `request_id` + +普通自动轮询由 Scheduler/Asynq Worker 触发,不是 HTTP 请求,因此没有 `request_id` 是正确语义: + +1. Worker 调用 Gateway 前创建 Integration Log,生成 `integration_id`。 +2. 普通轮询的 `tb_integration_log.request_id` 保持空;`trigger_series` 和 `correlation_id` 通常使用该 `integration_id`。 +3. Gateway 返回后,如果观测结果实际改变卡、设备、套餐或网络状态,Worker 才写 Audit Event。 +4. 该 Audit Event 的 `request_id` 仍为空,`correlation_id` 使用 Integration ID、Asynq Task ID 或稳定任务标识,从而与外部尝试和资源时间线关联。 +5. 前端看到 `request_id` 为空时隐藏“请求链路”;`correlation_id` 非空时显示“业务链路”;无论两者是否存在,都可以继续使用 `integration_id` 或资源引用。 + +人工轮询需要区分两个事实: + +- 管理员点击“手动触发”的 HTTP 命令会产生 Audit Event,因此该“谁发起了轮询”事件有 `request_id`。 +- 随后真正执行轮询的是 Worker。当前手动队列只携带卡 ID,执行阶段没有原 HTTP `request_id`;Gateway 尝试和实际观测变化按 Worker 自身的 Integration ID/correlation 串联。 +- `tb_polling_manual_trigger_log` 继续显示进度和结果;Audit Event 解释谁触发/取消;Integration Log 解释 Gateway 是否被调用以及调用结果。三类事实不能合并成一张记录。 + +## 三、前端选择入口的顺序 + +前端不应把 `request_id` 当作所有审计入口的必填条件。统一按以下顺序选择: + +1. 业务页面已有内部 ID:平台直接调用资源时间线。 +2. 代理或企业页面已有 ICCID、VirtualNo、分配单号、换货单号等业务 identifier:调用主体资源活动接口。 +3. 订单、退款、充值、钱包或店铺页面:直接以任一稳定业务 ID 调用资金时间线,关联 ID 由服务端补全。 +4. 审计节点返回 `investigation_refs`:按引用字段跳转事件、操作者、资源、请求、correlation 或 Integration 视角。 +5. 只有 `resource_key` 没有内部 ID:先调用资源精确搜索,零命中或多命中时让用户选择,不自动猜测。 +6. 必需字段不存在:隐藏入口;不得用名称、中文说明、相近时间、编号前缀或第三方流水猜关系。 + +## 四、15 条接口总览 + +| 接口 | 何时调用 | 必填入参 | 主要返回 | +|---|---|---|---| +| `GET /api/admin/audit/events` | 平台审计中心全局列表或其他节点带筛选跳转 | 无 | `EventPage` | +| `GET /api/admin/audit/events/{event_id}` | 查看一个稳定审计事件 | `event_id` | `EventDetail` | +| `GET /api/admin/audit/actors/{kind}/{id}/events` | 查看某个操作者行为 | `kind`、`id` | `EventPage` | +| `GET /api/admin/audit/resources/search` | 只有业务 Key、没有内部资源 ID | `resource_type`、`keyword` | `ResourceSearchPage` | +| `GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline` | 平台业务页面或资源引用进入审计 | `resource_type`、`resource_id` | `EventPage` | +| `GET /api/admin/audit/requests/{request_id}/timeline` | 已有真实 request ID 时调查一次 HTTP | `request_id` | `LinkTimeline` | +| `GET /api/admin/audit/correlations/{correlation_id}/timeline` | 调查跨请求、异步和外部业务链路 | `correlation_id` | `LinkTimeline` | +| `GET /api/admin/audit/finance/timeline` | 从订单、退款、充值、钱包、店铺等进入资金调查 | 至少一个稳定资金条件 | `FinanceTimelinePage` | +| `GET /api/admin/audit/risks/overview` | 平台风险中心总览 | 无;时间不传时使用在线窗口 | `RiskOverview` | +| `GET /api/admin/audit/risks/events` | 从风险分桶查看明细 | 无;分页和筛选可选 | `RiskEventPage` | +| `GET /api/admin/audit/integrations/overview` | 平台 Integration 调查总览 | 无;时间不传时使用在线窗口 | `IntegrationOverview` | +| `GET /api/admin/audit/integrations` | 外部交互列表和组合筛选 | 无 | `IntegrationListPage` | +| `GET /api/admin/audit/integrations/{integration_id}` | 查看单次外部交互和尝试序列 | `integration_id` | `IntegrationDetailResponse` | +| `GET /api/admin/agent/resource-activities/{resource_type}/{identifier}` | 代理查看授权范围内资源活动 | `resource_type`、`identifier` | `SubjectActivityPage` | +| `GET /api/admin/enterprise/resource-activities/{resource_type}/{identifier}` | 企业查看当前有效授权卡或设备活动 | `resource_type`、`identifier` | `SubjectActivityPage` | + +平台 `/audit/*` 接口仅允许超级管理员和平台账号访问;代理、企业必须使用各自的主体活动接口。全部为 GET,不提供修改、删除、导出、恢复、重试、补偿或风险处置。 + +## 五、请求参数字典 + +### 通用时间与分页 + +| 字段 | 类型 | 是否必填 | 含义 | +|---|---|---|---| +| `created_from` | RFC3339 字符串 | 否 | 起始时间,包含该时刻;未传时从在线留存窗口开始 | +| `created_to` | RFC3339 字符串 | 否 | 结束时间,不包含该时刻;未传时为当前时间 | +| `page` | int | 否 | 页码,默认 1 | +| `page_size` | int | 否 | 每页数量,默认 20,最大 100 | + +显式时间早于 `retention.online_from` 或跨越在线边界时返回“数据已归档”稳定错误,不会从对象存储读取部分结果。Integration 和风险查询最长连续 31 天。 + +### 全局事件 `GET /audit/events` + +| 字段 | 类型/枚举 | 来源与用途 | +|---|---|---| +| `action` | string | 稳定动作编码,使用返回的 `action_code` | +| `category` | `configuration/reliability/asset/security/identity/business` | 动作类别 | +| `actor_kind` | `account/personal_customer/openapi/system_task/scheduled_job/external_system` | 操作者类型 | +| `actor_id` | string | 与 actor_kind 共同定位操作者 | +| `source` | `admin_api/personal_api/openapi/worker/scheduler/callback` | 操作入口 | +| `result` | `success/failed/denied/partial/unknown` | 业务结果 | +| `risk` | `low/normal/high/critical` | 风险等级 | +| `scope_type` | `platform/shop/personal_customer` | 业务范围类型 | +| `scope_id` | string | 业务范围稳定 ID | +| `resource_type` | Registry 类型 | 资源类型 | +| `resource_id` | string | 资源内部稳定 ID | +| `resource_key` | string | 资源业务稳定 Key | +| `request_id` | string | 可选精确筛选;来自真实 HTTP 请求,不是必填入口 | +| `correlation_id` | string | 可选精确筛选业务链路 | +| `created_from/to/page/page_size` | 通用字段 | 时间和分页 | + +### 事件详情、操作者和资源 + +| 接口 | 参数 | 说明 | +|---|---|---| +| `/audit/events/{event_id}` | path `event_id` | 来自事件列表或 `investigation_refs.event_id` | +| `/audit/actors/{kind}/{id}/events` | path `kind/id`;query `action/result/risk/resource_type/resource_id/created_from/created_to/page/page_size` | kind/id 来自 `actor_ref`;action 使用返回的稳定编码 | +| `/audit/resources/search` | query `resource_type/keyword/page/page_size` | resource_type 仅 `iot_card/device/shop/order/refund`;keyword 为精确业务标识 | +| `/audit/resources/{resource_type}/{resource_id}/timeline` | path `resource_type/resource_id`;query `created_from/created_to/action/result/page/page_size` | path 来自业务页面内部 ID、搜索结果或 resource_refs | + +资源搜索 keyword 规则:卡支持 ICCID/VirtualNo,设备支持 VirtualNo/IMEI/SN,店铺使用店铺编号,订单使用订单号,退款使用退款单号。 + +### request 与 correlation 时间线 + +| 接口 | 参数 | 说明 | +|---|---|---| +| `/audit/requests/{request_id}/timeline` | path `request_id` | 来自 Event/Integration 返回字段、响应头或 Access Log;普通轮询为空时不调用 | +| `/audit/correlations/{correlation_id}/timeline` | path `correlation_id` | 来自 Event、Integration、Outbox/任务或业务详情稳定字段 | + +这两个接口不接受分页,返回在线窗口内全部已持久化关联节点。request 查询会同时返回 `access_log_lookup_request_id`,供运维复制到 Access Log 检索;接口自身不扫描 Access Log。 + +### 资金时间线 `GET /audit/finance/timeline` + +| 字段 | 类型 | 前端来源 | +|---|---|---| +| `shop_id` | uint | 店铺详情或资金概况 | +| `wallet_id` | uint | 代理主钱包或资产钱包详情 | +| `order_id/order_no` | uint/string | 订单列表或详情 | +| `payment_id/payment_no` | uint/string | 支付记录或已明确返回的支付单号 | +| `refund_id/refund_no` | uint/string | 退款列表或详情 | +| `recharge_id/recharge_no` | uint/string | 代理充值或个人资产充值 | +| `approval_instance_id` | uint | 审批实例 | +| `third_party_trade_no` | string | 已明确返回的第三方交易号 | +| `actor_kind/actor_id` | enum/string | 调查某操作者涉及的资金事实,必须成对提供 | +| `correlation_id` | string | 已有稳定业务链路时使用 | +| `created_from/to/page/page_size` | 通用字段 | 时间和分页 | + +调用方至少提供一个稳定资金条件;同一业务的其他 ID 由服务端解析,不要求前端补齐。 + +### 风险接口 + +| 接口 | 参数 | 映射 | +|---|---|---| +| `/audit/risks/overview` | `created_from/created_to/risk/result/action/source` | 独立风险中心筛选 | +| `/audit/risks/events` | 上述字段 + `page/page_size` | `risks[].code→risk`、`results[].code→result`、`actions[].code→action`、`sources[].code→source` | + +### Integration overview/list + +overview 和 list 共用以下筛选;overview 另有 `bucket=hour/day`,list 另有 `page/page_size`。 + +| 字段 | 类型/枚举 | 来源与用途 | +|---|---|---| +| `integration_id` | string | 列表、通知 target 或 investigation_refs 返回的稳定 ID | +| `provider` | `ctcc/cmcc/cucc/wechat_pay/alipay/fuiou/wecom/gateway` | 外部服务提供方 | +| `direction` | `inbound/outbound` | 入站或出站 | +| `operation` | OpenAPI enum | 外部操作稳定编码,使用列表/详情返回的 operation | +| `result` | `pending/success/failed/unknown/not_found/invalid_payload/conflict/ignored/merged/rate_limited/completed/cancelled` | 原始结果 | +| `result_category` | `processing/succeeded/indeterminate/failed/not_sent` | 服务端从 result 派生的固定类别 | +| `external_id` | string | 外部系统业务或请求标识 | +| `resource_type/resource_id/resource_key` | string | 本地主要资源稳定引用 | +| `trigger_source/trigger_scene/trigger_series` | string | 触发来源、业务场景和显式尝试序列 | +| `state_changed` | bool | 是否改变本地业务状态 | +| `http_status` | 100~599 | 外部 HTTP 状态码 | +| `provider_code` | string | 外部服务稳定结果码 | +| `request_id/correlation_id` | string | 已有稳定链路字段时筛选;自动轮询 request_id 为空 | +| `created_from/to` | 通用字段 | 时间范围,最长 31 天 | + +当前 operation 枚举为:`realname_callback`、`realname_removal_callback`、`payment_precreate`、`payment_query`、`payment_callback`、`get_access_token`、`list_visible_members`、`list_visible_departments`、`get_template_detail`、`upload_approval_attachment`、`submit_approval`、`approval_callback`、`get_approval_detail`、`get_approval_info`、`query_realname_status`、`query_flow`、`query_card_status`、`query_device_info`、`set_speed_tier`、`stop_card`、`start_card`、`set_device_wifi`、`set_device_switch_mode`、`switch_device_card`、`reboot_device`、`reset_device`。前端使用 OpenAPI 或接口返回值,不维护另一份中文到编码映射。 + +### 代理与企业主体活动 + +| 接口 | `resource_type` | `identifier` | 其他参数 | +|---|---|---|---| +| `/agent/resource-activities/{resource_type}/{identifier}` | `iot_card/device/asset_allocation_record/exchange_order/shop/enterprise` | 依次使用 ICCID、VirtualNo、分配单号、换货单号、店铺编号、企业编号 | `created_from/created_to/page/page_size` | +| `/enterprise/resource-activities/{resource_type}/{identifier}` | 仅 `iot_card/device` | 卡用 ICCID,设备用 VirtualNo | `created_from/created_to/page/page_size` | + +代理店铺范围和企业 ID 只来自认证上下文,前端不能传 shop_id/enterprise_id 证明权限。 + +## 六、返回参数字典 + +### 统一响应与留存信息 + +| 字段 | 含义 | +|---|---| +| `code` | 统一响应码,成功为 0 | +| `msg` | 中文响应消息 | +| `data` | 本接口业务数据 | +| `timestamp` | 服务端响应时间 | +| `retention.online_from` | PostgreSQL 当前可在线查询的最早时间 | +| `retention.archived_before` | 早于该时间的数据已归档,不可通过这些接口读取 | +| `retention.timezone` | 留存边界时区,当前为 `Asia/Shanghai` | + +### EventPage、EventDetail 与 EventView + +`EventPage` 返回 `total/page/page_size/items[]/retention`;`EventDetail` 返回单个 EventView 并附带 retention。 + +| EventView 字段 | 含义/前端用法 | +|---|---| +| `event_id` | 稳定审计事件 ID,可打开事件详情 | +| `occurred_at/created_at` | 业务事实发生时间/审计记录写入时间 | +| `category` | 动作类别稳定编码 | +| `action_code/action_name` | 稳定动作编码/中文展示名;筛选使用 code | +| `summary` | 中文事件摘要 | +| `actor_kind/actor_id/actor_name` | 操作者类型、稳定 ID、事件发生时名称快照 | +| `actor_shop_id/name` | 操作者所属店铺快照 | +| `actor_enterprise_id/name` | 操作者所属企业快照 | +| `source` | admin_api、worker、scheduler、callback 等入口来源 | +| `request_path/request_method/ip_address/user_agent` | HTTP 来源信息;Worker/Scheduler 可为空 | +| `scope_type/scope_id/scope_name` | 业务范围类型、ID 和名称快照 | +| `result/risk_level` | 结果和风险等级稳定编码 | +| `error_code/error_summary` | 失败或拒绝时的稳定错误码和脱敏摘要 | +| `request_id` | 有值才显示“请求链路”;自动轮询通常为空 | +| `correlation_id` | 有值显示“业务链路” | +| `parent_event_id` | 直接父事件 ID | +| `batch_total/success_count/fail_count` | 批量根事件计数;非批量为 0 | +| `metadata` | 已脱敏动作扩展字段,具体含义由 action_code 定义 | +| `content_hash` | 事件不可变内容摘要 | +| `resources[]` | 事件涉及的全部资源及各自快照 | +| `investigation_refs` | 前端跨视角跳转的唯一稳定来源 | + +`resources[]` 字段:`resource_type/resource_id/resource_key/display_name` 定位资源;`relation` 为 `primary/affected/reference`;`role` 为资源业务角色;`identity_snapshot` 为事件发生时身份;`before_data/after_data` 为该资源变更前后数据;`subject_visibility/subject_summary/subject_data` 为代理或企业安全投影;`sort_order/created_at` 为展示顺序和资源关联记录写入时间。 + +`investigation_refs` 字段:`event_id`、`actor_ref{kind,id}`、`resource_refs[]{resource_type,resource_id,resource_key,display_name}`、`request_id`、`correlation_id`、`integration_refs[]{integration_id}`。任一目标所需字段不完整时隐藏该入口。 + +### ResourceSearchPage + +返回 `total/page/page_size/items[]/retention`。候选项包含 `resource_type/resource_id/resource_key/display_name/identity_snapshot/historical`;`historical=true` 表示当前业务表未命中,只从在线历史事件快照解析,不代表资源当前仍存在。 + +### LinkTimeline + +| 字段 | 含义 | +|---|---| +| `request_id/correlation_id` | 本次查询使用的链路值,二者只会对应当前查询视角 | +| `access_log_lookup_request_id` | 可复制到 Access Log 的 request ID | +| `nodes[]` | 按发生时间稳定升序的跨事实节点 | +| `retention` | Audit 与 Integration 的共同在线边界 | + +`nodes[]` 包含:`record_source`、`node_id`、`occurred_at`、`code/title`、`result/result_name`、`summary`、`reference_only`、`request_id/correlation_id/parent_event_id`、`resources[]`、`investigation_refs`、`fidelity`。 + +`record_source` 可能是 `audit_event`、`integration_log`、`outbox_event`;由事件资源派生的只读引用还可能是 `asynq_task` 或 `domain_ledger_ref`。`reference_only=true` 只表示引用,不代表该节点独立改变了业务状态。`fidelity` 包含 `request_available`、`correlation_available`、`parent_event_available`、`direct_audit_link_available`、`stable_resource_available`;false 时禁止猜测补齐。 + +### FinanceTimelinePage + +返回 `total/page/page_size/items[]/retention`。每个资金节点包含: + +- `record_source/node_id/occurred_at`:事实来源、来源内稳定 ID、发生时间。 +- `code/title/result/result_name`:来源内稳定编码和中文名称。 +- `amount/balance_before/balance_after/currency`:金额和余额,单位分,人民币为 CNY;为空表示该节点不承载该金额。 +- `shop_id/wallet{resource_type,wallet_id}`:店铺和钱包引用。 +- `amount_authority{authoritative,table,field,conflict_rule}`:是否为权威金额及其数据库表字段;冲突时按 conflict_rule 展示。 +- `facts`:该事实来源的安全结构化业务字段。 +- `investigation_refs`:继续进入事件、资源、操作者、request、correlation 或 Integration。 + +### RiskOverview 与 RiskEventPage + +RiskOverview 返回:`total`、服务端选择的 `bucket=hour/day`、`signals/risks/results/actions/sources` 分布、`trend[]` 和 retention。所有分布项统一为 `{code,name,count}`,筛选使用 code、展示使用 name。 + +- `signals[].code`:`high_risk/finance/security/failed/denied/partial/unknown`。 +- `risks[].code`:`low/normal/high/critical`。 +- `results[].code`:`success/failed/denied/partial/unknown`。 +- `actions[].code`:稳定 action_code。 +- `sources[].code`:`admin_api/personal_api/openapi/worker/scheduler/callback`。 +- `trend[]`:`bucket_at/total/high_risk/finance/security/failed/denied/partial/unknown`。 + +RiskEventPage 与 EventPage 结构相同,items[] 只包含固定风险集合内的 EventView。 + +### IntegrationOverview、ListPage 和 Detail + +IntegrationOverview 返回 `total/anomaly_count/unknown_count/stale_pending_count/state_changed_count/average_duration_ms/p95_duration_ms/results/providers/directions/trend/retention`。`results[]` 为 `{code,name,category,count}`,providers/directions 为 `{code,name,count}`,`trend[]` 为 `{bucket_at,total,succeeded,processing,indeterminate,failed,not_sent}`。分布 code 可原样回填 list 筛选;name 只用于展示。 + +Integration ListPage 返回 `total/page/page_size/items[]/retention`。每项包含: + +- `integration_id`:详情入口。 +- `provider/provider_name`、`direction/direction_name`、`operation/operation_name`:稳定编码和中文名。 +- `resource{type,id,key}`:type/id 均存在时可进入资源时间线。 +- `result/result_name/result_category`:原始结果、中文名和派生类别。 +- `duration_ms/state_changed/created_at`:耗时、本地状态是否变化和创建时间。 +- `request_id/correlation_id`:分别控制请求链路和业务链路入口;自动轮询 request_id 可为空。 + +Integration Detail 按分组返回: + +| 分组 | 字段与用途 | +|---|---| +| `identity` | `integration_id/provider/provider_name/direction/direction_name/operation/operation_name/external_id` | +| `resource` | `type/id/key`;type/id 齐全时进入资源时间线 | +| `trigger` | `source/scene/series/attempt`;series 为空时不拼接其他重试 | +| `result` | `code/name/category/http_status/provider_code/provider_message/duration_ms/state_changed/recovery_strategy`;本接口只展示,不执行恢复 | +| `content` | 已脱敏 `request_summary/response_summary/metadata/content_hash` | +| `linkage` | `request_id/correlation_id/audit_event_id`;稳定事件跳转优先使用 investigation_refs 中的 event_id | +| `timestamps` | `scheduled_at/started_at/created_at/updated_at` | +| `attempts[]` | 同 series 的 `integration_id/attempt/operation/operation_name/sent/result/result_name/result_category/duration_ms/state_changed/created_at` | +| `fidelity` | `trigger_series_available/attempt_sequence_reliable/correlation_available/resource_id_available/provider_message_fidelity` | +| `retention` | 在线留存边界 | + +### SubjectActivityPage + +代理与企业安全活动返回 `resource/total/page/page_size/items[]/retention`。 + +- `resource` 与 `related_resources[]`:`resource_type/resource_id/resource_key/display_name` 安全摘要;主体前端不得据此调用平台 `/audit/*`。 +- `items[].action_code/action_name`:安全动作编码和中文名。 +- `items[].subject_summary/subject_data`:写入时生成的主体安全摘要和白名单业务字段,不是平台 before/after 删除字段后的结果。 +- `items[].result/occurred_at`:结果和发生时间。 +- 响应不包含平台 actor、risk、内部原因、before/after、Audit Event ID、request_id、correlation_id、Integration 内容或 investigation_refs。 + +## 七、跨视角只读接口 | 视角 | 接口 | 入参来源 | 响应重点 | |---|---|---|---| @@ -14,7 +332,7 @@ 以上接口仅允许超级管理员和平台账号访问,全部为 GET。认证身份只来自认证上下文;不提供导出、修改、删除、风险处置、自动封禁、重试、补偿或恢复能力。 -## 资产和组织页面逐行导航 +### 资产和组织页面逐行导航 | 源页面 | 前置接口 | `response.data` 稳定字段 | 入口名称与可见条件 | 目标接口与参数映射 | 降级行为 | |---|---|---|---|---|---| @@ -29,7 +347,7 @@ | 企业卡列表 | `GET /api/admin/enterprises/{id}/cards` | `items[].id/iccid/virtual_no/device_id` | 平台要求 `id`;企业要求 `iccid` 且当前授权有效;代理不从此列表进入 | 平台:`iot_card/{id}`;企业:`/enterprise/resource-activities/iot_card/{iccid}` | 路由中的企业 ID 不作为授权证明;后端始终使用认证上下文复核 | | 企业设备列表 | `GET /api/admin/enterprises/{id}/devices` | `items[].device_id/virtual_no` | 平台要求 `device_id`;企业要求 `virtual_no` 且当前授权有效;代理不从此列表进入 | 平台:`device/{device_id}`;企业:`/enterprise/resource-activities/device/{virtual_no}` | 字段为空或授权撤销时隐藏/显示活动不可用,不回退平台接口 | -## 账号、交易和资金页面逐行导航 +### 账号、交易和资金页面逐行导航 | 源页面 | 前置接口 | `response.data` 稳定字段 | 入口名称与可见条件 | 目标接口与参数映射 | 降级行为 | |---|---|---|---|---|---| @@ -45,7 +363,7 @@ | 店铺主钱包流水 | `GET /api/admin/shops/{shop_id}/main-wallet/transactions` | path `shop_id`;`items[].id/asset_type/asset_id/asset_identifier` | 始终可按合法 path 显示资金入口;资产类型和 ID 齐全时显示资产审计 | 资金:`finance/timeline?shop_id={shop_id}`;审计:`resources/{asset_type}/{asset_id}/timeline` | 缺资产 ID 仍保留店铺资金入口,不按资产编号猜测 | | 资产钱包流水 | `GET /api/admin/assets/{identifier}/wallet/transactions` | 上层钱包接口 `wallet_id`;`items[].id/reference_type/reference_no` | 上层 `wallet_id` 非零时显示“资金链路” | `finance/timeline?wallet_id={wallet_id}` | `reference_type/reference_no` 仅展示;需后端节点明确返回资源引用后才能继续跳转 | -## 调查节点逐行跳转 +### 调查节点逐行跳转 | `investigation_refs` 字段 | 入口名称 | 可见条件 | 目标接口与参数 | 降级行为 | |---|---|---|---|---| @@ -56,9 +374,45 @@ | `correlation_id` | “查看业务链路” | 非空 | `GET /api/admin/audit/correlations/{correlation_id}/timeline` | 空值隐藏,不按相近时间拼链路 | | `integration_refs[].integration_id` | “查看外部交互” | 非空 | `GET /api/admin/audit/integrations/{integration_id}` | 空值隐藏,不使用数据库主键或相似资源猜测 | -`actor_ref.kind` 第一阶段只使用 `account/openapi/system_task/scheduled_job/external_system`。代理和企业活动响应不得包含 `investigation_refs`。 +`actor_ref.kind` 使用 `account/personal_customer/openapi/system_task/scheduled_job/external_system`。代理和企业活动响应不得包含 `investigation_refs`。 -## 六条完整调用链 +## 八、完整调用链 + +### HTTP 同步业务操作 + +1. 前端调用现有业务写接口;可不传 `X-Request-ID`,服务端会生成并通过响应头返回。 +2. HTTP 中间件建立 `request_id`,审计上下文初始设置 `correlation_id=request_id`。 +3. 业务变更与 Audit Event 在同一事务提交,事件写入 `request_id/correlation_id` 和资源快照。 +4. 前端从业务资源页用资源 ID 查看时间线,或从 EventView 的 `request_id` 查看该请求链路。 +5. 请求时间线只返回已经落入 Audit、Integration、Outbox 的节点;Access Log 需另用 `access_log_lookup_request_id` 检索。 + +### HTTP → Outbox → Asynq/Worker + +1. HTTP 操作产生 `request_id`,业务确定或沿用 `correlation_id`。 +2. 创建 Outbox 时,未显式传入的 request/correlation/parent 从审计上下文继承并写入 `tb_outbox_event`。 +3. Relay 将三个字段随 envelope 交给消费者;需要继续入 Asynq 的任务在 payload 中显式携带。 +4. Worker 恢复系统操作者并写子 Audit Event;有外部调用时另写 Integration Log。 +5. 前端用 correlation 时间线查看跨进程链路;`record_source` 区分 Audit、Outbox、Integration 和任务引用,不能把 Outbox delivered 当作业务成功。 + +### 外部 Callback + +1. Callback 同样经过 HTTP RequestID 中间件,因此会有 `request_id`。 +2. Integration Log 先记录入站外部交互;correlation 可等于 request ID,也可使用 payment_no、审批 SPNo 等更稳定业务键。 +3. Callback 实际改变本地业务事实时,以 `external_system` actor 写 Audit Event;没有状态变化时只保留 Integration Log。 +4. 前端从 Integration detail 的 request/correlation/resource 引用继续调查。 + +### 自动轮询 + +1. Scheduler/Asynq 选择卡并执行 Worker,没有 HTTP request,因此不生成 `request_id`。 +2. Worker 创建 Gateway Integration Log:`integration_id=trigger_series=correlation_id`,`request_id=null`。 +3. Gateway 结果若改变内部事实,Audit Event 使用同一 correlation,actor 为 `system_task`,source 为 `worker`。 +4. 前端从 Integration 调查中心、资源时间线或 correlation 时间线进入;隐藏“请求链路”。 + +### 人工即时刷新与手动轮询入队 + +- “人工即时刷新卡数据”在一个 HTTP 用例内直接执行 Gateway 查询:有 request_id,外部尝试可继承该 request/correlation,能从请求时间线串起。 +- “手动触发轮询”先记录 HTTP 触发 Audit Event,因此触发命令有 request_id;但当前 Redis 手动队列只保存 card ID,后续 Worker 没有原 request/correlation,执行阶段按新的 Integration ID 建链。 +- 因此当前不能仅凭 request_id 从“手动触发事件”自动跳到后续 Gateway 轮询尝试。前端分别展示手动任务进度、触发审计和资源/Integration 活动,不按时间拼接。若产品要求强关联,需要单独实施稳定 trigger/correlation 传播,不属于本文前端接入范围。 ### 资产详情 @@ -102,7 +456,7 @@ 3. 从明细 `items[].investigation_refs` 直接进入事件、操作者、资源、request、correlation 或 Integration 视角。 4. 缺少的引用入口隐藏;风险中心不提供处置、封禁或恢复按钮。 -## 统一降级与事实边界 +## 九、统一降级与事实边界 - 缺少目标接口必需的稳定 ID 或 identifier 时隐藏入口,不按名称、中文描述、时间或编号前缀猜测。 - 平台只有 Registry Key 时先调用精确资源搜索;零命中或多命中停留在搜索结果。 diff --git a/internal/application/auditarchive/retention.go b/internal/application/auditarchive/retention.go index 84e746c..075ccf9 100644 --- a/internal/application/auditarchive/retention.go +++ b/internal/application/auditarchive/retention.go @@ -41,6 +41,7 @@ type RetentionResult struct { EventCount int64 ResourceCount int64 IntegrationCount int64 + EstimatedBatches int64 ManifestKeys []string Duration time.Duration } @@ -56,6 +57,36 @@ func (s *Service) CleanupPreviousMonth(ctx context.Context) (RetentionResult, er return s.CleanupMonth(ctx, now.AddDate(0, -1, 0)) } +// ValidatePreviousMonth 只读校验上一个完整自然月的归档与清理门禁。 +func (s *Service) ValidatePreviousMonth(ctx context.Context) (RetentionResult, error) { + now := time.Now().In(s.location) + return s.ValidateMonth(ctx, now.AddDate(0, -1, 0)) +} + +// ValidateMonth 只读校验指定完整自然月,不写清理断点且不删除在线数据。 +func (s *Service) ValidateMonth(ctx context.Context, month time.Time) (result RetentionResult, err error) { + if s.db == nil || s.store == nil { + return result, fmt.Errorf("日志留存演练数据库或对象存储未配置") + } + start, end, err := s.retentionMonthRange(month) + if err != nil { + return result, err + } + startedAt := time.Now() + result.Month = start.Format("2006-01") + runs, err := s.loadRetentionRuns(ctx, start, end) + if err != nil { + return result, err + } + if err := s.validateRetentionRuns(ctx, start, end, runs); err != nil { + return result, err + } + summarizeRetentionRuns(runs, &result) + result.EstimatedBatches = estimatedRetentionBatches(result) + result.Duration = time.Since(startedAt) + return result, nil +} + // CleanupMonth 校验归档硬门禁后按固定顺序物理清理指定完整自然月。 func (s *Service) CleanupMonth(ctx context.Context, month time.Time) (result RetentionResult, cleanupErr error) { if s.db == nil || s.store == nil || s.audit == nil { @@ -402,6 +433,13 @@ func summarizeRetentionRuns(runs retentionRuns, result *RetentionResult) { } } +func estimatedRetentionBatches(result RetentionResult) int64 { + batchSize := int64(constants.AuditRetentionDeleteBatchSize) + return (result.EventCount+batchSize-1)/batchSize + + (result.ResourceCount+batchSize-1)/batchSize + + (result.IntegrationCount+batchSize-1)/batchSize +} + func (s *Service) cleanupAuditMonth(ctx context.Context, start, end time.Time, runs []*model.LogArchiveRun) error { if allRunsCleaned(runs) { return nil diff --git a/internal/bootstrap/handlers.go b/internal/bootstrap/handlers.go index 5372e5a..7c31383 100644 --- a/internal/bootstrap/handlers.go +++ b/internal/bootstrap/handlers.go @@ -263,12 +263,13 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers { Asset: func() *admin.AssetHandler { pollingQueueMgr := pollingPkg.NewPollingQueueManager(deps.Redis, constants.PollingShardCount, deps.Logger) assetPollingSvc := pollingSvcPkg.NewAssetPollingService( + deps.DB, deviceStore, deviceSimBindingStore, svc.IotCard, pollingQueueMgr, deps.Logger, - svc.AssetAudit, + svc.AccessAudit, ) h := admin.NewAssetHandler(svc.Asset, svc.AssetAudit, svc.Device, svc.IotCard, svc.StopResumeService, assetPollingSvc, assetQuery.NewExchangeTraceQuery(deps.DB, deps.Logger)) h.SetLifecycleService(svc.AssetLifecycle) diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 65ef29d..28f533a 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -22,7 +22,6 @@ import ( wecomInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/wecom" "github.com/break/junhong_cmp_fiber/internal/polling" accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account" - accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit" agentOpenAPISvc "github.com/break/junhong_cmp_fiber/internal/service/agent_open_api" assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record" assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" @@ -80,7 +79,6 @@ type services struct { AccessAudit *auditInfra.Writer Approval *approvalApp.CreationService Account *accountSvc.Service - AccountAudit *accountAuditSvc.Service AssetAudit *assetAuditSvc.Service Role *roleSvc.Service Permission *permissionSvc.Service @@ -147,11 +145,10 @@ func initServices(s *stores, deps *Dependencies) *services { customerBinding := customerBindingSvc.New(deps.DB, s.IotCard, s.Device) purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopPackageAllocation) - accountAudit := accountAuditSvc.NewService(s.AccountOperationLog) assetAudit := assetAuditSvc.NewService(s.AssetOperationLog, deps.DB) auditWriter := auditInfra.NewWriter(auditInfra.NewRegistry(), nil) customerBinding.SetAccessAudit(auditWriter) - account := accountSvc.New(s.Account, s.Role, s.AccountRole, s.ShopRole, s.Shop, s.Enterprise, accountAudit) + account := accountSvc.New(s.Account, s.Role, s.AccountRole, s.ShopRole, s.Shop, s.Enterprise) account.SetLifecycleAudit(deps.DB, auditWriter) account.SetAccessAudit(deps.DB, deps.Redis, auditWriter) account.SetTokenManager(deps.TokenManager) @@ -169,7 +166,6 @@ func initServices(s *stores, deps *Dependencies) *services { s.PackageSeries, deps.GatewayClient, deps.Logger, - assetAudit, ) iotCard.SetAccessAudit(auditWriter) cardObservationOutbox := outbox.NewRepository() @@ -230,7 +226,6 @@ func initServices(s *stores, deps *Dependencies) *services { s.DeviceSimBinding, deps.GatewayClient, deps.Logger, - assetAudit, ) stopResumeService.SetPollingCallback(pollingLifecycleSvc) stopResumeService.SetObservationSeriesEventWriter(deps.DB, observationSeriesEvents) @@ -254,7 +249,6 @@ func initServices(s *stores, deps *Dependencies) *services { s.PackageSeries, deps.GatewayClient, s.AssetIdentifier, - assetAudit, s.EnterpriseDeviceAuthorization, s.Enterprise, ) @@ -283,7 +277,6 @@ func initServices(s *stores, deps *Dependencies) *services { s.AgentWallet, s.Shop, wechatConfig, - accountAudit, operationPassword, deps.Redis, deps.Logger, @@ -323,7 +316,8 @@ func initServices(s *stores, deps *Dependencies) *services { exchangeService := exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, customerBinding, deps.Logger) exchangeService.SetShippingCreatedNotifier(exchangeApp.NewShippingCreatedNotifier(exchangeInfra.NewShippingNotificationWriter(outbox.NewRepository()))) exchangeService.SetAccessAudit(auditWriter) - assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder, assetAudit) + assetService := assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder) + assetService.SetAccessAudit(auditWriter) agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet, s.DeviceSimBinding, s.Device) agentOpenAPI.SetObservationSeriesDispatcher(observationSeries) wecomApplicationRepository := wecomInfra.NewApplicationRepository(deps.DB) @@ -397,7 +391,6 @@ func initServices(s *stores, deps *Dependencies) *services { AccessAudit: auditWriter, Approval: approvalCreationService, Account: account, - AccountAudit: accountAudit, AssetAudit: assetAudit, Role: roleService, Permission: permissionService, @@ -428,10 +421,10 @@ func initServices(s *stores, deps *Dependencies) *services { EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger, auditWriter), Authorization: enterpriseCardSvc.NewAuthorizationService(deps.DB, s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger, auditWriter), IotCard: iotCard, - IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient, assetAudit, auditWriter), + IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient, auditWriter), ExportTask: exportTaskSvc.New(deps.DB, s.ExportTask, deps.QueueClient, deps.StorageService, auditWriter), Device: device, - DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient, assetAudit, auditWriter), + DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient, auditWriter), AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account), Carrier: carrierSvc.New(s.Carrier, auditWriter), PackageSeries: packageSeriesService, @@ -453,7 +446,7 @@ func initServices(s *stores, deps *Dependencies) *services { PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger), PollingManualTrigger: pollingManualTriggerService, Asset: assetService, - AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, assetAudit), + AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, auditWriter), AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction), StopResumeService: stopResumeService, WechatConfig: wechatConfig, diff --git a/internal/bootstrap/stores.go b/internal/bootstrap/stores.go index 2aebbf8..43cf911 100644 --- a/internal/bootstrap/stores.go +++ b/internal/bootstrap/stores.go @@ -6,7 +6,6 @@ import ( type stores struct { Account *postgres.AccountStore - AccountOperationLog *postgres.AccountOperationLogStore AssetOperationLog *postgres.AssetOperationLogStore Shop *postgres.ShopStore Role *postgres.RoleStore @@ -79,7 +78,6 @@ type stores struct { func initStores(deps *Dependencies) *stores { return &stores{ Account: postgres.NewAccountStore(deps.DB, deps.Redis), - AccountOperationLog: postgres.NewAccountOperationLogStore(deps.DB), AssetOperationLog: postgres.NewAssetOperationLogStore(deps.DB), Shop: postgres.NewShopStore(deps.DB, deps.Redis), Role: postgres.NewRoleStore(deps.DB), diff --git a/internal/bootstrap/worker_services.go b/internal/bootstrap/worker_services.go index dd716f0..dec8275 100644 --- a/internal/bootstrap/worker_services.go +++ b/internal/bootstrap/worker_services.go @@ -8,7 +8,6 @@ import ( "github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog" "github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox" walletinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/wallet" - assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" "github.com/break/junhong_cmp_fiber/internal/service/commission_calculation" "github.com/break/junhong_cmp_fiber/internal/service/commission_stats" deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device" @@ -31,7 +30,6 @@ type workerServices struct { } func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *queue.WorkerServices { - assetAudit := assetAuditSvc.NewService(stores.AssetOperationLog, deps.DB) auditWriter := auditInfra.NewWriter(auditInfra.NewRegistry(), nil) commissionStatsService := commission_stats.New(stores.ShopSeriesCommissionStats) @@ -105,7 +103,7 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q iotCardAuditService := iotCardSvc.New( deps.DB, stores.IotCard, stores.Shop, stores.AssetAllocationRecord, stores.ShopPackageAllocation, stores.ShopSeriesAllocation, stores.PackageSeries, - deps.GatewayClient, deps.Logger, assetAudit, + deps.GatewayClient, deps.Logger, ) iotCardAuditService.SetAccessAudit(auditWriter) cardObservationService.SetStateAuditWriter(iotCardAuditService) @@ -160,7 +158,6 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q stores.DeviceSimBinding, deps.GatewayClient, deps.Logger, - assetAudit, ) stopResumeService.SetObservationSeriesEventWriter(deps.DB, observationSeriesEvents) stopResumeService.SetUnifiedAudit(auditWriter, integrationlog.NewRepository(deps.DB)) @@ -172,7 +169,7 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q deviceBatchAllocator := deviceSvc.New( deps.DB, deps.Redis, stores.Device, stores.DeviceSimBinding, stores.IotCard, stores.Shop, stores.AssetAllocationRecord, stores.ShopPackageAllocation, stores.ShopSeriesAllocation, - stores.PackageSeries, deps.GatewayClient, stores.AssetIdentifier, assetAudit, nil, nil, + stores.PackageSeries, deps.GatewayClient, stores.AssetIdentifier, nil, nil, ) return &queue.WorkerServices{ diff --git a/internal/bootstrap/worker_stores.go b/internal/bootstrap/worker_stores.go index 915a8ae..0276b0a 100644 --- a/internal/bootstrap/worker_stores.go +++ b/internal/bootstrap/worker_stores.go @@ -6,7 +6,6 @@ import ( ) type workerStores struct { - AssetOperationLog *postgres.AssetOperationLogStore AssetAllocationRecord *postgres.AssetAllocationRecordStore IotCardImportTask *postgres.IotCardImportTaskStore IotCard *postgres.IotCardStore @@ -42,7 +41,6 @@ type workerStores struct { func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores { stores := &workerStores{ - AssetOperationLog: postgres.NewAssetOperationLogStore(deps.DB), AssetAllocationRecord: postgres.NewAssetAllocationRecordStore(deps.DB, deps.Redis), IotCardImportTask: postgres.NewIotCardImportTaskStore(deps.DB, deps.Redis), IotCard: postgres.NewIotCardStore(deps.DB, deps.Redis), @@ -77,7 +75,6 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores { } return &queue.WorkerStores{ - AssetOperationLog: stores.AssetOperationLog, AssetAllocationRecord: stores.AssetAllocationRecord, IotCardImportTask: stores.IotCardImportTask, IotCard: stores.IotCard, diff --git a/internal/governance/auditcoverage/scanner.go b/internal/governance/auditcoverage/scanner.go index f68054d..75f63cd 100644 --- a/internal/governance/auditcoverage/scanner.go +++ b/internal/governance/auditcoverage/scanner.go @@ -134,12 +134,14 @@ func scanFile(root, path string) ([]Entry, error) { if strings.HasPrefix(relative, "cmd/worker/") { if taskType, schedule, ok := scheduledTask(call); ok { entries = append(entries, classifySchedule(relative, position.Line, taskType, schedule)) + } else if strings.Contains(expression(selector.X), "outboxConsumers") && len(call.Args) >= 2 { + entries = append(entries, classifyOutboxConsumer(relative, position.Line, expression(call.Args[0]), expression(call.Args[1]))) } } case "LogOperation": entries = append(entries, classifyLegacyWriter(relative, position.Line, expression(call.Fun))) - case "Start", "Complete", "RecordInbound": - if isIntegrationLogCall(relative, expression(selector.X)) { + case "Start", "Complete", "RecordInbound", "ClaimExpiredInboundPending": + if selector.Sel.Name == "ClaimExpiredInboundPending" || isIntegrationLogCall(relative, expression(selector.X)) { entries = append(entries, classifyIntegrationLog(relative, position.Line, expression(call.Fun))) } } @@ -210,7 +212,7 @@ func classifyWorker(file string, line int, taskType, handler string) Entry { FailureStrategy: "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计", SensitivePolicy: "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL", BeforeAfterPolicy: "状态变化保存直接前后值;无业务变化时仅保留 Integration Log", - TestSeam: "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试;覆盖门禁静态比对本入口", + TestSeam: "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费幂等数据核对;覆盖门禁静态比对本入口", } return entry } @@ -230,6 +232,22 @@ func classifySchedule(file string, line int, taskType, schedule string) Entry { } } +func classifyOutboxConsumer(file string, line int, eventType, consumer string) Entry { + return Entry{ + Key: fmt.Sprintf("outbox_consumer:%s:%d:%s", file, line, eventType), Kind: "outbox_consumer", + CodeEntry: fmt.Sprintf("%s:%d %s", file, line, consumer), Owner: workerOwner(eventType), + Summary: "注册 Outbox 消费者 " + eventType, + AuditEvent: "N/A", DomainLedger: "N/A", IntegrationLog: "N/A", Outbox: "必须:消费已提交的可靠事件", + ActorSource: "system_task/outbox_consumer", Transaction: "N/A", + Visibility: "内部系统装配入口,不直接对用户展示", + FailureStrategy: "注册失败阻止 Worker 启动;实际消费失败由 Outbox 重试,业务审计由消费者用例负责", + SensitivePolicy: "注册入口不读取或记录事件载荷与安全凭据", + BeforeAfterPolicy: "N/A:注册入口不修改业务事实", + TestSeam: "静态扫描注册点、消费者实现和对应业务动作", + NAReason: "本入口只注册事件类型与消费者;实际业务事实和 Audit Event 由对应 Consumer/Application 完整用例负责", + } +} + func classifyBusinessMethod(file string, line int, method string) Entry { parts := strings.Split(file, "/") layer := parts[1] @@ -249,7 +267,7 @@ func classifyBusinessMethod(file string, line int, method string) Entry { Visibility: "由完整用例决定平台完整视图、主体安全投影或 internal_only", SensitivePolicy: "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品", BeforeAfterPolicy: "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影", - TestSeam: "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口", + TestSeam: "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用静态检查与数据核对;覆盖门禁静态比对本入口", } if layer == "domain" { entry.AuditEvent = "N/A" diff --git a/internal/infrastructure/audit/batch.go b/internal/infrastructure/audit/batch.go index 44fd4ad..4062164 100644 --- a/internal/infrastructure/audit/batch.go +++ b/internal/infrastructure/audit/batch.go @@ -19,6 +19,11 @@ func (w *Writer) AppendBatch(ctx context.Context, tx *gorm.DB, input BatchInput) if input.Root.EventID == "" { return pkgerrors.New(pkgerrors.CodeInvalidParam, "批次根事件缺少稳定事件ID") } + if input.Root.BatchTotal < 0 || input.Root.SuccessCount < 0 || input.Root.FailCount < 0 || + input.Root.SuccessCount+input.Root.FailCount > input.Root.BatchTotal || + len(input.Children) < input.Root.SuccessCount || len(input.Children) > input.Root.SuccessCount+input.Root.FailCount { + return pkgerrors.New(pkgerrors.CodeInvalidParam, "批次根子事件计数不一致") + } if err := w.Append(ctx, tx, input.Root); err != nil { return err } diff --git a/internal/infrastructure/audit/registry.go b/internal/infrastructure/audit/registry.go index cfafa8d..d362745 100644 --- a/internal/infrastructure/audit/registry.go +++ b/internal/infrastructure/audit/registry.go @@ -84,6 +84,10 @@ func NewRegistry() *Registry { iotCardCreated := iotCardAction(constants.AuditActionIotCardCreated, "创建 IoT 卡", constants.AuditActorSystemTask, constants.AuditSourceWorker) iotCardDeleted := iotCardAction(constants.AuditActionIotCardDeleted, "删除 IoT 卡", constants.AuditActorAccount, constants.AuditSourceAdminAPI) iotCardDeleted.Risk = constants.AuditRiskHigh + iotCardDeactivated := iotCardAction(constants.AuditActionIotCardDeactivated, "停用 IoT 卡资产", constants.AuditActorAccount, constants.AuditSourceAdminAPI) + iotCardDeactivated.Risk = constants.AuditRiskHigh + iotCardPollingStatusUpdated := iotCardAction(constants.AuditActionIotCardPollingStatusUpdated, "更新 IoT 卡轮询开关", constants.AuditActorAccount, constants.AuditSourceAdminAPI) + iotCardPollingStatusBatchUpdated := iotCardBatchAction(constants.AuditActionIotCardPollingStatusBatchUpdated, "批量更新 IoT 卡轮询开关") iotCardBatchDeleted := ActionDefinition{ Code: constants.AuditActionIotCardBatchDeleted, Name: "批量删除 IoT 卡", Category: constants.AuditCategoryAsset, Risk: constants.AuditRiskHigh, @@ -117,6 +121,9 @@ func NewRegistry() *Registry { deviceCreated := deviceAction(constants.AuditActionDeviceCreated, "导入创建设备", constants.AuditActorSystemTask, constants.AuditSourceWorker) deviceDeleted := deviceAction(constants.AuditActionDeviceDeleted, "删除设备", constants.AuditActorAccount, constants.AuditSourceAdminAPI) deviceDeleted.Risk = constants.AuditRiskHigh + deviceDeactivated := deviceAction(constants.AuditActionDeviceDeactivated, "停用设备资产", constants.AuditActorAccount, constants.AuditSourceAdminAPI) + deviceDeactivated.Risk = constants.AuditRiskHigh + devicePollingStatusUpdated := deviceAction(constants.AuditActionDevicePollingStatusUpdated, "更新设备轮询开关", constants.AuditActorAccount, constants.AuditSourceAdminAPI) deviceAllocationBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceAllocationBatch, "批量分配设备") deviceAllocated := deviceMultiOriginAction(constants.AuditActionDeviceAllocated, "分配设备") deviceRecallBatch := deviceMultiOriginBatchAction(constants.AuditActionDeviceRecallBatch, "批量回收设备") @@ -180,17 +187,6 @@ func NewRegistry() *Registry { constants.AuditActionOutboxExpiredLeaseReleased, "人工释放 Outbox 过期租约", ) - deviceBatchCompleted := deviceBatchAction( - constants.AuditActionDeviceBatchAllocationCompleted, - "完成设备批量分配", - constants.AuditResourceDeviceBatchTask, - ) - deviceBatchItem := deviceBatchAction( - constants.AuditActionDeviceBatchAllocationItem, - "处理设备批量分配项", - constants.AuditResourceDevice, - ) - deviceBatchItem.AllowedVisibility = []string{constants.AuditSubjectInternalOnly, constants.AuditSubjectResult} iotCardImportTaskCreated := taskAction(constants.AuditActionIotCardImportTaskCreated, "创建 IoT 卡导入任务", constants.AuditResourceIotCardImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI) iotCardImportTaskCompleted := taskAction(constants.AuditActionIotCardImportTaskCompleted, "完成 IoT 卡导入任务", constants.AuditResourceIotCardImportTask, constants.AuditActorSystemTask, constants.AuditSourceWorker) deviceImportTaskCreated := taskAction(constants.AuditActionDeviceImportTaskCreated, "创建设备导入任务", constants.AuditResourceDeviceImportTask, constants.AuditActorAccount, constants.AuditSourceAdminAPI) @@ -274,6 +270,8 @@ func NewRegistry() *Registry { packageUsageTrafficReset := packageUsageAction(constants.AuditActionPackageUsageTrafficReset, "重置套餐权益流量") packageUsageRefundInvalidated := packageUsageAction(constants.AuditActionPackageUsageRefundInvalidated, "退款失效套餐权益") packageUsageAssetInvalidated := packageUsageAction(constants.AuditActionPackageUsageAssetInvalidated, "资产失效套餐权益") + packageUsageExpiresAtUpdated := packageUsageAction(constants.AuditActionPackageUsageExpiresAtUpdated, "调整套餐权益过期时间") + packageUsageTrafficAdjusted := packageUsageAction(constants.AuditActionPackageUsageTrafficAdjusted, "调整套餐权益已用量") orderCreated := orderAction(constants.AuditActionOrderCreated, "创建订单") orderCancelled := orderAction(constants.AuditActionOrderCancelled, "取消订单") orderWalletPaid := orderAction(constants.AuditActionOrderWalletPaid, "钱包支付订单") @@ -405,6 +403,9 @@ func NewRegistry() *Registry { constants.AuditActionPersonalCustomerAssetBindingMigrated: personalAssetBindingMigrated, constants.AuditActionIotCardCreated: iotCardCreated, constants.AuditActionIotCardDeleted: iotCardDeleted, + constants.AuditActionIotCardDeactivated: iotCardDeactivated, + constants.AuditActionIotCardPollingStatusUpdated: iotCardPollingStatusUpdated, + constants.AuditActionIotCardPollingStatusBatchUpdated: iotCardPollingStatusBatchUpdated, constants.AuditActionIotCardBatchDeleted: iotCardBatchDeleted, constants.AuditActionIotCardAllocationBatch: iotCardAllocationBatch, constants.AuditActionIotCardAllocated: iotCardAllocated, @@ -430,6 +431,8 @@ func NewRegistry() *Registry { constants.AuditActionIotCardAutoStopReasonUpdated: iotCardAutoStopReasonUpdated, constants.AuditActionDeviceCreated: deviceCreated, constants.AuditActionDeviceDeleted: deviceDeleted, + constants.AuditActionDeviceDeactivated: deviceDeactivated, + constants.AuditActionDevicePollingStatusUpdated: devicePollingStatusUpdated, constants.AuditActionDeviceAllocationBatch: deviceAllocationBatch, constants.AuditActionDeviceAllocated: deviceAllocated, constants.AuditActionDeviceRecallBatch: deviceRecallBatch, @@ -476,8 +479,6 @@ func NewRegistry() *Registry { constants.AuditActionWeComApprovalSceneSaved: wecomApprovalSceneSaved, constants.AuditActionOutboxReplayed: outboxReplayed, constants.AuditActionOutboxExpiredLeaseReleased: outboxExpiredLeaseReleased, - constants.AuditActionDeviceBatchAllocationCompleted: deviceBatchCompleted, - constants.AuditActionDeviceBatchAllocationItem: deviceBatchItem, constants.AuditActionIotCardImportTaskCreated: iotCardImportTaskCreated, constants.AuditActionIotCardImportTaskCompleted: iotCardImportTaskCompleted, constants.AuditActionDeviceImportTaskCreated: deviceImportTaskCreated, @@ -546,6 +547,8 @@ func NewRegistry() *Registry { constants.AuditActionPackageUsageTrafficReset: packageUsageTrafficReset, constants.AuditActionPackageUsageRefundInvalidated: packageUsageRefundInvalidated, constants.AuditActionPackageUsageAssetInvalidated: packageUsageAssetInvalidated, + constants.AuditActionPackageUsageExpiresAtUpdated: packageUsageExpiresAtUpdated, + constants.AuditActionPackageUsageTrafficAdjusted: packageUsageTrafficAdjusted, constants.AuditActionOrderCreated: orderCreated, constants.AuditActionOrderCancelled: orderCancelled, constants.AuditActionOrderWalletPaid: orderWalletPaid, diff --git a/internal/infrastructure/audit/writer.go b/internal/infrastructure/audit/writer.go index 83273b7..5489e10 100644 --- a/internal/infrastructure/audit/writer.go +++ b/internal/infrastructure/audit/writer.go @@ -900,14 +900,14 @@ func (w *Writer) Append(ctx context.Context, tx *gorm.DB, input AppendInput) err } event := model.AuditEvent{ OccurredAt: occurredAt, Category: action.Category, ActionCode: action.Code, ActionName: action.Name, - Summary: input.Summary, ActorKind: input.Actor.Kind, ActorID: input.Actor.ID, ActorName: input.Actor.Name, - ActorShopID: input.Actor.ShopID, ActorShopName: input.Actor.ShopName, - ActorEnterpriseID: input.Actor.EnterpriseID, ActorEnterpriseName: input.Actor.EnterpriseName, - Source: input.Source, RequestPath: input.RequestPath, RequestMethod: input.RequestMethod, - IPAddress: input.IPAddress, UserAgent: input.UserAgent, - ScopeType: input.ScopeType, ScopeID: input.ScopeID, ScopeName: input.ScopeName, + Summary: sanitizer.SanitizeText(input.Summary), ActorKind: input.Actor.Kind, ActorID: input.Actor.ID, ActorName: sanitizer.SanitizeText(input.Actor.Name), + ActorShopID: input.Actor.ShopID, ActorShopName: sanitizer.SanitizeText(input.Actor.ShopName), + ActorEnterpriseID: input.Actor.EnterpriseID, ActorEnterpriseName: sanitizer.SanitizeText(input.Actor.EnterpriseName), + Source: input.Source, RequestPath: sanitizer.SanitizeText(input.RequestPath), RequestMethod: input.RequestMethod, + IPAddress: input.IPAddress, UserAgent: sanitizer.SanitizeText(input.UserAgent), + ScopeType: input.ScopeType, ScopeID: input.ScopeID, ScopeName: sanitizer.SanitizeText(input.ScopeName), Result: input.Result, RiskLevel: action.Risk, RequestID: input.RequestID, - ErrorCode: input.ErrorCode, ErrorSummary: input.ErrorSummary, + ErrorCode: input.ErrorCode, ErrorSummary: sanitizer.SanitizeText(input.ErrorSummary), CorrelationID: input.CorrelationID, ParentEventID: input.ParentEventID, Metadata: metadata, BatchTotal: input.BatchTotal, SuccessCount: input.SuccessCount, FailCount: input.FailCount, } @@ -1036,10 +1036,10 @@ func (w *Writer) buildResources(inputs []ResourceInput, action ActionDefinition) return nil, err } resources = append(resources, model.AuditEventResource{ - ResourceType: input.Type, ResourceID: input.ID, ResourceKey: input.Key, DisplayName: input.DisplayName, + ResourceType: input.Type, ResourceID: input.ID, ResourceKey: sanitizer.SanitizeText(input.Key), DisplayName: sanitizer.SanitizeText(input.DisplayName), Relation: input.Relation, Role: input.Role, IdentitySnapshot: identity, BeforeData: before, AfterData: after, SubjectVisibility: visibility, - SubjectSummary: input.SubjectSummary, SubjectData: subjectData, SortOrder: input.SortOrder, + SubjectSummary: sanitizer.SanitizeText(input.SubjectSummary), SubjectData: subjectData, SortOrder: input.SortOrder, }) } if primaryCount != 1 { diff --git a/internal/infrastructure/cardobservation/series_runner.go b/internal/infrastructure/cardobservation/series_runner.go index e122d88..cc19d8b 100644 --- a/internal/infrastructure/cardobservation/series_runner.go +++ b/internal/infrastructure/cardobservation/series_runner.go @@ -37,7 +37,7 @@ func (l *SeriesAttemptLogger) Record(ctx context.Context, payload cardapp.Series return apperrors.New(apperrors.CodeInternalError, "卡观测 Integration Log 未配置") } resourceID := payload.ResourceID - source, scene, seriesID := payload.Source, payload.Scene, payload.SeriesID + source, scene, seriesID := payload.Source, payload.Scene, payload.SeriesID+":"+payload.SyncType requestID, correlationID := optionalText(payload.RequestID), optionalText(payload.CorrelationID) attempt, err := l.repository.Start(ctx, integrationlog.Attempt{ IntegrationID: unsentIntegrationID(payload), Provider: constants.IntegrationProviderGateway, @@ -59,12 +59,12 @@ func (l *SeriesAttemptLogger) Record(ctx context.Context, payload cardapp.Series // RecordMerged 记录同场景重复触发被合并,不创建第二组三任务。 func (l *SeriesAttemptLogger) RecordMerged(ctx context.Context, request cardapp.SeriesRequest, seriesID string) error { resourceID := request.ResourceID - source, scene := request.Source, request.Scene + source, scene, technicalSeriesID := request.Source, request.Scene, seriesID+":"+request.SyncType now := time.Now().UTC() _, err := l.repository.Start(ctx, integrationlog.Attempt{ Provider: constants.IntegrationProviderGateway, Direction: constants.IntegrationDirectionOutbound, Operation: operationForSyncType(request.SyncType), ResourceType: request.ResourceType, - ResourceID: &resourceID, TriggerSource: &source, TriggerScene: &scene, TriggerSeries: &seriesID, + ResourceID: &resourceID, TriggerSource: &source, TriggerScene: &scene, TriggerSeries: &technicalSeriesID, ScheduledAt: &now, Attempt: 1, InitialResult: constants.IntegrationResultMerged, RequestID: optionalText(request.RequestID), CorrelationID: optionalText(request.CorrelationID), Metadata: map[string]any{"reason": "同场景未结束序列已存在", "sync_type": request.SyncType}, @@ -215,7 +215,7 @@ func (r *SeriesRunner) runDeviceInfo(ctx context.Context, payload cardapp.Series } func (r *SeriesRunner) startDeviceAttempt(ctx context.Context, payload cardapp.SeriesTaskPayload, device *model.Device) (*model.IntegrationLog, error) { - resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID + resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID+":"+payload.SyncType resourceKey := "device:" + strconv.FormatUint(uint64(device.ID), 10) return r.integration.Start(ctx, integrationlog.Attempt{ IntegrationID: gatewayIntegrationID(payload), Provider: constants.IntegrationProviderGateway, @@ -525,7 +525,7 @@ func parseGatewayTime(raw gateway.FlexString) *time.Time { } func (r *SeriesRunner) startAttempt(ctx context.Context, payload cardapp.SeriesTaskPayload, card *model.IotCard) (*model.IntegrationLog, error) { - resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID + resourceID, source, scene, seriesID := payload.ResourceID, payload.Source, payload.Scene, payload.SeriesID+":"+payload.SyncType resourceKey := "card:" + formatCardID(card.ID) return r.integration.Start(ctx, integrationlog.Attempt{ IntegrationID: gatewayIntegrationID(payload), Provider: constants.IntegrationProviderGateway, diff --git a/internal/infrastructure/integrationlog/repository.go b/internal/infrastructure/integrationlog/repository.go index 7bcad22..e49fa8f 100644 --- a/internal/infrastructure/integrationlog/repository.go +++ b/internal/infrastructure/integrationlog/repository.go @@ -107,15 +107,9 @@ func (r *Repository) Start(ctx context.Context, input Attempt) (*model.Integrati if input.IntegrationID == "" { input.IntegrationID = uuid.NewString() } + autoAttempt := input.Attempt <= 0 && input.TriggerSeries != nil if input.Attempt <= 0 { input.Attempt = 1 - if input.TriggerSeries != nil { - if err := r.db.WithContext(ctx).Model(&model.IntegrationLog{}). - Select("COALESCE(MAX(attempt), 0) + 1"). - Where("trigger_series = ?", *input.TriggerSeries).Scan(&input.Attempt).Error; err != nil { - return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "计算 Integration Log 尝试序号失败") - } - } } if input.StartedAt == nil { startedAt := r.now().UTC() @@ -128,16 +122,35 @@ func (r *Repository) Start(ctx context.Context, input Attempt) (*model.Integrati resourceType := optionalString(input.ResourceType) log := &model.IntegrationLog{ IntegrationID: input.IntegrationID, Provider: input.Provider, Direction: input.Direction, - Operation: input.Operation, ExternalID: input.ExternalID, ResourceType: resourceType, - ResourceID: input.ResourceID, ResourceKey: input.ResourceKey, TriggerSource: input.TriggerSource, - TriggerScene: input.TriggerScene, TriggerSeries: input.TriggerSeries, ScheduledAt: input.ScheduledAt, + Operation: input.Operation, ExternalID: sanitizedOptionalText(input.ExternalID), ResourceType: resourceType, + ResourceID: input.ResourceID, ResourceKey: sanitizedOptionalText(input.ResourceKey), TriggerSource: input.TriggerSource, + TriggerScene: sanitizedOptionalText(input.TriggerScene), TriggerSeries: input.TriggerSeries, ScheduledAt: input.ScheduledAt, StartedAt: input.StartedAt, Attempt: input.Attempt, Result: result, RequestSummary: requestSummary, Metadata: metadata, RequestID: input.RequestID, CorrelationID: input.CorrelationID, AuditEventID: input.AuditEventID, - RecoveryStrategy: input.RecoveryStrategy, + RecoveryStrategy: sanitizedOptionalText(input.RecoveryStrategy), } - if err := r.db.WithContext(ctx).Create(log).Error; err != nil { - return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "写入 Integration Log 失败") + createAttempt := func(tx *gorm.DB) error { + if !autoAttempt { + return tx.Create(log).Error + } + if err := tx.Exec("SELECT pg_advisory_xact_lock(hashtext(?))", *input.TriggerSeries).Error; err != nil { + return err + } + if err := tx.Model(&model.IntegrationLog{}).Select("COALESCE(MAX(attempt), 0) + 1"). + Where("trigger_series = ?", *input.TriggerSeries).Scan(&log.Attempt).Error; err != nil { + return err + } + return tx.Create(log).Error + } + var createErr error + if autoAttempt { + createErr = r.db.WithContext(ctx).Transaction(createAttempt) + } else { + createErr = createAttempt(r.db.WithContext(ctx)) + } + if createErr != nil { + return nil, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, createErr, "写入 Integration Log 失败") } return log, nil } @@ -156,7 +169,7 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet if completion.Result == constants.IntegrationResultUnknown && strings.TrimSpace(completion.RecoveryStrategy) == "" { return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "结果未知必须记录明确恢复策略") } - safeProviderMessage := strings.TrimSpace(completion.SafeProviderMessage) + safeProviderMessage := sanitizer.SanitizeText(strings.TrimSpace(completion.SafeProviderMessage)) if safeProviderMessage != "" && utf8.RuneCountInString(constants.IntegrationSafeMessagePrefix+safeProviderMessage) > constants.IntegrationProviderMessageMaxLength { return nil, pkgerrors.New(pkgerrors.CodeInvalidParam, "Integration Log 安全结果摘要过长") } @@ -187,10 +200,10 @@ func (r *Repository) Complete(ctx context.Context, integrationID string, complet updates["resource_id"] = completion.ResourceID } if completion.ResourceKey != nil { - updates["resource_key"] = completion.ResourceKey + updates["resource_key"] = sanitizedOptionalText(completion.ResourceKey) } if completion.RecoveryStrategy != "" { - updates["recovery_strategy"] = completion.RecoveryStrategy + updates["recovery_strategy"] = sanitizer.SanitizeText(completion.RecoveryStrategy) } result := r.db.WithContext(ctx).Model(&model.IntegrationLog{}). Where("integration_id = ? AND result = ?", integrationID, constants.IntegrationResultPending). @@ -237,8 +250,8 @@ func (r *Repository) RecordInbound(ctx context.Context, input InboundAttempt) (* log := &model.IntegrationLog{ IntegrationID: input.IntegrationID, IdempotencyKey: &input.IdempotencyKey, Provider: input.Provider, Direction: constants.IntegrationDirectionInbound, Operation: input.Operation, - ExternalID: optionalString(input.ExternalID), ResourceType: optionalString(input.ResourceType), - ResourceID: input.ResourceID, ResourceKey: input.ResourceKey, StartedAt: &now, Attempt: 1, + ExternalID: sanitizedOptionalText(optionalString(input.ExternalID)), ResourceType: optionalString(input.ResourceType), + ResourceID: input.ResourceID, ResourceKey: sanitizedOptionalText(input.ResourceKey), StartedAt: &now, Attempt: 1, TriggerSeries: &triggerSeries, Result: constants.IntegrationResultPending, RequestSummary: summary, ContentHash: hex.EncodeToString(hash[:]), RequestID: input.RequestID, CorrelationID: input.CorrelationID, @@ -380,3 +393,11 @@ func optionalString(value string) *string { } return &value } + +func sanitizedOptionalText(value *string) *string { + if value == nil { + return nil + } + sanitized := sanitizer.SanitizeText(*value) + return &sanitized +} diff --git a/internal/model/dto/audit_dto.go b/internal/model/dto/audit_dto.go index 28c6a6c..c45377c 100644 --- a/internal/model/dto/audit_dto.go +++ b/internal/model/dto/audit_dto.go @@ -2,24 +2,24 @@ package dto // AuditEventListRequest 是平台审计事件列表的组合筛选参数。 type AuditEventListRequest struct { - CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区)"` - CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻)"` - Action string `json:"action" query:"action" description:"稳定动作编码"` - Category string `json:"category" query:"category" description:"动作类别"` - ActorKind string `json:"actor_kind" query:"actor_kind" description:"操作者类型"` + CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间,RFC3339且含时区" example:"2026-08-01T00:00:00+08:00"` + CreatedTo string `json:"created_to" query:"created_to" description:"结束时间,RFC3339且含时区,不包含该时刻" example:"2026-08-08T00:00:00+08:00"` + Action string `json:"action" query:"action" description:"稳定动作编码;直接使用事件响应的action_code,不按中文名称猜测"` + Category string `json:"category" query:"category" enum:"configuration,reliability,asset,security,identity,business" description:"动作类别 (configuration:配置, reliability:可靠性, asset:资产, security:安全, identity:身份, business:业务)"` + ActorKind string `json:"actor_kind" query:"actor_kind" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统)"` ActorID string `json:"actor_id" query:"actor_id" description:"操作者稳定ID"` - Source string `json:"source" query:"source" description:"操作入口来源"` - Result string `json:"result" query:"result" description:"结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知)"` - Risk string `json:"risk" query:"risk" description:"风险等级 (low:低, normal:普通, high:高, critical:严重)"` - ScopeType string `json:"scope_type" query:"scope_type" description:"业务范围类型"` + Source string `json:"source" query:"source" enum:"admin_api,personal_api,openapi,worker,scheduler,callback" description:"操作入口来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调)"` + Result string `json:"result" query:"result" enum:"success,failed,denied,partial,unknown" description:"结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知)"` + Risk string `json:"risk" query:"risk" enum:"low,normal,high,critical" description:"风险等级 (low:低, normal:普通, high:高, critical:严重)"` + ScopeType string `json:"scope_type" query:"scope_type" enum:"platform,shop,personal_customer" description:"业务范围类型 (platform:平台, shop:店铺, personal_customer:个人客户)"` ScopeID string `json:"scope_id" query:"scope_id" description:"业务范围稳定ID"` ResourceType string `json:"resource_type" query:"resource_type" description:"Resource Registry 注册类型"` ResourceID string `json:"resource_id" query:"resource_id" description:"资源内部稳定ID"` ResourceKey string `json:"resource_key" query:"resource_key" description:"资源业务稳定Key"` RequestID string `json:"request_id" query:"request_id" description:"HTTP请求关联ID"` CorrelationID string `json:"correlation_id" query:"correlation_id" description:"跨步骤业务链路ID"` - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // AuditEventIDParams 是审计事件详情路径参数。 @@ -29,25 +29,25 @@ type AuditEventIDParams struct { // AuditActorEventsRequest 是操作者行为时间线参数。 type AuditActorEventsRequest struct { - Kind string `json:"kind" path:"kind" required:"true" description:"操作者类型 (account:人工账号, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统)"` + Kind string `json:"kind" path:"kind" required:"true" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统)"` ID string `json:"id" path:"id" required:"true" description:"操作者稳定ID"` - Action string `json:"action" query:"action" description:"稳定动作编码"` - Result string `json:"result" query:"result" description:"事件结果"` - Risk string `json:"risk" query:"risk" description:"风险等级"` - ResourceType string `json:"resource_type" query:"resource_type" description:"资源类型"` + Action string `json:"action" query:"action" description:"稳定动作编码;直接使用事件响应的action_code"` + Result string `json:"result" query:"result" enum:"success,failed,denied,partial,unknown" description:"事件结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知)"` + Risk string `json:"risk" query:"risk" enum:"low,normal,high,critical" description:"风险等级 (low:低, normal:普通, high:高, critical:严重)"` + ResourceType string `json:"resource_type" query:"resource_type" description:"Resource Registry注册类型;直接使用事件resources或investigation_refs返回的resource_type"` ResourceID string `json:"resource_id" query:"resource_id" description:"资源内部稳定ID"` CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区)"` CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻)"` - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // AuditResourceSearchRequest 是首批注册资源的精确搜索参数。 type AuditResourceSearchRequest struct { - ResourceType string `json:"resource_type" query:"resource_type" required:"true" description:"资源类型 (iot_card:IoT卡, device:设备, shop:店铺, order:订单, refund:退款单)"` + ResourceType string `json:"resource_type" query:"resource_type" required:"true" enum:"iot_card,device,shop,order,refund" description:"资源类型 (iot_card:IoT卡, device:设备, shop:店铺, order:订单, refund:退款单)"` Keyword string `json:"keyword" query:"keyword" required:"true" description:"精确业务标识;卡支持ICCID/VirtualNo,设备支持VirtualNo/IMEI/SN"` - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // AuditResourceTimelineRequest 是通用资源时间线参数。 @@ -56,10 +56,10 @@ type AuditResourceTimelineRequest struct { ResourceID string `json:"resource_id" path:"resource_id" required:"true" description:"资源内部稳定ID"` CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区)"` CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻)"` - Action string `json:"action" query:"action" description:"稳定动作编码"` - Result string `json:"result" query:"result" description:"事件结果"` - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Action string `json:"action" query:"action" description:"稳定动作编码;直接使用事件响应的action_code"` + Result string `json:"result" query:"result" enum:"success,failed,denied,partial,unknown" description:"事件结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知)"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // AuditRequestTimelineParams 是请求链路时间线的路径参数。 @@ -86,23 +86,23 @@ type AuditFinanceTimelineRequest struct { RechargeNo string `json:"recharge_no" query:"recharge_no" description:"充值单号"` ApprovalInstanceID uint `json:"approval_instance_id" query:"approval_instance_id" description:"审批实例ID"` ThirdPartyTradeNo string `json:"third_party_trade_no" query:"third_party_trade_no" description:"第三方交易号"` - ActorKind string `json:"actor_kind" query:"actor_kind" description:"操作者类型;与actor_id同时提供"` + ActorKind string `json:"actor_kind" query:"actor_kind" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型;与actor_id同时提供 (account:人工账号, personal_customer:个人客户, openapi:开放接口账号, system_task:系统任务, scheduled_job:计划任务, external_system:外部系统)"` ActorID string `json:"actor_id" query:"actor_id" description:"操作者稳定ID;与actor_kind同时提供"` CorrelationID string `json:"correlation_id" query:"correlation_id" description:"跨步骤业务链路ID"` CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区)"` CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻)"` - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // AuditRiskFilterRequest 是风险总览和明细共用的受控筛选参数。 type AuditRiskFilterRequest struct { CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区);默认从在线窗口开始"` CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻,最长31天);默认当前时间"` - Risk string `json:"risk" query:"risk" description:"风险等级 (low:低, normal:普通, high:高, critical:严重)"` - Result string `json:"result" query:"result" description:"结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知)"` - Action string `json:"action" query:"action" description:"稳定动作编码"` - Source string `json:"source" query:"source" description:"来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调)"` + Risk string `json:"risk" query:"risk" enum:"low,normal,high,critical" description:"风险等级 (low:低, normal:普通, high:高, critical:严重)"` + Result string `json:"result" query:"result" enum:"success,failed,denied,partial,unknown" description:"结果 (success:成功, failed:失败, denied:拒绝, partial:部分成功, unknown:未知)"` + Action string `json:"action" query:"action" description:"稳定动作编码;直接使用事件响应的action_code"` + Source string `json:"source" query:"source" enum:"admin_api,personal_api,openapi,worker,scheduler,callback" description:"来源 (admin_api:后台管理API, personal_api:个人客户API, openapi:代理OpenAPI, worker:异步Worker, scheduler:计划任务, callback:外部系统回调)"` } // AuditRiskOverviewRequest 是风险总览请求参数。 @@ -113,18 +113,28 @@ type AuditRiskOverviewRequest struct { // AuditRiskEventsRequest 是风险事件明细请求参数。 type AuditRiskEventsRequest struct { AuditRiskFilterRequest - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } -// SubjectResourceActivityRequest 是代理和企业安全资源活动的路径及分页参数。 +// SubjectResourceActivityRequest 是代理安全资源活动的路径及分页参数。 type SubjectResourceActivityRequest struct { - ResourceType string `json:"resource_type" path:"resource_type" required:"true" description:"资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业)"` + ResourceType string `json:"resource_type" path:"resource_type" required:"true" enum:"iot_card,device,asset_allocation_record,exchange_order,shop,enterprise" description:"代理资源类型 (iot_card:IoT卡, device:设备, asset_allocation_record:资产分配记录, exchange_order:换货单, shop:店铺, enterprise:企业)"` Identifier string `json:"identifier" path:"identifier" required:"true" description:"业务稳定标识;卡使用ICCID,设备使用VirtualNo,其他资源使用对应业务编号"` CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区);默认从在线窗口开始"` CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻);默认当前时间"` - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` +} + +// EnterpriseResourceActivityRequest 是企业安全资源活动的路径及分页参数。 +type EnterpriseResourceActivityRequest struct { + ResourceType string `json:"resource_type" path:"resource_type" required:"true" enum:"iot_card,device" description:"企业资源类型 (iot_card:IoT卡, device:设备)"` + Identifier string `json:"identifier" path:"identifier" required:"true" description:"业务稳定标识;卡使用ICCID,设备使用VirtualNo"` + CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区);默认从在线窗口开始"` + CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻);默认当前时间"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // IntegrationFilterRequest 是外部集成调查的公共受控筛选参数。 @@ -132,13 +142,13 @@ type IntegrationFilterRequest struct { CreatedFrom string `json:"created_from" query:"created_from" description:"开始时间(RFC3339,含时区);默认从在线窗口开始"` CreatedTo string `json:"created_to" query:"created_to" description:"结束时间(RFC3339,含时区,不包含该时刻);默认当前时间"` IntegrationID string `json:"integration_id" query:"integration_id" description:"稳定外部集成记录ID"` - Provider string `json:"provider" query:"provider" description:"外部服务提供方稳定编码"` - Direction string `json:"direction" query:"direction" description:"交互方向 (inbound:入站, outbound:出站)"` - Operation string `json:"operation" query:"operation" description:"外部操作稳定编码"` - Result string `json:"result" query:"result" description:"原始结果 (pending:待处理, success:成功, failed:失败, unknown:结果未知, not_found:未找到, invalid_payload:无效载荷, conflict:冲突, ignored:已忽略, merged:已合并, rate_limited:已限频, completed:已提前完成, cancelled:已取消)"` - ResultCategory string `json:"result_category" query:"result_category" description:"派生结果类别 (processing:处理中, succeeded:成功, indeterminate:结果不确定, failed:失败, not_sent:未发送)"` + Provider string `json:"provider" query:"provider" enum:"ctcc,cmcc,cucc,wechat_pay,alipay,fuiou,wecom,gateway" description:"外部服务提供方 (ctcc:中国电信, cmcc:中国移动, cucc:中国联通, wechat_pay:微信支付, alipay:支付宝, fuiou:富友, wecom:企业微信, gateway:设备网关)"` + Direction string `json:"direction" query:"direction" enum:"inbound,outbound" description:"交互方向 (inbound:入站, outbound:出站)"` + Operation string `json:"operation" query:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码;直接使用列表或详情响应的operation,不按中文名称猜测"` + Result string `json:"result" query:"result" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"原始结果 (pending:待处理, success:成功, failed:失败, unknown:结果未知, not_found:未找到, invalid_payload:无效载荷, conflict:冲突, ignored:已忽略, merged:已合并, rate_limited:已限频, completed:已提前完成, cancelled:已取消)"` + ResultCategory string `json:"result_category" query:"result_category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"派生结果类别 (processing:处理中, succeeded:成功, indeterminate:结果不确定, failed:失败, not_sent:未发送)"` ExternalID string `json:"external_id" query:"external_id" description:"外部系统业务或请求标识"` - ResourceType string `json:"resource_type" query:"resource_type" description:"本地主要资源类型"` + ResourceType string `json:"resource_type" query:"resource_type" description:"本地主要资源类型;直接使用列表resource.type或调查引用的resource_type"` ResourceID string `json:"resource_id" query:"resource_id" description:"本地主要资源稳定ID"` ResourceKey string `json:"resource_key" query:"resource_key" description:"本地主要资源稳定Key"` TriggerSource string `json:"trigger_source" query:"trigger_source" description:"触发来源稳定编码"` @@ -154,14 +164,14 @@ type IntegrationFilterRequest struct { // IntegrationOverviewRequest 是外部集成交互总览参数。 type IntegrationOverviewRequest struct { IntegrationFilterRequest - Bucket string `json:"bucket" query:"bucket" description:"趋势时间粒度 (hour:小时, day:自然日),默认hour"` + Bucket string `json:"bucket" query:"bucket" enum:"hour,day" default:"hour" description:"趋势时间粒度 (hour:小时, day:自然日)"` } // IntegrationListRequest 是外部集成交互列表参数。 type IntegrationListRequest struct { IntegrationFilterRequest - Page int `json:"page" query:"page" minimum:"1" description:"页码,默认1"` - PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" description:"每页数量,默认20,最大100"` + Page int `json:"page" query:"page" minimum:"1" default:"1" description:"页码"` + PageSize int `json:"page_size" query:"page_size" minimum:"1" maximum:"100" default:"20" description:"每页数量,最大100"` } // IntegrationIDParams 是外部集成详情路径参数。 diff --git a/internal/query/audit/events.go b/internal/query/audit/events.go index c6fa53e..ce44dde 100644 --- a/internal/query/audit/events.go +++ b/internal/query/audit/events.go @@ -40,104 +40,104 @@ type EventFilter struct { // EventPage 是平台全局事件稳定分页结果。 type EventPage struct { - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Items []EventView `json:"items"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"符合条件的事件总数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页数量"` + Items []EventView `json:"items" description:"审计事件列表,按发生时间和主键稳定倒序"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // EventDetail 是单个审计事件及在线留存边界。 type EventDetail struct { EventView - Retention retentionquery.Info `json:"retention"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // EventView 是不暴露 GORM Model 的审计事件投影。 type EventView struct { - EventID string `json:"event_id"` - OccurredAt time.Time `json:"occurred_at"` - Category string `json:"category"` - ActionCode string `json:"action_code"` - ActionName string `json:"action_name"` - Summary string `json:"summary"` - ActorKind string `json:"actor_kind"` - ActorID string `json:"actor_id"` - ActorName string `json:"actor_name"` - ActorShopID *uint `json:"actor_shop_id,omitempty"` - ActorShopName string `json:"actor_shop_name"` - ActorEnterpriseID *uint `json:"actor_enterprise_id,omitempty"` - ActorEnterpriseName string `json:"actor_enterprise_name"` - Source string `json:"source"` - RequestPath string `json:"request_path"` - RequestMethod string `json:"request_method"` - IPAddress string `json:"ip_address"` - UserAgent string `json:"user_agent"` - ScopeType string `json:"scope_type"` - ScopeID string `json:"scope_id"` - ScopeName string `json:"scope_name"` - Result string `json:"result"` - RiskLevel string `json:"risk_level"` - ErrorCode string `json:"error_code"` - ErrorSummary string `json:"error_summary"` - RequestID string `json:"request_id"` - CorrelationID string `json:"correlation_id"` - ParentEventID string `json:"parent_event_id"` - BatchTotal int `json:"batch_total"` - SuccessCount int `json:"success_count"` - FailCount int `json:"fail_count"` - Metadata map[string]any `json:"metadata"` - ContentHash string `json:"content_hash"` - CreatedAt time.Time `json:"created_at"` - Resources []ResourceView `json:"resources"` - InvestigationRefs InvestigationRefs `json:"investigation_refs"` + EventID string `json:"event_id" description:"稳定审计事件ID,可传给事件详情接口"` + OccurredAt time.Time `json:"occurred_at" description:"业务事实发生时间"` + Category string `json:"category" enum:"configuration,reliability,asset,security,identity,business" description:"动作类别稳定编码"` + ActionCode string `json:"action_code" description:"稳定动作编码;筛选和跳转必须使用该值"` + ActionName string `json:"action_name" description:"action_code对应的中文展示名称"` + Summary string `json:"summary" description:"事件中文摘要"` + ActorKind string `json:"actor_kind" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型稳定编码"` + ActorID string `json:"actor_id" description:"操作者稳定ID,与actor_kind共同定位操作者时间线"` + ActorName string `json:"actor_name" description:"事件发生时的操作者名称快照"` + ActorShopID *uint `json:"actor_shop_id,omitempty" description:"操作者所属店铺ID快照"` + ActorShopName string `json:"actor_shop_name" description:"操作者所属店铺名称快照"` + ActorEnterpriseID *uint `json:"actor_enterprise_id,omitempty" description:"操作者所属企业ID快照"` + ActorEnterpriseName string `json:"actor_enterprise_name" description:"操作者所属企业名称快照"` + Source string `json:"source" enum:"admin_api,personal_api,openapi,worker,scheduler,callback" description:"操作入口来源稳定编码"` + RequestPath string `json:"request_path" description:"触发操作的HTTP路径;非HTTP入口可为空"` + RequestMethod string `json:"request_method" description:"触发操作的HTTP方法;非HTTP入口可为空"` + IPAddress string `json:"ip_address" description:"触发请求的IP地址;非HTTP入口可为空"` + UserAgent string `json:"user_agent" description:"触发请求的User-Agent;非HTTP入口可为空"` + ScopeType string `json:"scope_type" enum:"platform,shop,personal_customer" description:"业务范围类型稳定编码"` + ScopeID string `json:"scope_id" description:"业务范围稳定ID,与scope_type共同使用"` + ScopeName string `json:"scope_name" description:"业务范围名称快照"` + Result string `json:"result" enum:"success,failed,denied,partial,unknown" description:"事件结果稳定编码"` + RiskLevel string `json:"risk_level" enum:"low,normal,high,critical" description:"风险等级稳定编码"` + ErrorCode string `json:"error_code" description:"失败或拒绝时的稳定错误码"` + ErrorSummary string `json:"error_summary" description:"已脱敏的失败原因摘要"` + RequestID string `json:"request_id" description:"HTTP请求关联ID,可传给请求时间线接口"` + CorrelationID string `json:"correlation_id" description:"跨请求业务链路ID,可传给关联时间线接口"` + ParentEventID string `json:"parent_event_id" description:"批量或异步链路的父审计事件ID"` + BatchTotal int `json:"batch_total" description:"批次声明处理总数,非批次为0"` + SuccessCount int `json:"success_count" description:"批次成功数,非批次为0"` + FailCount int `json:"fail_count" description:"批次失败数,非批次为0"` + Metadata map[string]any `json:"metadata" description:"已脱敏的动作扩展元数据,字段由action_code定义"` + ContentHash string `json:"content_hash" description:"事件不可变内容摘要"` + CreatedAt time.Time `json:"created_at" description:"审计记录写入时间"` + Resources []ResourceView `json:"resources" description:"事件涉及的全部资源及各自前后快照"` + InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"跨审计视角的稳定跳转参数集合"` } // InvestigationRefs 是平台调查视角间唯一允许使用的稳定跳转引用。 type InvestigationRefs struct { - EventID *string `json:"event_id"` - ActorRef *ActorRef `json:"actor_ref"` - ResourceRefs []InvestigationResourceRef `json:"resource_refs"` - RequestID *string `json:"request_id"` - CorrelationID *string `json:"correlation_id"` - IntegrationRefs []IntegrationRef `json:"integration_refs"` + EventID *string `json:"event_id" description:"传给GET /audit/events/{event_id}"` + ActorRef *ActorRef `json:"actor_ref" description:"kind/id传给GET /audit/actors/{kind}/{id}/events"` + ResourceRefs []InvestigationResourceRef `json:"resource_refs" description:"resource_type/resource_id传给GET /audit/resources/{resource_type}/{resource_id}/timeline;resource_id为空时不可跳转"` + RequestID *string `json:"request_id" description:"传给GET /audit/requests/{request_id}/timeline"` + CorrelationID *string `json:"correlation_id" description:"传给GET /audit/correlations/{correlation_id}/timeline"` + IntegrationRefs []IntegrationRef `json:"integration_refs" description:"integration_id传给GET /audit/integrations/{integration_id}"` } // ActorRef 是操作者时间线的稳定引用。 type ActorRef struct { - Kind string `json:"kind"` - ID string `json:"id"` + Kind string `json:"kind" enum:"account,personal_customer,openapi,system_task,scheduled_job,external_system" description:"操作者类型"` + ID string `json:"id" description:"操作者稳定ID"` } // InvestigationResourceRef 是通用资源时间线的稳定引用。 type InvestigationResourceRef struct { - ResourceType string `json:"resource_type"` - ResourceID *string `json:"resource_id"` - ResourceKey string `json:"resource_key"` - DisplayName string `json:"display_name"` + ResourceType string `json:"resource_type" description:"Resource Registry注册类型"` + ResourceID *string `json:"resource_id" description:"资源内部稳定ID;为空时不展示平台资源时间线入口"` + ResourceKey string `json:"resource_key" description:"资源业务稳定Key,用于展示或精确搜索"` + DisplayName string `json:"display_name" description:"事件发生时的资源展示名称"` } // IntegrationRef 是 Integration 详情的稳定引用。 type IntegrationRef struct { - IntegrationID string `json:"integration_id"` + IntegrationID string `json:"integration_id" description:"稳定外部集成记录ID"` } // ResourceView 是事件发生时独立资源身份与变化的只读投影。 type ResourceView struct { - ResourceType string `json:"resource_type"` - ResourceID *string `json:"resource_id,omitempty"` - ResourceKey string `json:"resource_key"` - DisplayName string `json:"display_name"` - Relation string `json:"relation"` - Role string `json:"role"` - IdentitySnapshot map[string]any `json:"identity_snapshot"` - BeforeData map[string]any `json:"before_data"` - AfterData map[string]any `json:"after_data"` - SubjectVisibility string `json:"subject_visibility"` - SubjectSummary string `json:"subject_summary"` - SubjectData map[string]any `json:"subject_data"` - SortOrder int `json:"sort_order"` - CreatedAt time.Time `json:"created_at"` + ResourceType string `json:"resource_type" description:"Resource Registry注册类型"` + ResourceID *string `json:"resource_id,omitempty" description:"资源内部稳定ID"` + ResourceKey string `json:"resource_key" description:"资源业务稳定Key"` + DisplayName string `json:"display_name" description:"事件发生时的资源展示名称"` + Relation string `json:"relation" enum:"primary,affected,reference" description:"资源关系 (primary:主要资源, affected:受影响资源, reference:引用资源)"` + Role string `json:"role" description:"Resource Registry定义的资源业务角色编码"` + IdentitySnapshot map[string]any `json:"identity_snapshot" description:"事件发生时的资源身份快照"` + BeforeData map[string]any `json:"before_data" description:"该资源变更前的完整平台审计数据"` + AfterData map[string]any `json:"after_data" description:"该资源变更后的完整平台审计数据"` + SubjectVisibility string `json:"subject_visibility" enum:"internal_only,subject_result,subject_detail" description:"主体可见性 (internal_only:仅平台, subject_result:主体可见结论, subject_detail:主体可见安全详情)"` + SubjectSummary string `json:"subject_summary" description:"允许代理或企业查看的安全摘要"` + SubjectData map[string]any `json:"subject_data" description:"写入时生成的主体安全字段,不等同于before_data或after_data"` + SortOrder int `json:"sort_order" description:"资源在事件内的稳定展示顺序"` + CreatedAt time.Time `json:"created_at" description:"资源关联记录写入时间"` } // Query 提供平台统一审计事件列表与详情读取。 diff --git a/internal/query/audit/finance.go b/internal/query/audit/finance.go index a0041b7..e45837b 100644 --- a/internal/query/audit/finance.go +++ b/internal/query/audit/finance.go @@ -40,45 +40,45 @@ type FinanceFilter struct { // FinanceTimelinePage 是资金多源投影的稳定分页结果。 type FinanceTimelinePage struct { - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Items []FinanceTimelineNode `json:"items"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"关联资金事实总数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页数量"` + Items []FinanceTimelineNode `json:"items" description:"按发生时间稳定倒序的资金事实节点"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // FinanceTimelineNode 是明确事实来源和金额权威的资金时间线节点。 type FinanceTimelineNode struct { - RecordSource string `json:"record_source"` - NodeID string `json:"node_id"` - OccurredAt time.Time `json:"occurred_at"` - Code string `json:"code"` - Title string `json:"title"` - Result string `json:"result"` - ResultName string `json:"result_name"` - Amount *int64 `json:"amount"` - BalanceBefore *int64 `json:"balance_before"` - BalanceAfter *int64 `json:"balance_after"` - Currency string `json:"currency"` - ShopID *uint `json:"shop_id"` - Wallet *FinanceWalletRef `json:"wallet"` - AmountAuthority FinanceAmountAuthority `json:"amount_authority"` - Facts map[string]any `json:"facts"` - InvestigationRefs InvestigationRefs `json:"investigation_refs"` + RecordSource string `json:"record_source" enum:"audit_event,domain_ledger_ref,agent_wallet_transaction,asset_wallet_transaction,agent_wallet_reservation,order,payment,refund,agent_recharge,recharge_order,commission_record,commission_withdrawal,approval_instance" description:"资金事实来源稳定编码"` + NodeID string `json:"node_id" description:"该事实来源内的稳定节点ID"` + OccurredAt time.Time `json:"occurred_at" description:"资金事实发生时间"` + Code string `json:"code" description:"来源内稳定业务动作或状态编码"` + Title string `json:"title" description:"code对应的中文展示名称"` + Result string `json:"result" description:"来源内原始结果或状态编码"` + ResultName string `json:"result_name" description:"result对应的中文展示名称"` + Amount *int64 `json:"amount" description:"本节点金额,单位分;为空表示该节点不承载金额"` + BalanceBefore *int64 `json:"balance_before" description:"变更前余额,单位分"` + BalanceAfter *int64 `json:"balance_after" description:"变更后余额,单位分"` + Currency string `json:"currency" description:"币种编码,人民币为CNY"` + ShopID *uint `json:"shop_id" description:"关联店铺ID"` + Wallet *FinanceWalletRef `json:"wallet" description:"关联钱包稳定引用"` + AmountAuthority FinanceAmountAuthority `json:"amount_authority" description:"金额是否权威及权威字段来源"` + Facts map[string]any `json:"facts" description:"该事实来源的安全结构化业务字段"` + InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"可继续跳转的稳定调查引用"` } // FinanceWalletRef 是资金节点关联的钱包稳定引用。 type FinanceWalletRef struct { - ResourceType string `json:"resource_type"` - WalletID uint `json:"wallet_id"` + ResourceType string `json:"resource_type" enum:"agent_wallet,asset_wallet" description:"钱包资源类型"` + WalletID uint `json:"wallet_id" description:"钱包内部稳定ID,可作为finance/timeline的wallet_id"` } // FinanceAmountAuthority 说明当前金额是否为业务权威及其字段来源。 type FinanceAmountAuthority struct { - Authoritative bool `json:"authoritative"` - Table string `json:"table"` - Field string `json:"field"` - ConflictRule string `json:"conflict_rule"` + Authoritative bool `json:"authoritative" description:"当前amount或余额是否来自业务权威表"` + Table string `json:"table" description:"权威金额所在业务表;非权威节点可为空"` + Field string `json:"field" description:"权威金额所在字段;非权威节点可为空"` + ConflictRule string `json:"conflict_rule" description:"多来源冲突时的取值规则说明"` } type financeRefs struct { diff --git a/internal/query/audit/resources.go b/internal/query/audit/resources.go index eb60573..319a281 100644 --- a/internal/query/audit/resources.go +++ b/internal/query/audit/resources.go @@ -8,12 +8,15 @@ import ( "gorm.io/datatypes" "gorm.io/gorm" + auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "github.com/break/junhong_cmp_fiber/internal/model" retentionquery "github.com/break/junhong_cmp_fiber/internal/query/retention" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) +var timelineRegistry = auditinfra.NewRegistry() + // ResourceSearchFilter 定义注册资源的精确标识搜索。 type ResourceSearchFilter struct { ResourceType string @@ -25,21 +28,21 @@ type ResourceSearchFilter struct { // ResourceSearchPage 是资源候选稳定分页结果。 type ResourceSearchPage struct { - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Items []ResourceCandidate `json:"items"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"符合精确标识的资源总数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页数量"` + Items []ResourceCandidate `json:"items" description:"当前资源或历史快照解析出的候选资源"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // ResourceCandidate 是当前业务表或历史事件快照解析出的稳定资源候选。 type ResourceCandidate struct { - ResourceType string `json:"resource_type"` - ResourceID string `json:"resource_id"` - ResourceKey string `json:"resource_key"` - DisplayName string `json:"display_name"` - IdentitySnapshot map[string]any `json:"identity_snapshot"` - Historical bool `json:"historical"` + ResourceType string `json:"resource_type" enum:"iot_card,device,shop,order,refund" description:"资源类型稳定编码"` + ResourceID string `json:"resource_id" description:"资源内部稳定ID,可传给通用资源时间线接口"` + ResourceKey string `json:"resource_key" description:"资源业务稳定Key"` + DisplayName string `json:"display_name" description:"资源展示名称"` + IdentitySnapshot map[string]any `json:"identity_snapshot" description:"当前业务表或历史事件保存的资源身份快照"` + Historical bool `json:"historical" description:"是否仅由历史事件快照解析,true不代表资源当前仍存在"` } // ResourceTimelineFilter 定义通用资源时间线筛选。 @@ -95,16 +98,8 @@ func (q *Query) ResourceTimeline(ctx context.Context, filter ResourceTimelineFil } func timelineResourceType(resourceType string) bool { - switch resourceType { - case constants.AuditResourceAccount, constants.AuditResourceShop, constants.AuditResourceEnterprise, - constants.AuditResourceIotCard, constants.AuditResourceDevice, constants.AuditResourceDeviceSIMBinding, - constants.AuditResourceAssetAllocationRecord, constants.AuditResourceExchangeOrder, constants.AuditResourceOrder, - constants.AuditResourceRefund, constants.AuditResourceAgentRecharge, constants.AuditResourceAssetWallet, - constants.AuditResourceApprovalInstance: - return true - default: - return false - } + _, ok := timelineRegistry.Resource(resourceType) + return ok } func (q *Query) searchCurrent(ctx context.Context, filter ResourceSearchFilter) ([]ResourceCandidate, int64, error) { diff --git a/internal/query/audit/risks.go b/internal/query/audit/risks.go index dea5935..f8b87dd 100644 --- a/internal/query/audit/risks.go +++ b/internal/query/audit/risks.go @@ -26,44 +26,44 @@ type RiskFilter struct { // RiskOverview 是风险信号、固定维度与时间趋势的只读聚合。 type RiskOverview struct { - Total int64 `json:"total"` - Bucket string `json:"bucket"` - Signals []RiskNamedCount `json:"signals"` - Risks []RiskNamedCount `json:"risks"` - Results []RiskNamedCount `json:"results"` - Actions []RiskNamedCount `json:"actions"` - Sources []RiskNamedCount `json:"sources"` - Trend []RiskTrendPoint `json:"trend"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"固定风险集合内的事件总数"` + Bucket string `json:"bucket" enum:"hour,day" description:"服务端选择的趋势时间粒度"` + Signals []RiskNamedCount `json:"signals" description:"固定信号分布,code为high_risk、finance、security、failed、denied、partial或unknown,name为中文展示名"` + Risks []RiskNamedCount `json:"risks" description:"风险等级分布,code为low、normal、high或critical,name为中文展示名"` + Results []RiskNamedCount `json:"results" description:"结果分布,code为success、failed、denied、partial或unknown,name为中文展示名"` + Actions []RiskNamedCount `json:"actions" description:"动作分布,code为稳定action_code,name为中文action_name"` + Sources []RiskNamedCount `json:"sources" description:"来源分布,code为admin_api、personal_api、openapi、worker、scheduler或callback,name为中文展示名"` + Trend []RiskTrendPoint `json:"trend" description:"固定风险信号的时间趋势"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // RiskNamedCount 是风险聚合维度的稳定编码、中文名称和数量。 type RiskNamedCount struct { - Code string `json:"code"` - Name string `json:"name"` - Count int64 `json:"count"` + Code string `json:"code" description:"当前聚合维度的稳定编码,具体枚举域由所属数组字段说明"` + Name string `json:"name" description:"code对应的中文展示名称"` + Count int64 `json:"count" description:"该编码的事件数量"` } // RiskTrendPoint 是固定时间桶内的风险信号趋势。 type RiskTrendPoint struct { - BucketAt time.Time `json:"bucket_at"` - Total int64 `json:"total"` - HighRisk int64 `json:"high_risk"` - Finance int64 `json:"finance"` - Security int64 `json:"security"` - Failed int64 `json:"failed"` - Denied int64 `json:"denied"` - Partial int64 `json:"partial"` - Unknown int64 `json:"unknown"` + BucketAt time.Time `json:"bucket_at" description:"时间桶起点"` + Total int64 `json:"total" description:"桶内固定风险集合事件数"` + HighRisk int64 `json:"high_risk" description:"桶内high或critical风险事件数"` + Finance int64 `json:"finance" description:"桶内资金类风险事件数"` + Security int64 `json:"security" description:"桶内安全类风险事件数"` + Failed int64 `json:"failed" description:"桶内failed事件数"` + Denied int64 `json:"denied" description:"桶内denied事件数"` + Partial int64 `json:"partial" description:"桶内partial事件数"` + Unknown int64 `json:"unknown" description:"桶内unknown事件数"` } // RiskEventPage 是风险事件的稳定分页结果。 type RiskEventPage struct { - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Items []EventView `json:"items"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"符合条件的风险事件总数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页数量"` + Items []EventView `json:"items" description:"风险事件及稳定调查引用"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // RiskOverview 查询指定时间范围内的固定风险调查总览。 diff --git a/internal/query/audit/subject_activities.go b/internal/query/audit/subject_activities.go index c685cce..2bb6bb6 100644 --- a/internal/query/audit/subject_activities.go +++ b/internal/query/audit/subject_activities.go @@ -27,31 +27,31 @@ type SubjectActivityFilter struct { // SubjectActivityPage 是不包含平台调查字段的代理资源活动分页结果。 type SubjectActivityPage struct { - Resource SubjectResourceSummary `json:"resource"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Items []SubjectActivity `json:"items"` - Retention retentionquery.Info `json:"retention"` + Resource SubjectResourceSummary `json:"resource" description:"已完成授权校验的目标资源"` + Total int64 `json:"total" description:"主体可见活动总数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页数量"` + Items []SubjectActivity `json:"items" description:"不包含平台内部调查字段的安全活动列表"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // SubjectActivity 是写入时已生成的主体安全活动投影。 type SubjectActivity struct { - ActionCode string `json:"action_code"` - ActionName string `json:"action_name"` - SubjectSummary string `json:"subject_summary"` - SubjectData map[string]any `json:"subject_data"` - Result string `json:"result"` - OccurredAt time.Time `json:"occurred_at"` - RelatedResources []SubjectResourceSummary `json:"related_resources"` + ActionCode string `json:"action_code" description:"稳定动作编码"` + ActionName string `json:"action_name" description:"action_code对应的中文展示名称"` + SubjectSummary string `json:"subject_summary" description:"写入时生成的主体安全摘要"` + SubjectData map[string]any `json:"subject_data" description:"写入时生成的主体安全业务字段,不包含平台before/after或内部原因"` + Result string `json:"result" enum:"success,failed,denied,partial,unknown" description:"活动结果稳定编码"` + OccurredAt time.Time `json:"occurred_at" description:"业务事实发生时间"` + RelatedResources []SubjectResourceSummary `json:"related_resources" description:"当前主体授权范围内的相关资源摘要"` } // SubjectResourceSummary 是主体活动允许公开的资源摘要。 type SubjectResourceSummary struct { - ResourceType string `json:"resource_type"` - ResourceID string `json:"resource_id"` - ResourceKey string `json:"resource_key"` - DisplayName string `json:"display_name"` + ResourceType string `json:"resource_type" description:"资源类型稳定编码"` + ResourceID string `json:"resource_id" description:"资源内部稳定ID;主体前端不据此调用平台审计接口"` + ResourceKey string `json:"resource_key" description:"资源业务稳定Key"` + DisplayName string `json:"display_name" description:"资源安全展示名称"` } type subjectTarget struct { @@ -60,14 +60,15 @@ type subjectTarget struct { } type subjectActivityRow struct { - ID uint - ActionCode string - ActionName string - Result string - OccurredAt time.Time - SubjectSummary string - SubjectData datatypes.JSON - TargetResourceID uint + ID uint + ActionCode string + ActionName string + Result string + OccurredAt time.Time + SubjectVisibility string + SubjectSummary string + SubjectData datatypes.JSON + TargetResourceID uint } type subjectResourceAuthorizer func(context.Context, []model.AuditEventResource) (map[string]bool, error) @@ -78,9 +79,12 @@ func (q *Query) AgentResourceActivities(ctx context.Context, filter SubjectActiv if err != nil { return nil, err } - if filter.Identifier == "" || !agentActivityResourceType(filter.ResourceType) || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize { + if filter.Identifier == "" || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize { return nil, errors.New(errors.CodeInvalidParam) } + if !agentActivityResourceType(filter.ResourceType) { + return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize) retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceAudit) if err != nil { @@ -108,9 +112,12 @@ func (q *Query) EnterpriseResourceActivities(ctx context.Context, filter Subject if enterpriseID == 0 { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } - if filter.Identifier == "" || !enterpriseActivityResourceType(filter.ResourceType) || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize { + if filter.Identifier == "" || filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize { return nil, errors.New(errors.CodeInvalidParam) } + if !enterpriseActivityResourceType(filter.ResourceType) { + return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize) retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceAudit) if err != nil { @@ -142,13 +149,13 @@ func (q *Query) subjectActivitiesForTarget(ctx context.Context, filter SubjectAc } rows := make([]subjectActivityRow, 0, filter.PageSize) - if err := base.Select("tb_audit_event.id, action_code, action_name, result, occurred_at, target.subject_summary, target.subject_data, target.id AS target_resource_id"). + if err := base.Select("tb_audit_event.id, action_code, action_name, result, occurred_at, target.subject_visibility, target.subject_summary, target.subject_data, target.id AS target_resource_id"). Joins("JOIN tb_audit_event_resource AS target ON target.audit_event_id = tb_audit_event.id AND target.resource_type = ? AND target.resource_id = ?", filter.ResourceType, target.id). Where("target.subject_visibility IN ?", []string{constants.AuditSubjectResult, constants.AuditSubjectDetail}). Order("occurred_at DESC, tb_audit_event.id DESC").Offset((filter.Page - 1) * filter.PageSize).Limit(filter.PageSize).Scan(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理资源活动失败") } - items, err := q.projectSubjectActivities(ctx, rows, authorize) + items, err := q.projectSubjectActivities(ctx, rows, target, authorize) if err != nil { return nil, err } @@ -265,24 +272,28 @@ func newSubjectTarget(resourceType string, id uint, key, name string) subjectTar return subjectTarget{summary: SubjectResourceSummary{ResourceType: resourceType, ResourceID: resourceID, ResourceKey: key, DisplayName: name}, id: resourceID} } -func (q *Query) projectSubjectActivities(ctx context.Context, rows []subjectActivityRow, authorize subjectResourceAuthorizer) ([]SubjectActivity, error) { +func (q *Query) projectSubjectActivities(ctx context.Context, rows []subjectActivityRow, target subjectTarget, authorize subjectResourceAuthorizer) ([]SubjectActivity, error) { items := make([]SubjectActivity, 0, len(rows)) - if len(rows) == 0 { - return items, nil - } eventIDs := make([]uint, 0, len(rows)) for _, row := range rows { eventIDs = append(eventIDs, row.ID) } - var resources []model.AuditEventResource - if err := q.db.WithContext(ctx).Where("audit_event_id IN ? AND subject_visibility IN ?", eventIDs, []string{constants.AuditSubjectResult, constants.AuditSubjectDetail}). - Order("audit_event_id ASC, sort_order ASC, id ASC").Find(&resources).Error; err != nil { - return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询主体可见关联资源失败") + resources := make([]model.AuditEventResource, 0) + if len(eventIDs) > 0 { + if err := q.db.WithContext(ctx).Where("audit_event_id IN ? AND subject_visibility IN ?", eventIDs, []string{constants.AuditSubjectResult, constants.AuditSubjectDetail}). + Order("audit_event_id ASC, sort_order ASC, id ASC").Find(&resources).Error; err != nil { + return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询主体可见关联资源失败") + } } - allowed, err := authorize(ctx, resources) + targetResourceID := target.id + authorizationResources := append(resources, model.AuditEventResource{ResourceType: target.summary.ResourceType, ResourceID: &targetResourceID}) + allowed, err := authorize(ctx, authorizationResources) if err != nil { return nil, err } + if !allowed[resourceAccessKey(target.summary.ResourceType, target.id)] { + return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } related := make(map[uint][]SubjectResourceSummary, len(rows)) for _, resource := range resources { if resource.ResourceID == nil || !allowed[resourceAccessKey(resource.ResourceType, *resource.ResourceID)] { @@ -294,9 +305,12 @@ func (q *Query) projectSubjectActivities(ctx context.Context, rows []subjectActi }) } for _, row := range rows { - data, err := decodeObject(row.SubjectData) - if err != nil { - return nil, err + data := map[string]any{} + if row.SubjectVisibility == constants.AuditSubjectDetail { + data, err = decodeObject(row.SubjectData) + if err != nil { + return nil, err + } } items = append(items, SubjectActivity{ActionCode: row.ActionCode, ActionName: row.ActionName, SubjectSummary: row.SubjectSummary, SubjectData: data, Result: row.Result, diff --git a/internal/query/audit/timeline.go b/internal/query/audit/timeline.go index 4ce2fd9..f054174 100644 --- a/internal/query/audit/timeline.go +++ b/internal/query/audit/timeline.go @@ -15,39 +15,39 @@ import ( // LinkTimeline 是 request 或 correlation 的跨事实只读时间线。 type LinkTimeline struct { - RequestID *string `json:"request_id"` - CorrelationID *string `json:"correlation_id"` - AccessLogLookupRequestID *string `json:"access_log_lookup_request_id"` - Nodes []LinkTimelineNode `json:"nodes"` - Retention retentionquery.Info `json:"retention"` + RequestID *string `json:"request_id" description:"本次按请求查询的稳定ID"` + CorrelationID *string `json:"correlation_id" description:"本次按业务关联查询的稳定ID"` + AccessLogLookupRequestID *string `json:"access_log_lookup_request_id" description:"可复制到Access Log检索的request_id;本接口自身不扫描Access Log"` + Nodes []LinkTimelineNode `json:"nodes" description:"跨事实来源按发生时间稳定排序的节点"` + Retention retentionquery.Info `json:"retention" description:"Audit与Integration共同在线留存边界"` } // LinkTimelineNode 是保留各事实源权威边界的时间线节点。 type LinkTimelineNode struct { - RecordSource string `json:"record_source"` - NodeID string `json:"node_id"` - OccurredAt time.Time `json:"occurred_at"` - Code string `json:"code"` - Title string `json:"title"` - Result string `json:"result"` - ResultName string `json:"result_name"` - Summary string `json:"summary"` - ReferenceOnly bool `json:"reference_only"` - RequestID *string `json:"request_id"` - CorrelationID *string `json:"correlation_id"` - ParentEventID *string `json:"parent_event_id"` - Resources []InvestigationResourceRef `json:"resources"` - InvestigationRefs InvestigationRefs `json:"investigation_refs"` - Fidelity LinkageFidelity `json:"fidelity"` + RecordSource string `json:"record_source" enum:"audit_event,integration_log,outbox_event,asynq_task,domain_ledger_ref" description:"事实来源 (audit_event:审计事件, integration_log:外部交互, outbox_event:可靠事件引用, asynq_task:异步任务引用, domain_ledger_ref:业务账本引用)"` + NodeID string `json:"node_id" description:"该事实来源内的稳定节点ID"` + OccurredAt time.Time `json:"occurred_at" description:"节点发生时间"` + Code string `json:"code" description:"来源内稳定动作、操作或事件编码"` + Title string `json:"title" description:"code对应的中文展示名称"` + Result string `json:"result" description:"来源内原始结果稳定编码"` + ResultName string `json:"result_name" description:"result对应的中文展示名称"` + Summary string `json:"summary" description:"已脱敏节点摘要"` + ReferenceOnly bool `json:"reference_only" description:"true表示仅保存其他事实的引用,不代表该来源独立完成业务状态变更"` + RequestID *string `json:"request_id" description:"HTTP请求关联ID"` + CorrelationID *string `json:"correlation_id" description:"跨请求业务链路ID"` + ParentEventID *string `json:"parent_event_id" description:"父审计事件ID"` + Resources []InvestigationResourceRef `json:"resources" description:"节点可稳定定位的资源引用"` + InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"可继续跳转的稳定调查引用"` + Fidelity LinkageFidelity `json:"fidelity" description:"历史字段完整度和可关联能力"` } // LinkageFidelity 明确节点已有的稳定关联能力,不补猜历史缺失字段。 type LinkageFidelity struct { - RequestAvailable bool `json:"request_available"` - CorrelationAvailable bool `json:"correlation_available"` - ParentEventAvailable bool `json:"parent_event_available"` - DirectAuditLinkAvailable bool `json:"direct_audit_link_available"` - StableResourceAvailable bool `json:"stable_resource_available"` + RequestAvailable bool `json:"request_available" description:"是否有稳定request_id"` + CorrelationAvailable bool `json:"correlation_available" description:"是否有稳定correlation_id"` + ParentEventAvailable bool `json:"parent_event_available" description:"是否有稳定parent_event_id"` + DirectAuditLinkAvailable bool `json:"direct_audit_link_available" description:"是否可直接跳转审计事件详情"` + StableResourceAvailable bool `json:"stable_resource_available" description:"是否至少有一个含resource_id的稳定资源引用"` } // RequestTimeline 按精确 request ID 组合已持久化事实,不扫描 Access Log。 diff --git a/internal/query/integration/logs.go b/internal/query/integration/logs.go index 61abd13..3425dd0 100644 --- a/internal/query/integration/logs.go +++ b/internal/query/integration/logs.go @@ -36,134 +36,137 @@ type ListFilter struct { // ListPage 是按创建时间和主键稳定倒序的分页结果。 type ListPage struct { - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - Items []ListItem `json:"items"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"符合条件的外部交互总数"` + Page int `json:"page" description:"当前页码"` + PageSize int `json:"page_size" description:"每页数量"` + Items []ListItem `json:"items" description:"按创建时间和主键稳定倒序的外部交互"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // ListItem 是 Integration Log 列表投影。 type ListItem struct { - IntegrationID string `json:"integration_id"` - Provider string `json:"provider"` - ProviderName string `json:"provider_name"` - Direction string `json:"direction"` - DirectionName string `json:"direction_name"` - Operation string `json:"operation"` - OperationName string `json:"operation_name"` - Resource ResourceView `json:"resource"` - Result string `json:"result"` - ResultName string `json:"result_name"` - ResultCategory string `json:"result_category"` - DurationMS int64 `json:"duration_ms"` - StateChanged bool `json:"state_changed"` - RequestID *string `json:"request_id"` - CorrelationID *string `json:"correlation_id"` - CreatedAt time.Time `json:"created_at"` + IntegrationID string `json:"integration_id" description:"稳定外部集成记录ID,可传给详情接口"` + Provider string `json:"provider" enum:"ctcc,cmcc,cucc,wechat_pay,alipay,fuiou,wecom,gateway" description:"外部服务提供方稳定编码"` + ProviderName string `json:"provider_name" description:"provider对应的中文展示名称"` + Direction string `json:"direction" enum:"inbound,outbound" description:"交互方向稳定编码"` + DirectionName string `json:"direction_name" description:"direction对应的中文展示名称"` + Operation string `json:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码,可直接用于列表筛选"` + OperationName string `json:"operation_name" description:"operation对应的中文展示名称"` + Resource ResourceView `json:"resource" description:"外部交互直接关联的本地主要资源"` + Result string `json:"result" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"外部交互原始结果稳定编码"` + ResultName string `json:"result_name" description:"result对应的中文展示名称"` + ResultCategory string `json:"result_category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由result派生的固定结果类别"` + DurationMS int64 `json:"duration_ms" description:"交互耗时,单位毫秒"` + StateChanged bool `json:"state_changed" description:"本次交互是否改变本地业务状态"` + RequestID *string `json:"request_id" description:"来源HTTP请求ID,可跳转请求时间线"` + CorrelationID *string `json:"correlation_id" description:"跨步骤业务链路ID,可跳转关联时间线"` + CreatedAt time.Time `json:"created_at" description:"外部交互记录创建时间"` } // ResourceView 是外部交互直接主资源投影。 type ResourceView struct { - Type *string `json:"type"` - ID *string `json:"id"` - Key *string `json:"key"` + Type *string `json:"type" description:"Resource Registry注册类型"` + ID *string `json:"id" description:"资源内部稳定ID;type和id均有值时可跳转资源时间线"` + Key *string `json:"key" description:"资源业务稳定Key"` } // Detail 是按稳定 integration_id 返回的结构化详情。 type Detail struct { - Identity IdentityView `json:"identity"` - Resource ResourceView `json:"resource"` - Trigger TriggerView `json:"trigger"` - Result ResultView `json:"result"` - Content ContentView `json:"content"` - Linkage LinkageView `json:"linkage"` - Timestamps TimestampView `json:"timestamps"` - Attempts []AttemptView `json:"attempts"` - Fidelity FidelityView `json:"fidelity"` + Identity IdentityView `json:"identity" description:"提供方、方向、操作和外部标识"` + Resource ResourceView `json:"resource" description:"本地主要资源引用"` + Trigger TriggerView `json:"trigger" description:"触发来源、场景和显式尝试序列"` + Result ResultView `json:"result" description:"原始结果、派生类别和本地状态变化"` + Content ContentView `json:"content" description:"已脱敏的结构化请求、响应和元数据摘要"` + Linkage LinkageView `json:"linkage" description:"可跳转请求、关联和审计视角的稳定字段"` + Timestamps TimestampView `json:"timestamps" description:"调度、开始、创建和更新时间"` + Attempts []AttemptView `json:"attempts" description:"同一trigger_series下按attempt排序的技术尝试"` + Fidelity FidelityView `json:"fidelity" description:"历史记录字段完整度,false时禁止前端猜测关联"` } // DetailResponse 是外部交互详情及在线留存边界。 type DetailResponse struct { Detail - Retention retentionquery.Info `json:"retention"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // AttemptView 是显式 trigger_series 下的单次技术尝试。 type AttemptView struct { - IntegrationID string `json:"integration_id"` - Attempt int `json:"attempt"` - Sent bool `json:"sent"` - Result string `json:"result"` - ResultName string `json:"result_name"` - ResultCategory string `json:"result_category"` - DurationMS int64 `json:"duration_ms"` - StateChanged bool `json:"state_changed"` - CreatedAt time.Time `json:"created_at"` + IntegrationID string `json:"integration_id" description:"本次尝试的稳定外部集成记录ID"` + Attempt int `json:"attempt" description:"同一显式序列内从1开始的尝试序号"` + Operation string `json:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码"` + OperationName string `json:"operation_name" description:"operation对应的中文展示名称"` + Sent bool `json:"sent" description:"是否实际向外部系统发送请求"` + Result string `json:"result" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"本次尝试原始结果"` + ResultName string `json:"result_name" description:"result对应的中文展示名称"` + ResultCategory string `json:"result_category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"本次尝试的派生结果类别"` + DurationMS int64 `json:"duration_ms" description:"本次尝试耗时,单位毫秒"` + StateChanged bool `json:"state_changed" description:"本次尝试是否改变本地业务状态"` + CreatedAt time.Time `json:"created_at" description:"本次尝试记录创建时间"` } // FidelityView 明确历史记录可关联能力,不推断缺失字段。 type FidelityView struct { - TriggerSeriesAvailable bool `json:"trigger_series_available"` - CorrelationAvailable bool `json:"correlation_available"` - ResourceIDAvailable bool `json:"resource_id_available"` - ProviderMessageFidelity string `json:"provider_message_fidelity"` + TriggerSeriesAvailable bool `json:"trigger_series_available" description:"是否存在显式trigger_series"` + AttemptSequenceReliable bool `json:"attempt_sequence_reliable" description:"attempt是否连续且operation一致"` + CorrelationAvailable bool `json:"correlation_available" description:"是否存在稳定correlation_id"` + ResourceIDAvailable bool `json:"resource_id_available" description:"是否存在稳定本地resource.id"` + ProviderMessageFidelity string `json:"provider_message_fidelity" description:"外部消息保真等级;受限时只展示已脱敏摘要"` } // IdentityView 是外部交互身份分组。 type IdentityView struct { - IntegrationID string `json:"integration_id"` - Provider string `json:"provider"` - ProviderName string `json:"provider_name"` - Direction string `json:"direction"` - DirectionName string `json:"direction_name"` - Operation string `json:"operation"` - OperationName string `json:"operation_name"` - ExternalID *string `json:"external_id"` + IntegrationID string `json:"integration_id" description:"稳定外部集成记录ID"` + Provider string `json:"provider" enum:"ctcc,cmcc,cucc,wechat_pay,alipay,fuiou,wecom,gateway" description:"外部服务提供方稳定编码"` + ProviderName string `json:"provider_name" description:"provider对应的中文展示名称"` + Direction string `json:"direction" enum:"inbound,outbound" description:"交互方向稳定编码"` + DirectionName string `json:"direction_name" description:"direction对应的中文展示名称"` + Operation string `json:"operation" enum:"realname_callback,realname_removal_callback,payment_precreate,payment_query,payment_callback,get_access_token,list_visible_members,list_visible_departments,get_template_detail,upload_approval_attachment,submit_approval,approval_callback,get_approval_detail,get_approval_info,query_realname_status,query_flow,query_card_status,query_device_info,set_speed_tier,stop_card,start_card,set_device_wifi,set_device_switch_mode,switch_device_card,reboot_device,reset_device" description:"外部操作稳定编码"` + OperationName string `json:"operation_name" description:"operation对应的中文展示名称"` + ExternalID *string `json:"external_id" description:"外部系统业务或请求标识"` } // TriggerView 是外部交互触发分组。 type TriggerView struct { - Source *string `json:"source"` - Scene *string `json:"scene"` - Series *string `json:"series"` - Attempt int `json:"attempt"` + Source *string `json:"source" description:"触发来源稳定编码"` + Scene *string `json:"scene" description:"触发业务场景"` + Series *string `json:"series" description:"显式技术尝试序列ID;为空时禁止按时间或资源猜测重试关系"` + Attempt int `json:"attempt" description:"显式序列内的尝试序号"` } // ResultView 是外部交互结果分组。 type ResultView struct { - Code string `json:"code"` - Name string `json:"name"` - Category string `json:"category"` - HTTPStatus *int `json:"http_status"` - ProviderCode *string `json:"provider_code"` - ProviderMessage *string `json:"provider_message"` - DurationMS int64 `json:"duration_ms"` - StateChanged bool `json:"state_changed"` - RecoveryStrategy *string `json:"recovery_strategy"` + Code string `json:"code" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"原始结果稳定编码"` + Name string `json:"name" description:"code对应的中文展示名称"` + Category string `json:"category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由code派生的固定结果类别"` + HTTPStatus *int `json:"http_status" description:"外部HTTP响应状态码"` + ProviderCode *string `json:"provider_code" description:"外部服务稳定结果码"` + ProviderMessage *string `json:"provider_message" description:"已脱敏的外部结果摘要"` + DurationMS int64 `json:"duration_ms" description:"交互耗时,单位毫秒"` + StateChanged bool `json:"state_changed" description:"是否改变本地业务状态"` + RecoveryStrategy *string `json:"recovery_strategy" description:"已脱敏的既有恢复策略说明;本接口不执行恢复"` } // ContentView 是已持久化安全摘要分组。 type ContentView struct { - RequestSummary map[string]any `json:"request_summary"` - ResponseSummary map[string]any `json:"response_summary"` - Metadata map[string]any `json:"metadata"` - ContentHash string `json:"content_hash"` + RequestSummary map[string]any `json:"request_summary" description:"按白名单重新清理的请求摘要"` + ResponseSummary map[string]any `json:"response_summary" description:"按白名单重新清理的响应摘要"` + Metadata map[string]any `json:"metadata" description:"按白名单重新清理的扩展元数据"` + ContentHash string `json:"content_hash" description:"持久化内容摘要"` } // LinkageView 是外部交互关联分组。 type LinkageView struct { - RequestID *string `json:"request_id"` - CorrelationID *string `json:"correlation_id"` - AuditEventID *uint `json:"audit_event_id"` + RequestID *string `json:"request_id" description:"传给GET /audit/requests/{request_id}/timeline"` + CorrelationID *string `json:"correlation_id" description:"传给GET /audit/correlations/{correlation_id}/timeline"` + AuditEventID *uint `json:"audit_event_id" description:"内部审计事件数据库引用;前端优先使用调查接口返回的稳定event_id"` } // TimestampView 是外部交互时间分组。 type TimestampView struct { - ScheduledAt *time.Time `json:"scheduled_at"` - StartedAt *time.Time `json:"started_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ScheduledAt *time.Time `json:"scheduled_at" description:"计划发送时间"` + StartedAt *time.Time `json:"started_at" description:"实际开始时间"` + CreatedAt time.Time `json:"created_at" description:"记录创建时间"` + UpdatedAt time.Time `json:"updated_at" description:"记录最后更新时间"` } // Query 提供平台 Integration Log 列表和详情读取。 @@ -253,7 +256,7 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse, if err != nil { return nil, err } - attempts, err := q.loadAttempts(ctx, row, retention.OnlineFrom) + attempts, attemptSequenceReliable, err := q.loadAttempts(ctx, row, retention.OnlineFrom) if err != nil { return nil, err } @@ -261,13 +264,14 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse, return &DetailResponse{Detail: Detail{ Identity: IdentityView{IntegrationID: row.IntegrationID, Provider: row.Provider, ProviderName: constants.IntegrationProviderName(row.Provider), Direction: row.Direction, DirectionName: constants.IntegrationDirectionName(row.Direction), Operation: row.Operation, OperationName: constants.IntegrationOperationName(row.Operation), ExternalID: row.ExternalID}, Resource: resourceView(row), Trigger: TriggerView{Source: row.TriggerSource, Scene: row.TriggerScene, Series: row.TriggerSeries, Attempt: row.Attempt}, - Result: ResultView{Code: row.Result, Name: constants.IntegrationResultName(row.Result), Category: constants.IntegrationResultCategory(row.Result), HTTPStatus: row.HTTPStatus, ProviderCode: row.ProviderCode, ProviderMessage: providerMessage, DurationMS: row.DurationMS, StateChanged: row.StateChanged, RecoveryStrategy: row.RecoveryStrategy}, + Result: ResultView{Code: row.Result, Name: constants.IntegrationResultName(row.Result), Category: constants.IntegrationResultCategory(row.Result), HTTPStatus: row.HTTPStatus, ProviderCode: row.ProviderCode, ProviderMessage: providerMessage, DurationMS: row.DurationMS, StateChanged: row.StateChanged, RecoveryStrategy: sanitizedTextPointer(row.RecoveryStrategy)}, Content: ContentView{RequestSummary: requestSummary, ResponseSummary: responseSummary, Metadata: metadata, ContentHash: row.ContentHash}, Linkage: LinkageView{RequestID: row.RequestID, CorrelationID: row.CorrelationID, AuditEventID: row.AuditEventID}, Timestamps: TimestampView{ScheduledAt: row.ScheduledAt, StartedAt: row.StartedAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}, Attempts: attempts, Fidelity: FidelityView{ TriggerSeriesAvailable: row.TriggerSeries != nil && *row.TriggerSeries != "", + AttemptSequenceReliable: attemptSequenceReliable, CorrelationAvailable: row.CorrelationID != nil && *row.CorrelationID != "", ResourceIDAvailable: row.ResourceID != nil && *row.ResourceID != "", ProviderMessageFidelity: providerMessageFidelity, @@ -275,25 +279,31 @@ func (q *Query) Get(ctx context.Context, integrationID string) (*DetailResponse, }, Retention: retention}, nil } -func (q *Query) loadAttempts(ctx context.Context, current model.IntegrationLog, onlineFrom time.Time) ([]AttemptView, error) { +func (q *Query) loadAttempts(ctx context.Context, current model.IntegrationLog, onlineFrom time.Time) ([]AttemptView, bool, error) { rows := []model.IntegrationLog{current} if current.TriggerSeries != nil && *current.TriggerSeries != "" { if err := q.db.WithContext(ctx).Where("trigger_series = ? AND created_at >= ?", *current.TriggerSeries, onlineFrom.UTC()). Order("attempt ASC, created_at ASC, id ASC").Find(&rows).Error; err != nil { - return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互尝试序列失败") + return nil, false, errors.Wrap(errors.CodeDatabaseError, err, "查询外部交互尝试序列失败") } } items := make([]AttemptView, len(rows)) + reliable := current.TriggerSeries != nil && *current.TriggerSeries != "" + expectedAttempt := 1 for index, row := range rows { category := constants.IntegrationResultCategory(row.Result) items[index] = AttemptView{ - IntegrationID: row.IntegrationID, Attempt: row.Attempt, + IntegrationID: row.IntegrationID, Attempt: row.Attempt, Operation: row.Operation, OperationName: constants.IntegrationOperationName(row.Operation), Sent: category != constants.IntegrationResultCategoryNotSent, Result: row.Result, ResultName: constants.IntegrationResultName(row.Result), ResultCategory: category, DurationMS: row.DurationMS, StateChanged: row.StateChanged, CreatedAt: row.CreatedAt, } + if row.Operation != current.Operation || row.Attempt != expectedAttempt { + reliable = false + } + expectedAttempt = row.Attempt + 1 } - return items, nil + return items, reliable, nil } func (q *Query) authorize(ctx context.Context) error { @@ -418,12 +428,23 @@ func safeProviderMessage(value *string) (*string, string) { } if len(*value) >= len(constants.IntegrationSafeMessagePrefix) && (*value)[:len(constants.IntegrationSafeMessagePrefix)] == constants.IntegrationSafeMessagePrefix { message := (*value)[len(constants.IntegrationSafeMessagePrefix):] + if sanitized := sanitizer.SanitizeText(message); sanitized != message { + return &sanitized, "redacted" + } return &message, "readable" } summary := sanitizer.TextSummary(*value) return &summary, "historical_redacted" } +func sanitizedTextPointer(value *string) *string { + if value == nil { + return nil + } + sanitized := sanitizer.SanitizeText(*value) + return &sanitized +} + func normalizePage(page, pageSize int) (int, int) { if page < 1 { page = constants.DefaultPage diff --git a/internal/query/integration/overview.go b/internal/query/integration/overview.go index 4e928be..a60d98a 100644 --- a/internal/query/integration/overview.go +++ b/internal/query/integration/overview.go @@ -20,44 +20,44 @@ type OverviewFilter struct { // Overview 是外部交互固定维度聚合结果。 type Overview struct { - Total int64 `json:"total"` - AnomalyCount int64 `json:"anomaly_count"` - UnknownCount int64 `json:"unknown_count"` - StalePendingCount int64 `json:"stale_pending_count"` - StateChangedCount int64 `json:"state_changed_count"` - AverageDurationMS float64 `json:"average_duration_ms"` - P95DurationMS float64 `json:"p95_duration_ms"` - Results []ResultCount `json:"results"` - Providers []NamedCount `json:"providers"` - Directions []NamedCount `json:"directions"` - Trend []TrendPoint `json:"trend"` - Retention retentionquery.Info `json:"retention"` + Total int64 `json:"total" description:"外部交互总数"` + AnomalyCount int64 `json:"anomaly_count" description:"failed或indeterminate类别的异常交互数"` + UnknownCount int64 `json:"unknown_count" description:"原始结果为unknown的交互数"` + StalePendingCount int64 `json:"stale_pending_count" description:"超过既定阈值仍为pending的交互数"` + StateChangedCount int64 `json:"state_changed_count" description:"改变本地业务状态的交互数"` + AverageDurationMS float64 `json:"average_duration_ms" description:"平均交互耗时,单位毫秒"` + P95DurationMS float64 `json:"p95_duration_ms" description:"P95交互耗时,单位毫秒"` + Results []ResultCount `json:"results" description:"原始结果分布,code/name/category分别为稳定编码、中文名和派生类别"` + Providers []NamedCount `json:"providers" description:"提供方分布,code为provider枚举,name为中文展示名"` + Directions []NamedCount `json:"directions" description:"方向分布,code为inbound或outbound,name为中文展示名"` + Trend []TrendPoint `json:"trend" description:"五类派生结果的时间趋势"` + Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // ResultCount 是原始结果及其派生类别计数。 type ResultCount struct { - Code string `json:"code"` - Name string `json:"name"` - Category string `json:"category"` - Count int64 `json:"count"` + Code string `json:"code" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"外部交互原始结果稳定编码"` + Name string `json:"name" description:"code对应的中文展示名称"` + Category string `json:"category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由code派生的固定结果类别"` + Count int64 `json:"count" description:"该原始结果的交互数量"` } // NamedCount 是稳定编码、中文名称和数量。 type NamedCount struct { - Code string `json:"code"` - Name string `json:"name"` - Count int64 `json:"count"` + Code string `json:"code" description:"当前聚合维度的稳定编码,枚举域由所属数组字段说明"` + Name string `json:"name" description:"code对应的中文展示名称"` + Count int64 `json:"count" description:"该编码的交互数量"` } // TrendPoint 是固定时间桶内的结果类别趋势。 type TrendPoint struct { - BucketAt time.Time `json:"bucket_at"` - Total int64 `json:"total"` - Succeeded int64 `json:"succeeded"` - Processing int64 `json:"processing"` - Indeterminate int64 `json:"indeterminate"` - Failed int64 `json:"failed"` - NotSent int64 `json:"not_sent"` + BucketAt time.Time `json:"bucket_at" description:"时间桶起点"` + Total int64 `json:"total" description:"桶内外部交互总数"` + Succeeded int64 `json:"succeeded" description:"桶内succeeded类别数量"` + Processing int64 `json:"processing" description:"桶内processing类别数量"` + Indeterminate int64 `json:"indeterminate" description:"桶内indeterminate类别数量"` + Failed int64 `json:"failed" description:"桶内failed类别数量"` + NotSent int64 `json:"not_sent" description:"桶内not_sent类别数量"` } // Overview 查询指定时间范围的固定维度外部交互总览。 diff --git a/internal/routes/audit.go b/internal/routes/audit.go index 25d53d5..56b2a2e 100644 --- a/internal/routes/audit.go +++ b/internal/routes/audit.go @@ -15,14 +15,14 @@ func registerAuditRoutes(router fiber.Router, handler *admin.AuditHandler, doc * agent := router.Group("/agent/resource-activities") Register(agent, doc, basePath+"/agent/resource-activities", "GET", "/:resource_type/:identifier", handler.AgentResourceActivities, RouteSpec{ Summary: "查询代理资源活动", - Description: "resource_type/identifier 来自代理当前业务页面稳定字段;店铺范围只读取认证上下文。仅查询 retention 标明的在线窗口,归档范围不从对象存储读取。", + Description: "代理业务页映射:卡详情使用 `resource_type=iot_card`、`identifier=response.data.iccid`;设备详情使用 `device`、`response.data.virtual_no`;分配详情使用 `asset_allocation_record` 和分配单号;换货详情使用 `exchange_order` 和换货单号;店铺详情使用 `shop` 和店铺编号;企业详情使用 `enterprise` 和企业编号。身份与店铺范围只读取认证上下文,前端不得传入或推断。缺少稳定 identifier 时隐藏入口。仅查询 `retention` 标明的在线窗口。", Tags: []string{"资源活动"}, Input: new(dto.SubjectResourceActivityRequest), Output: new(auditquery.SubjectActivityPage), Auth: true, }) enterprise := router.Group("/enterprise/resource-activities") Register(enterprise, doc, basePath+"/enterprise/resource-activities", "GET", "/:resource_type/:identifier", handler.EnterpriseResourceActivities, RouteSpec{ Summary: "查询企业资源活动", - Description: "仅支持企业当前有效授权的卡和设备;企业身份只读取认证上下文。仅查询 retention 标明的在线窗口,响应不包含平台内部调查字段。", - Tags: []string{"资源活动"}, Input: new(dto.SubjectResourceActivityRequest), Output: new(auditquery.SubjectActivityPage), Auth: true, + Description: "企业仅支持当前有效授权资产:卡列表/详情使用 `resource_type=iot_card`、`identifier=response.data.iccid`;设备列表/详情使用 `resource_type=device`、`identifier=response.data.virtual_no`。企业身份与授权范围只读取认证上下文,前端不得传入或推断。缺少稳定 identifier 时隐藏入口。响应只含主体安全投影,不含平台操作者、风险、内部原因或 before/after。", + Tags: []string{"资源活动"}, Input: new(dto.EnterpriseResourceActivityRequest), Output: new(auditquery.SubjectActivityPage), Auth: true, }) audit := router.Group("/audit") @@ -30,69 +30,69 @@ func registerAuditRoutes(router fiber.Router, handler *admin.AuditHandler, doc * Register(audit, doc, groupPath, "GET", "/events", handler.ListEvents, RouteSpec{ Summary: "查询全局审计事件", - Description: "筛选值来自调查人员输入或其他调查节点的稳定引用;缺省只查 retention 标明的在线窗口,归档范围返回稳定错误。固定倒序分页,不提供导出、修改或删除。", + Description: "平台业务页按内部 ID 进入:卡 `resource_type=iot_card&resource_id=response.data.id`,设备 `device/id`,账号 `account/id`,店铺 `shop/id`,企业 `enterprise/id`,订单 `order/id`,退款 `refund/id`,充值 `agent_recharge/id`。筛选 action 必须使用响应 `action_code`,不可用中文名称反推。缺少稳定 ID 时隐藏入口。固定倒序分页,只查 `retention` 在线窗口,不提供导出、修改或删除。", Tags: []string{"审计调查"}, Input: new(dto.AuditEventListRequest), Output: new(auditquery.EventPage), Auth: true, }) Register(audit, doc, groupPath, "GET", "/events/:event_id", handler.GetEvent, RouteSpec{ Summary: "查询审计事件详情", - Description: "event_id 来自 investigation_refs;只查询在线 PostgreSQL,未命中仍返回资源不存在,不扫描对象存储。返回全部资源快照和各资源 before/after。", + Description: "`event_id` 来自列表的 `event_id` 或 `investigation_refs.event_id`。响应 `investigation_refs` 映射:`actor_ref.kind/id` → 操作者时间线;`resource_refs[].resource_type/resource_id` → 资源时间线;`request_id` → 请求时间线;`correlation_id` → 关联时间线;`integration_refs[].integration_id` → 外部集成详情。引用字段为空时隐藏对应入口,不按名称、时间或摘要猜测。", Tags: []string{"审计调查"}, Input: new(dto.AuditEventIDParams), Output: new(auditquery.EventDetail), Auth: true, }) Register(audit, doc, groupPath, "GET", "/actors/:kind/:id/events", handler.ListActorEvents, RouteSpec{ Summary: "查询操作者行为时间线", - Description: "kind/id 来自事件 actor_ref 或平台账号选择器;历史名称直接使用事件快照,不查询当前账号名称覆盖历史。", + Description: "`kind/id` 来自事件 `investigation_refs.actor_ref`;人工账号页也可使用 `kind=account`、`id=response.data.id`。历史名称使用响应 `actor_name` 快照,不以当前账号名称覆盖。action 使用事件 `action_code`,resource_type/resource_id 使用事件资源引用。", Tags: []string{"审计调查"}, Input: new(dto.AuditActorEventsRequest), Output: new(auditquery.EventPage), Auth: true, }) // 资源搜索静态路径必须先于资源动态时间线路径,避免被动态参数吞掉。 Register(audit, doc, groupPath, "GET", "/resources/search", handler.SearchResources, RouteSpec{ Summary: "精确搜索注册资源", - Description: "卡支持 ICCID/VirtualNo,设备支持 VirtualNo/IMEI/SN,店铺、订单、退款使用各自稳定编号。当前资源不存在时仅按 Registry 白名单快照字段精确查找历史,不做任意 JSON 模糊搜索。", + Description: "用于平台调查选择器:卡 keyword 使用 ICCID/VirtualNo,设备使用 VirtualNo/IMEI/SN,店铺使用店铺编号,订单使用订单号,退款使用退款单号。选择结果后将 `items[].resource_type/resource_id` 原样传给资源时间线;`historical=true` 表示仅由历史快照命中。仅精确搜索,不做任意 JSON 模糊搜索。", Tags: []string{"审计调查"}, Input: new(dto.AuditResourceSearchRequest), Output: new(auditquery.ResourceSearchPage), Auth: true, }) Register(audit, doc, groupPath, "GET", "/resources/:resource_type/:resource_id/timeline", handler.ResourceTimeline, RouteSpec{ Summary: "查询通用资源时间线", - Description: "resource_type/resource_id 必须来自业务页面稳定字段、资源搜索结果或 investigation_refs。事件在资源作为 primary、affected 或 reference 时均会返回。", + Description: "`resource_type/resource_id` 必须来自平台业务页的内部 `response.data.id`、资源搜索 `items[]` 或 `investigation_refs.resource_refs[]`。设备卡槽可使用资源引用返回的卡槽类型和 ID,不自行拼接。事件在资源作为 `primary/affected/reference` 时均返回;缺少 resource_id 时隐藏入口。", Tags: []string{"审计调查"}, Input: new(dto.AuditResourceTimelineRequest), Output: new(auditquery.EventPage), Auth: true, }) Register(audit, doc, groupPath, "GET", "/requests/:request_id/timeline", handler.RequestTimeline, RouteSpec{ Summary: "查询请求关联时间线", - Description: "request_id 来自审计或外部集成节点,也可从 Access Log 粘贴。只组合 retention 在线窗口内的持久化事实,不扫描 Access Log 或对象存储。", + Description: "`request_id` 来自事件 `investigation_refs.request_id`、Integration `request_id/linkage.request_id`,也可从 Access Log 粘贴。响应节点的 `investigation_refs` 可继续跳转事件、资源、操作者或 Integration 详情。只组合在线持久化事实,不扫描 Access Log 或对象存储。", Tags: []string{"审计调查"}, Input: new(dto.AuditRequestTimelineParams), Output: new(auditquery.LinkTimeline), Auth: true, }) Register(audit, doc, groupPath, "GET", "/correlations/:correlation_id/timeline", handler.CorrelationTimeline, RouteSpec{ Summary: "查询业务关联时间线", - Description: "correlation_id 来自稳定调查引用。只组合 retention 在线窗口内的持久化事实;相同 correlation 不用于猜测技术重试。", + Description: "`correlation_id` 来自事件 `investigation_refs.correlation_id` 或 Integration `correlation_id/linkage.correlation_id`。响应节点的 `investigation_refs` 可继续跳转其他视角。只组合在线持久化事实;相同 correlation 不等于技术重试,重试序列只认 Integration 的 `trigger.series`。", Tags: []string{"审计调查"}, Input: new(dto.AuditCorrelationTimelineParams), Output: new(auditquery.LinkTimeline), Auth: true, }) Register(audit, doc, groupPath, "GET", "/finance/timeline", handler.FinanceTimeline, RouteSpec{ Summary: "查询资金调查时间线", - Description: "可使用任一稳定资金条件进入;缺省只查 retention 在线窗口,归档范围不返回部分结果。关联事实由服务端解析,金额以业务账本为权威。", + Description: "任一稳定条件即可进入,关联事实由服务端补全:订单页 `order_id=response.data.id`,退款页 `refund_id=response.data.id`,充值页 `recharge_id=response.data.id`,钱包页 `wallet_id=response.data.id`,店铺页 `shop_id=response.data.id`;也支持各业务编号、第三方交易号、actor 或 correlation。金额单位为分,以 `amount_authority.authoritative=true` 指向的业务表字段为权威。", Tags: []string{"审计调查"}, Input: new(dto.AuditFinanceTimelineRequest), Output: new(auditquery.FinanceTimelinePage), Auth: true, }) Register(audit, doc, groupPath, "GET", "/risks/overview", handler.RiskOverview, RouteSpec{ Summary: "查询风险调查总览", - Description: "时间范围最长31天,缺省时使用当前在线窗口;只聚合高风险、资金、安全、失败、拒绝、部分成功和结果未知事件。", + Description: "时间范围最长31天,缺省使用当前在线窗口。`signals[].code` 固定为 high_risk、finance、security、failed、denied、partial、unknown;`risks/results/sources[].code` 可原样回填同名筛选参数,`actions[].code` 回填 action。name 字段只用于中文展示。", Tags: []string{"审计调查"}, Input: new(dto.AuditRiskOverviewRequest), Output: new(auditquery.RiskOverview), Auth: true, }) Register(audit, doc, groupPath, "GET", "/risks/events", handler.RiskEvents, RouteSpec{ Summary: "查询风险事件明细", - Description: "筛选条件来自风险总览分桶或调查人员输入,缺省只查 retention 在线窗口;明细返回 investigation_refs,不提供处置或封禁能力。", + Description: "筛选值来自风险总览:`risks[].code→risk`、`results[].code→result`、`actions[].code→action`、`sources[].code→source`。明细 `investigation_refs` 按事件详情相同规则跳转。缺省只查在线窗口,不提供处置或封禁能力。", Tags: []string{"审计调查"}, Input: new(dto.AuditRiskEventsRequest), Output: new(auditquery.RiskEventPage), Auth: true, }) // Integration 总览静态路径必须先于动态详情路径,避免 overview 被当作 integration_id。 Register(audit, doc, groupPath, "GET", "/integrations/overview", handler.IntegrationOverview, RouteSpec{ Summary: "查询外部集成交互总览", - Description: "筛选和时间范围来自调查输入或关联视角跳转;缺省只查 retention 在线窗口,归档范围返回稳定错误。总览区分五类结果。", + Description: "筛选来自调查输入或其他视角稳定字段。`results[].code→result`,`results[].category→result_category`,`providers[].code→provider`,`directions[].code→direction`;所有 name 仅用于中文展示。趋势严格分为 processing、succeeded、indeterminate、failed、not_sent 五类,bucket 为 hour 或 day。", Tags: []string{"审计调查"}, Input: new(dto.IntegrationOverviewRequest), Output: new(integrationquery.Overview), Auth: true, }) Register(audit, doc, groupPath, "GET", "/integrations", handler.ListIntegrations, RouteSpec{ Summary: "查询外部集成交互列表", - Description: "组合筛选来自调查输入或稳定引用;缺省只查 retention 在线窗口,归档范围不返回空页或部分结果。固定倒序分页,不提供任意摘要搜索。", + Description: "组合筛选来自总览 code、事件调查引用或业务页稳定资源字段;operation 必须使用列表/详情返回的稳定编码。列表 `integration_id` 原样传给详情;`request_id/correlation_id` 可跳转链路时间线;`resource.type/resource.id` 均存在时可跳转资源时间线。固定倒序分页,不提供任意摘要搜索。", Tags: []string{"审计调查"}, Input: new(dto.IntegrationListRequest), Output: new(integrationquery.ListPage), Auth: true, }) Register(audit, doc, groupPath, "GET", "/integrations/:integration_id", handler.GetIntegration, RouteSpec{ Summary: "查询外部集成交互详情", - Description: "integration_id 来自稳定引用;只查询在线 PostgreSQL,未命中仍返回资源不存在。展示结构化详情和在线尝试序列,不提供归档读取、恢复、修改、删除或导出。", + Description: "`integration_id` 来自列表、事件 `investigation_refs.integration_refs[]`,或通知 `GET /notifications/{id}/target`:仅当 `available=true` 且 `target_type=integration_log` 时,将 `target_key` 原样作为 integration_id;否则隐藏入口。`linkage.request_id/correlation_id` 可跳转链路时间线;`fidelity` 为 false 时禁止按时间、资源或摘要猜测缺失关系。只读,不提供恢复、修改、删除或导出。", Tags: []string{"审计调查"}, Input: new(dto.IntegrationIDParams), Output: new(integrationquery.DetailResponse), Auth: true, }) } diff --git a/internal/service/account/service.go b/internal/service/account/service.go index f2400aa..89a1cdc 100644 --- a/internal/service/account/service.go +++ b/internal/service/account/service.go @@ -47,7 +47,6 @@ type Service struct { shopRoleStore *postgres.ShopRoleStore shopStore ShopStoreInterface enterpriseStore middleware.EnterpriseStoreInterface - auditService AuditServiceInterface wecomMembers WeComMemberFinder tokenManager *pkgAuth.TokenManager } @@ -70,10 +69,6 @@ func (s *Service) SetTokenManager(tokenManager *pkgAuth.TokenManager) { s.tokenManager = tokenManager } -type AuditServiceInterface interface { - LogOperation(ctx context.Context, log *model.AccountOperationLog) -} - // WeComMemberFinder 定义账号绑定时校验应用可见成员的边界。 type WeComMemberFinder interface { GetVisible(ctx context.Context, applicationID uint, userID string) (*model.WeComMember, error) @@ -87,7 +82,6 @@ func New( shopRoleStore *postgres.ShopRoleStore, shopStore ShopStoreInterface, enterpriseStore middleware.EnterpriseStoreInterface, - auditService AuditServiceInterface, ) *Service { return &Service{ accountStore: accountStore, @@ -96,7 +90,6 @@ func New( shopRoleStore: shopRoleStore, shopStore: shopStore, enterpriseStore: enterpriseStore, - auditService: auditService, } } diff --git a/internal/service/account_audit/service.go b/internal/service/account_audit/service.go deleted file mode 100644 index 5411bd1..0000000 --- a/internal/service/account_audit/service.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package account_audit 提供账号操作审计日志服务 -// 负责记录所有账号管理操作,用于审计追踪和合规要求 -package account_audit - -import ( - "context" - - "github.com/break/junhong_cmp_fiber/internal/model" - "github.com/break/junhong_cmp_fiber/pkg/logger" - "go.uber.org/zap" -) - -// AccountOperationLogStore 账号操作日志存储接口 -type AccountOperationLogStore interface { - Create(ctx context.Context, log *model.AccountOperationLog) error -} - -// Service 账号审计服务 -type Service struct { - store AccountOperationLogStore -} - -// NewService 创建账号审计服务实例 -func NewService(store AccountOperationLogStore) *Service { - return &Service{ - store: store, - } -} - -// LogOperation 记录账号操作日志(异步写入,不阻塞主流程) -func (s *Service) LogOperation(ctx context.Context, log *model.AccountOperationLog) { - // 异步写入审计日志,不阻塞业务操作 - go func() { - if err := s.store.Create(context.Background(), log); err != nil { - // 写入失败只记录错误日志,不影响业务 - logger.GetAppLogger().Error("写入账号操作日志失败", - zap.Uint("operator_id", log.OperatorID), - zap.String("operation_type", log.OperationType), - zap.Error(err)) - } - }() -} diff --git a/internal/service/agent_recharge/service.go b/internal/service/agent_recharge/service.go index f27927f..e4fbbb4 100644 --- a/internal/service/agent_recharge/service.go +++ b/internal/service/agent_recharge/service.go @@ -24,11 +24,6 @@ import ( "github.com/break/junhong_cmp_fiber/pkg/middleware" ) -// AuditServiceInterface 审计日志服务接口 -type AuditServiceInterface interface { - LogOperation(ctx context.Context, log *model.AccountOperationLog) -} - // OperationPasswordServiceInterface 全局操作密码服务接口 type OperationPasswordServiceInterface interface { Verify(ctx context.Context, inputPassword string) error @@ -49,7 +44,6 @@ type Service struct { offlineCreation *agentrechargeapp.OfflineCreationService shopStore *postgres.ShopStore wechatConfigService WechatConfigServiceInterface - auditService AuditServiceInterface operationPasswordService OperationPasswordServiceInterface redis *redis.Client logger *zap.Logger @@ -63,7 +57,6 @@ func New( agentWalletStore *postgres.AgentWalletStore, shopStore *postgres.ShopStore, wechatConfigService WechatConfigServiceInterface, - auditService AuditServiceInterface, operationPasswordService OperationPasswordServiceInterface, rdb *redis.Client, logger *zap.Logger, @@ -74,7 +67,6 @@ func New( agentWalletStore: agentWalletStore, shopStore: shopStore, wechatConfigService: wechatConfigService, - auditService: auditService, operationPasswordService: operationPasswordService, redis: rdb, logger: logger, diff --git a/internal/service/asset/lifecycle_service.go b/internal/service/asset/lifecycle_service.go index 9361abc..04778d5 100644 --- a/internal/service/asset/lifecycle_service.go +++ b/internal/service/asset/lifecycle_service.go @@ -3,10 +3,14 @@ package asset import ( "context" stderrors "errors" + "strconv" + infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "github.com/break/junhong_cmp_fiber/internal/model" assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" "github.com/break/junhong_cmp_fiber/internal/store/postgres" + "github.com/break/junhong_cmp_fiber/pkg/auditcontext" + "github.com/break/junhong_cmp_fiber/pkg/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "gorm.io/gorm" @@ -14,255 +18,154 @@ import ( var deactivatableAssetStatuses = []int{constants.AssetStatusInStock, constants.AssetStatusSold} -// LifecycleService 资产生命周期服务 +// LifecycleService 资产生命周期服务。 type LifecycleService struct { - db *gorm.DB - iotCardStore *postgres.IotCardStore - deviceStore *postgres.DeviceStore - assetAuditService assetAuditSvc.OperationLogger + db *gorm.DB + iotCardStore *postgres.IotCardStore + deviceStore *postgres.DeviceStore + auditWriter *infraAudit.Writer } -// NewLifecycleService 创建资产生命周期服务 -func NewLifecycleService( - db *gorm.DB, - iotCardStore *postgres.IotCardStore, - deviceStore *postgres.DeviceStore, - assetAuditService assetAuditSvc.OperationLogger, -) *LifecycleService { - return &LifecycleService{ - db: db, - iotCardStore: iotCardStore, - deviceStore: deviceStore, - assetAuditService: assetAuditService, - } +// NewLifecycleService 创建资产生命周期服务。 +func NewLifecycleService(db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, auditWriter *infraAudit.Writer) *LifecycleService { + return &LifecycleService{db: db, iotCardStore: iotCardStore, deviceStore: deviceStore, auditWriter: auditWriter} } -func (s *LifecycleService) logLifecycleAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) { - if s == nil || s.assetAuditService == nil { - return - } - if p.Operator.Type == "" { - p.Operator = assetAuditSvc.OperatorFromContext(ctx) - } - if p.OperationType == "" { - p.OperationType = constants.AssetAuditOpAssetDeactivate - } - s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p)) -} - -// DeactivateIotCard 手动停用 IoT 卡 +// DeactivateIotCard 手动停用 IoT 卡。 func (s *LifecycleService) DeactivateIotCard(ctx context.Context, id uint) error { card, err := s.iotCardStore.GetByID(ctx, id) if err != nil { - if stderrors.Is(err, gorm.ErrRecordNotFound) { - appErr := errors.New(errors.CodeIotCardNotFound) - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: id, - OperationDesc: "统一入口停用IoT卡失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - }) - return appErr - } appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: id, - OperationDesc: "统一入口停用IoT卡失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - }) + if stderrors.Is(err, gorm.ErrRecordNotFound) { + appErr = errors.New(errors.CodeIotCardNotFound) + } + result := constants.AuditResultFailed + if stderrors.Is(err, gorm.ErrRecordNotFound) { + result = constants.AuditResultDenied + } + s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡失败", result, cardResourceStub(id), nil, appErr) return appErr } - - beforeData := map[string]any{ - "asset_status": card.AssetStatus, - "iccid": card.ICCID, - "virtual_no": card.VirtualNo, - } - + beforeData := map[string]any{"asset_status": card.AssetStatus} if !canDeactivateAsset(card.AssetStatus) { appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - OperationDesc: "统一入口停用IoT卡被拒绝", - ResultStatus: constants.AssetAuditResultDenied, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - }) + s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡被拒绝", constants.AuditResultDenied, card, beforeData, appErr) return appErr } - - result := s.db.WithContext(ctx).Model(&model.IotCard{}). - Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses). - Update("asset_status", constants.AssetStatusDeactivated) - if result.Error != nil { - appErr := errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - OperationDesc: "统一入口停用IoT卡失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - AfterData: map[string]any{ - "asset_status": constants.AssetStatusDeactivated, - "iccid": card.ICCID, - "virtual_no": card.VirtualNo, - }, - }) - return appErr - } - if result.RowsAffected == 0 { - appErr := errors.New(errors.CodeConflict, "状态已变更,请刷新后重试") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - OperationDesc: "统一入口停用IoT卡失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - }) - return appErr - } - - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - OperationDesc: "统一入口停用IoT卡", - ResultStatus: constants.AssetAuditResultSuccess, - BeforeData: beforeData, - AfterData: map[string]any{ - "asset_status": constants.AssetStatusDeactivated, - "iccid": card.ICCID, - "virtual_no": card.VirtualNo, - }, + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + result := tx.Model(&model.IotCard{}).Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).Update("asset_status", constants.AssetStatusDeactivated) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败") + } + if result.RowsAffected == 0 { + return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试") + } + return s.appendCardDeactivationAudit(ctx, tx, card, beforeData, map[string]any{"asset_status": constants.AssetStatusDeactivated}, constants.AuditResultSuccess, nil) }) - - return nil + if err != nil { + s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡失败", constants.AuditResultFailed, card, beforeData, err) + } + return err } -// DeactivateDevice 手动停用设备 +// DeactivateDevice 手动停用设备。 func (s *LifecycleService) DeactivateDevice(ctx context.Context, id uint) error { device, err := s.deviceStore.GetByID(ctx, id) if err != nil { - if stderrors.Is(err, gorm.ErrRecordNotFound) { - appErr := errors.New(errors.CodeNotFound, "设备不存在") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: id, - OperationDesc: "统一入口停用设备失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - }) - return appErr - } appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: id, - OperationDesc: "统一入口停用设备失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - }) + if stderrors.Is(err, gorm.ErrRecordNotFound) { + appErr = errors.New(errors.CodeNotFound, "设备不存在") + } + result := constants.AuditResultFailed + if stderrors.Is(err, gorm.ErrRecordNotFound) { + result = constants.AuditResultDenied + } + s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备失败", result, deviceResourceStub(id), nil, appErr) return appErr } - - beforeData := map[string]any{ - "asset_status": device.AssetStatus, - "virtual_no": device.VirtualNo, - } - + beforeData := map[string]any{"asset_status": device.AssetStatus} if !canDeactivateAsset(device.AssetStatus) { appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: device.ID, - AssetIdentifier: device.VirtualNo, - OperationDesc: "统一入口停用设备被拒绝", - ResultStatus: constants.AssetAuditResultDenied, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - }) + s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备被拒绝", constants.AuditResultDenied, device, beforeData, appErr) return appErr } - - result := s.db.WithContext(ctx).Model(&model.Device{}). - Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses). - Update("asset_status", constants.AssetStatusDeactivated) - if result.Error != nil { - appErr := errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: device.ID, - AssetIdentifier: device.VirtualNo, - OperationDesc: "统一入口停用设备失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - AfterData: map[string]any{ - "asset_status": constants.AssetStatusDeactivated, - "virtual_no": device.VirtualNo, - }, - }) - return appErr - } - if result.RowsAffected == 0 { - appErr := errors.New(errors.CodeConflict, "状态已变更,请刷新后重试") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr) - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: device.ID, - AssetIdentifier: device.VirtualNo, - OperationDesc: "统一入口停用设备失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - }) - return appErr - } - - s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: device.ID, - AssetIdentifier: device.VirtualNo, - OperationDesc: "统一入口停用设备", - ResultStatus: constants.AssetAuditResultSuccess, - BeforeData: beforeData, - AfterData: map[string]any{ - "asset_status": constants.AssetStatusDeactivated, - "virtual_no": device.VirtualNo, - }, + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + result := tx.Model(&model.Device{}).Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).Update("asset_status", constants.AssetStatusDeactivated) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败") + } + if result.RowsAffected == 0 { + return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试") + } + return s.appendDeviceDeactivationAudit(ctx, tx, device, beforeData, map[string]any{"asset_status": constants.AssetStatusDeactivated}, constants.AuditResultSuccess, nil) }) - - return nil + if err != nil { + s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备失败", constants.AuditResultFailed, device, beforeData, err) + } + return err } +func (s *LifecycleService) appendCardDeactivationAudit(ctx context.Context, tx *gorm.DB, card *model.IotCard, beforeData, afterData map[string]any, result string, businessErr error) error { + if s.auditWriter == nil || card == nil || card.ID == 0 { + return errors.New(errors.CodeInvalidStatus, "IoT 卡资产统一审计接缝未配置或资源不完整") + } + id := strconv.FormatUint(uint64(card.ID), 10) + errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) + return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{ + ActionCode: constants.AuditActionIotCardDeactivated, Summary: "停用 IoT 卡资产", Result: result, + ErrorCode: errorCode, ErrorSummary: errorSummary, ScopeType: constants.AuditScopePlatform, + Resources: []infraAudit.ResourceInput{{ + Type: constants.AuditResourceIotCard, ID: &id, Key: infraAudit.IotCardResourceKey(card), DisplayName: card.ICCID, + Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget, + IdentitySnapshot: infraAudit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData, + SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡资产已停用", + }}, + }) +} + +func (s *LifecycleService) appendDeviceDeactivationAudit(ctx context.Context, tx *gorm.DB, device *model.Device, beforeData, afterData map[string]any, result string, businessErr error) error { + if s.auditWriter == nil || device == nil || device.ID == 0 { + return errors.New(errors.CodeInvalidStatus, "设备资产统一审计接缝未配置或资源不完整") + } + id := strconv.FormatUint(uint64(device.ID), 10) + errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) + return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{ + ActionCode: constants.AuditActionDeviceDeactivated, Summary: "停用设备资产", Result: result, + ErrorCode: errorCode, ErrorSummary: errorSummary, ScopeType: constants.AuditScopePlatform, + Resources: []infraAudit.ResourceInput{{ + Type: constants.AuditResourceDevice, ID: &id, Key: infraAudit.DeviceResourceKey(device), DisplayName: device.VirtualNo, + Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget, + IdentitySnapshot: infraAudit.DeviceIdentitySnapshot(device), BeforeData: beforeData, AfterData: afterData, + SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备资产已停用", + }}, + }) +} + +func (s *LifecycleService) recordLifecycleFailure(ctx context.Context, actionCode, summary, result string, resource any, beforeData map[string]any, businessErr error) { + if s == nil || s.db == nil || s.auditWriter == nil { + return + } + err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + switch value := resource.(type) { + case *model.IotCard: + return s.appendCardDeactivationAudit(ctx, tx, value, beforeData, nil, result, businessErr) + case *model.Device: + return s.appendDeviceDeactivationAudit(ctx, tx, value, beforeData, nil, result, businessErr) + default: + return errors.New(errors.CodeInvalidStatus, "资产审计资源类型无效") + } + }) + if err == nil { + return + } + linkage := auditcontext.From(ctx) + errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr) + auditfailure.RecordSecondaryWriteFailure(actionCode, summary, linkage.RequestID, linkage.CorrelationID, errorCode, err) +} + +func cardResourceStub(id uint) *model.IotCard { return &model.IotCard{Model: gorm.Model{ID: id}} } +func deviceResourceStub(id uint) *model.Device { return &model.Device{Model: gorm.Model{ID: id}} } + func canDeactivateAsset(assetStatus int) bool { return assetStatus == constants.AssetStatusInStock || assetStatus == constants.AssetStatusSold } diff --git a/internal/service/asset/manual_adjustment_audit.go b/internal/service/asset/manual_adjustment_audit.go new file mode 100644 index 0000000..9203c5c --- /dev/null +++ b/internal/service/asset/manual_adjustment_audit.go @@ -0,0 +1,94 @@ +package asset + +import ( + "context" + "strconv" + + infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" + "github.com/break/junhong_cmp_fiber/internal/model" + assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" + "github.com/break/junhong_cmp_fiber/pkg/auditcontext" + "github.com/break/junhong_cmp_fiber/pkg/auditfailure" + "github.com/break/junhong_cmp_fiber/pkg/constants" + "github.com/break/junhong_cmp_fiber/pkg/errors" + "gorm.io/gorm" +) + +func (s *Service) appendPackageAdjustmentAudit( + ctx context.Context, + tx *gorm.DB, + actionCode, summary, result string, + usage *model.PackageUsage, + assetType string, + assetID uint, + assetIdentifier string, + beforeData, afterData map[string]any, + businessErr error, +) error { + if s.auditWriter == nil || usage == nil || usage.ID == 0 { + return errors.New(errors.CodeInvalidStatus, "资产套餐统一审计接缝未配置或资源不完整") + } + usageID := strconv.FormatUint(uint64(usage.ID), 10) + assetResourceID := strconv.FormatUint(uint64(assetID), 10) + var resourceType string + switch assetType { + case constants.AssetTypeIotCard: + resourceType = constants.AuditResourceIotCard + case constants.AssetTypeDevice: + resourceType = constants.AuditResourceDevice + default: + return errors.New(errors.CodeInvalidParam, "资产类型无效") + } + errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) + return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{ + ActionCode: actionCode, + Summary: summary, + Result: result, + ErrorCode: errorCode, + ErrorSummary: errorSummary, + ScopeType: constants.AuditScopePlatform, + Resources: []infraAudit.ResourceInput{ + { + Type: resourceType, ID: &assetResourceID, Key: assetIdentifier, DisplayName: assetIdentifier, + Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePackageUsageAsset, + IdentitySnapshot: map[string]any{"id": assetID, "asset_type": assetType, "identifier": assetIdentifier}, + SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary, + }, + { + Type: constants.AuditResourcePackageUsage, ID: &usageID, + Key: "package_usage:" + usageID, DisplayName: "套餐权益#" + usageID, + Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRolePackageUsageTarget, + IdentitySnapshot: map[string]any{ + "id": usage.ID, "order_id": usage.OrderID, "package_id": usage.PackageID, + "iot_card_id": usage.IotCardID, "device_id": usage.DeviceID, "status": usage.Status, + }, + BeforeData: beforeData, AfterData: afterData, + SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary, + }, + }, + }) +} + +func (s *Service) recordPackageAdjustmentFailure( + ctx context.Context, + actionCode, summary string, + usage *model.PackageUsage, + assetType string, + assetID uint, + assetIdentifier string, + beforeData, afterData map[string]any, + businessErr error, +) { + if s == nil || s.db == nil || usage == nil { + return + } + err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + return s.appendPackageAdjustmentAudit(ctx, tx, actionCode, summary, constants.AuditResultFailed, usage, assetType, assetID, assetIdentifier, beforeData, afterData, businessErr) + }) + if err == nil { + return + } + linkage := auditcontext.From(ctx) + errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr) + auditfailure.RecordSecondaryWriteFailure(actionCode, strconv.FormatUint(uint64(usage.ID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err) +} diff --git a/internal/service/asset/service.go b/internal/service/asset/service.go index 59fdc89..1438762 100644 --- a/internal/service/asset/service.go +++ b/internal/service/asset/service.go @@ -11,10 +11,10 @@ import ( "time" "github.com/break/junhong_cmp_fiber/internal/gateway" + infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry" - assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" @@ -52,7 +52,7 @@ type Service struct { iotCardService IotCardRefresher gatewayClient *gateway.Client assetIdentifierStore *postgres.AssetIdentifierStore - assetAuditService assetAuditSvc.OperationLogger + auditWriter *infraAudit.Writer packageExpiryQuery PackageExpiryResolver } @@ -78,7 +78,6 @@ func New( orderStore *postgres.OrderStore, orderItemStore *postgres.OrderItemStore, exchangeOrderStore *postgres.ExchangeOrderStore, - assetAuditService assetAuditSvc.OperationLogger, ) *Service { return &Service{ db: db, @@ -96,11 +95,15 @@ func New( iotCardService: iotCardService, gatewayClient: gatewayClient, assetIdentifierStore: assetIdentifierStore, - assetAuditService: assetAuditService, packageExpiryQuery: packageexpiry.NewQuery(db), } } +// SetAccessAudit 注入资产人工调整的统一审计 Writer。 +func (s *Service) SetAccessAudit(writer *infraAudit.Writer) { + s.auditWriter = writer +} + // Resolve 通过任意标识符解析资产 // 主路径:查注册表(精确匹配 ICCID 或 VirtualNo) // Fallback:原有跨表 OR 查询(处理 IMEI/SN/MSISDN 等非注册标识符) @@ -912,35 +915,26 @@ func (s *Service) UpdatePackageExpiresAt(ctx context.Context, assetType string, } beforeData := packageUsageExpiresAtAuditData(before) - rows, err := s.packageUsageStore.UpdateExpiresAtForCarrier(ctx, packageUsageID, carrierType, assetID, expiresAt) + var updated *model.PackageUsage + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + store := postgres.NewPackageUsageStore(tx, nil) + rows, updateErr := store.UpdateExpiresAtForCarrier(ctx, packageUsageID, carrierType, assetID, expiresAt) + if updateErr != nil { + return errors.Wrap(errors.CodeDatabaseError, updateErr, "修改套餐过期时间失败") + } + if rows == 0 { + return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } + updated, updateErr = store.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID) + if updateErr != nil { + return errors.Wrap(errors.CodeDatabaseError, updateErr, "查询套餐使用记录失败") + } + return s.appendPackageAdjustmentAudit(ctx, tx, constants.AuditActionPackageUsageExpiresAtUpdated, "修改资产套餐过期时间", constants.AuditResultSuccess, updated, assetType, assetID, assetIdentifier, beforeData, packageUsageExpiresAtAuditData(updated), nil) + }) if err != nil { - appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐过期时间失败") - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{ - "package_usage_id": packageUsageID, - "expires_at": expiresAt, - }, appErr) - return nil, appErr + s.recordPackageAdjustmentFailure(ctx, constants.AuditActionPackageUsageExpiresAtUpdated, "修改资产套餐过期时间失败", before, assetType, assetID, assetIdentifier, beforeData, map[string]any{"package_usage_id": packageUsageID, "expires_at": expiresAt}, err) + return nil, err } - if rows == 0 { - appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{ - "package_usage_id": packageUsageID, - "expires_at": expiresAt, - }, appErr) - return nil, appErr - } - - updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID) - if err != nil { - appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败") - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{ - "package_usage_id": packageUsageID, - "expires_at": expiresAt, - }, appErr) - return nil, appErr - } - - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageExpiresAt, "修改资产套餐过期时间", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageExpiresAtAuditData(updated), nil) return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil } @@ -962,35 +956,26 @@ func (s *Service) UpdatePackageUsage(ctx context.Context, assetType string, asse beforeData := packageUsageTrafficAuditData(before) nextStatus := statusForManualDataUsage(before, dataUsageMB) - rows, err := s.packageUsageStore.UpdateDataUsageForCarrier(ctx, packageUsageID, carrierType, assetID, dataUsageMB, nextStatus) + var updated *model.PackageUsage + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + store := postgres.NewPackageUsageStore(tx, nil) + rows, updateErr := store.UpdateDataUsageForCarrier(ctx, packageUsageID, carrierType, assetID, dataUsageMB, nextStatus) + if updateErr != nil { + return errors.Wrap(errors.CodeDatabaseError, updateErr, "修改套餐已用量失败") + } + if rows == 0 { + return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } + updated, updateErr = store.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID) + if updateErr != nil { + return errors.Wrap(errors.CodeDatabaseError, updateErr, "查询套餐使用记录失败") + } + return s.appendPackageAdjustmentAudit(ctx, tx, constants.AuditActionPackageUsageTrafficAdjusted, "修改资产套餐已用量", constants.AuditResultSuccess, updated, assetType, assetID, assetIdentifier, beforeData, packageUsageTrafficAuditData(updated), nil) + }) if err != nil { - appErr := errors.Wrap(errors.CodeDatabaseError, err, "修改套餐已用量失败") - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{ - "package_usage_id": packageUsageID, - "data_usage_mb": dataUsageMB, - }, appErr) - return nil, appErr + s.recordPackageAdjustmentFailure(ctx, constants.AuditActionPackageUsageTrafficAdjusted, "修改资产套餐已用量失败", before, assetType, assetID, assetIdentifier, beforeData, map[string]any{"package_usage_id": packageUsageID, "data_usage_mb": dataUsageMB}, err) + return nil, err } - if rows == 0 { - appErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{ - "package_usage_id": packageUsageID, - "data_usage_mb": dataUsageMB, - }, appErr) - return nil, appErr - } - - updated, err := s.packageUsageStore.GetByIDForCarrier(ctx, packageUsageID, carrierType, assetID) - if err != nil { - appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询套餐使用记录失败") - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量失败", constants.AssetAuditResultFailed, assetType, assetID, assetIdentifier, beforeData, map[string]any{ - "package_usage_id": packageUsageID, - "data_usage_mb": dataUsageMB, - }, appErr) - return nil, appErr - } - - s.logPackageAdjustmentAudit(ctx, constants.AssetAuditOpAssetPackageUsage, "修改资产套餐已用量", constants.AssetAuditResultSuccess, assetType, assetID, assetIdentifier, beforeData, packageUsageTrafficAuditData(updated), nil) return s.buildAssetPackageResponse(ctx, updated, constants.OwnerTypePlatform), nil } @@ -1210,31 +1195,6 @@ func packageUsageTrafficAuditData(usage *model.PackageUsage) map[string]any { } } -func (s *Service) logPackageAdjustmentAudit(ctx context.Context, operationType, operationDesc, resultStatus, assetType string, assetID uint, assetIdentifier string, beforeData, afterData map[string]any, err error) { - if s == nil || s.assetAuditService == nil { - return - } - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - wrappedBefore, wrappedAfter := assetAuditSvc.WrapOperationContent(beforeData, afterData, map[string]any{ - "asset_type": assetAuditSvc.NormalizeAssetType(assetType), - "asset_id": assetID, - "asset_identifier": assetIdentifier, - }) - s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, assetAuditSvc.BuildLogParams{ - Operator: assetAuditSvc.OperatorFromContext(ctx), - AssetType: assetType, - AssetID: assetID, - AssetIdentifier: assetIdentifier, - OperationType: operationType, - OperationDesc: operationDesc, - BeforeData: wrappedBefore, - AfterData: wrappedAfter, - ResultStatus: resultStatus, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - })) -} - // tracePreviousGenerations 通过换货链逆向追溯前代资产的订单,最多追溯10代 func (s *Service) tracePreviousGenerations(ctx context.Context, assetType string, assetID uint) ([]*dto.PreviousGenerationOrders, bool) { const maxDepth = 10 diff --git a/internal/service/asset_audit/service.go b/internal/service/asset_audit/service.go index 2f81ac3..eb8fb8b 100644 --- a/internal/service/asset_audit/service.go +++ b/internal/service/asset_audit/service.go @@ -9,14 +9,11 @@ import ( "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" - "github.com/break/junhong_cmp_fiber/pkg/logger" - "go.uber.org/zap" "gorm.io/gorm" ) // AssetOperationLogStore 资产操作日志存储接口。 type AssetOperationLogStore interface { - Create(ctx context.Context, log *model.AssetOperationLog) error ListByAssetPaged( ctx context.Context, assetType string, @@ -28,11 +25,6 @@ type AssetOperationLogStore interface { ) ([]*model.AssetOperationLog, int64, error) } -// OperationLogger 资产审计记录接口。 -type OperationLogger interface { - LogOperation(ctx context.Context, log *model.AssetOperationLog) -} - // Service 资产审计服务。 type Service struct { store AssetOperationLogStore @@ -57,24 +49,6 @@ func NewService(store AssetOperationLogStore, db *gorm.DB) *Service { } } -// LogOperation 记录资产操作日志(异步写入,不阻塞主流程)。 -func (s *Service) LogOperation(ctx context.Context, log *model.AssetOperationLog) { - if s == nil || s.store == nil || log == nil { - return - } - - go func() { - if err := s.store.Create(context.Background(), log); err != nil { - logger.GetAppLogger().Error("写入资产操作日志失败", - zap.String("asset_type", log.AssetType), - zap.Uint("asset_id", log.AssetID), - zap.String("operation_type", log.OperationType), - zap.String("result_status", log.ResultStatus), - zap.Error(err)) - } - }() -} - // ListByAsset 按资产分页查询操作日志。 func (s *Service) ListByAsset(ctx context.Context, params ListByAssetParams) (*dto.AssetOperationLogListResponse, error) { if s == nil || s.store == nil { diff --git a/internal/service/device/audit.go b/internal/service/device/audit.go deleted file mode 100644 index 29d3dd1..0000000 --- a/internal/service/device/audit.go +++ /dev/null @@ -1,287 +0,0 @@ -package device - -import ( - "context" - "fmt" - "strings" - - "github.com/break/junhong_cmp_fiber/internal/model" - assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" - "github.com/break/junhong_cmp_fiber/pkg/constants" -) - -// AssetAuditService 资产审计服务接口。 -type AssetAuditService interface { - LogOperation(ctx context.Context, log *model.AssetOperationLog) -} - -func (s *Service) logDeviceAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) { - if s == nil || s.assetAuditService == nil { - return - } - if p.Operator.Type == "" { - p.Operator = assetAuditSvc.OperatorFromContext(ctx) - } - if p.AssetType == "" { - p.AssetType = constants.AssetTypeDevice - } - s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p)) -} - -func (s *Service) logDeviceOperation( - ctx context.Context, - operationType string, - operationDesc string, - resultStatus string, - device *model.Device, - beforeData map[string]any, - afterData map[string]any, - batchTotal int, - successCount int, - failCount int, - err error, -) { - if strings.TrimSpace(operationDesc) == "" { - operationDesc = operationType - } - if strings.TrimSpace(resultStatus) == "" { - if err != nil { - resultStatus = constants.AssetAuditResultFailed - } else { - resultStatus = constants.AssetAuditResultSuccess - } - } - - params := assetAuditSvc.BuildLogParams{ - OperationType: operationType, - OperationDesc: operationDesc, - ResultStatus: resultStatus, - BatchTotal: batchTotal, - SuccessCount: successCount, - FailCount: failCount, - } - snapshot := deviceSnapshot(device) - beforeContent := stripDeviceOperationMeta(beforeData) - afterContent := stripDeviceOperationMeta(afterData) - beforeContent = ensureDeviceOperationContentMap(beforeContent, beforeData) - afterContent = ensureDeviceOperationContentMap(afterContent, afterData) - enrichDeviceOperationContent(beforeContent, beforeData) - enrichDeviceOperationContent(afterContent, afterData) - params.BeforeData, params.AfterData = assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot) - - if device != nil { - params.AssetID = device.ID - params.AssetIdentifier = device.VirtualNo - } - if err != nil { - params.ErrorCode, params.ErrorMsg = assetAuditSvc.BuildErrorInfo(err) - } - - s.logDeviceAudit(ctx, params) -} - -func stripDeviceOperationMeta(data map[string]any) map[string]any { - if len(data) == 0 { - return nil - } - out := make(map[string]any, len(data)) - for k, v := range data { - if k == "device" || k == "devices" || k == "card" || k == "cards" || k == "asset_snapshot" || k == "operation_content" { - continue - } - out[k] = v - } - if len(out) == 0 { - return nil - } - return out -} - -func ensureDeviceOperationContentMap(content map[string]any, raw map[string]any) map[string]any { - if content != nil || len(raw) == 0 { - return content - } - if _, ok := raw["device"].(map[string]any); ok { - return make(map[string]any) - } - if _, ok := raw["card"].(map[string]any); ok { - return make(map[string]any) - } - switch raw["devices"].(type) { - case []map[string]any, []any: - return make(map[string]any) - } - switch raw["cards"].(type) { - case []map[string]any, []any: - return make(map[string]any) - } - return content -} - -func enrichDeviceOperationContent(content map[string]any, raw map[string]any) { - if len(raw) == 0 { - return - } - - if deviceRaw, ok := raw["device"].(map[string]any); ok { - mergeReadableDeviceFields(content, deviceRaw) - } - if cardRaw, ok := raw["card"].(map[string]any); ok { - mergeReadableCardFields(content, cardRaw) - } - - switch devicesRaw := raw["devices"].(type) { - case []map[string]any: - deviceIDs, virtualNos := collectDeviceReadableLists(devicesRaw) - if len(deviceIDs) > 0 && content["device_ids"] == nil { - content["device_ids"] = deviceIDs - } - if len(virtualNos) > 0 && content["device_virtual_nos"] == nil { - content["device_virtual_nos"] = virtualNos - } - case []any: - devices := make([]map[string]any, 0, len(devicesRaw)) - for _, item := range devicesRaw { - deviceMap, ok := item.(map[string]any) - if !ok { - continue - } - devices = append(devices, deviceMap) - } - deviceIDs, virtualNos := collectDeviceReadableLists(devices) - if len(deviceIDs) > 0 && content["device_ids"] == nil { - content["device_ids"] = deviceIDs - } - if len(virtualNos) > 0 && content["device_virtual_nos"] == nil { - content["device_virtual_nos"] = virtualNos - } - } - - switch cardsRaw := raw["cards"].(type) { - case []map[string]any: - cardIDs, iccids := collectDeviceAuditCardReadableLists(cardsRaw) - if len(cardIDs) > 0 && content["card_ids"] == nil { - content["card_ids"] = cardIDs - } - if len(iccids) > 0 && content["iccids"] == nil { - content["iccids"] = iccids - } - case []any: - cards := make([]map[string]any, 0, len(cardsRaw)) - for _, item := range cardsRaw { - cardMap, ok := item.(map[string]any) - if !ok { - continue - } - cards = append(cards, cardMap) - } - cardIDs, iccids := collectDeviceAuditCardReadableLists(cards) - if len(cardIDs) > 0 && content["card_ids"] == nil { - content["card_ids"] = cardIDs - } - if len(iccids) > 0 && content["iccids"] == nil { - content["iccids"] = iccids - } - } -} - -func mergeReadableDeviceFields(content map[string]any, device map[string]any) { - if len(device) == 0 { - return - } - if _, ok := content["device_id"]; !ok { - if v, exists := device["id"]; exists { - content["device_id"] = v - } - } - if _, ok := content["device_virtual_no"]; !ok { - if v := stringifyDeviceAuditValue(device["virtual_no"]); v != "" { - content["device_virtual_no"] = v - } - } - if _, ok := content["device_imei"]; !ok { - if v := stringifyDeviceAuditValue(device["imei"]); v != "" { - content["device_imei"] = v - } - } - if _, ok := content["device_sn"]; !ok { - if v := stringifyDeviceAuditValue(device["sn"]); v != "" { - content["device_sn"] = v - } - } -} - -func mergeReadableCardFields(content map[string]any, card map[string]any) { - if len(card) == 0 { - return - } - if _, ok := content["iot_card_id"]; !ok { - if v, exists := card["id"]; exists { - content["iot_card_id"] = v - } - } - if _, ok := content["iccid"]; !ok { - if v := stringifyDeviceAuditValue(card["iccid"]); v != "" { - content["iccid"] = v - } - } -} - -func collectDeviceReadableLists(devices []map[string]any) ([]any, []string) { - deviceIDs := make([]any, 0, len(devices)) - virtualNos := make([]string, 0, len(devices)) - for _, device := range devices { - if id, ok := device["id"]; ok { - deviceIDs = append(deviceIDs, id) - } - if virtualNo := stringifyDeviceAuditValue(device["virtual_no"]); virtualNo != "" { - virtualNos = append(virtualNos, virtualNo) - } - } - return deviceIDs, virtualNos -} - -func collectDeviceAuditCardReadableLists(cards []map[string]any) ([]any, []string) { - cardIDs := make([]any, 0, len(cards)) - iccids := make([]string, 0, len(cards)) - for _, card := range cards { - if id, ok := card["id"]; ok { - cardIDs = append(cardIDs, id) - } - if iccid := stringifyDeviceAuditValue(card["iccid"]); iccid != "" { - iccids = append(iccids, iccid) - } - } - return cardIDs, iccids -} - -func stringifyDeviceAuditValue(v any) string { - switch vv := v.(type) { - case string: - return vv - case fmt.Stringer: - return vv.String() - default: - if vv == nil { - return "" - } - return fmt.Sprint(vv) - } -} - -func deviceSnapshot(device *model.Device) map[string]any { - if device == nil { - return nil - } - return map[string]any{ - "id": device.ID, - "virtual_no": device.VirtualNo, - "imei": device.IMEI, - "sn": device.SN, - "shop_id": device.ShopID, - "status": device.Status, - "series_id": device.SeriesID, - "enable_polling": device.EnablePolling, - "realname_policy": device.RealnamePolicy, - } -} diff --git a/internal/service/device/service.go b/internal/service/device/service.go index 662de67..15d600b 100644 --- a/internal/service/device/service.go +++ b/internal/service/device/service.go @@ -39,7 +39,6 @@ type Service struct { packageSeriesStore *postgres.PackageSeriesStore gatewayClient *gateway.Client assetIdentifierStore *postgres.AssetIdentifierStore - assetAuditService AssetAuditService enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore enterpriseStore *postgres.EnterpriseStore packageExpiryQuery *packageexpiry.Query @@ -152,7 +151,6 @@ func New( packageSeriesStore *postgres.PackageSeriesStore, gatewayClient *gateway.Client, assetIdentifierStore *postgres.AssetIdentifierStore, - assetAuditService AssetAuditService, enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore, enterpriseStore *postgres.EnterpriseStore, ) *Service { @@ -169,7 +167,6 @@ func New( packageSeriesStore: packageSeriesStore, gatewayClient: gatewayClient, assetIdentifierStore: assetIdentifierStore, - assetAuditService: assetAuditService, enterpriseDeviceAuthStore: enterpriseDeviceAuthStore, enterpriseStore: enterpriseStore, packageExpiryQuery: packageexpiry.NewQuery(db), diff --git a/internal/service/device_import/audit.go b/internal/service/device_import/audit.go index b90808b..6c72077 100644 --- a/internal/service/device_import/audit.go +++ b/internal/service/device_import/audit.go @@ -15,11 +15,6 @@ import ( "github.com/break/junhong_cmp_fiber/pkg/middleware" ) -// AssetAuditService 资产审计服务接口。 -type AssetAuditService interface { - LogOperation(ctx context.Context, log *model.AssetOperationLog) -} - func (s *Service) writeDeviceImportTaskAudit(ctx context.Context, tx *gorm.DB, task *model.DeviceImportTask, before, after map[string]any, result, phase, errorCode, errorSummary string) error { scopeType, scopeID := constants.AuditScopePlatform, "" if task.OperatorShopID != nil { diff --git a/internal/service/device_import/service.go b/internal/service/device_import/service.go index 17cb693..a3c486a 100644 --- a/internal/service/device_import/service.go +++ b/internal/service/device_import/service.go @@ -23,7 +23,6 @@ type Service struct { db *gorm.DB importTaskStore *postgres.DeviceImportTaskStore queueClient *queue.Client - assetAudit AssetAuditService auditWriter *audit.Writer } @@ -35,14 +34,12 @@ func New( db *gorm.DB, importTaskStore *postgres.DeviceImportTaskStore, queueClient *queue.Client, - assetAudit AssetAuditService, auditWriters ...*audit.Writer, ) *Service { service := &Service{ db: db, importTaskStore: importTaskStore, queueClient: queueClient, - assetAudit: assetAudit, } if len(auditWriters) > 0 { service.auditWriter = auditWriters[0] diff --git a/internal/service/iot_card/audit.go b/internal/service/iot_card/audit.go deleted file mode 100644 index bf84d1e..0000000 --- a/internal/service/iot_card/audit.go +++ /dev/null @@ -1,235 +0,0 @@ -package iot_card - -import ( - "context" - "fmt" - - "github.com/break/junhong_cmp_fiber/internal/model" - assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" - "github.com/break/junhong_cmp_fiber/pkg/constants" -) - -// AssetAuditService 资产审计服务接口。 -type AssetAuditService interface { - LogOperation(ctx context.Context, log *model.AssetOperationLog) -} - -func (s *Service) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) { - if s == nil || s.assetAuditService == nil { - return - } - if p.Operator.Type == "" { - p.Operator = assetAuditSvc.OperatorFromContext(ctx) - } - if p.AssetType == "" { - p.AssetType = constants.AssetTypeIotCard - } - p.BeforeData, p.AfterData = normalizeCardAuditPayload(p.BeforeData, p.AfterData) - s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p)) -} - -func (s *StopResumeService) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) { - if s == nil || s.assetAuditService == nil { - return - } - if p.Operator.Type == "" { - p.Operator = assetAuditSvc.OperatorFromContext(ctx) - } - if p.AssetType == "" { - p.AssetType = constants.AssetTypeIotCard - } - p.BeforeData, p.AfterData = normalizeCardAuditPayload(p.BeforeData, p.AfterData) - s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p)) -} - -func normalizeCardAuditPayload(beforeData, afterData map[string]any) (map[string]any, map[string]any) { - snapshot := map[string]any(nil) - if raw, ok := beforeData["card"]; ok { - if m, ok := raw.(map[string]any); ok { - snapshot = m - } - } - if snapshot == nil { - if raw, ok := afterData["card"]; ok { - if m, ok := raw.(map[string]any); ok { - snapshot = m - } - } - } - beforeContent := stripCardOperationMeta(beforeData) - afterContent := stripCardOperationMeta(afterData) - beforeContent = ensureCardOperationContentMap(beforeContent, beforeData) - afterContent = ensureCardOperationContentMap(afterContent, afterData) - enrichCardOperationContent(beforeContent, beforeData) - enrichCardOperationContent(afterContent, afterData) - return assetAuditSvc.WrapOperationContent(beforeContent, afterContent, snapshot) -} - -func stripCardOperationMeta(data map[string]any) map[string]any { - if len(data) == 0 { - return nil - } - out := make(map[string]any, len(data)) - for k, v := range data { - if k == "device" || k == "card" || k == "cards" || k == "asset_snapshot" || k == "operation_content" { - continue - } - out[k] = v - } - if len(out) == 0 { - return nil - } - return out -} - -func ensureCardOperationContentMap(content map[string]any, raw map[string]any) map[string]any { - if content != nil || len(raw) == 0 { - return content - } - if _, ok := raw["card"].(map[string]any); ok { - return make(map[string]any) - } - if _, ok := raw["device"].(map[string]any); ok { - return make(map[string]any) - } - switch raw["cards"].(type) { - case []map[string]any, []any: - return make(map[string]any) - default: - return content - } -} - -func enrichCardOperationContent(content map[string]any, raw map[string]any) { - if len(raw) == 0 { - return - } - - if cardRaw, ok := raw["card"].(map[string]any); ok { - mergeReadableCardFields(content, cardRaw) - } - if deviceRaw, ok := raw["device"].(map[string]any); ok { - mergeReadableDeviceFields(content, deviceRaw) - } - - switch cardsRaw := raw["cards"].(type) { - case []map[string]any: - cardIDs, iccids := collectCardReadableLists(cardsRaw) - if len(cardIDs) > 0 && content["card_ids"] == nil { - content["card_ids"] = cardIDs - } - if len(iccids) > 0 && content["iccids"] == nil { - content["iccids"] = iccids - } - case []any: - cards := make([]map[string]any, 0, len(cardsRaw)) - for _, item := range cardsRaw { - cardMap, ok := item.(map[string]any) - if !ok { - continue - } - cards = append(cards, cardMap) - } - cardIDs, iccids := collectCardReadableLists(cards) - if len(cardIDs) > 0 && content["card_ids"] == nil { - content["card_ids"] = cardIDs - } - if len(iccids) > 0 && content["iccids"] == nil { - content["iccids"] = iccids - } - } -} - -func mergeReadableCardFields(content map[string]any, card map[string]any) { - if len(card) == 0 { - return - } - if _, ok := content["card_id"]; !ok { - if v, exists := card["id"]; exists { - content["card_id"] = v - } - } - if _, ok := content["iccid"]; !ok { - if v := stringifyCardAuditValue(card["iccid"]); v != "" { - content["iccid"] = v - } - } - if _, ok := content["device_virtual_no"]; !ok { - if v := stringifyCardAuditValue(card["device_virtual_no"]); v != "" { - content["device_virtual_no"] = v - } - } -} - -func mergeReadableDeviceFields(content map[string]any, device map[string]any) { - if len(device) == 0 { - return - } - if _, ok := content["device_id"]; !ok { - if v, exists := device["id"]; exists { - content["device_id"] = v - } - } - if _, ok := content["device_virtual_no"]; !ok { - if v := stringifyCardAuditValue(device["virtual_no"]); v != "" { - content["device_virtual_no"] = v - } - } - if _, ok := content["device_imei"]; !ok { - if v := stringifyCardAuditValue(device["imei"]); v != "" { - content["device_imei"] = v - } - } - if _, ok := content["device_sn"]; !ok { - if v := stringifyCardAuditValue(device["sn"]); v != "" { - content["device_sn"] = v - } - } -} - -func collectCardReadableLists(cards []map[string]any) ([]any, []string) { - cardIDs := make([]any, 0, len(cards)) - iccids := make([]string, 0, len(cards)) - for _, card := range cards { - if id, ok := card["id"]; ok { - cardIDs = append(cardIDs, id) - } - if iccid := stringifyCardAuditValue(card["iccid"]); iccid != "" { - iccids = append(iccids, iccid) - } - } - return cardIDs, iccids -} - -func stringifyCardAuditValue(v any) string { - switch vv := v.(type) { - case string: - return vv - case fmt.Stringer: - return vv.String() - default: - if vv == nil { - return "" - } - return fmt.Sprint(vv) - } -} - -func cardSnapshot(card *model.IotCard) map[string]any { - if card == nil { - return nil - } - return map[string]any{ - "id": card.ID, - "iccid": card.ICCID, - "device_virtual_no": card.DeviceVirtualNo, - "shop_id": card.ShopID, - "status": card.Status, - "series_id": card.SeriesID, - "enable_polling": card.EnablePolling, - "realname_policy": card.RealnamePolicy, - "real_name_status": card.RealNameStatus, - "network_status": card.NetworkStatus, - "stop_reason": card.StopReason, - } -} diff --git a/internal/service/iot_card/card_snapshot.go b/internal/service/iot_card/card_snapshot.go new file mode 100644 index 0000000..b5794d1 --- /dev/null +++ b/internal/service/iot_card/card_snapshot.go @@ -0,0 +1,16 @@ +package iot_card + +import "github.com/break/junhong_cmp_fiber/internal/model" + +func cardSnapshot(card *model.IotCard) map[string]any { + if card == nil { + return nil + } + return map[string]any{ + "id": card.ID, "iccid": card.ICCID, "device_virtual_no": card.DeviceVirtualNo, + "shop_id": card.ShopID, "status": card.Status, "series_id": card.SeriesID, + "enable_polling": card.EnablePolling, "realname_policy": card.RealnamePolicy, + "real_name_status": card.RealNameStatus, "network_status": card.NetworkStatus, + "stop_reason": card.StopReason, + } +} diff --git a/internal/service/iot_card/polling_audit.go b/internal/service/iot_card/polling_audit.go new file mode 100644 index 0000000..5f6ae94 --- /dev/null +++ b/internal/service/iot_card/polling_audit.go @@ -0,0 +1,85 @@ +package iot_card + +import ( + "context" + "strconv" + + "github.com/google/uuid" + "gorm.io/gorm" + + infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" + "github.com/break/junhong_cmp_fiber/internal/model" + assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" + "github.com/break/junhong_cmp_fiber/pkg/auditcontext" + "github.com/break/junhong_cmp_fiber/pkg/auditfailure" + "github.com/break/junhong_cmp_fiber/pkg/constants" + "github.com/break/junhong_cmp_fiber/pkg/errors" +) + +func (s *Service) appendPollingStatusAudit(ctx context.Context, tx *gorm.DB, card *model.IotCard, before, after bool) error { + return s.appendCardLifecycleAudit(ctx, tx, constants.AuditActionIotCardPollingStatusUpdated, "更新 IoT 卡轮询开关", constants.AuditResultSuccess, card, + map[string]any{"enable_polling": before}, map[string]any{"enable_polling": after}, nil) +} + +func (s *Service) appendBatchPollingStatusAudit(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, enablePolling bool) error { + if s.auditWriter == nil { + return errors.New(errors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置") + } + linkage := auditcontext.From(ctx) + batchKey := linkage.RequestID + if batchKey == "" { + batchKey = uuid.NewString() + } + rootEventID := "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("iot-card-polling-status:"+batchKey)).String() + children := make([]infraAudit.AppendInput, 0, len(cards)) + for _, card := range cards { + if card == nil || card.ID == 0 { + continue + } + id := strconv.FormatUint(uint64(card.ID), 10) + children = append(children, infraAudit.AppendInput{ + EventID: "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte(rootEventID+":"+id)).String(), + ActionCode: constants.AuditActionIotCardPollingStatusUpdated, + Summary: "批量更新 IoT 卡轮询开关", + Result: constants.AuditResultSuccess, + Resources: []infraAudit.ResourceInput{{ + Type: constants.AuditResourceIotCard, ID: &id, Key: infraAudit.IotCardResourceKey(card), DisplayName: card.ICCID, + Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget, + IdentitySnapshot: infraAudit.IotCardIdentitySnapshot(card), + BeforeData: map[string]any{"enable_polling": card.EnablePolling}, AfterData: map[string]any{"enable_polling": enablePolling}, + SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡轮询开关已更新", + }}, + }) + } + return s.auditWriter.AppendBatch(ctx, tx, infraAudit.BatchInput{ + Root: infraAudit.AppendInput{ + EventID: rootEventID, + ActionCode: constants.AuditActionIotCardPollingStatusBatchUpdated, + Summary: "批量更新 IoT 卡轮询开关", Result: constants.AuditResultSuccess, + BatchTotal: len(children), SuccessCount: len(children), + Resources: []infraAudit.ResourceInput{{ + Type: constants.AuditResourceIotCardBatch, Key: batchKey, DisplayName: batchKey, + Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch, + IdentitySnapshot: map[string]any{"card_count": len(children), "enable_polling": enablePolling}, + SubjectVisibility: constants.AuditSubjectInternalOnly, + }}, + }, + Children: children, + }) +} + +func (s *Service) recordPollingStatusFailure(ctx context.Context, actionCode, result string, card *model.IotCard, enablePolling bool, businessErr error) { + if card == nil || card.ID == 0 || s.db == nil || s.auditWriter == nil { + return + } + err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + return s.appendCardLifecycleAudit(ctx, tx, actionCode, "更新 IoT 卡轮询开关失败", result, card, nil, + map[string]any{"enable_polling": enablePolling}, businessErr) + }) + if err == nil { + return + } + linkage := auditcontext.From(ctx) + errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr) + auditfailure.RecordSecondaryWriteFailure(actionCode, strconv.FormatUint(uint64(card.ID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err) +} diff --git a/internal/service/iot_card/service.go b/internal/service/iot_card/service.go index 1605e43..e3c1880 100644 --- a/internal/service/iot_card/service.go +++ b/internal/service/iot_card/service.go @@ -14,7 +14,6 @@ import ( "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" packageexpiry "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry" - assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" "github.com/break/junhong_cmp_fiber/internal/store" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" @@ -68,7 +67,6 @@ type Service struct { deviceSimBindingStore *postgres.DeviceSimBindingStore redis *redis.Client assetIdentifierStore *postgres.AssetIdentifierStore - assetAuditService AssetAuditService enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore enterpriseStore *postgres.EnterpriseStore packageExpiryQuery *packageexpiry.Query @@ -109,7 +107,6 @@ func New( packageSeriesStore *postgres.PackageSeriesStore, gatewayClient *gateway.Client, logger *zap.Logger, - assetAuditService AssetAuditService, ) *Service { return &Service{ db: db, @@ -121,7 +118,6 @@ func New( packageSeriesStore: packageSeriesStore, gatewayClient: gatewayClient, logger: logger, - assetAuditService: assetAuditService, packageExpiryQuery: packageexpiry.NewQuery(db), } } @@ -1696,77 +1692,33 @@ func parseGatewayRealnameStatus(realStatus bool) int { func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePolling bool) error { card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { + appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询 IoT 卡失败") + result := constants.AuditResultFailed if err == gorm.ErrRecordNotFound { - denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr) - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "更新卡轮询状态被拒绝", - ResultStatus: constants.AssetAuditResultDenied, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - AssetID: cardID, - AssetIdentifier: "", - AfterData: map[string]any{ - "enable_polling": enablePolling, - }, - }) - return denyErr + appErr = errors.New(errors.CodeNotFound, "IoT卡不存在") + result = constants.AuditResultDenied } - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "更新卡轮询状态执行失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - AssetID: cardID, - AfterData: map[string]any{ - "enable_polling": enablePolling, - }, - }) + s.recordPollingStatusFailure(ctx, constants.AuditActionIotCardPollingStatusUpdated, result, &model.IotCard{Model: gorm.Model{ID: cardID}}, enablePolling, appErr) + return appErr + } + beforePolling := card.EnablePolling + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if beforePolling != enablePolling { + result := tx.Model(&model.IotCard{}).Where("id = ? AND enable_polling = ?", card.ID, beforePolling).Update("enable_polling", enablePolling) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新 IoT 卡轮询状态失败") + } + if result.RowsAffected == 0 { + return errors.New(errors.CodeConflict, "轮询状态已变更,请刷新后重试") + } + } + return s.appendPollingStatusAudit(ctx, tx, card, beforePolling, enablePolling) + }) + if err != nil { + s.recordPollingStatusFailure(ctx, constants.AuditActionIotCardPollingStatusUpdated, constants.AuditResultFailed, card, enablePolling, err) return err } - - // 检查是否需要更新 - if card.EnablePolling == enablePolling { - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "更新卡轮询状态", - ResultStatus: constants.AssetAuditResultSuccess, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - BeforeData: map[string]any{ - "enable_polling": card.EnablePolling, - }, - AfterData: map[string]any{ - "enable_polling": enablePolling, - }, - }) - return nil // 状态未变化 - } - - // 更新数据库 card.EnablePolling = enablePolling - if err := s.iotCardStore.Update(ctx, card); err != nil { - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "更新卡轮询状态执行失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - BeforeData: map[string]any{ - "enable_polling": !enablePolling, - }, - AfterData: map[string]any{ - "enable_polling": enablePolling, - }, - }) - return err - } s.logger.Info("更新卡轮询状态", zap.Uint("card_id", cardID), @@ -1782,20 +1734,6 @@ func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePo } } - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "更新卡轮询状态", - ResultStatus: constants.AssetAuditResultSuccess, - AssetID: card.ID, - AssetIdentifier: card.ICCID, - BeforeData: map[string]any{ - "enable_polling": !enablePolling, - }, - AfterData: map[string]any{ - "enable_polling": enablePolling, - }, - }) - return nil } @@ -1805,23 +1743,17 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint, return nil } - // 批量更新数据库 - if err := s.iotCardStore.BatchUpdatePollingStatus(ctx, cardIDs, enablePolling); err != nil { - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "批量更新卡轮询状态执行失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BatchTotal: len(cardIDs), - FailCount: len(cardIDs), - AfterData: map[string]any{ - "card_ids": cardIDs, - "enable_polling": enablePolling, - "trigger_source": "batch", - }, - }) + cards, err := s.iotCardStore.GetByIDs(ctx, cardIDs) + if err != nil { + return err + } + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if updateErr := tx.Model(&model.IotCard{}).Where("id IN ?", cardIDs).Update("enable_polling", enablePolling).Error; updateErr != nil { + return errors.Wrap(errors.CodeDatabaseError, updateErr, "批量更新 IoT 卡轮询状态失败") + } + return s.appendBatchPollingStatusAudit(ctx, tx, cards, enablePolling) + }) + if err != nil { return err } @@ -1841,19 +1773,6 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint, } } - s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{ - OperationType: constants.AssetAuditOpCardPollingStatus, - OperationDesc: "批量更新卡轮询状态", - ResultStatus: constants.AssetAuditResultSuccess, - BatchTotal: len(cardIDs), - SuccessCount: len(cardIDs), - AfterData: map[string]any{ - "card_ids": cardIDs, - "enable_polling": enablePolling, - "trigger_source": "batch", - }, - }) - return nil } diff --git a/internal/service/iot_card/stop_resume_service.go b/internal/service/iot_card/stop_resume_service.go index e4378aa..38ce086 100644 --- a/internal/service/iot_card/stop_resume_service.go +++ b/internal/service/iot_card/stop_resume_service.go @@ -45,7 +45,6 @@ type StopResumeService struct { deviceSimBindingStore *postgres.DeviceSimBindingStore gatewayClient *gateway.Client logger *zap.Logger - assetAuditService AssetAuditService pollingCallback PollingCallback observationSeriesEvents cardObservationApp.SeriesEventWriter auditWriter *audit.Writer @@ -75,7 +74,6 @@ func NewStopResumeService( deviceSimBindingStore *postgres.DeviceSimBindingStore, gatewayClient *gateway.Client, logger *zap.Logger, - assetAuditService AssetAuditService, ) *StopResumeService { return &StopResumeService{ redis: redis, @@ -84,7 +82,6 @@ func NewStopResumeService( deviceSimBindingStore: deviceSimBindingStore, gatewayClient: gatewayClient, logger: logger, - assetAuditService: assetAuditService, maxRetries: 3, retryInterval: 2 * time.Second, } diff --git a/internal/service/iot_card_import/audit.go b/internal/service/iot_card_import/audit.go index 5406355..43a6a5e 100644 --- a/internal/service/iot_card_import/audit.go +++ b/internal/service/iot_card_import/audit.go @@ -13,11 +13,6 @@ import ( "github.com/break/junhong_cmp_fiber/pkg/middleware" ) -// AssetAuditService 资产审计服务接口。 -type AssetAuditService interface { - LogOperation(ctx context.Context, log *model.AssetOperationLog) -} - func (s *Service) writeImportTaskAudit(ctx context.Context, tx *gorm.DB, task *model.IotCardImportTask, before, after map[string]any, result, phase, errorCode, errorSummary string) error { return s.auditWriter.WriteTask(ctx, tx, infraAudit.TaskInput{ EventID: infraAudit.TaskEventID(constants.AuditResourceIotCardImportTask, task.ID, phase), diff --git a/internal/service/iot_card_import/service.go b/internal/service/iot_card_import/service.go index e8f9768..58c390d 100644 --- a/internal/service/iot_card_import/service.go +++ b/internal/service/iot_card_import/service.go @@ -24,7 +24,6 @@ type Service struct { importTaskStore *postgres.IotCardImportTaskStore carrierStore carrierGetter queueClient *queue.Client - assetAudit AssetAuditService auditWriter *audit.Writer } @@ -52,7 +51,6 @@ func New( db *gorm.DB, importTaskStore *postgres.IotCardImportTaskStore, queueClient *queue.Client, - assetAudit AssetAuditService, auditWriters ...*audit.Writer, ) *Service { service := &Service{ @@ -60,7 +58,6 @@ func New( importTaskStore: importTaskStore, carrierStore: NewCarrierStore(db), queueClient: queueClient, - assetAudit: assetAudit, } if len(auditWriters) > 0 { service.auditWriter = auditWriters[0] diff --git a/internal/service/polling/asset_polling_service.go b/internal/service/polling/asset_polling_service.go index 31a9b39..e553cd7 100644 --- a/internal/service/polling/asset_polling_service.go +++ b/internal/service/polling/asset_polling_service.go @@ -2,13 +2,19 @@ package polling import ( "context" + stderrors "errors" + "strconv" "go.uber.org/zap" + "gorm.io/gorm" + auditinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" + "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/polling" - assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card" "github.com/break/junhong_cmp_fiber/internal/store/postgres" + "github.com/break/junhong_cmp_fiber/pkg/auditcontext" + "github.com/break/junhong_cmp_fiber/pkg/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) @@ -17,46 +23,36 @@ import ( // 管理 IoT 卡和设备的轮询启用状态 // S2 修复:card 类型委托给 IotCardService(含 DB 写入 + callback 通知),避免绕过生命周期 type AssetPollingService struct { + db *gorm.DB deviceStore *postgres.DeviceStore deviceBindingStore *postgres.DeviceSimBindingStore iotCardService *iotCardSvc.Service queueMgr *polling.PollingQueueManager logger *zap.Logger - assetAuditService assetAuditSvc.OperationLogger + auditWriter *auditinfra.Writer } // NewAssetPollingService 创建资产轮询管控服务 func NewAssetPollingService( + db *gorm.DB, deviceStore *postgres.DeviceStore, deviceBindingStore *postgres.DeviceSimBindingStore, iotCardService *iotCardSvc.Service, queueMgr *polling.PollingQueueManager, logger *zap.Logger, - assetAuditService assetAuditSvc.OperationLogger, + auditWriter *auditinfra.Writer, ) *AssetPollingService { return &AssetPollingService{ + db: db, deviceStore: deviceStore, deviceBindingStore: deviceBindingStore, iotCardService: iotCardService, queueMgr: queueMgr, logger: logger, - assetAuditService: assetAuditService, + auditWriter: auditWriter, } } -func (s *AssetPollingService) logAssetPollingAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) { - if s == nil || s.assetAuditService == nil { - return - } - if p.OperationType == "" { - p.OperationType = constants.AssetAuditOpAssetPollingStatus - } - if p.Operator.Type == "" { - p.Operator = assetAuditSvc.OperatorFromContext(ctx) - } - s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p)) -} - // UpdatePollingStatus 更新资产轮询状态 // assetType: "card" 或 "device" // assetID: 资产ID @@ -64,87 +60,23 @@ func (s *AssetPollingService) logAssetPollingAudit(ctx context.Context, p assetA func (s *AssetPollingService) UpdatePollingStatus(ctx context.Context, assetType string, assetID uint, enablePolling bool) error { switch assetType { case constants.AssetTypeIotCard: - beforeData := map[string]any{ - "asset_type": constants.AssetTypeIotCard, - "asset_id": assetID, - "enable_polling": "unknown", - "source_service": "asset_polling", - } - afterData := map[string]any{ - "asset_type": constants.AssetTypeIotCard, - "asset_id": assetID, - "enable_polling": enablePolling, - } // S2 修复:委托给 IotCardService,确保 DB 写入 + PollingCallback 回调一并触发 - if err := s.iotCardService.UpdatePollingStatus(ctx, assetID, enablePolling); err != nil { - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: assetID, - OperationDesc: "统一入口更新轮询状态失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - AfterData: afterData, - }) - return err - } - s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeIotCard, - AssetID: assetID, - OperationDesc: "统一入口更新轮询状态", - ResultStatus: constants.AssetAuditResultSuccess, - BeforeData: beforeData, - AfterData: afterData, - }) - return nil + return s.iotCardService.UpdatePollingStatus(ctx, assetID, enablePolling) case constants.AssetTypeDevice: device, getErr := s.deviceStore.GetByID(ctx, assetID) if getErr != nil { - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(getErr) - s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: assetID, - OperationDesc: "统一入口更新轮询状态失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - AfterData: map[string]any{ - "asset_type": constants.AssetTypeDevice, - "asset_id": assetID, - "enable_polling": enablePolling, - }, - }) - return getErr + appErr := errors.Wrap(errors.CodeDatabaseError, getErr, "查询设备失败") + result := constants.AuditResultFailed + if stderrors.Is(getErr, gorm.ErrRecordNotFound) { + appErr = errors.New(errors.CodeNotFound, "设备不存在") + result = constants.AuditResultDenied + } + s.recordDevicePollingFailure(ctx, &model.Device{Model: gorm.Model{ID: assetID}}, enablePolling, result, appErr) + return appErr } - beforeData := map[string]any{ - "asset_type": constants.AssetTypeDevice, - "asset_id": device.ID, - "asset_identifier": device.VirtualNo, - "enable_polling": device.EnablePolling, - } - afterData := map[string]any{ - "asset_type": constants.AssetTypeDevice, - "asset_id": device.ID, - "asset_identifier": device.VirtualNo, - "enable_polling": enablePolling, - } - // 1. 更新设备的 enable_polling 字段 - if err := s.deviceStore.UpdatePollingStatus(ctx, assetID, enablePolling); err != nil { - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: device.ID, - AssetIdentifier: device.VirtualNo, - OperationDesc: "统一入口更新轮询状态失败", - ResultStatus: constants.AssetAuditResultFailed, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - BeforeData: beforeData, - AfterData: afterData, - }) + if err := s.updateDevicePollingStatus(ctx, device, enablePolling); err != nil { + s.recordDevicePollingFailure(ctx, device, enablePolling, constants.AuditResultFailed, err) return err } bindings, err := s.deviceBindingStore.ListByDeviceID(ctx, assetID) @@ -173,33 +105,76 @@ func (s *AssetPollingService) UpdatePollingStatus(ctx context.Context, assetType } } } - s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: constants.AssetTypeDevice, - AssetID: device.ID, - AssetIdentifier: device.VirtualNo, - OperationDesc: "统一入口更新轮询状态", - ResultStatus: constants.AssetAuditResultSuccess, - BeforeData: beforeData, - AfterData: afterData, - }) return nil default: - err := errors.New(errors.CodeInvalidParam, "资产类型无效,支持 card 或 device") - errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err) - s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{ - AssetType: assetType, - AssetID: assetID, - OperationDesc: "统一入口更新轮询状态被拒绝", - ResultStatus: constants.AssetAuditResultDenied, - ErrorCode: errorCode, - ErrorMsg: errorMsg, - AfterData: map[string]any{ - "asset_type": assetType, - "asset_id": assetID, - "enable_polling": enablePolling, - }, - }) - return err + return errors.New(errors.CodeInvalidParam, "资产类型无效,支持 card 或 device") } } + +func (s *AssetPollingService) updateDevicePollingStatus(ctx context.Context, device *model.Device, enablePolling bool) error { + if s.db == nil || s.auditWriter == nil { + return errors.New(errors.CodeInvalidStatus, "设备统一审计接缝未配置") + } + return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if device.EnablePolling != enablePolling { + result := tx.Model(&model.Device{}).Where("id = ? AND enable_polling = ?", device.ID, device.EnablePolling).Update("enable_polling", enablePolling) + if result.Error != nil { + return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备轮询状态失败") + } + if result.RowsAffected == 0 { + return errors.New(errors.CodeConflict, "轮询状态已变更,请刷新后重试") + } + } + return s.appendDevicePollingAudit(ctx, tx, device, enablePolling, constants.AuditResultSuccess, nil) + }) +} + +func (s *AssetPollingService) appendDevicePollingAudit(ctx context.Context, tx *gorm.DB, device *model.Device, enablePolling bool, result string, businessErr error) error { + if device == nil || device.ID == 0 { + return errors.New(errors.CodeInvalidStatus, "设备审计资源不完整") + } + id := strconv.FormatUint(uint64(device.ID), 10) + errorCode, errorSummary := auditErrorInfo(businessErr) + return s.auditWriter.Append(ctx, tx, auditinfra.AppendInput{ + ActionCode: constants.AuditActionDevicePollingStatusUpdated, + Summary: "更新设备轮询开关", + Result: result, + ErrorCode: errorCode, + ErrorSummary: errorSummary, + ScopeType: constants.AuditScopePlatform, + Resources: []auditinfra.ResourceInput{{ + Type: constants.AuditResourceDevice, ID: &id, Key: auditinfra.DeviceResourceKey(device), DisplayName: device.VirtualNo, + Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget, + IdentitySnapshot: auditinfra.DeviceIdentitySnapshot(device), + BeforeData: map[string]any{"enable_polling": device.EnablePolling}, AfterData: map[string]any{"enable_polling": enablePolling}, + SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备轮询开关已更新", + }}, + }) +} + +func (s *AssetPollingService) recordDevicePollingFailure(ctx context.Context, device *model.Device, enablePolling bool, result string, businessErr error) { + if s == nil || s.db == nil || s.auditWriter == nil || device == nil || device.ID == 0 { + return + } + err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + return s.appendDevicePollingAudit(ctx, tx, device, enablePolling, result, businessErr) + }) + if err == nil { + return + } + linkage := auditcontext.From(ctx) + errorCode, _ := auditErrorInfo(businessErr) + auditfailure.RecordSecondaryWriteFailure(constants.AuditActionDevicePollingStatusUpdated, strconv.FormatUint(uint64(device.ID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, err) +} + +func auditErrorInfo(err error) (string, string) { + if err == nil { + return "", "" + } + var appErr *errors.AppError + if stderrors.As(err, &appErr) { + return strconv.Itoa(appErr.Code), appErr.Error() + } + return "", err.Error() +} diff --git a/internal/store/postgres/account_operation_log_store.go b/internal/store/postgres/account_operation_log_store.go deleted file mode 100644 index 3825c6c..0000000 --- a/internal/store/postgres/account_operation_log_store.go +++ /dev/null @@ -1,25 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/break/junhong_cmp_fiber/internal/model" - "gorm.io/gorm" -) - -// AccountOperationLogStore 账号操作日志存储层 -type AccountOperationLogStore struct { - db *gorm.DB -} - -// NewAccountOperationLogStore 创建账号操作日志存储实例 -func NewAccountOperationLogStore(db *gorm.DB) *AccountOperationLogStore { - return &AccountOperationLogStore{ - db: db, - } -} - -// Create 创建账号操作日志记录 -func (s *AccountOperationLogStore) Create(ctx context.Context, log *model.AccountOperationLog) error { - return s.db.WithContext(ctx).Create(log).Error -} diff --git a/internal/store/postgres/asset_operation_log_store.go b/internal/store/postgres/asset_operation_log_store.go index 9b3cd73..90eefec 100644 --- a/internal/store/postgres/asset_operation_log_store.go +++ b/internal/store/postgres/asset_operation_log_store.go @@ -17,11 +17,6 @@ func NewAssetOperationLogStore(db *gorm.DB) *AssetOperationLogStore { return &AssetOperationLogStore{db: db} } -// Create 创建资产操作日志记录。 -func (s *AssetOperationLogStore) Create(ctx context.Context, log *model.AssetOperationLog) error { - return s.db.WithContext(ctx).Create(log).Error -} - // ListByAssetPaged 按资产分页查询日志。 func (s *AssetOperationLogStore) ListByAssetPaged( ctx context.Context, diff --git a/internal/task/audit_monthly_retention.go b/internal/task/audit_monthly_retention.go index 3aaf1e9..6d67a13 100644 --- a/internal/task/audit_monthly_retention.go +++ b/internal/task/audit_monthly_retention.go @@ -19,17 +19,21 @@ type AuditMonthlyRetentionPayload struct { // AuditMonthlyRetentionHandler 处理归档完整性门禁与上月在线日志物理清理。 type AuditMonthlyRetentionHandler struct { - service *auditarchive.Service - logger *zap.Logger + service *auditarchive.Service + logger *zap.Logger + cleanupEnabled bool } // NewAuditMonthlyRetentionHandler 创建月度日志留存清理处理器。 -func NewAuditMonthlyRetentionHandler(service *auditarchive.Service, logger *zap.Logger) *AuditMonthlyRetentionHandler { - return &AuditMonthlyRetentionHandler{service: service, logger: logger} +func NewAuditMonthlyRetentionHandler(service *auditarchive.Service, logger *zap.Logger, cleanupEnabled bool) *AuditMonthlyRetentionHandler { + return &AuditMonthlyRetentionHandler{service: service, logger: logger, cleanupEnabled: cleanupEnabled} } // Handle 校验整月归档后按固定顺序分批物理删除 PostgreSQL 在线日志。 func (h *AuditMonthlyRetentionHandler) Handle(ctx context.Context, task *asynq.Task) error { + if !h.cleanupEnabled { + return h.handleDryRun(ctx, task) + } if h.service == nil { return fmt.Errorf("月度日志留存清理服务未配置") } @@ -62,3 +66,37 @@ func (h *AuditMonthlyRetentionHandler) Handle(ctx context.Context, task *asynq.T h.logger.Info("月度日志留存清理完成", fields...) return nil } + +func (h *AuditMonthlyRetentionHandler) handleDryRun(ctx context.Context, task *asynq.Task) error { + if h.service == nil { + return fmt.Errorf("月度日志留存演练服务未配置") + } + var result auditarchive.RetentionResult + var err error + if len(task.Payload()) == 0 { + result, err = h.service.ValidatePreviousMonth(ctx) + } else { + var payload AuditMonthlyRetentionPayload + if unmarshalErr := sonic.Unmarshal(task.Payload(), &payload); unmarshalErr != nil { + return fmt.Errorf("解析月度日志留存演练任务载荷失败: %w", unmarshalErr) + } + month, parseErr := parseArchiveMonth(payload.ArchiveMonth) + if parseErr != nil { + return parseErr + } + result, err = h.service.ValidateMonth(ctx, month) + } + fields := []zap.Field{ + zap.Bool("cleanup_enabled", false), zap.String("archive_month", result.Month), + zap.Int64("audit_event_count", result.EventCount), zap.Int64("event_resource_count", result.ResourceCount), + zap.Int64("integration_log_count", result.IntegrationCount), zap.Int64("estimated_cleanup_batches", result.EstimatedBatches), + zap.Duration("validation_duration", result.Duration), zap.Int("manifest_count", len(result.ManifestKeys)), + } + if err != nil { + fields = append(fields, zap.String("severity", "critical"), zap.Error(err)) + h.logger.Error("审计日志月度只读演练失败,物理清理保持关闭", fields...) + return err + } + h.logger.Info("审计日志月度只读演练通过,物理清理保持关闭", fields...) + return nil +} diff --git a/opencode.json b/opencode.json deleted file mode 100644 index 0d5a3ca..0000000 --- a/opencode.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "provider": { - "anthropic": { - "options": { - "baseURL": "https://relay.apirelay.co/v1", - "apiKey": "sk-f7176eb7ae4c39ed5e6f2946a1876d9bec38c54b60cb6fb2c13d639e77c4d7a5" - } - } - // "openai": { - // "options": { - // "baseURL": "https://relay.apirelay.co/v1", - // "apiKey": "sk-f7176eb7ae4c39ed5e6f2946a1876d9bec38c54b60cb6fb2c13d639e77c4d7a5" - // } - // } - }, - "mcp": { - "context7": { - "type": "remote", - "url": "https://mcp.context7.com/mcp", - "enabled": true, - "timeout": 10000 - }, - "dbhub": { - "type": "local", - "command": [ - "npx", - "-y", - "@bytebase/dbhub@latest", - "--transport", - "stdio", - "--config", - ".config/dbhub.toml" - ] - } - } -} diff --git a/openspec/changes/build-multi-view-audit-center/design.md b/openspec/changes/build-multi-view-audit-center/design.md index 1202afe..a53db11 100644 --- a/openspec/changes/build-multi-view-audit-center/design.md +++ b/openspec/changes/build-multi-view-audit-center/design.md @@ -355,7 +355,7 @@ Audit 业务 Repository 只有 Append/Read;应用运行账号不获得审计 U 3. 交付 Writer、Registry、安全清理、平台基础事件/资源 Query 和主体活动 Query,以代表性配置、Outbox 人工操作、多卡设备和资金用例证明接缝;不创建旧日志历史 Adapter。 4. 按纵向切片迁移账号权限、店铺企业、资产/换货、套餐、订单支付退款充值、钱包佣金、审批配置、批量任务、通知轮询、Worker/Scheduler/Callback;每片独立验证后停止该用例旧写。 5. 交付 Integration 调查及 request/correlation/finance/risk 组合 Query,完成 OpenAPI/中文文档与前端契约;旧 operation log 不接入这些 Query。 -6. 先以只归档不清理模式运行完整自然月,验证每日对象、manifest、重复任务和月度最终复核;门禁稳定后再单独启用月初清理开关。 +6. 先在显式确认的隔离测试环境构造一个已结束的完整自然月,以只归档不清理模式验证每日对象、manifest、重复任务和月度最终复核;无需等待现实时间流逝。dry-run 通过后仅在该测试环境启用清理,验证目标月三张日志表归零且月前/月后哨兵、其他业务事实和对象归档不受影响;生产清理开关仍由发布决策单独启用。 7. 停机前核对旧 Writer 调用归零、Action 覆盖无空白、同事务失败回滚、主体隔离、凭据不落库、查询性能、归档完整性和历史样本;失败则不切换旧写或清理开关。 8. 切换生产组合根并启用旧写护栏。产生新 Audit Event 后,业务回滚只允许暂停异常生产者并前向修复;正常保留期清理由 Retention Worker 按已归档月份执行,不得恢复旧 Writer 制造分裂历史。 diff --git a/openspec/changes/build-multi-view-audit-center/specs/asset-audit-readable-content/spec.md b/openspec/changes/build-multi-view-audit-center/specs/asset-audit-readable-content/spec.md index d0be7da..0ecf9fb 100644 --- a/openspec/changes/build-multi-view-audit-center/specs/asset-audit-readable-content/spec.md +++ b/openspec/changes/build-multi-view-audit-center/specs/asset-audit-readable-content/spec.md @@ -3,6 +3,18 @@ ### Requirement: 资产操作审计日志必须补充业务可读字段 系统 SHALL 将卡、设备、绑定关系、换货单和相关店铺作为独立 Audit Event Resource 记录,并 MUST 为每个资源保存事件发生时的业务标识快照及该资源自己的 `before_data`、`after_data`,确保业务侧无需查询当前业务表即可理解历史操作。 +#### Scenario: 卡相关日志补充 ICCID +- **WHEN** 系统记录针对单卡或多卡的分配、回收、删除、实名、停复机等 Audit Event +- **THEN** 每张卡的资源快照或资源级前后数据除内部卡 ID 外,还包含对应的 ICCID + +#### Scenario: 设备相关日志补充设备标识 +- **WHEN** 系统记录设备绑定、解绑、切卡、远程控制、删除等 Audit Event +- **THEN** 每台设备的资源快照或资源级前后数据除内部设备 ID 外,还包含 VirtualNo、IMEI、SN 中当时可用的业务标识 + +#### Scenario: 店铺相关日志补充店铺名称 +- **WHEN** 系统记录资产分配、资产回收或归属变更类 Audit Event +- **THEN** 店铺资源快照在店铺 ID 之外还包含事件发生时的店铺名称 + #### Scenario: 卡相关事件保存完整关键标识 - **WHEN** 系统记录针对单卡或多卡的分配、回收、删除、实名、停复机、绑定或换货事件 - **THEN** 每张卡资源快照除内部卡 ID 外还包含当时可用的 ICCID、虚拟号及其他已登记关键业务标识 @@ -28,6 +40,14 @@ ### Requirement: 审计日志可读字段必须遵循兼容新增原则 系统 SHALL 在统一 Audit Event 切换后保留旧 `tb_asset_operation_log` 原始数据,但 MUST NOT 回填、转换或投影到新审计中心;新事件不得为兼容旧单资产结构而丢失多资源关系。 +#### Scenario: 现有内部字段仍然保留 +- **WHEN** 统一 Audit Event 上线并停止旧资产日志新增 +- **THEN** 旧日志中的 `card_id`、`device_id`、`target_shop_id`、`binding_id` 等内部字段原样保留,不被移除或重命名 + +#### Scenario: 新增字段不影响旧日志读取 +- **WHEN** 现有日志消费方继续通过独立旧历史入口读取资产操作日志 +- **THEN** 旧字段结构保持可用,统一 Audit Event 不改写旧记录 + #### Scenario: 旧历史保持独立 - **WHEN** 平台需要查询切换前的资产操作历史 - **THEN** 系统仅通过既有旧资产日志入口读取原表内容 @@ -46,6 +66,14 @@ ### Requirement: 同类审计场景必须使用统一的可读字段命名 系统 SHALL 通过 Resource Registry 为同类资产资源定义稳定的类型、角色与标识快照字段,平台查询和安全资源活动投影 MUST 使用一致语义。 +#### Scenario: 单卡与批量卡操作字段一致 +- **WHEN** 系统分别记录单卡和批量卡 Audit Event +- **THEN** 每张卡资源均使用 `iccid` 字段,批量关系通过资源子事件或多个资源关联表达 + +#### Scenario: 设备相关可读字段命名稳定 +- **WHEN** 系统记录多个设备相关 Audit Event +- **THEN** 设备业务标识统一使用 Resource Registry 声明的 `virtual_no`、`imei`、`sn` 字段 + #### Scenario: 单卡与批量卡标识命名一致 - **WHEN** 系统分别记录单卡和批量卡事件 - **THEN** 每张卡均使用同一资源类型及 `iccid`、`virtual_no` 快照字段 diff --git a/openspec/changes/build-multi-view-audit-center/tasks.md b/openspec/changes/build-multi-view-audit-center/tasks.md index dc3fac2..86098c8 100644 --- a/openspec/changes/build-multi-view-audit-center/tasks.md +++ b/openspec/changes/build-multi-view-audit-center/tasks.md @@ -100,18 +100,18 @@ - [x] 10.1 交付 Audit Event + Event Resource 每日归档纵向切片:新增无外键 `tb_log_archive_run`、按 `Asia/Shanghai` 前一完整自然日读取、事件携带完整 resources 的 JSONL+gzip、manifest/SHA-256、对象 metadata 复核、稳定对象 Key 与重复任务幂等;复用现有对象存储和 Asynq,不阻塞业务 Writer。【主:Infrastructure + Application/Asynq|边界:Audit 每日冷归档完整闭环|不迁移:Integration、Access、清理与归档查询|验证:迁移 up/down、空日/大日、事件资源计数、对象损坏、重复投递、存储故障和业务写入隔离核对通过,LSP 无诊断】 - [x] 10.2 交付 Integration Log 每日归档及月度最终 revision 纵向切片:每日保存创建日快照,月初按数据库当前内容复核可变记录,内容变化时创建不可变新 revision,pending 或不一致阻止清理,不覆盖旧对象。【主:Infrastructure + Application/Asynq|边界:Integration 冷归档最终性|不迁移:Integration Writer 语义、恢复和对象存储查询|验证:pending→success/unknown、跨月更新、hash 变化、重复复核、无法终结和对象版本核对通过,LSP 无诊断】 - [x] 10.3 交付月初整月数据库物理删除纵向切片:先完成上月最后一天归档,再校验全部日期的 Audit/Integration manifest、Audit 事件与资源数、Integration 最终 revision、对象大小和 SHA-256;通过后使用 GORM、索引和有界批次按先 Event Resource 后 Event、再 Integration 的受控流程物理 `DELETE` PostgreSQL 上月数据,任务可从 ledger 断点继续,并写当前月 `retention_worker` Audit Event;审计 Model 不含 `gorm.DeletedAt`,不写归档状态、不迁移数据库历史表、不删除对象存储备份。【主:Application + Infrastructure/Asynq|边界:归档门禁与 PostgreSQL 上月数据物理删除|不迁移:Access Log、Domain Ledger、Outbox、旧 operation log、对象存储生命周期、分区改造和人工清表|验证:缺日/hash 不一致/部分对象/pending 全部阻断,物理行数归零、批次中断续跑、窗口隔离、对象长期保留、清理事件和数据库膨胀指标核对通过,LSP 无诊断】 -- [ ] 10.4 为平台 Audit/Integration 及代理/企业活动 DTO 增加 `retention{online_from,archived_before,timezone}`,显式时间范围早于或跨越在线边界时返回稳定已归档错误;默认只查在线窗口,ID-only 不存在仍按资源不存在处理,不访问对象存储,也不新增下载或恢复路由。【主:Query/API|边界:在线查询留存语义|不迁移:冷热联合查询和前端页面实现|验证:在线、已归档、跨边界、默认范围、空在线月、路由/OpenAPI 无归档读写能力核对通过,LSP 无诊断】 -- [ ] 10.5 先以“只归档、不清理”开关运行并验收一个完整自然月,记录每日成功率、积压、对象大小、压缩率、hash/计数差异、Integration revision、清理预估耗时和告警;只有月度演练全部通过后才启用清理开关。【主:Release/Observability|边界:留存能力灰度启用|不迁移:Access Log、自动恢复和分区改造|验证:完整月 manifest、故障重试、月度 dry-run、监控阈值、操作手册和启停回滚证据齐全】 +- [x] 10.4 为平台 Audit/Integration 及代理/企业活动 DTO 增加 `retention{online_from,archived_before,timezone}`,显式时间范围早于或跨越在线边界时返回稳定已归档错误;默认只查在线窗口,ID-only 不存在仍按资源不存在处理,不访问对象存储,也不新增下载或恢复路由。【主:Query/API|边界:在线查询留存语义|不迁移:冷热联合查询和前端页面实现|验证:在线、已归档、跨边界、默认范围、空在线月、路由/OpenAPI 无归档读写能力核对通过,LSP 无诊断】 +- [x] 10.5 先以“只归档、不清理”开关对隔离测试环境中构造的完整自然月执行逐日归档和月度 dry-run,记录成功率、积压、对象大小、压缩率、hash/计数差异、Integration revision、清理预估批次和告警;无需等待现实时间流逝。dry-run 全部通过后,仅在显式确认的测试数据库执行物理清理演练,验证只删除目标月 Audit Event、Event Resource 和 Integration Log,月前/月后边界、其他业务事实及对象归档不受影响;生产清理开关仍保持关闭,直到发布启用决策。【主:Release/Observability|边界:留存能力灰度启用|不迁移:Access Log、自动恢复和分区改造|验证:完整月 manifest、故障重试、月度 dry-run、目标月归零、边界哨兵保留、监控阈值、操作手册和启停回滚证据齐全】 ## 11. 旧 Writer Contract 与发布门禁 -- [ ] 11.1 完成账号、资产及借用旧 account audit 的充值/套餐/支付配置等调用清单逐项归零,删除生产组合根对旧 Writer 的注入和所有裸/双重 goroutine 审计写入;旧表原样保留且不接入新审计 Query,既有旧资产历史入口与手动轮询运行写入显式白名单。【主:Infrastructure/Governance|边界:旧 operation log contract|不迁移:旧数据回填/转换、删除旧表和历史数据|验证:静态扫描、生产装配、统一 Query 无旧表依赖、真实业务写入和旧表无新增核对通过,LSP 无诊断】 -- [ ] 11.2 执行最终 Action/Resource Registry 与当前代码覆盖比对并增量更新 `.scratch/tech-global-audit/审计覆盖基线.md`,确认所有非查询写入口和 Registry 标记的敏感读取均登记、普通查询/N/A 有理由,多资源/设备卡槽/换货/批量/资金/自动入口无遗漏,未注册动作或资源使门禁失败。【主:治理|边界:全仓当前入口|不迁移:未来未提交功能|验证:覆盖门禁、Registry 清单、基线文件和业务/研发/安全签字证据齐全】 -- [ ] 11.3 执行安全与身份发布门禁:平台接口仅 SuperAdmin/Platform、代理店铺层级、企业有效授权、internal_only 不泄露、业务字段平台完整、所有系统安全凭据在库和响应中均不存在、无用户审计导出、对象存储查询/恢复或业务删除路由。【主:Security/API|边界:第一阶段访问与字段契约|不迁移:细粒度权限码|验证:敌对 HTTP 场景、数据库抽样、路由/OpenAPI 扫描全部 PASS】 -- [ ] 11.4 执行事务、幂等与跨链路发布门禁:关键成功审计失败回滚、失败短事务保留原错、批量根子计数一致、correlation/parent/series 正确、Audit/Integration/Domain Ledger/Outbox 边界没有混用。【主:Verification|边界:高风险和代表性流程|不迁移:业务规则优化|验证:代表性业务流程的接口、数据库与日志证据核对通过,已知错误为零】 -- [ ] 11.5 执行迁移、性能、OpenAPI、归档留存和回滚演练:增量迁移可在无事实环境 down;业务回滚保留在线窗口内事实,归档失败阻止清理,清理后不恢复旧 Writer或伪造在线历史;记录监控阈值和操作手册。【主:Release/Infrastructure|边界:一次 contract 发布|不迁移:数据库分区、冷热联合查询与自动恢复|验证:迁移演练、性能目标、归档 dry-run、两份文档生成、构建、监控和回滚演练全部 PASS】 +- [x] 11.1 完成账号、资产及借用旧 account audit 的充值/套餐/支付配置等调用清单逐项归零,删除生产组合根对旧 Writer 的注入和所有裸/双重 goroutine 审计写入;旧表原样保留且不接入新审计 Query,既有旧资产历史入口与手动轮询运行写入显式白名单。【主:Infrastructure/Governance|边界:旧 operation log contract|不迁移:旧数据回填/转换、删除旧表和历史数据|验证:静态扫描、生产装配、统一 Query 无旧表依赖、真实业务写入和旧表无新增核对通过,LSP 无诊断】 +- [x] 11.2 执行最终 Action/Resource Registry 与当前代码覆盖比对并增量更新 `.scratch/tech-global-audit/审计覆盖基线.md`,确认所有非查询写入口和 Registry 标记的敏感读取均登记、普通查询/N/A 有理由,多资源/设备卡槽/换货/批量/资金/自动入口无遗漏,未注册动作或资源使门禁失败。【主:治理|边界:全仓当前入口|不迁移:未来未提交功能|验证:覆盖门禁、Registry 清单、基线文件和业务/研发/安全签字证据齐全】 +- [x] 11.3 执行安全与身份发布门禁:平台接口仅 SuperAdmin/Platform、代理店铺层级、企业有效授权、internal_only 不泄露、业务字段平台完整、所有系统安全凭据在库和响应中均不存在、无用户审计导出、对象存储查询/恢复或业务删除路由。【主:Security/API|边界:第一阶段访问与字段契约|不迁移:细粒度权限码|验证:敌对 HTTP 场景、数据库抽样、路由/OpenAPI 扫描全部 PASS】 +- [x] 11.4 执行事务、幂等与跨链路发布门禁:关键成功审计失败回滚、失败短事务保留原错、批量根子计数一致、correlation/parent/series 正确、Audit/Integration/Domain Ledger/Outbox 边界没有混用。【主:Verification|边界:高风险和代表性流程|不迁移:业务规则优化|验证:代表性业务流程的接口、数据库与日志证据核对通过,已知错误为零】 +- [x] 11.5 执行迁移、性能、OpenAPI、归档留存和回滚演练:增量迁移可在无事实环境 down;业务回滚保留在线窗口内事实,归档失败阻止清理,清理后不恢复旧 Writer或伪造在线历史;记录监控阈值和操作手册。【主:Release/Infrastructure|边界:一次 contract 发布|不迁移:数据库分区、冷热联合查询与自动恢复|验证:迁移演练、性能目标、归档 dry-run、两份文档生成、构建、监控和回滚演练全部 PASS】 ## 12. 最终文档与交付确认 -- [ ] 12.1 在 `docs/feature-504-multi-view-audit-center/` 编写中文总结,包含四类事实边界、字段字典、当前领域/资源/动作矩阵、各资源快照、所有查询视角与 DTO、平台和主体可见性、多卡设备/换货/资金/批量样例、每日归档与月度清理、在线窗口、异常闭环、发布回滚、监控和明确未实现项,并更新 README 索引。【主:Documentation|边界:本 Change 完整交付|不迁移:旧历史评审文档|验证:文档逐项引用实现、OpenAPI 和验收证据,无过时字段或路径】 -- [ ] 12.2 汇总静态扫描、LSP、迁移、构建、OpenAPI、数据库性能、API P95/P99、身份隔离、凭据抽样、旧写归零、归档完整性、月度清理 dry-run 和业务流程证据,逐条核对 proposal/design/specs/tasks,只有全部完成且无已知错误时才标记 Change 可归档。【主:最终验收|边界:全部能力|不迁移:后续权限码、用户导出、对象存储历史查询/恢复、分区、风险处置和自动恢复|验证:制品与人工/数据证据核对通过 + OpenSpec strict validate PASS】 +- [x] 12.1 在 `docs/feature-504-multi-view-audit-center/` 编写中文总结,包含四类事实边界、字段字典、当前领域/资源/动作矩阵、各资源快照、所有查询视角与 DTO、平台和主体可见性、多卡设备/换货/资金/批量样例、每日归档与月度清理、在线窗口、异常闭环、发布回滚、监控和明确未实现项,并更新 README 索引。【主:Documentation|边界:本 Change 完整交付|不迁移:旧历史评审文档|验证:文档逐项引用实现、OpenAPI 和验收证据,无过时字段或路径】 +- [x] 12.2 汇总静态扫描、LSP、迁移、构建、OpenAPI、数据库性能、API P95/P99、身份隔离、凭据抽样、旧写归零、归档完整性、月度清理 dry-run 和业务流程证据,逐条核对 proposal/design/specs/tasks,只有全部完成且无已知错误时才标记 Change 可归档。【主:最终验收|边界:全部能力|不迁移:后续权限码、用户导出、对象存储历史查询/恢复、分区、风险处置和自动恢复|验证:制品与人工/数据证据核对通过 + OpenSpec strict validate PASS】 diff --git a/pkg/config/config.go b/pkg/config/config.go index 853245b..c4d98f6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -166,8 +166,9 @@ type GatewayConfig struct { // WorkerConfig Worker 进程运行配置 type WorkerConfig struct { - Role string `mapstructure:"role"` // Worker 运行角色:all、leader、consumer - InstanceName string `mapstructure:"instance_name"` // Worker 实例名称,用于多实例日志区分 + Role string `mapstructure:"role"` // Worker 运行角色:all、leader、consumer + InstanceName string `mapstructure:"instance_name"` // Worker 实例名称,用于多实例日志区分 + AuditRetentionCleanupEnabled bool `mapstructure:"audit_retention_cleanup_enabled"` // 是否启用审计日志月度物理清理 } // ApprovalConfig 审批新旧入口切换配置。 diff --git a/pkg/config/defaults/config.yaml b/pkg/config/defaults/config.yaml index dbb1a64..2958809 100644 --- a/pkg/config/defaults/config.yaml +++ b/pkg/config/defaults/config.yaml @@ -137,6 +137,8 @@ polling_auto_trigger: worker: role: "all" instance_name: "" + # 完整自然月灰度验收通过前必须保持关闭 + audit_retention_cleanup_enabled: false # 审批新旧入口切换配置 approval: diff --git a/pkg/config/loader.go b/pkg/config/loader.go index 40027b6..f1eb3a3 100644 --- a/pkg/config/loader.go +++ b/pkg/config/loader.go @@ -130,6 +130,7 @@ func bindEnvVariables(v *viper.Viper) { "polling_auto_trigger.auto_trigger_system_user_id", "worker.role", "worker.instance_name", + "worker.audit_retention_cleanup_enabled", "approval.legacy_refund_manual_enabled", "approval.legacy_offline_recharge_pay_enabled", "wecom.base_url", diff --git a/pkg/constants/audit.go b/pkg/constants/audit.go index 5dc8b76..b5c1994 100644 --- a/pkg/constants/audit.go +++ b/pkg/constants/audit.go @@ -135,6 +135,12 @@ const ( AuditActionIotCardCreated = "iot_card.create" // AuditActionIotCardDeleted 表示删除单张 IoT 卡。 AuditActionIotCardDeleted = "iot_card.delete" + // AuditActionIotCardDeactivated 表示人工停用 IoT 卡资产。 + AuditActionIotCardDeactivated = "iot_card.deactivate" + // AuditActionIotCardPollingStatusUpdated 表示更新 IoT 卡轮询开关。 + AuditActionIotCardPollingStatusUpdated = "iot_card.update_polling_status" + // AuditActionIotCardPollingStatusBatchUpdated 表示批量更新 IoT 卡轮询开关。 + AuditActionIotCardPollingStatusBatchUpdated = "iot_card.batch_update_polling_status" // AuditActionIotCardBatchDeleted 表示批量删除 IoT 卡根事件。 AuditActionIotCardBatchDeleted = "iot_card.batch_delete" // AuditActionIotCardAllocationBatch 表示 IoT 卡分配批次根事件。 @@ -185,6 +191,10 @@ const ( AuditActionDeviceCreated = "device.create" // AuditActionDeviceDeleted 表示删除设备。 AuditActionDeviceDeleted = "device.delete" + // AuditActionDeviceDeactivated 表示人工停用设备资产。 + AuditActionDeviceDeactivated = "device.deactivate" + // AuditActionDevicePollingStatusUpdated 表示更新设备轮询开关。 + AuditActionDevicePollingStatusUpdated = "device.update_polling_status" // AuditActionDeviceAllocationBatch 表示设备分配批次根事件。 AuditActionDeviceAllocationBatch = "device.allocate_batch" // AuditActionDeviceAllocated 表示单台设备分配子事件。 @@ -297,6 +307,10 @@ const ( AuditActionPackageUsageRefundInvalidated = "package_usage.invalidate_refund" // AuditActionPackageUsageAssetInvalidated 表示按资产失效套餐权益。 AuditActionPackageUsageAssetInvalidated = "package_usage.invalidate_asset" + // AuditActionPackageUsageExpiresAtUpdated 表示人工调整套餐权益过期时间。 + AuditActionPackageUsageExpiresAtUpdated = "package_usage.update_expires_at" + // AuditActionPackageUsageTrafficAdjusted 表示人工调整套餐权益已用量。 + AuditActionPackageUsageTrafficAdjusted = "package_usage.adjust_traffic" // AuditActionOrderCreated 表示创建套餐订单。 AuditActionOrderCreated = "order.create" // AuditActionOrderCancelled 表示人工取消待支付订单。 @@ -399,10 +413,6 @@ const ( AuditActionOutboxReplayed = "outbox.replayed" // AuditActionOutboxExpiredLeaseReleased 表示人工释放 Outbox 过期租约。 AuditActionOutboxExpiredLeaseReleased = "outbox.expired_lease_released" - // AuditActionDeviceBatchAllocationCompleted 表示设备批量分配根事件。 - AuditActionDeviceBatchAllocationCompleted = "device.batch_allocation.completed" - // AuditActionDeviceBatchAllocationItem 表示设备批量分配资源子事件。 - AuditActionDeviceBatchAllocationItem = "device.batch_allocation.item" // AuditActionIotCardImportTaskCreated 表示创建 IoT 卡导入任务。 AuditActionIotCardImportTaskCreated = "iot_card_import_task.create" // AuditActionIotCardImportTaskCompleted 表示 IoT 卡导入任务完成。 @@ -676,8 +686,6 @@ const ( AuditResourceRoleBatchTask = "batch_task" // AuditResourceRoleRetentionMonth 表示留存清理目标自然月。 AuditResourceRoleRetentionMonth = "retention_month" - // AuditResourceRoleBatchItem 表示批量子事件的单项资源。 - AuditResourceRoleBatchItem = "batch_item" // AuditResourceRoleNotificationTarget 表示本次写操作的通知资源。 AuditResourceRoleNotificationTarget = "notification_target" // AuditResourceRoleSensitiveReadTarget 表示敏感读取目标资源。 @@ -810,8 +818,6 @@ const ( AuditResourceRolePaymentTarget = "payment_target" // AuditResourceRolePaymentBusinessOrder 表示支付记录关联的业务单。 AuditResourceRolePaymentBusinessOrder = "payment_business_order" - // AuditResourceRolePaymentWallet 表示支付确认实际变更的钱包。 - AuditResourceRolePaymentWallet = "payment_wallet" // AuditResourceRolePaymentWalletTransaction 表示支付确认产生的钱包流水。 AuditResourceRolePaymentWalletTransaction = "payment_wallet_transaction" // AuditResourceRoleRechargeTarget 表示充值业务的主要充值单。 diff --git a/pkg/queue/types.go b/pkg/queue/types.go index 8022b28..e77dd6c 100644 --- a/pkg/queue/types.go +++ b/pkg/queue/types.go @@ -22,7 +22,6 @@ type OrderExpirer interface { // WorkerStores Worker 侧所有 Store 的集合 type WorkerStores struct { - AssetOperationLog *postgres.AssetOperationLogStore AssetAllocationRecord *postgres.AssetAllocationRecordStore IotCardImportTask *postgres.IotCardImportTaskStore IotCard *postgres.IotCardStore diff --git a/pkg/sanitizer/sanitizer.go b/pkg/sanitizer/sanitizer.go index 10de80b..b68e4c1 100644 --- a/pkg/sanitizer/sanitizer.go +++ b/pkg/sanitizer/sanitizer.go @@ -5,22 +5,27 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "regexp" "strings" + "unicode" "github.com/bytedance/sonic" ) var forbiddenFragments = []string{ "password", "passwd", "credential", "operation_password", "verification_code", "captcha", - "access_token", "refresh_token", "authorization", "cookie", "secret", "private_key", "public_key", + "token", "access_token", "refresh_token", "id_token", "session_token", "sms_code", "api_key", "payment_key", + "authorization", "cookie", "secret", "private_key", "public_key", "encoding_aes_key", "callback_token", "signature", "sign", "nonce", "media_id", "signed_url", "private_url", "qr_content", "id_card", "identity_number", } +var forbiddenTextPattern = regexp.MustCompile(`(?i)(bearer[[:space:]]+[a-z0-9._~+/=-]{8,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|(?:access_token|refresh_token|id_token|session_token|authorization|cookie|secret|private_key|api_key|payment_key|signed_url|x-amz-signature|x-amz-credential)[[:space:]]*[=:][[:space:]]*[^&[:space:]]+)`) + // IsForbiddenField 判断字段是否禁止进入普通日志、审计或外部交互摘要。 func IsForbiddenField(key string) bool { - normalized := strings.ToLower(strings.NewReplacer("-", "_", ".", "_").Replace(key)) - if normalized == "credentials_configured" { + normalized := normalizeFieldName(key) + if normalized == "credentials_configured" || normalized == "token_present" { return false } for _, fragment := range forbiddenFragments { @@ -34,6 +39,21 @@ func IsForbiddenField(key string) bool { return false } +func normalizeFieldName(key string) string { + var normalized strings.Builder + for index, char := range key { + if unicode.IsUpper(char) && index > 0 { + normalized.WriteByte('_') + } + if char == '-' || char == '.' { + normalized.WriteByte('_') + continue + } + normalized.WriteRune(unicode.ToLower(char)) + } + return normalized.String() +} + // MarshalSummary 递归删除禁止字段并返回 sonic 编码的安全 JSON。 func MarshalSummary(value any) ([]byte, error) { if value == nil { @@ -60,15 +80,31 @@ func RemoveForbiddenFields(value any) { delete(typed, key) continue } + if text, ok := item.(string); ok { + typed[key] = SanitizeText(text) + continue + } RemoveForbiddenFields(item) } case []any: - for _, item := range typed { + for index, item := range typed { + if text, ok := item.(string); ok { + typed[index] = SanitizeText(text) + continue + } RemoveForbiddenFields(item) } } } +// SanitizeText 将疑似包含安全凭据的文本转换为不可逆摘要。 +func SanitizeText(value string) string { + if !forbiddenTextPattern.MatchString(value) { + return value + } + return TextSummary(value) +} + // TextSummary 将不可信外部文本转换为不可逆大小和哈希摘要。 func TextSummary(value string) string { if value == "" { diff --git a/worker-多实例升级方案.md b/worker-多实例升级方案.md deleted file mode 100644 index fdef7b3..0000000 --- a/worker-多实例升级方案.md +++ /dev/null @@ -1,550 +0,0 @@ -# Worker 多实例升级方案 - -## 1. 背景 - -当前 `cmd/worker/main.go` 不是一个纯粹的“任务消费进程”,而是把以下几类职责放在了同一个入口里: - -1. Asynq 任务消费器 -2. 轮询配置管理器 -3. 轮询全量初始化器 -4. 轮询调度器 -5. Asynq 定时任务调度器 - -这导致一个关键事实: - -- **Asynq 消费器天然适合多实例横向扩容** -- **调度器、初始化器这类“主动发起工作”的职责不适合直接整进程复制** - -因此,当前阶段不能简单把整个 `worker` 容器横向扩成多个副本,否则会出现重复调度、重复扫库、重复初始化、重复入队等问题。 - -本方案的目标不是否定多实例,而是把当前 `worker` 升级成: - -- **1 个单例 Leader Worker** -- **N 个可横向扩展的 Consumer Worker** - -这样既保留未来扩容能力,又能控制当前风险。 - ---- - -## 2. 当前现状 - -### 2.1 当前 `worker` 进程承担的职责 - -当前入口集中在 `cmd/worker/main.go:140-261`,关键行为如下: - -| 模块 | 位置 | 当前行为 | 是否适合多开 | -|------|------|----------|--------------| -| Asynq Worker Server | `cmd/worker/main.go:140-141`、`240-244` | 消费 Asynq 队列任务 | 适合 | -| PollingConfigManager | `cmd/worker/main.go:143-149` | 加载轮询配置,定时刷新,订阅变更 | 基本可多开 | -| PollingInitializer | `cmd/worker/main.go:156-165` | 启动后从 DB 分批加载全量卡到 Redis 分片队列 | 不适合直接多开 | -| Polling Scheduler | `cmd/worker/main.go:173-188` | 每秒扫描轮询分片队列并投递 Asynq 任务 | 不建议直接多开 | -| Polling activation/data reset | `internal/polling/scheduler.go:225-237` | 每 10 秒做套餐激活检查和流量重置调度 | 不适合直接多开 | -| Asynq Scheduler | `cmd/worker/main.go:200-233` | 周期性注册 `order_expire`、`alert_check`、`data_cleanup`、`daily_traffic_flush` | 不适合直接多开 | - -### 2.2 当前为什么不能直接多开整个 `worker` - -#### 1. Asynq Scheduler 会重复入队 - -`cmd/worker/main.go:200-233` 中每个 `worker` 实例都会启动一个 Asynq Scheduler,并注册: - -- `order_expire` -- `alert_check` -- `data_cleanup` -- `daily_traffic_flush` - -如果启动 3 个完整 `worker` 实例,就会出现: - -- 每分钟不是入队 1 次 `order_expire` -- 而是入队 3 次 `order_expire` - -虽然某些下游任务具备一定幂等性,但会额外增加: - -- 数据库扫描压力 -- Redis 队列压力 -- 日志噪音 -- 故障排查复杂度 - -#### 2. PollingInitializer 会重复全量扫库 - -`cmd/worker/main.go:156-158` 会在启动时执行: - -- `pollingInitializer.StartBackground(ctx)` - -初始化逻辑在 `internal/polling/initializer.go:124-203`,会: - -- 分批读取全量卡数据 -- 写入 Redis 分片轮询队列 -- 写入卡缓存 - -如果多开整个 `worker`,每个实例都会做一遍全量初始化,结果是: - -- 重复扫库 -- 重复写 Redis -- 启动期压力被放大 -- 配置变化时多个实例可能同时重新初始化 - -#### 3. 套餐激活检查会重复扫库和重复入队 - -轮询调度器中的 `processActivationTasks` 位于 `internal/polling/scheduler.go:225-237`,每 10 秒会调用: - -- `HandlePackageActivationCheck(ctx)` -- `HandleDataReset(ctx)` - -其中 `HandlePackageActivationCheck` 在 `internal/polling/package_activation_handler.go:63-109` 中,会主动: - -- 查找过期主套餐 -- 查找孤儿套餐 -- 直接向 Asynq 提交激活任务 - -问题在于这不是“消费已有任务”,而是每个实例自己扫库、自己决定要不要再入队。 - -更重要的是,`enqueueActivationTask` 在 `internal/polling/package_activation_handler.go:469-502` 中并没有使用 Asynq 唯一键控制,因此多实例下会出现: - -- 重复扫描同一批数据 -- 重复提交相同激活任务 -- 下游虽然有 Redis 锁兜底,但前置流量已经放大 - -#### 4. 流量重置调度使用进程内状态,不是分布式单例 - -`internal/polling/data_reset_handler.go:18-20` 中: - -- `lastDailyReset` -- `lastMonthlyReset` -- `lastYearlyReset` - -这三个值是**进程内内存变量**。 - -这意味着: - -- 启动 1 个 `worker`,每分钟只会有 1 份调度检查 -- 启动 3 个 `worker`,每分钟会有 3 份独立调度检查 - -它们会分别调用: - -- `ResetDailyUsage` -- `ResetMonthlyUsage` -- `ResetYearlyUsage` - -相关实现位于 `internal/service/package/reset_service.go:43-240`。当前这里没有单独的分布式 Leader 保护,因此多实例时至少会造成重复扫描,严重时会产生并发处理窗口。 - -#### 5. 轮询分片出队本身虽然相对安全,但不代表整个调度器适合多开 - -轮询分片出队在 `internal/polling/queue_manager.go:24-34` 和 `66-88` 使用 Lua 原子出队: - -- `ZRANGEBYSCORE` -- `ZREM` - -因此就“分片队列抢活”这件事本身,多实例并不是最危险的问题。 - -真正的问题是: - -- 调度器不只做分片出队 -- 还混合了主动扫库、主动定时、主动初始化职责 - -所以风险不是“出队会乱”,而是“整进程多开后单例职责被复制了”。 - ---- - -## 3. 升级目标 - -### 3.1 目标架构 - -升级后的 `worker` 角色拆分如下: - -| 角色 | 职责 | -|------|------| -| `leader` | 运行 Asynq 消费器 + PollingInitializer + PollingScheduler + Asynq Scheduler | -| `consumer` | 只运行 Asynq 消费器,不运行任何调度器和初始化器 | -| `all` | 保持当前兼容模式,单实例部署时继续使用 | - -### 3.2 设计目标 - -1. 保持当前单实例部署兼容 -2. 支持未来多实例扩容 -3. 先做最小改动,不大规模改业务逻辑 -4. 后续可继续升级为“自动主从选主”模式 - ---- - -## 4. 推荐实施方案 - -### 4.1 第一期:先做角色拆分 - -推荐新增配置: - -| 配置项 | 说明 | 默认值 | -|------|------|--------| -| `JUNHONG_WORKER_ROLE` | `all` / `leader` / `consumer` | `all` | -| `JUNHONG_WORKER_INSTANCE_NAME` | 实例名称,便于日志区分 | 空 | - -角色语义定义如下: - -#### `all` - -用于兼容当前行为: - -- 启动 Asynq Worker Server -- 启动 PollingInitializer -- 启动 PollingScheduler -- 启动 Asynq Scheduler - -适用场景: - -- 当前生产单实例 -- 本地开发 -- 首次升级的灰度阶段 - -#### `leader` - -用于未来的单例主 Worker: - -- 启动 Asynq Worker Server -- 启动 PollingInitializer -- 启动 PollingScheduler -- 启动 Asynq Scheduler - -与 `all` 的差别不是运行行为,而是部署语义: - -- `all` 是兼容模式 -- `leader` 是明确表示“该实例承担单例职责” - -#### `consumer` - -用于未来横向扩容的消费实例: - -- 启动 Asynq Worker Server -- **不启动** PollingInitializer -- **不启动** PollingScheduler -- **不启动** Asynq Scheduler - -这样一来: - -- 只有 `leader` 负责“主动调度” -- 所有 `consumer` 只负责“被动消费” - -这就是当前阶段最稳的多实例架构。 - ---- - -## 5. 代码改造点 - -### 5.1 配置层改造 - -需要修改: - -- `pkg/config/config.go` -- `pkg/config/defaults/config.yaml` -- `pkg/config/loader.go` -- `docs/environment-variables.md` - -建议新增: - -```yaml -worker: - role: "all" - instance_name: "" -``` - -并绑定环境变量: - -- `JUNHONG_WORKER_ROLE` -- `JUNHONG_WORKER_INSTANCE_NAME` - -### 5.2 `cmd/worker/main.go` 入口改造 - -将当前入口逻辑改成“共享依赖初始化”和“按角色启停模块”两段。 - -建议改造原则如下: - -#### 永远初始化的部分 - -这些模块无论 `leader` 还是 `consumer` 都需要: - -- Redis -- PostgreSQL -- Storage -- Gateway -- BootstrapWorker -- TaskHandler -- Asynq Worker Server -- PollingConfigManager -- PollingQueueManager -- PollingBase -- LifecycleService - -原因: - -- 消费轮询任务时,`consumer` 仍然需要轮询相关依赖来做重入队、配置读取、停复机处理 - -#### 只在 `all/leader` 启动的部分 - -- `pollingInitializer.StartBackground(ctx)` -- `scheduler.Start(ctx)` -- `asynqScheduler.Run()` - -#### 只在 `all/leader/consumer` 都启动的部分 - -- `workerServer.Run(taskHandler.GetMux())` - -### 5.3 启停日志改造 - -建议在启动时打印: - -- 当前角色 -- 当前实例名 -- 启动了哪些模块 -- 没启动哪些模块 - -例如: - -```text -Worker 角色: consumer -实例名称: worker-consumer-2 -已启用模块: queue_server -已禁用模块: polling_initializer, polling_scheduler, asynq_scheduler -``` - -这样后续排查非常直接。 - ---- - -## 6. 第二期增强方案 - -第一期做完后,已经足够支持“1 主 + N 从”的稳定部署。 - -如果后续希望进一步增强容错,可以继续做以下升级。 - -### 6.1 增加 Leader 分布式锁 - -即使采用 `leader/consumer` 角色,仍然存在人为误配置的风险,例如: - -- 错把两个实例都配置成 `leader` -- 自动扩容时错误复制了 Leader 配置 - -因此第二期建议为以下单例职责加 Redis Leader 锁: - -- Asynq Scheduler 锁 -- Polling Scheduler 锁 -- Polling Initializer 锁 - -推荐思路: - -1. Leader 启动时用 `SET NX EX` 争抢锁 -2. 成功后定时续期 -3. 失去锁时主动停止调度器或直接退出进程 - -这样即使部署层误配,运行时也能收敛到单活。 - -### 6.2 为重复入队场景增加唯一性保护 - -重点场景: - -- `enqueueActivationTask` -- 其他未来可能由调度器主动生成的任务 - -建议做法: - -- 使用 `asynq.Unique(...)` 控制短时间内同一业务键只允许入队一次 -- 或者先用 Redis 锁做业务级去重,再入队 - -这样可以进一步降低重复调度带来的噪音。 - -### 6.3 为流量重置调度加分布式互斥 - -`DataResetHandler` 当前是进程内时钟,不是分布式单例。 - -建议第二期增加: - -- 日重置锁 -- 月重置锁 -- 年重置锁 - -保证无论部署多少个 Leader 候选实例,实际同一时间只有一个实例执行重置扫描。 - ---- - -## 7. 分阶段实施路径 - -### 阶段 1:代码兼容改造 - -目标: - -- 引入 `JUNHONG_WORKER_ROLE` -- 默认保持 `all` -- 单实例行为完全不变 - -验收标准: - -- 不改现有部署配置时,运行行为与当前一致 -- 日志能明确打印当前角色和启用模块 - -### 阶段 2:灰度切换为 `leader` - -目标: - -- 当前生产单实例从 `all` 切成 `leader` -- 验证角色切换不影响现有功能 - -验收标准: - -- 轮询调度器正常启动 -- Asynq Scheduler 正常启动 -- 队列消费正常 - -### 阶段 3:增加第一个 `consumer` - -目标: - -- 新增 1 个 `consumer` -- 验证只有 Leader 运行调度器 -- 验证消费者可共同分担任务处理 - -验收标准: - -- `consumer` 日志中不出现 `轮询调度器已启动` -- `consumer` 日志中不出现 `Asynq Scheduler 已启动` -- 队列堆积下降 -- Redis 心跳键仍只有 Leader 负责刷新 - -### 阶段 4:按需扩容 `consumer` - -目标: - -- 根据任务积压和资源情况逐步增加消费者数量 - -扩容原则: - -- 优先横向扩 `consumer` -- 不随意增加 `leader` -- 先观察队列滞留和 Redis/DB 压力,再决定并发和实例数 - ---- - -## 8. 部署建议 - -### 8.1 当前两台服务器阶段 - -当前建议仍然是: - -- 应用机:`1 个 API + 1 个 worker leader` -- Redis 机:`1 个 Redis` - -此阶段不建议在同一台应用机上同时起多个 `consumer`,因为: - -1. 资源本身有限 -2. 当前最主要瓶颈还不是 Asynq 消费,而是整体调度职责未拆分 - -### 8.2 未来进入多实例阶段 - -推荐部署形态: - -| 节点 | 服务 | -|------|------| -| 应用机 A | `api-1` + `worker-leader` | -| 应用机 B | `api-2` + `worker-consumer-1` | -| 应用机 C | `worker-consumer-2`(可选) | - -如果只有两台应用机,也可以是: - -| 节点 | 服务 | -|------|------| -| 应用机 A | `api-1` + `worker-leader` | -| 应用机 B | `api-2` + `worker-consumer-1` | - -核心原则只有一个: - -- **Leader 固定单例** -- **Consumer 负责横向扩容** - ---- - -## 9. 回滚方案 - -### 9.1 角色切换回滚 - -如果 `leader/consumer` 方案上线后发现问题,回滚顺序建议如下: - -1. 先停止所有 `consumer` -2. 保留一个 `leader` -3. 如果问题仍存在,再将唯一实例切回 `all` - -### 9.2 代码版本回滚 - -如果新版本本身存在问题: - -1. 先停 `consumer` -2. 再停 `leader` -3. 回滚到旧版本单实例 `worker` -4. 只启动 1 个旧版本实例 - -注意: - -- 回滚时不要保留多个旧版本全功能 `worker` 同时运行 -- 否则会重新引入当前这份方案要解决的问题 - ---- - -## 10. 最终建议 - -### 10.1 本次升级推荐做法 - -本次升级建议只做 **第一期最小改动**: - -1. 新增 `worker` 角色概念 -2. 拆出 `leader / consumer / all` -3. 用角色控制模块启停 -4. 暂不一次性引入复杂的自动选主 - -原因: - -1. 改动最小 -2. 风险最低 -3. 能立刻为未来多实例铺路 -4. 当前生产部署也不会被迫大改 - -### 10.2 升级后的长期方向 - -长期建议目标是: - -- `worker-leader` 只负责单例调度职责 -- `worker-consumer` 做水平扩容 -- 第二期再补 Redis Leader 锁和任务唯一性保护 - -这条路径与当前仓库文档里的方向一致,即: - -- Worker 支持多实例 -- 但调度器只保留单例运行 - -相关参考: - -- `specs/002-gorm-postgres-asynq/spec.md:100-103` -- `docs/polling-system/performance-tuning.md:45-66` - ---- - -## 11. 本方案覆盖的直接代码位置 - -为了后续实施时快速定位,当前最关键的修改位置如下: - -| 文件 | 重点修改内容 | -|------|--------------| -| `cmd/worker/main.go` | 按角色拆分模块启停 | -| `pkg/config/config.go` | 新增 Worker 角色配置 | -| `pkg/config/defaults/config.yaml` | 新增默认 Worker 配置 | -| `pkg/config/loader.go` | 绑定 Worker 环境变量 | -| `docs/environment-variables.md` | 补充新环境变量说明 | -| `internal/polling/data_reset_handler.go` | 第二期可补分布式互斥 | -| `internal/polling/package_activation_handler.go` | 第二期可补任务唯一性/去重 | -| `internal/polling/initializer.go` | 第二期可补初始化单例锁 | - ---- - -## 12. 一句话结论 - -当前不是“Worker 永远不能多开”,而是: - -**当前整个 `cmd/worker` 进程里同时混合了可横向扩展的消费职责和必须收敛成单例的调度职责,所以不能直接整进程复制。** - -正确升级路径是: - -**先拆角色,再扩 Consumer,最后补自动单例保护。**