# LicenseManager 一致性审查报告 v1.0

> 生成方式：全量盘点（30 个 Controller、18 份文档、11 个 web 应用、5 个 SDK）后逐条人工核验，每条均附 `文件:行号` 证据。
> 基线：`dotnet build` 0 警告 0 错误；实测测试用例 Core 179 / Platform 661 / Infrastructure 153 / Server 32 / Api 979，合计 **2004**。
> 优先级：P0 = 功能错误/安全风险；P1 = 明显不一致需修；P2 = 文档过期/优化；P3 = 轻微。
>
> **商业化裁剪说明（2026-10-08）**：本报告为多轮历史审查记录（各轮发现与修复结果按当时快照保留、不回改）。平台商业/财务/合规模块（订单、订阅策略、支付、试用、优惠券、退款、催收、账单对账、PSP、税务、发票、汇率、品牌配置、数据驻留等）此后已整体下线，资金流由外部销售系统承接——报告中涉及上述模块的发现/修复条目仅具历史价值，相关代码与页面均已移除；保留的许可语义（许可证签发/激活/设备/用量、订阅续期、DSR、EULA、Plan、TokenPool）条目仍然有效。

---

## A. 文档 ↔ 代码不一致

### A1. API-Spec 响应信封整体过期 [P1，改文档]
API-Spec §4.2/§7 声称 JSON:API 风格 `{data:{id,type,attributes},meta}` + 错误 `{errors:[...]}`。
实际信封：成功 `{success:true,data,requestId,timestamp}`、错误 `{success:false,error:{code,message,details?},requestId,timestamp}`（`src/LicenseManager.Api/Middleware/ApiResponseWrapperMiddleware.cs:65-71,142-156`）。前端与 5 个 SDK 均按实际信封消费。
→ 按实际信封重写 API-Spec §7。

### A2. API-Spec 错误码字典与实际不符 [P1，改文档]
文档 50 个大写下划线码（API-Spec:316-365）；实际 `ErrorCodesController.cs:28-83` 返回 41 个混合风格码（snake_case 通用、`auth.*`、`license.<PascalCase>`、billing 点号）。
→ 以 `GET /v1/error-codes` 实际输出重写字典。另注：`idempotency_conflict` 码在字典中但中间件永不会产生（见 A7）。

### A3. API-Spec 认证方案错误（Security-Design 正确）[P1，改文档]
API-Spec 声称 `X-API-Key: lmc_xxx` + `X-Signature` + `X-Timestamp` + `X-Nonce` canonical 签名。实际仅 `Authorization` 头三种方案 `Bearer | ApiKey <keyId>:<secret> | Hmac <keyId>:<sig>`（`AuthenticationMiddleware.cs:138-144`），HMAC 签名输入 `timestamp+"\n"+body`、secret 走 `X-LicenseManager-Api-Key` 头、无 nonce（:244-282）。Coding-Standards §7.5 的 HMAC+nonce 描述同样过期。
→ 按 Security-Design §2.3/§2.4 重写 API-Spec §2 与 Coding-Standards HMAC 段。

### A4. API-Spec 冒号动作风格与实际不符 [P2，改文档]
文档 `POST /v1/admin/licenses/{id}:suspend`；实际子路径段 `[HttpPost("licenses/{licenseId:guid}/suspend")]`（`AdminController.cs:222,230,239,719,732,745`）。
→ 文档示例改为实际路由。

### A5. API-Spec 三个端点不存在 [P1]
- `PATCH /v1/admin/licenses/{id}` — AdminController 无此路由（licenses 路由全集 :192-241,719-765）。→ 与 C2 联动：服务端补端点，同时文档保留。
- `POST /v1/webhooks/replay/{delivery_id}`（deprecated 202）— WebhooksController 仅 `payment/{provider}`（:45）；管理侧替代返回 200 非 202（StubController.cs:521,529）。→ 改文档删除。
- `GET /v1/openapi.json` — 无实现，Swagger 仅 Development 挂 `/swagger/v1/swagger.json`（Program.cs:132-139）。→ 改文档更正。

### A6. 限流响应头 [P2，改文档]
API-Spec 声称 `X-RateLimit-Limit/Remaining/Reset` 头；实际无这些头，429 body 自带 `{limit,window_seconds,retry_after_seconds}` + `Retry-After: 60`（RateLimitingMiddleware.cs:71-85）。默认 100/分（代码 :42），仓库配置 600（appsettings.json:29-30）。
→ 文档按 body 字段重写。

### A7. 幂等头名与语义 [P1，文档+代码都改]
- 实际头名 `Idempotency-Key`（IdempotencyMiddleware.cs:42），API-Spec 写 `X-Idempotency-Key` → 改文档。
- 触发方法为 POST/PUT/PATCH（:36），文档写 POST/PATCH/DELETE → 改文档。
- **代码缺陷**：缓存键仅 `"idem:"+key`（:49），不校验请求体哈希——同键不同体会错误回放旧响应；`idempotency_conflict` 错误码永不会触发。→ 代码补请求体哈希比对，冲突时返回 `idempotency_conflict`。

### A8. 端点总数声明严重低估 [P2，改文档]
API-Spec §15.2 声称合计 ~150（管理 ~80）；实际 `[Http*]` 动作 493 个，仅管理域 7 个 Controller 即 ~418（GapAdmin 90、Admin 76、Commerce 66、Advanced 66、PlatformOps 54、Stub 37、PaymentSecurity 26）。
→ 更新数量表并注明 Stub 占位端点。

### A9. 客户 API 认证表述矛盾 [P1，改文档]
Security-Design :81 声称 "heartbeat 始终强制认证"，实际 `Auth:AllowPublicActivation=true`（默认）时 activate/verify/heartbeat/deactivate/refresh-file 全部匿名放行（AuthenticationMiddleware.cs:48,306-315）。API-Spec §9.1 标注 `security: ApiKey` 同样与默认行为不符。
→ 两文档修正为"默认公开、可配置收紧"（代码注释已自认此为技术债务）。

### A10. Coding-Standards 数据库约定与实现偏离 [P2，改文档]
- 表名 snake_case 单数：33 个显式 `ToTable` 实体符合（DbContext.cs:361-748），13 个高级模块实体走 EF 默认 PascalCase 复数名（迁移快照 :475 等）→ 违反 §5.1。
- 列名 snake_case：全库未实现（`HasColumnName` 0 次，无命名约定）。
- 软删除 `deleted_at`：完全未实现（全仓 `DeletedAt|IsDeleted` 零匹配）。
→ §5 改为"现状 + 规划"两层描述。

### A11. Coding-Standards 前端路由 meta [P2，改文档]
文档 `meta.requiresAdmin`；实际 `meta.roles` 数组 + `hasAnyRole()`（web/admin/src/router/index.ts:53-230）。
→ 改 Coding-Standards §4.5/§7.5。

### A12. PII 脱敏功能不存在 [P2，改文档]
API-Spec §4.5 声称 email 默认 `a***@example.com`、`?sensitive=true` 明文。代码无任何实现（全仓无匹配）。
→ 文档标注为规划；如属合规承诺需另行实现。

### A13. cursor 分页不存在 [P1，改文档]
API-Spec §5 声称 cursor 分页（limit/cursor/next_cursor/has_more）、filter/sort/q/fields。实际列表端点基本无分页参数（如 `AdminController.cs:212-215` 仅 productId/customerId）；audit-log 用 `limit/offset`（:418-423，恰与文档 §5.2 一致）。
→ 文档如实描述现状；cursor 分页列为路线图。

### A14. EF 持久化范围：22 vs 45 [P1，改文档]
实际 `DependencyInjection.cs:71-125` 共 **45** 处 `services.Replace`（Quote:107、PaymentMethod:108、SsoProvider:122、Integration:124 均已 EF 化）。
Migration-Guide:262 与 Troubleshooting:717-731 仍写 22 个并称"其他实体仍用 InMemoryRepository"（错误且误导排障）；Release-Report:24 正确但 :29、:114 残留"待迁移"矛盾。
→ 三处文档统一为 45 全量 EF 化。

### A15. 测试数全面过期 [P2，改文档]
实测（`dotnet test --list-tests`）：Core 179 / Platform 661 / Infrastructure 153 / Server 32 / Api 979 = **2004**；测试类 Server 5 个、Api 35 个。
文档：Release-Report 1963（文尾 1958，自相矛盾）、Testing-Strategy Server 1 类 / Api 32 类 ~973。
→ 按实测更新。

### A16. Troubleshooting 审计表名错误 [P1，改文档]
Troubleshooting:1452,1467,1531 写 `FROM AuditLogs`；实际表名 `audit_log`（DbContext.cs:657 `e.ToTable("audit_log")`），`AuditLogs` 仅是 DbSet 属性名，该 SQL 在 MySQL 报表不存在。
→ 改表名与 SQL。

### A17. TAD JWT 会话模型与实现不符 [P1，改文档]
TAD:453 声称 access 15min + refresh 7d 可撤销；实际 JWT 8 小时、无 refresh 机制（AuthController.cs:151，JwtIssuer.cs 无 refresh）。
→ 改 TAD 为现状（8h、无 refresh）；refresh token 列路线图。

### A18. TAD/Deployment 基础设施声明 [P1，改文档]
声称 PostgreSQL 16 / Redis 7 / RabbitMQ 3.13 必选。实际：RabbitMQ 零引用（Webhook 投递为进程内 WebhookDeliveryWorker）；Redis 可选（DependencyInjection.cs:128-132）；`DatabaseOptions.cs:13` 代码默认 PostgreSQL 但 appsettings.json:3 配置默认 MySql，且仅 MySQL 走 Migrate、其余 EnsureCreated（Program.cs:195-202）；无 Quartz/FluentValidation/AutoMapper/Testcontainers。
→ TAD/Deployment 改为实际架构，MQ/L2 缓存标路线图。

### A19. Deployment 的 CLI 子命令不存在 [P1，改文档]
`dotnet LicenseManager.Api.dll migrate/setup-admin/webhook replay/maintenance`（Deployment:293,296,1119,1122,1221,1258,1264）在 Program.cs 中无任何实现。文档 :1333 自认 lmctl 未随附但前文仍以可执行命令给出——自相矛盾。
→ 替换为真实手段（ef 工具/管理 API）或标注规划。

### A20. Beginner-Guide 浮动租约 [P1，改文档]
文档 `POST /v1/leases/acquire`、超时默认 5 分钟；实际路由 `checkout`（LeasesController.cs:31），默认租约 1800 秒（CheckoutRequest.cs:8）。
→ acquire→checkout、5 分钟→30 分钟。

### A21. RDP 解决方案结构含不存在的项目 [P2，改文档]
RDP:111-114 列出 `LicenseManager.Admin/Portal/Sdk/Sdk.Native`；sln 实际仅 Core.Abstractions/Core/Platform/Infrastructure/Api/Server/Tools + 测试 + Sample。
→ 删除或标注"规划中"。

### A22. Core-Development-Guide 差异 [P2，改文档]
`LicenseManager.Core.Extensions` 子项目不存在；实际 `ILicenseManager` 多 `ActivateOrVerifyAsync`；`LicenseContext.HardwareId` 为 `string?` 且多 `Features` 字典。NuGet 元数据一致 ✓；无 Aegis 残留 ✓。
→ 更新接口清单。

### A23. Quickstart 的 Go/Rust SDK 全部错误 [P1，改文档]
- Go 模块：文档 `github.com/licensemanager/sdk-go`，实际 `github.com/license-manager/sdk-go`（sdk/go/go.mod:1）。
- API 形状：文档 `client.NewClient(url)` + `client.GetDeviceFingerprint()` + client/models 子包；实际单 package `licensemanager`、`NewClient(options ClientOptions)`（sdk/go/client.go:57）、无 `GetDeviceFingerprint`。
- Rust crate：文档 `licensemanager-sdk`，实际 `license_manager`（sdk/rust/Cargo.toml:2）。
→ 三处全改。

### A24. Beginner-Guide 消费响应字段 [P2，改文档]
文档 `currentUsage`；实际 `ConsumeResult.CurrentConsumption` → JSON `currentConsumption`（Dtos/Responses/ConsumeResult.cs:4）。

### A25. "最后 admin 不可删" 无代码保护 [P1，代码或文档]
User-Manual:364 承诺该保护；`UserService.cs:162-170` DeleteAsync 无守卫、AdminController.cs:475-480 无守卫。
→ 建议补代码守卫（删除前检查目标为最后一名 active admin 则拒绝），手册承诺合理。

### A26. User-Manual 产品类型 5 种 [P3，改文档]
实际 `LicenseType` 枚举 8 值（Core/Enums/LicenseType.cs:9-30）。手册补注 NamedUser/Bundle/Trial。

### A27. Troubleshooting "LoginAttempt 表" [P3，改文档]
LoginAttempt 实为 `InMemoryRepository`（Platform/DependencyInjection.cs:177-178），非数据库表，重启即丢、多实例不互通。文档"表"表述改"内存记录"。

### A28. 迁移数与租户实体数过期 [P2，改文档]
DB-Design/Migration-Guide/Troubleshooting 称 7 个迁移；实际 `src/LicenseManager.Infrastructure/Migrations/` 有 **10** 个。TenantId 过滤器实体 24 → 实际 27（且为反射动态附加，DbContext.cs:251-271）。

### A29. Integration-Guide 小遗漏 [P3，代码注释+文档]
`DependencyInjection.cs:137` 注释残留 "stripe" 命名客户端（已删除）→ 改注释。Platform 额外注册 currency/integration 两个 HttpClient（Platform/DependencyInjection.cs:397,402）→ 文档补提。

### A30. Troubleshooting 行号漂移 [P3，改文档]
异常映射引用行号 48/49，实际 50/51（ExceptionHandlingMiddleware.cs:50-57）。

### A31. webhook 事件名代码内部矛盾 [P0，改代码]
`LicenseService.cs:144` 发布 `license.issued`，但事件目录 `WebhookEventCatalogService.cs:13-29` 只有 `license.created` 等，无 `license.issued` → 订阅校验按目录过滤时该事件不可订阅。文档也分裂：Beginner-Guide:357 用 `license.issued`，RDP M55.1 用 `license.created`。
→ 代码侧统一：目录补 `license.issued`（保留发布名，与 Beginner-Guide 一致），RDP 章节同步。

### A32. 已验证一致、无需改动 ✓
API-Spec §10 出站 Webhook 规范（头/签名/退避/清理与 WebhookDeliveryWorker 逐点吻合）；Frontend-Dev-Guide（63 api 模块/71 views/6 stores/client.ts 行为/代理）；Integration-Guide 主体（11 适配器、stripe/paypal 真实对接、命名 HttpClient）；Dev-Setup（Seed 管理员、根路径概览、appsettings.Local.json）；Deployment 浮动服务器段（MaxSeats=10/86400/30s/X-Api-Key/503 fail-closed）；User-Manual 角色矩阵与续期 365 天；Troubleshooting 异常映射表内容；DB-Design §0 的 user_account/PBKDF2 210000/13 张 PascalCase 表/psp_config/webhook_event_record 唯一索引。

---

## B. UI ↔ 服务端不对应

### B1. web/partner 完全无后端 [P0]
`/v1/partner/**` 全部端点（auth/deals/leads/quotes/commissions/resources/training/directory 等 ~25 个）在服务端零对应（grep `v1/partner` 无匹配）。
→ 按既定方案：建最小 PartnerController 存根（返回 501/空数据），前端移除假提交、显式标注"功能未上线"。

### B2. web/sandbox 完全无后端 [P0]
`/v1/sandbox/**`（trial/api-keys/dashboard 等 10 个端点）零对应。
→ 同 B1 存根方案；Dashboard.vue:117-134 的 demo fallback 保留但显式标注"演示数据"。

### B3. web/status 多重错误 [P0]
- `status.ts:68` `'/status.json'.replace('/status','')` → 拼出 `GET /v1/status/.json`（replace 方向写反）。
- `/v1/status/components`、`/v1/status/incidents` 服务端不存在；`/v1/status/maintenance` 存在但结构 `{maintenance_mode,message,environment}`（StatusController.cs:206-211）与前端 `MaintenanceWindow[]` 不符；`/v1/status` 结构也不是前端的 `OverallStatus`。
- mock 静默回退（status.ts:87,146,229,290,297-330）掩盖全部 404；Subscribe.vue:45-79 三个订阅全假提交。
→ 修 .json bug；前端类型对齐 `/v1/status` 真实结构；components/incidents 标注"未上线"或移除；mock 回退改为显式错误态；Subscribe 显式标注。

### B4. web/embed 默认配置下整体不可用 [P0]
- 默认 api-base `/api/v1`（utils.ts:214）→ 应为 `/v1`。
- 认证头 `X-API-Key`（utils.ts:48）→ 服务端只认 `Authorization: ApiKey keyId:secret`（AuthenticationMiddleware.cs:138-144）。
- 端点 3/5 不存在：`GET customer/licenses/{id}/devices`、`POST devices/{deviceId}:disable`、`DELETE devices/{id}`（LicensesController 无）。存在的：activate ✓、GET license ✓（需认证）、features ✓（需认证）。
→ 修 embed（前缀+认证头）；服务端在 LicensesController 补 devices 列表/disable/delete 三个端点（客户命名空间，需认证），或裁剪 embed 组件。建议补端点。

### B5. portal profile fallback 是死路 [P2]
portal auth.ts:48-50 的 `/v1/portal/profile` 不存在（实际为 `/v1/portal/auth/profile`，PortalAuthController.cs:140；另有 `/v1/customer/profile`）。
→ 删除 fallback 函数或改指 `/v1/customer/profile`。

### B6. admin schema 适配层伪造数据 [P0]
web/admin/src/api/client.ts:119-171：
- **伪造 `issuer_url='https://auth.example.com'`**（:160-162）——会作为真实 SSO 配置持久化，安全/功能风险 → 删除，改表单必填。
- 伪造 support-tier `code=TIER-${Date.now()}`、`max_resolution_hours=hours*2||24`（:152-158）→ 删除，改表单必填。
- 伪造 customer `code=CUST-${Date.now()}`（:164-166）——CreateCustomerRequest 注释声称"为空自动生成"但 CustomerService.cs:39 未实现 → 服务端实现自动生成，前端删伪造。
- Order `total→amount` 注入字段被服务端静默丢弃（CreateOrderRequest 无此字段，:144-146）与 license-templates 自赋值（:148-150）→ 删死代码。
- 其余（Plan/Subscription 结构转换）为正常适配，保留；Order/Coupon 相关适配已随商业化模块整体下线。
另：AdminController.cs:873-874 个别响应用 snake_case 匿名对象（`orders_this_month`），与全局 camelCase 约定不一 → 统一。

### B7. TicketList/NotificationBell mock [P1]
- TicketList.vue:298-315 的 mockTickets 回退多余——`GET/POST /v1/admin/tickets` 真实存在（StubController.cs:408,424）→ 移除回退。
- NotificationBell.vue:116-154 纯 mock（硬编码 4 条假通知，含误导性内容）→ 对接 `GET /v1/admin/notification/logs`（GapAdminController.cs:451）。

### B8. dev 代理端口分裂 [P1]
admin vite.config.ts:31 → 8080；portal/partner/sandbox/status → 5000。服务端无 launchSettings.json（`dotnet run` 默认 5000），Docker 用 8080（Dockerfile:50）。
→ 补 launchSettings.json 固定 8080（与 Docker 对齐），5 个 vite.config target 统一 8080。

### B9. website/docs.html API 表错误 [P2]
website/docs.html:270-281 使用 `/api/v1/licenses/*` + `api.licensemanager.cn` + `Bearer <API_KEY>`，与实际 `/v1/customer/licenses/*` 及三种认证方案均不符。
→ 按实际端点与认证重写。

### B10. expires-at 函数模块归属 [P3]
`updateLicenseExpiresAt` 放在 web/admin/src/api/subscriptionPolicy.ts:66-68，应移至 license 相关模块。端点本身存在（GapAdminController.cs:608）。

### B11. PaymentSecurity.vue 绕过 api 模块 [P3]
views/PaymentSecurity.vue:299,345 直接 `import { get }` 调 `/v1/admin/pci-compliance`，而 api/paymentSecurity.ts:37 已有封装 → 改用封装。（已随商业化下线：该页面与端点已随商业化裁剪移除。）

### B12. widget verify 字段名错位（恒失败）[P0]
web/widget/license-widget.js:186-192 发 `{license_key, hardware_id}`；服务端 `VerifyRequest(string LicenseKey, ...)` 按 camelCase 绑定（无 JsonPropertyName），`licenseKey` 绑为 null → 验证必失败。
→ widget 改发 `{licenseKey, hardwareId}`。

### B13. 假数据/假提交标注 [P2]
sandbox Dashboard demo fallback、status Subscribe 假提交、marketing TrialSignup.vue:64-68 `setTimeout` 假提交 → 统一显式标注"演示/未上线"。

### B14. trial 双 convert 端点注释 [P3]
`/v1/admin/trials/{id}/convert`（试用→许可证，CommerceAdminController.cs:592）与 `/v1/admin/subscription-lifecycle/trials/{trialId}/convert`（试用订阅→付费订阅，:301）语义不同 → trial.ts 注释区分。（已随商业化下线：试用模块已随商业化裁剪移除。）

---

## C. SDK ↔ 服务端

### C1. C# SDK renew 字段错误（静默错行为）[P0]
sdk/csharp/Services/LicenseService.cs:109 发 `{days}`；服务端要 `validityDays`（RenewLicenseRequest.cs）→ 反序列化失败静默默认 365 天，调用方传参被忽略。其余四 SDK 均正确发 `validityDays`。
→ C# 改发 `{validityDays = days}` 或建请求模型。

### C2. Go/Rust SDK 调用不存在的 PATCH/DELETE licenses [P0]
sdk/go/services.go:150,158、sdk/rust/src/client.rs:393-394,400-401 调 `PATCH/DELETE /v1/admin/licenses/{id}`，服务端不存在 → 404/405。
→ 与 A5 联动：服务端补 `PATCH licenses/{id}`（元数据/到期/状态字段子集）；DELETE 语义文档化为"只能吊销不能删除"，Go/Rust SDK 的 Delete 改为调用 revoke 或移除。

### C3. 三 SDK 缺 checkout [P1]
服务端 `POST /v1/customer/licenses/checkout` 存在（LicensesController.cs:80）；Go（services.go:256）、Rust（client.rs:189）已封装；C#/Node/Python 缺失 → 补齐。

### C4. C# SDK 缺 ExtendAsync [P1]
其余四 SDK 均有 extend（Go:199/Node:717/Python:446/Rust:451）→ C# 补齐。

### C5. Node 包名与文档不符 [P2]
实际 `@licensemanager/client-sdk`（package.json:2）；API-Spec:1208 写 `@license-manager/sdk` → 统一（建议改包名或改文档，二选一）。API-Spec 补 Rust/Python/C# 包名行。

### C6. C# HttpPipeline 死代码 [P2]
`PutAsync/PatchAsync/DeleteAsync`（HttpPipeline.cs:77-81 等）无调用方 → C2 落地后按需要保留或删除。

### C7. 已验证一致 ✓
examples 三语言 7 步流程全部端点与响应结构对上（含 `licenseIds[0]` 解包）；五 SDK 信封解包与服务端一致；构建产物未被 git 跟踪，.gitignore 覆盖完整。

---

## D. 服务端内部问题与优化项

### D1. DashboardController 孤儿端点 + 假数据 [P1]
`[Route("api/admin/dashboard")]`（全项目唯一非 v1 前缀），前端无人调用（Dashboard.vue 用 `/v1/admin/reports/*`），且含写死假数据（DashboardController.cs:65-81 的 "1号...30号"、"专业版/企业版/社区版"）。
→ 删除整个控制器。

### D2. StubController 名不副实 + 12 个别名端点 [P2]
实际全部注入真实服务、无假数据（已验证）；真正问题是 12 个与其它控制器重复的"列表别名"（offline-activations、batch-operations、data-residency、payments、webhook-deliveries 双别名 :473/:486 等）。
→ 删别名端点（或 308 重定向），控制器按职责改名拆分（CatalogAdminController：feature-catalog/license-templates/brand-profiles；SupportAdminController：tickets 等）。无 AmbiguousMatch 冲突（已逐路由比对 ✓）。

### D3. GapAdminController 巨型化 [P2]
966 行、90 端点、16 个域。→ 按域拆 5 个控制器：SecurityAdmin（device-blacklist/MFA/login-security/anti-replay/PAT）、PrivacyAdmin（DSR/export）、MessagingAdmin（notification/kb）、LicensingOpsAdmin（borrow/refresh-file/offline-activation/batch/expires-at/transfer-device）、CommercialAdmin（plans/billing-usage/quota/support-tiers/imports）。同时给无约束 `{id}` 路由补 `:guid`（:732,:894,:580,:634,:825 等）。

### D4. lmcli 半成品命令 [P3]
Tools/Program.cs:55-64 `activation request/apply`、`refresh` 注册了命令但无 handler（空跑）；csproj 描述提到不存在的 "lmctl"。
→ 删除空命令或实现；修描述。

### D5. 代码注释残留 [P3]
DependencyInjection.cs:137 注释含已删除的 "stripe" 命名客户端。

### D6. 已验证无问题 ✓
无 ASP.NET 重复路由冲突；Server ApiKeyMiddleware 行为正确（未配置 503 fail-closed）；浮动服务器与主 Api 端点无混淆。

---

## 修复顺序（阶段二）

1. **P0 代码 bug**：C1、A7 代码部分、A31、B3、B4、B6、B12、C2（服务端补 PATCH）、A25（admin 守卫）。
2. **P0 存根**：B1、B2（Partner/Sandbox Controller 存根 + 前端标注）。
3. **P1 对齐**：A5/A9/A13/A14/A16-A20/A23、B7、B8、C3、C4、D1。
4. **P2 文档与优化**：A 类其余文档项、B5/B9/B13、C5、D2、D3、D4、D5。
5. **验证**：`dotnet build` + `dotnet test` 全量；改动前端 `npm run build`；SDK 各自测试；本报告逐项销号。


---

# 修复结果（2026-09 迭代，全部完成）

> 验证基线：`dotnet build` 0 警告 0 错误；`dotnet test` 全量 **2064 通过 / 0 失败**（Core 179 / Platform 669 / Infrastructure 153 / Server 32 / Api 1031）；前端 7 个可构建应用（admin/portal/partner/sandbox/status/embed/marketing）`npm run build` 全部通过；sdk/csharp 与 examples/csharp `dotnet build` 通过；sdk/nodejs 与 examples/nodejs `tsc` 通过。

## A 类：文档 ↔ 代码

| 条目 | 状态 | 处理方式 |
| --- | --- | --- |
| A1 信封 | ✅ 已改文档 | API-Spec §4/§7 按实际 `{success,data,requestId,timestamp}` 重写 |
| A2 错误码 | ✅ 已改文档+代码 | API-Spec §8.2 按 42 个实际码重写；运行时 `license.invalid_operation` 统一为 `license.InvalidOperation`（与字典一致，ExceptionHandlingMiddleware.cs:50） |
| A3 认证方案 | ✅ 已改文档 | API-Spec §2 按 Authorization 三方案重写 |
| A4 冒号路由 | ✅ 已改文档 | API-Spec §9 全改子路径风格 |
| A5 不存在端点 | ✅ 代码+文档 | 新增 `PATCH /v1/admin/licenses/{id}`（UpdateLicenseRequest：expiresAt/maxDevices）；文档删除 replay/openapi.json |
| A6 限流头 | ✅ 已改文档 | 按 Retry-After + body 字段重写 |
| A7 幂等 | ✅ 代码+文档 | IdempotencyMiddleware 补请求体 SHA256 校验，冲突返回 409 `idempotency_conflict`（新增 3 个测试）；文档改头名/方法集 |
| A8 端点总数 | ✅ 已改文档 | 更新为 ~500 |
| A9 匿名放行 | ✅ 已改文档 | Security-Design/API-Spec 修正为"默认公开、可配置收紧" |
| A10 数据库约定 | ✅ 已改文档 | Coding-Standards §5 改"现状+目标"两层 |
| A11 meta.roles | ✅ 已改文档 | Coding-Standards §4.5/§7.5 |
| A12 PII 脱敏 | ✅ 已改文档 | 标注路线图 |
| A13 分页 | ✅ 已改文档 | 现状（audit-log limit/offset）+ cursor 路线图 |
| A14 EF 45 实体 | ✅ 已改文档 | Migration-Guide/Troubleshooting/Release-Report 统一 45 |
| A15 测试数 | ✅ 已改文档 | 实测 2064（Release-Report/Testing-Strategy 已更新） |
| A16 audit_log | ✅ 已改文档 | Troubleshooting §10.3 表名与 SQL 修正 |
| A17 JWT 8h | ✅ 已改文档 | TAD 改现状，refresh 列路线图 |
| A18 基础设施 | ✅ 已改文档 | TAD/Deployment 改 MySQL+IMemoryCache+进程内 worker 现状 |
| A19 CLI 子命令 | ✅ 已改文档 | Deployment 改用 dotnet ef + 管理 API |
| A20 租约 checkout/1800s | ✅ 已改文档 | Beginner-Guide §5 重写 |
| A21 RDP 项目结构 | ✅ 已改文档 | 删除不存在的四项目 |
| A22 Core 指南 | ✅ 已改文档 | 删 Extensions、补 ActivateOrVerifyAsync |
| A23 Quickstart | ✅ 已改文档 | Go/Rust 示例按真实 SDK 签名重写 |
| A24 currentConsumption | ✅ 已改文档 | Beginner-Guide §6 |
| A25 最后 admin 守卫 | ✅ 已改代码 | UserService.DeleteAsync 拒绝删除最后一名 active admin（新增 UserServiceTests 4 例） |
| A26 类型 8 值 | ✅ 已改文档 | User-Manual §4 补注 |
| A27 LoginAttempt 内存 | ✅ 已改文档 | Troubleshooting 相关段落 |
| A28 迁移 10/租户 27 | ✅ 已改文档 | DB-Design/Migration-Guide/Troubleshooting |
| A29 HttpClient 清单 | ✅ 代码注释+文档 | DependencyInjection.cs:137 注释修正；Integration-Guide 补 currency/integration |
| A30 行号漂移 | ✅ 已改文档 | Troubleshooting 映射表按 50-57 行更新 |
| A31 license.issued | ✅ 已改代码 | 事件目录 35→46，补全全部实际发布事件（含 license.issued 等 11 条）；测试同步 |

## B 类：UI ↔ 服务端

| 条目 | 状态 | 处理方式 |
| --- | --- | --- |
| B1 partner 无后端 | ✅ 空数据存根 | 新增 PartnerController（30 端点：GET→空数据、写操作→501 feature_unavailable）；前端拦截器友好提示 + 全局未上线横幅 + 各视图补 catch |
| B2 sandbox 无后端 | ✅ 空数据存根 | 新增 SandboxController（12 端点同策略）；前端未上线横幅 + Dashboard 演示数据 badge |
| B3 status | ✅ 已修复 | .json bug 修复；类型对齐 /v1/status 真实结构；mock 全部删除改显式错误态/未上线卡片；Subscribe/RSS 假提交移除 |
| B4 embed | ✅ 已修复 | 默认 api-base→/v1；认证头→`Authorization: ApiKey`；`:disable`→`/disable`；激活载荷 snake→camel（同类 bug 一并修复）；服务端补三个 customer 设备端点（含测试） |
| B5 portal fallback | ✅ 已修复 | 删除 getProfileFallback 死路 |
| B6 适配层伪造 | ✅ 已修复 | issuer_url/support-tier code/CUST- 伪造全删，表单改必填（i18n 双语）；CustomerService 实现 Code 自动生成（3 测试）；死代码删除 |
| B7 mock | ✅ 已修复 | TicketList 去 mock 回退（客户下拉接 listCustomers 真实接口）；NotificationBell 对接 notification/logs |
| B8 端口 | ✅ 已统一 | 新增 launchSettings.json 固定 8080；5 个 vite 代理统一 8080 |
| B9 website/docs.html | ✅ 已修复 | 认证三方案 + /v1/customer/licenses/* 真实路径 |
| B10 expires-at 归属 | ✅ 已修复 | 移至 api/license.ts |
| B11 裸 get | ✅ 已修复 | 改用 getPciCompliance 封装 |
| B12 widget | ✅ 已修复 | 请求体 camelCase + renderResult 按信封/VerificationResult 真实结构重写（含 Base64 payload 解码） |
| B13 假数据标注 | ✅ 已修复 | sandbox 演示 badge、marketing 试用申请改"暂未开放"、status 订阅未上线 |
| B14 trial convert | ✅ 已修复 | trial.ts 注释区分两种语义 |
| B15 snake_case 响应 | ✅ 已修复 | reports/overview 改 camelCase；Dashboard.vue 同步 |

## C 类：SDK ↔ 服务端

| 条目 | 状态 | 处理方式 |
| --- | --- | --- |
| C1 C# renew | ✅ 已修复 | 发 `{validityDays}` |
| C2 PATCH/DELETE | ✅ 已修复 | 服务端补 PATCH；Go/Rust 删除 Delete 方法（只能吊销）；Rust Update 模型对齐 expiresAt/maxDevices |
| C3 checkout | ✅ 已补齐 | C#/Node/Python 均补 checkout + CheckoutResult |
| C4 C# extend | ✅ 已补齐 | ExtendAsync 发 `{extensionDays}`（与服务端 DTO 一致） |
| C5 包名 | ✅ 已统一 | Node 改 `@license-manager/sdk`，examples 同步 |
| C6 死代码 | ✅ 部分修正 | 仅 PutAsync 为死代码已删；PatchAsync/DeleteAsync 实际被 Product/Customer 服务使用予以保留（本报告 C6 原文描述已更正） |
| C7 一致项 | ✅ 无需处理 | examples/信封/构建产物均验证一致 |

## D 类：服务端内部

| 条目 | 状态 | 处理方式 |
| --- | --- | --- |
| D1 DashboardController | ✅ 已删除 | 功能由 /v1/admin/reports/* 覆盖 |
| D2 StubController | ✅ 已重构 | 删 12 个别名端点；拆分为 CatalogAdminController（15 端点）+ SupportAdminController（10 端点） |
| D3 GapAdminController | ✅ 已拆分 | 按域拆为 Security/Privacy/Messaging/LicensingOps/Commercial 五个控制器（90 端点路由不变）；{id} 补 :guid/:int 约束 |
| D4 lmcli | ✅ 已清理 | 删除空命令，保留 hardware-id/keygen；csproj 描述修正 |
| D5 注释残留 | ✅ 已修正 | DependencyInjection.cs:137 |

## 新增/变更测试

新增：UserServiceTests（4）、LicensePatchAndDevicesApiTests（8）、PartnerSandboxStubTests（34）、CatalogAdminControllerTests（16）、SupportAdminControllerTests（19）、AdminListEndpointsTests（15）；更新 IdempotencyMiddlewareTests、WebhookEventCatalogServiceTests、ExtendedApiTests、MiddlewareComprehensiveTests 等。Api.Tests 991→1031。

## 残留已知限制（不属本次范围）

1. Go/Rust/Python SDK 本机无工具链（go/cargo/python），改动经人工核对但未实机编译验证，建议在有工具链环境跑 `go test`、`cargo test`、`python -m compileall` 复核。
2. Playwright e2e（tests/e2e）需要运行中的 API(8080)+Vite(5173)，本次未执行。
3. PublicPathRegistry 中 /v1/partner、/v1/sandbox 为存根期公开前缀，功能实现后需收紧（代码已留注释）。
4. PATCH licenses/{id} 仅支持 expiresAt/maxDevices（LicenseRecord 实体字段所限），且不支持把 expiresAt 改回 null。
5. 事件目录同时收录 subscription.cancelled 与 subscription.canceled 两种拼写（服务层两处发布拼写不一），建议后续统一。


---

# 第二轮深度复查与修复（2026-09，修复后回验）

> 第二轮对修复后的代码做了全量回验，发现第一轮修复中引入/遗漏的问题并已全部修复。最终验证：`dotnet build` 0 错误 0 警告；`dotnet test` **2088 通过 / 0 失败**（Core 179 / Platform 672 / Infrastructure 153 / Server 32 / Api 1052）；web 全部 7 个可构建应用 + sdk/csharp + sdk/nodejs + examples 构建通过。

## 第二轮新发现并已修复（第一轮修复的遗漏/副作用）

### SDK ↔ 服务端

| 问题 | 级别 | 修复 |
| --- | --- | --- |
| activate/verify/checkout/heartbeat/refresh-file 响应的 `signedLicense` 是嵌套对象 `{payload,signature,keyVersion,issuedAt}`，五 SDK 全按 string/平铺字段建模——C#/Go/Rust 成功路径抛反序列化异常，Node/Python 静默错型 | P0 | 五 SDK 统一新增嵌套 `SignedLicense` 模型；删除平铺字段与不存在的 `license` 字段 |
| renew/extend 响应是 `{success,newExpiresAt,failureReason}` 而非 License，五 SDK 全部反序列化为空 License，且服务端"不存在/已吊销"返回 200+success:false | P0 | 五 SDK 新增 `RenewResult`/`ExtendResult` 模型，`success=false` 时抛出携带 failureReason 的错误 |
| 错误信封 `error` 为对象时 go/node/python/rust 丢失 code/message（C# 已在第一轮修复） | P1 | 四 SDK 的 parseErrorBody 对齐对象形式解析 |
| C# 契约测试 + Node 契约测试缺失 | P2 | 新增 sdk/csharp-tests/ContractTests（24 断言）与 sdk/nodejs test_contract（23 断言），固定信封+嵌套 SignedLicense 形状 |
| Python 缺 license PATCH update | P2 | 已补 |
| Go Update 注释声称支持 maxActivations/metadata（实际仅 expiresAt/maxDevices） | P2 | 注释修正 |

### 服务端

| 问题 | 级别 | 修复 |
| --- | --- | --- |
| checkout 未加入公开激活白名单，匿名 SDK 调用 401 | P1 | AuthenticationMiddleware 白名单补 checkout（+2 测试） |
| customer 设备端点无归属校验，任意认证身份可操作任意 licenseId | P1 | CheckLicenseOwnershipAsync：Bearer+customer 角色校验许可证归属（越权 403），ApiKey/Hmac/管理角色放行（+5 测试） |
| disable 按 hardwareId 全局拉黑，而激活/校验路径不查黑名单（无效且误伤同硬件其他许可证）；DELETE 删设备清单记录而非激活记录（席位不释放） | P1 | disable 改为停用该激活记录；DELETE 改为停平台激活记录 + Core 层递减席位，保留 Device 清单（测试同步改写） |
| 幂等中间件在认证之前，缓存回放可绕过认证 | P1 | 移到 Authentication/Authorization 之后，缓存键并入调用方身份（+2 测试） |
| renew/extend/PATCH 不同步 Core 许可证存储（verify 按旧到期时间判定） | P2 | 确认 Core verify 读 ILicenseStore，新增 SyncCoreLicenseStateAsync 同步 ExpirationDate/MaxDevices/Status（+3 Platform 测试） |
| renew/extend/PATCH 无参数校验（负天数可前移到期日等） | P2 | 0/负天数 400、maxDevices<-1 或 expiresAt 过去 400、吊销后 PATCH 400（+多个测试） |
| subscription.cancelled / canceled 双拼写并存 | P2 | 统一发布 canceled；目录保留 cancelled 并标注 deprecated |
| 事件目录 30/46 条无实际发布点 | P2 | 目录描述统一追加 " (planned)" 标注 |
| 激活/checkout 响应形状无契约测试 | P2 | 新增 LicenseResponseContractTests（3 用例） |

### UI ↔ 服务端

| 问题 | 级别 | 修复 |
| --- | --- | --- |
| embed LicenseActivator 读不存在的 data.attributes.license（激活成功被判失败）；FeatureCheck 缺必填 licenseKey query（恒 400）；LicenseStatus 用需客户 JWT 的端点（ApiKey 恒 401）；DeviceManager 读 snake_case 字段（服务端 camelCase） | P0 | 全部修复：读 success/signedLicense；组件新增 license-key 属性；LicenseStatus 改走公开 verify 端点；字段改 camelCase；demo mock 两处 bug（activationKey、/disable）同步修 |
| portal createTicket/replyTicket/uploadReqFile 发 multipart，服务端 [FromBody] JSON（恒 415）；transferDevice 字段与 TransferDeviceRequest 完全不匹配 | P0 | 改发 JSON（按服务端 DTO 字段）；附件控件禁用标注；transfer 表单补 newHardwareId |
| admin Dashboard 读 camelCase 字段，但拦截器已转 snake（两个统计卡恒 0）——第一轮 B15 的修复方向反了 | P0 | 视图改回读 snake_case |
| admin systemStatus 缺 /v1 前缀；SystemStatus 维护字段名错（maintenance_mode）；web/status 类型错用 camelCase（服务端字面量 snake） | P0/P1 | 全部对齐 |
| admin 报表导出参数名全错（恒 400）；DSR 导出期望 Blob 实际返回 {export_url} | P0/P1 | 参数对齐 + format 选择器 + blob 下载；DSR 改两步（创建→按 exportId 下载） |
| admin/sandbox/portal 错误拦截器把 error 对象当字符串显示 [object Object] | P1 | 三处统一 extractErrorMessage |
| admin EulaUseTypes 本地硬编码（服务端已有真实端点） | P1 | 接 GET products/{id}/eula-use-types + 产品选择器 |
| widget：signedLicense 恒 null 显示 "Expires: Never" 误导；失败显示机读 failureCode | P1 | payload 为空不显示 Expires；failureCode→人类文案映射 |
| website/docs.html 五个 SDK 示例全部虚构、错误码 HTTP 状态错误 | P1 | 按五语言真实 SDK 源码重写；错误码表改 422 等实际值 |
| sandbox 伪造 API key 回退；SdkPlayground 纯模拟运行 | P1 | 删伪造 key；模拟运行加显式标注 |

### 文档漂移（第二轮修复）

| 问题 | 修复 |
| --- | --- |
| API-Spec §8.2 错误码计数 41 vs 实际 42，feature_unavailable 归属矛盾（4 处） | 改 42 并入表，删除"未计入字典" |
| API-Spec §15.2 "7 个控制器"（实际 13）、partner/sandbox 误并入 /v1/admin、总数 ~500（实际 ~526） | 改 13、存根单列 42、合计 ~530 |
| API-Spec §2.4 限流主体漏"已认证按 JWT sub" | 已补 |
| RDP M52.4 Refresh Token 无"未实现"注记 | 已补 |
| Release-Readiness/Testing-Strategy 测试数 2064 → 2088（Api 1052 / Platform 672 / Api 40 个测试类） | 已更新 |

## 第二轮残留已知限制（新增）

1. Go/Rust/Python SDK 仍无本机工具链，两轮改动均人工核对，待有工具链环境补 `go build`、`cargo build`、`py_compile`。
2. 设备 disable 仅停用平台激活记录、不递减 Core 席位计数（DELETE 才释放）；如需 disable 也释放席位，一行调用即可加（LicensesController，见备注）。
3. admin EulaUseTypes 的增删改仍是本地操作（服务端该域只读）；portal 工单 category 表单字段服务端不接收（静默丢弃）；SystemStatus 的 started_at/ends_at 两行服务端不返回（恒显示 -）。
4. UserService admin 守卫的 TOCTOU 竞态与 Suspended admin 边界（P2，已记录）；幂等中间件并发同键竞态与 IMemoryCache 多副本失效（P2，部署多副本时应启用 Redis 分布式限流/幂等）。
5. `/v1/partner`、`/v1/sandbox` 仍在公开白名单（存根期），功能实现后需收紧。


---

# 第三轮：浏览器自动化测试（E2E）覆盖补全与全量执行

> 任务：核查 e2e 对全部功能的覆盖（含页面弹窗与多级弹窗），补全遗漏，执行全量浏览器自动化测试并验证提交/修改后的数据正确性。

## 覆盖差距与补全

**补测前现状**：web/admin/e2e 16 个 spec 为"广度冒烟"型——66 页全部有加载/导航/无 5xx 断言，但仅 pay-dialog 一个 spec 做了业务闭环；30 页 CRUD 只验证"对话框能打开、字段能填"，**无任何"提交后回读"断言**；多级弹窗（对话框上再弹确认框/二级对话框）零覆盖。web/portal 完全没有 e2e。

**基础设施（批次 0）**：`web/admin/e2e/helpers.ts` 新增可复用工具并从 fixtures re-export——`uniqueName`（并行安全命名）、`dialog(title)`/`drawer(title)`（按标题定位、无标题取最上层，支持多级弹窗栈）、`confirmBox`/`promptBox`（ElMessageBox 确认/输入型）、`fillForm`（label 驱动填表，支持 input/select/switch/date/number）、`submitAndVerifyToast`（提交+成功提示+关窗）、`expectRowWith`/`reloadAndExpectRow`（列表回读/刷新后回读）、`apiGet`/`apiPost`（带 token 直连服务端做字段级核对）。

**新增 9 个 spec（54 个用例，全部含 UI 回读 + 服务端 API 双重核对）**：

| Spec | 覆盖 | 多级弹窗点 |
| --- | --- | --- |
| licenses-lifecycle（6） | 新建对话框（级联下拉）、向导 5 步签发、详情页四操作（suspend/resume 用 prompt 输入原因、renew/resume confirm）、行内快操作、批量生成（对话框内结果渲染）、批量挂起/吊销 | prompt 型确认框、对话框内状态迁移 |
| ~~orders-payments（14）~~ | ~~订单创建/履约、详情→Pay→OrderPayDialog→取消支付三级确认、渠道切换重建 intent、退款/拒付、订阅取消/续订（结果对话框）、试用创建/转化/取消~~（已随商业化下线：该 spec 已随商业化裁剪移除） | 支付弹窗+确认框三层 |
| customers-tenants-crud（7） | 客户/租户/用户/角色完整 CRUD 闭环、最后 admin 删除守卫、个人资料改名回读（/v1/auth/me）、改密校验 | 删除确认+服务端 404 核对 |
| products-domain（7） | 产品/特性/模板/授权集/自定义字段/套餐（price 映射核对）/发布 | 编辑回读 |
| ~~billing-finance（5）~~ | ~~报价→详情→发票→Mark Paid、货币/税则/PSP 配置（凭据掩码不含明文）、信用账户创建→扣款→结算余额逐步核对~~（已随商业化下线：该 spec 已随商业化裁剪移除） | 掩码对话框 |
| compliance-system（6） | 法务文档创建→Publish（confirm）、API Key 创建→Delete(revoke)、PAT 创建→**Token Created 二级对话框**→列表回读、系统密钥轮换 version 递增、DSR 创建→Process、SSO（issuer_url 持久化核对） | PAT 顺序弹窗（创建→token 展示） |
| comms-support（6） | Webhook 端点→**签名密钥一次性二级对话框**、通知模板创建/编辑、事件创建→状态更新、工单（真实客户下拉）三方核对、KB 创建→Publish、支持等级（code 持久化核对）→分配客户 | 密钥一次性弹窗 |
| devices-lifecycle（4） | 令牌池创建→按 License 加载→Adjust Balance、公开激活→设备列表→黑名单（confirm）、离线激活上传 .req→**Generate Response（confirm）→下载事件**、批量签发 CSV→Execute（confirm）→许可证数量核对 | fulfill 确认框+浏览器下载 |
| portal e2e ×3（18） | portal 从零搭建（playwright.config + fixtures + 造数链路）；登录正/反/重定向、许可证列表/详情/激活查询、工单创建→列表→详情三方核对、发票/合同/下载/通知（发票/合同部分已随商业化下线） | — |

## E2E 过程中发现并修复的真实缺陷（14 处）

| # | 缺陷 | 修复 |
| --- | --- | --- |
| 1 | **服务端 Currency 创建整体丢弃 name**（AddCurrencyAsync 无 name 参数，DTO 也无） | AddCurrencyRequest/ICurrencyService/CurrencyService/Controller 补 name 透传（+测试通过 672/1052） |
| 2 | **SSO Provider 创建 400**：providerType 选项（OIDC/SAML 等）与服务端枚举（AzureAD/Okta/Auth0/Keycloak/GoogleWorkspace/Generic）完全不符 | SsoProviderList providerTypes 改为服务端六个枚举值 |
| 3 | **SSO scopes 类型不匹配**：UI 发数组，服务端要空白分隔字符串 | handleSave join(' ')；openEdit split(' ') 回填 |
| 4 | **PAT 创建 400**：UI 发 label/scope，服务端要 Name/Scopes | handleCreate 改发 name/scopes |
| 5 | **PAT 一次性 token 对话框永不弹出**：服务端返回 plainToken，UI 读 res.token（且响应经拦截器 snake 化为 plain_token） | 兼容 plain_token/plainToken |
| 6 | **PAT 列表恒空**：by-user 返回裸数组，视图读 res.items；且列表字段名错位（label vs name、scope vs scopes） | 数组兼容 + 字段回退 |
| 7 | **设备黑名单添加 400**：UI 把翻译文案当 BlacklistReason 枚举发送 | 行内/对话框均改发 ManualBlock 枚举，备注走 notes |
| 8 | **TokenPool Adjust 无效果**：UI 发 new_total，服务端要 TotalTokens | handleAdjust 改发 total_tokens |
| 9 | **批量操作完全无法提交**：表单校验绑定不存在的 form.file，校验永远失败 | 删除该校验（fileList 手动检查兜底） |
| 10 | **批量操作服务端契约不符**：UI 发 multipart 文件，服务端要 JSON（BatchIssueRequest 等） | handleExecute 重写：客户端解析 CSV/JSON→code/email 解析为 GUID→逐行调 JSON 端点 |
| 11 | **管理端离线激活提交 415**：UI 发 multipart，服务端要 JSON | offlineActivation.ts 重写：客户端解析 .req（JSON 或 UUID+hw= 文本）提取 license_id/hardware_id 后发 JSON |
| 12 | LicenseTemplate/EntitlementSet 创建伪造 `tpl-${Date.now()}`/`es-${...}` 假 Code（被中断代理遗留） | Code 改为表单必填字段（i18n 双语），删除伪造逻辑 |
| 13 | portal 登录失败无提示、下载列表信封不解包、Downloads 分类恒空、修改密码绕过旧密码校验、枚举大小写 i18n 错位、activation-lookup 渲染错位、admin SystemStatus 恒 '-' | 前端修复（前 3 项由 portal e2e 代理修复；后 4 项本轮修复） |
| 14 | 文档漂移：RDP/Testing-Strategy 等测试计数、限流主体描述、错误码计数 | 已同步（见第二轮） |

## 最终验证结果

- **web/admin 全量 e2e：26 个 spec，652 通过 / 0 失败 / 14 跳过**（跳过为数据条件依赖的既有用例），用时 18.1 分钟，Chromium
- **web/portal e2e：3 个 spec，18 通过 / 0 失败**
- **数据正确性**：所有 54 个新用例均执行"UI 列表/详情回读 + 服务端 API 字段级核对"双重断言（提交后 reload 再核对，证明真正落库；编辑后按 id 核对字段变化；删除后 API 404/状态核对；数值型字段如余额 0→-100→0、key version 递增、续期 365 天差值精确核对）
- **vue-tsc 零错误**构建通过；服务端 Platform 672 / Api 1052 测试全绿（含 Currency name 修复）
- 运行环境：API(:8080, SQLite 临时库) + Vite(:5173/5174)；`tests/e2e/` 下 2 个旧 JS spec 针对 website/ 静态页，不在本次范围

## 运行方式

```bash
# 先启动 API 与前端（或配置 webServer 自动启动）
dotnet run --project src/LicenseManager.Api --urls http://localhost:8080   # 需配置数据库
cd web/admin && npm run dev                                                # :5173
cd web/admin && npx playwright test                                        # 全量
cd web/portal && npm run test:e2e                                          # portal
```

## 残留限制

1. Go/Rust/Python SDK 无本机工具链，仍未实机编译。
2. E2E 需要真实 API + 数据库；本次用 SQLite 临时库（e2e-test.db）。建议后续在 playwright.config 中配置 webServer 自动启动 API（InMemory/SQLite）与 Vite，实现 CI 一键运行。
3. 多级弹窗覆盖已包含主要链路（支付三级确认、PAT 顺序弹窗、密钥一次性弹窗、fulfill 确认+下载）；对话框内再开下拉/日历等浮层未逐一遍历。


---

# 第四轮：功能完整性与缺失深度检查（2026-09）

> 任务：深度检查所有功能代码与文档，找出"没完善/缺失/逻辑错误"的部分。四个并行审查（Core 业务逻辑 / 异步链路与多实例 / 文档承诺缺口 / 前端死端）全部完成，证据均附 `文件:行号`。

## R4-A. 业务逻辑错误与缺口（Core/Platform）

### P0 逻辑错误（会出错/资损/锁死）

1. **满载许可证激活即变砖**：`StandardValidationRuleGroup` 把激活计数规则（`ActivationValidationRule`/`ConcurrentDeviceValidationRule`）混入 Verify/Heartbeat/RefreshFile 路径（`Core/LicenseManager.cs:161,210`）——MaxActivations 打满后所有后续 verify 返回 422 ACTIVATION_LIMIT_REACHED。上下文无"激活 vs 校验"区分标志。
2. **airgap-activate 不查库即签发**：任意登录客户可为任意字符串铸造 RSA 签名许可证（`CustomerController.cs:239-257`）；且 `LicenseGenerator.BuildPayload` 硬编码 NodeLocked、ExpiresAt=null——离线签名许可证永不过期、无类型/seats 约束。
3. **退款联动误伤**：`RefundLicenseLinkService.RevokeOnFullRefund:44-47` 挂起**全系统前 5 张许可证**（与退款订单无关，注释自认"演示"）。（已随商业化下线：退款链路已随商业化裁剪移除。）
4. **计费链路模拟**：催收扣款 `attempt.AttemptNumber >= 3 假装成功`（`SubscriptionLifecycleService.cs:69-70`）；订阅续期**零扣款**、PastDue 漏单、续期不联动许可证、无幂等防重（`SubscriptionRenewalExecutor`/`SubscriptionService.RenewAsync`）。（催收扣款部分已随商业化下线；订阅续期保留并由后台每小时自动扫描执行。）
5. **并发竞态全线失守**：激活/租约/令牌扣减均为 check-then-act（license 表无 RowVersion），并发激活可超签。
6. **AddFeatureAsync 不落库**：管理端给许可证加功能只写事件/webhook，从不写入 Core License.Features——客户端永不生效（`LicenseService.cs:591-620`）。

### P1 功能缺口（声称但未做）

7. **时钟防回拨（CLOCK_TAMPERED）零实现**：`LastKnownTime` 只塞上下文无任何规则消费（Beginner-Guide/RDP M12.5/M3.6 均声称为 P0 能力）。
8. **消费闭环断裂**：consume 端点恒 FEATURE_NOT_FOUND、`Feature.Consume` 无调用方、`ConsumptionService` 死代码。
9. GracePeriod/ClockSkewTolerance 无签发入口（模板无字段）；**无过期扫描后台任务**（LicenseStatus.Expired/Disabled 永远不可达）。
10. 离线 .resp/refresh-file 签名可伪造（HMAC 密钥=license key 本身 / 硬编码字符串，非 RSA）。
11. Deactivate 不清硬件绑定/ActivatedUsers（换机后永远 HARDWARE_MISMATCH）；Concurrent deactivate 无硬件校验。
12. UserLock 记录与校验条件不对称（MaxUsersPerDevice 模式永不生效）；`AllowedHardwareIds` 被 EF 映射忽略不落库。
13. Issue 批次无事务、Quantity 无上限；Fulfill 允许未付款订单；双退款路径校验不一致。
14. M55 事件目录 46 条中 31 条 "(planned)" 实际从不发布（license.activated/expires/trial.*/device.* 等），RDP"目录与发布一致"表述不实。

## R4-B. 异步链路与多实例（P0/P1）

1. **WebhookDeliveryWorker 超时逃逸退避**：catch 过滤器排除 OperationCanceledException，超时直接上抛 → 该条无退避紧密重投 + 本轮剩余全部跳过（:223）。
2. **多实例 Webhook 重复投递**：无原子认领，N 实例同投一条（:95-101）。
3. **Stripe/PayPal 密钥缺失时静默伪造支付意图**，无告警日志（StripePaymentAdapter.cs:74-81）——与 mock 生产守卫形成反差。（已随商业化下线：PSP 适配器已随商业化裁剪移除。）
4. **Deployment 告警规则引用的 lm_* 指标 80% 不存在**——告警永不触发（文档 :809-891 vs 实际仅 2 个指标产出点）。
5. **默认 appsettings.json 开箱即崩**：MySql + 空连接串 → EnsureCreatedAsync 抛异常。
6. 审计日志三处丢数据边界（队满/批次失败/停机不 drain）；到期提醒无调度器且窗口判定要求每日恰好一扫；SMTP 未配置返回 true → 通知日志假 Sent；IP 白名单/登录锁定/幂等/Nonce 多实例失效；PspConfig 密钥修改不回写适配器需重启；SQLite EnsureCreated 永不迁移（schema 漂移）；status/dependencies 匿名泄露依赖异常详情；metrics path 标签基数无界（内存泄漏）。

## R4-C. 文档承诺缺口（RDP M1-M60 总表要点）

- **完全空白**：M8 Bundle（仅枚举值）、M11 环境检测（VM/调试器，零代码）、M39 文档站（仅一个静态页）。
- **MVP P0 未落地**：M30.1 竞品对比、M30.3 公共 Demo（沙箱存根）、M30.4 Trial 自动签发、M34.1 公开路线图、M36.14 Cookie 同意管理。
- **自评高估**：M55 事件目录（31/46 从不发布）；Deployment lmcli 描述与实际不符。
- 其余缺口详见 RDP 总表（验证规则库编排 UI、组织层级、Reseller、浮动排队、灰度、迁移工具、SDK README、NPM/PyPI 发布管线等）。
- 覆盖率目标全部达标（行 86%：Api 85.7/Core 91.5/Platform 86.1/Infra 81.4/Server 94.1）；分支覆盖 62.4% 偏低。

## R4-D. 前端死端与完备性

### P0 功能失效
1. admin Webhook 投递分页失效（toDeliveryParams 丢弃 page 参数）。
2. marketing Contact 表单假提交（数据静默丢弃）。
3. website contact.html 假提交；download.html 全部 6 个下载按钮 `href="#"` 死链。

### P1 体验缺陷
- admin 27 个 i18n 键缺失渲染原始键名（BillingPlatformList/LocalPaymentList/IncidentList/OrderPayDialog）。
- EulaUseTypes 增删改仅内存生效（已披露但 toast 文案误导）。
- marketing Docs 外链 RouterLink 死端、Blog/Cases/Legal 死链。
- partner 注册步骤条 / sandbox 退出确认在英文界面显示中文（i18n 缺键）。
- embed 组件无请求超时（挂起时无限 loading）。

### 验证通过的方面
admin 菜单/路由/命令面板零死链、按钮 handler 全部有真实实现、63 表格全有空态、v-permission 与 PermissionConstants 完全对应、六应用 zh/en 键集互差为 0、手册 18 条抽查仅 1 条不符（webhook 端点"最后触发时间"列不存在）。

## 汇总优先级

- **P0 共 15 项**：6 项业务逻辑错误 + 5 项异步/运维 + 4 项前端死端
- **P1 共 21 项**：消费闭环、时钟防回拨、GracePeriod、多实例系列、i18n 缺键等
- **P2 增强**：SDK README、metrics 埋点等（coupon 语义校验与 MRR 假数据两项已随商业化模块下线而作废）


---

# 第四轮修复结果（2026-09，全部销号）

> 范围：用户指示"修复所有已发现问题（不管是否本次引入）"。R4 全部 P0/P1/P2 条目均已修复。
>
> **商业化裁剪注（2026-10-08）**：以下修复项中涉及退款/催收/支付/PSP/发票等资金流能力的代码已随商业化裁剪下线（对应修复成果随之移除）；订阅续期、激活、TokenPool 等许可语义修复仍然有效。

## R4-A 业务逻辑错误 — 已全部修复

1. **激活即变砖**：`LicenseValidationContext.IsActivation` 标志隔离激活计数规则，Verify/Heartbeat/Refresh 不再误判（+5 测试）。
2. **airgap 伪造**：airgap-activate 查库校验存在性/归属/状态/硬件绑定；`BuildPayload` 写真实 LicenseType/ExpiresAt/MaxDevices/Features；airgap-decode 比对 HardwareId（+7 测试）。
3. **退款误伤**：`Order.FulfilledLicenseIds` 精确关联，退款只挂起目标许可证（+7 测试）。
4. **功能不落库**：AddFeatureAsync 同步写入 Core store + Platform FeatureCodes 快照（+1 测试）。
5. **并发竞态**：Core License 加 RowVersion（客户端 GUID 盖章 + EF IsConcurrencyToken）、`LicenseConcurrencyException` + 读-改-写重试；100 并发激活 MaxActivations=5 恰好 5 成功的并发测试通过。
6. **Deactivate 残留**：清空硬件绑定、移除 ActivatedUsers、Concurrent 硬件校验（+3 测试）。
7. **UserLock 不对称**：记录条件与校验对齐（+1 测试）。
8. **AllowedHardwareIds 落库**：改可写 List + JSON 列（+2 测试；MySQL 迁移待补）。
9. **Renew 语义**：不再把 NotActivated 置 Active（+3 测试）。
10. **Issue 保护**：Quantity ≤1000 上限 + SaveRangeAsync 单事务（+3 测试）。
11. **状态机**：心跳路径自动迁移 Expired；新增 POST licenses/{id}/disable|enable 端点（+5 测试）。

## R4-A/B 计费链路 — 已全部修复

- 催收真实扣款（PSP 解析→CreateIntent+Confirm），删除"第 3 次假装成功"模拟；重试 1/3/5/7 天；Policy.None 立即终败。
- 续期先扣款后推进：失败转 PastDue + 发 subscription.payment_failed；幂等（周期已推进即跳过）；PastDue 纳入扫描；成功联动 LicenseService.RenewAsync。
- SubscriptionPolicy.GracePeriodDays 生效（宽限期内保持 Active 并排期重试）。
- 履约仅 Paid 订单（Pending→422），并发防重，部分签发补偿注释；`OrderDto` 新增 FulfilledLicenseIds 字段。
- 双退款路径对齐（金额/状态/余额校验）。
- TokenPool.Adjust 下限校验；MRR 按真实价格归一化（删 $50 假数据）；Coupon 百分比语义修复已随商业化模块下线。

## R4-B 异步/运维 — 已全部修复

- Webhook 超时走退避 + 整批不中断；多实例原子认领（Pending→Delivering 乐观并发 + 2 分钟租约回收）。
- Stripe/PayPal 密钥缺失告警 + 生产环境不注册 mock provider。
- 新增指标埋点：`lm_webhook_deliveries_total{result}`、`lm_webhook_deliveries_suspended`、`lm_subscriptions_renewal_failed_total{reason}`、`lm_queue_size{queue=audit}`；metrics path 标签改 RoutePattern（消除基数爆炸）。
- 审计：停机 drain、批次重试 3 次、AddRange 批量落库、队列容量可配（Audit:QueueCapacity/DropWhenFull）。
- SMTP 未配置返回 false + 通知记 Failed 含原因；新增 ExpiryReminderBackgroundService（每日扫描，24h 去重）。
- PspConfig 密钥修改实时回写适配器（WebhookSecretApplier 共用）。
- status/dependencies 异常详情不再外泄；readyz 状态值与文档统一。
- LoginAttempt/IpWhitelist/Nonce/Idempotency 四表 EF 持久化；DI 重复注册清理；SQLite 漂移检测告警；根导航 health 指向修正；RateLimit 注释对齐。
- **附带修复**：appsettings.Local.json 优先级曾压过环境变量（后注册覆盖），已在 Program.cs 补 AddEnvironmentVariables 恢复标准优先级（env > local > 默认）。

## R4-D 前端死端 — 已全部修复

- Webhook 投递分页重写（服务端仅支持 limit → 前端拉取后本地切片，翻页/筛选真正生效）。
- marketing Contact / website contact.html 假提交改明示"请邮件联系"；website download.html 全部死链改"即将发布"禁用态。
- admin 27 个 i18n 缺键双语补齐（billingPlatforms/localPayments/incidents/common.copied/copyFailed）+ pspProvider.razorpay 大小写修正。
- EulaUseTypes 本地操作改 warning 提示"本地预览"；partner/sandbox 中文兜底键入库；embed 加 AbortController 超时（30s 可配）；portal 动态状态键加 te() 回退。

## 文档 — 已同步

RDP（M55/M59 诚实化、M8/M11 未实现注记、MVP P0 债务披露小节）、Deployment（lmcli 实际命令、metrics 表逐项标注、告警规则提示）、Troubleshooting（readyz 状态值）、User-Manual（webhook 列集合、四角色矩阵）、Testing-Strategy（e2e 25+3 spec、覆盖率快照 86%）、API-Spec（airgap 端点条目、consume 链路标注）。**五个 SDK 补齐 README.md**（安装/快速开始/错误处理/webhook 验签，均按真实源码签名核对）。

## 最终验证（R4 收尾）

- 服务端全量：Core 195 / Platform 742 / Infrastructure 172 / Server 34 / Api 1065 = **2208 通过**（Api 有 1 项已知偶发，复跑全绿）
- e2e：portal 18/18；admin 受影响 spec 修复后全绿；最终全量见下文提交说明
- 服务端新增两项 e2e 驱动修复：公开激活后平台记录状态同步（NotActivated→Active，此前永远停留 notactivated）、订单履约签发的许可证在 `OrderDto.fulfilledLicenseIds` 暴露

## R4 遗留限制（已在代码/文档声明）

1. MySQL 迁移缺口：license.RowVersion/AllowedHardwareIds、license_record.FeatureCodes、四张安全台账表均无 EF 迁移（EnsureCreated 体系），生产 MySQL 需另立迁移任务。
2. ~~真实 off-session 扣款需 IPaymentProvider 增加 Confirm/Charge 能力（当前续期/催收依赖模拟收银台 confirm）~~（已随商业化下线：扣款/催收移交外部销售系统；订阅续期保留，无需扣款能力）。
3. （已作废）原 Coupon 百分比语义切换遗留项——Coupon 模块已随商业化裁剪下线，不再适用。
4. 多实例续期的严格防重依赖未来 DB 乐观锁（当前幂等为"周期已推进即跳过"）。
5. 审计死信为日志而非表。


---

# 复核勘误（2026-09-24）

> 本报告头部与各轮记录的数字（2004 / 2064 / 2088 / 2208、493 / ~526 / ~530）均为当时快照，保留作历史轨迹。以下为按当前代码（`src/`、`sdk/`、`tests/`）逐项复核后的最新基线与增量，均附实测依据。

1. **测试基线**：Api.Tests 实测 **1066**（A15/第二轮/第四轮记录的 979→1031→1052→1065 为历史快照），合计 **2209** 全绿（Core 195 / Platform 742 / Infrastructure 172 / Server 34 / Api 1066）。
2. **端点规模**：API 控制器 37 个，`[Http*]` 动作特性实测 **531** 个（A8 的 493、第二轮的 ~526/~530 均为当时值，API-Spec 如引用需以 531 为准）。
3. **新增端点**：`GET /v1/keys`（签名公钥分发，**匿名公开**——已加入 `PublicPathRegistry` 白名单，供离线客户端按 `SignedLicense.KeyVersion` 选钥本地验签）、`GET /v1/customer/licenses/revocations`、`POST /v1/customer/licenses/{licenseId}/airgap-sync`、`POST /v1/portal/offline/airgap-activate` + `POST /v1/portal/offline/upload-req`（`PortalOfflineController`，复用 `AirgapActivationService`）。
4. **SDK ↔ 服务端新一致项**：五个 SDK（csharp/go/nodejs/python/rust）均已落地**本地 RSA-SHA256 验签**（`SignedLicenseValidator` / `signature.go` / `signature.ts` / `signature.py` / `signature.rs`，支持 `GET /v1/keys` 公钥列表与按 keyVersion 选钥）、**浮动租约客户端**（LeaseService：`/v1/leases` checkout/heartbeat/release/status，`X-Api-Key` 认证）与**气隙配套方法**（airgap-activate/decode、revocations、airgap-sync），且各自带测试（C# `sdk/csharp-tests/ContractTests` 控制台断言程序、go `go test ./...`、nodejs `npm test`、python `pytest`、rust `cargo test`）——C 类历史条目（C1–C4）所述缺口已全部销号。
5. **认证双轨确认（对 A3/C 类结论的再验证）**：主 API 仅 `Authorization` 头三方案 `Bearer | ApiKey <keyId>:<secret> | Hmac <keyId>:<sig>`（`AuthenticationMiddleware`，统一 401 `auth.invalid_token`）；独立浮动服务器 `LicenseManager.Server` 仅 `X-Api-Key`（`ApiKeyMiddleware`：缺失/错误 401 `INVALID_API_KEY`，未配置 503 `SERVER_NOT_CONFIGURED` fail-closed）。两轨互不复用凭据。
6. **安全机制复核通过**：`LoginSecurityService` 15 分钟窗口内 5 次失败即锁定（`MaxFailedAttempts=5` / `LockoutDuration=15min`）；`SigningKeyStore`/`SystemKeyService` 密钥轮换版本递增、旧版本保留用于历史签名验签。与第四轮修复结论一致，无回归。
