Files
yunhai.netcore.tool/yh-platform-dotnet建设方案.md
2026-09-02 09:38:45 +08:00

371 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# yh-platform-dotnet 建设方案 v3 —— 从空文件夹到可发布 NuGet
> 版本:v3.0 | 日期:2026-08-28 | 状态:方案评审
> 定位:**平台侧工件建设** —— 建设与 Java `yh-platform` 同构的 .NET SDK(`yh-platform-net`),使平台基础能力(用户信息、/oapi 开放接口、应用签名、CAS SSO)具备跨语言辐射能力
> v2 增量:基于 GrowthHealth 既有 .NET 代码实证,给出可直接从空文件夹落地的工程实施方案(框架选型、仓库脚手架、命令序列、分阶段实施)
> v3 增量:把 v2 的多阶段门禁简化为**做完后一次性自检 + 功能逻辑审计核对**——建完后照单跑一遍,有问题修,没问题即收
***
## 一、消费方现状实证(决定 SDK 设计约束)
### 1.1 GrowthHealth 技术栈考古(2026-08-28 代码核查)
| 项 | 实证 | 对 SDK 设计的约束 |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 目标框架 | **net5.0**(五项目:Bsch.Web/Entity/Common/Repository/Service,[Bsch.Web.csproj:4](file:///d:/SoftProject/yunhai/GrowthHealth/service/Bsch.Web/Bsch.Web.csproj)) | SDK 最低必须可被 net5.0 消费 |
| JSON | **Newtonsoft.Json 13.0.1** + `AddNewtonsoftJson`(非 System.Text.Json) | 模型须 Newtonsoft 可反序列化(纯 POCO 即可,Newtonsoft 属性匹配大小写不敏感) |
| ORM | EF Core 5.0.9 + SqlServer + Dapper(本地 dll) | 与 SDK 无关,仅确认不冲突 |
| 旧平台 SDK | **BschOpenApi.dll**(本地 Library 引用,无 NuGet):`new BaseOpenApi(host, appKey, appSecret)`,命名空间 `BschBaseOpenApi`,类型 `BoaUser` / `CurrentRole`([ControllerAbstract.cs:18](file:///d:/SoftProject/yunhai/GrowthHealth/service/Bsch.Web/Controllers/ControllerAbstract.cs)、[SchClassService.cs:22](file:///d:/SoftProject/yunhai/GrowthHealth/service/Bsch.Service/SchClassService.cs)) | **yh-platform-net = BschOpenApi 的重生**。需提供同构构造函数形态 `(host, appKey, appSecret)` 降低迁移摩擦;`BoaUser` 类型名保留 |
| CAS 认证 | 本地 dll `AspNetCore.Security.CAS.dll` + Cookie scheme(`AddCasSso()`,[ServiceCollectionExtensions.cs:158-184](file:///d:/SoftProject/yunhai/GrowthHealth/service/Bsch.Web/Code/Extensions/ServiceCollectionExtensions.cs)) | Auth 包需内置等价 CAS handler,摆脱本地 dll |
| 配置 | 静态 `SiteConfig.AppSetting`(appsettings.json `AppTicket.AppKey=100476`、`UserCenterHost`) | SDK 用 Options 模式,同时提供静态兼容入口 |
| Redis | `Microsoft.Extensions.Caching.Redis 2.2.0`(旧 IDistributedCache 风格) | SDK 自带 StackExchange.Redis,不依赖消费方缓存栈 |
| 部署 | Docker `sdk:5.0`/`aspnet:5.0` | SDK 发布无运行时假定 |
### 1.2 核心判断(承 v1)
* yh-platform 的本质是"HTTP 契约 + 可移植协议 + 客户端封装",服务本体是 UserBase/CasSso/Redis,C# 侧做**同构 NuGet SDK**即可,无需 Java sidecar。
* **原则:不要让 .NET 去"依赖 Java",而是让两个 SDK 依赖同一份契约。**
* v2 新增判断:**BschOpenApi.dll 已证明".NET 版平台 SDK"这条路的可行性**(GrowthHealth 全面使用其形态);yh-platform-net 不是发明新事物,而是把它升级为:有源码、有版本、有 CI、对齐新 UserBase /oapi 契约(含 BoaRoleInfo 多角色体系)的正规工件。
### 1.3 建设目标
1. 产出 `yh-platform-net` 独立仓库与 NuGet 包族,能力与 `yh-platform` 同构,且 **net5.0 旧栈可直接消费**;
2. 产出跨语言契约工件(/oapi OpenAPI spec、签名协议规范、共享测试向量),确立 UserBase 为契约权威源;
3. 建立双 SDK 版本治理机制(集中版本管理 + 兼容矩阵 + 契约测试),防双端漂移。
## 二、目标框架与兼容策略(v2 新增,先拍板)
| 决策点 | 结论 | 理由 |
| -------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| SDK 目标框架 | **多目标** **`net5.0; net8.0`**(`netstandard2.0` 不选:需要 `IHttpClientFactory`/`AsyncLocal`/中间件等运行时能力,且 net5 消费方天然兼容) | net5.0 覆盖 GrowthHealth 现状;net8.0(LTS)覆盖新项目与未来升级 |
| 构建工具链 | .NET SDK **8.0**(可构建 net5.0 目标,targeting pack 经 NuGet 自动还原) | 单一工具链构建双目标 |
| 测试运行 | 单测跑 **net8.0**;net5.0 仅做编译 + 契约 fixture 反序列化冒烟(net5 runtime 已 EOL,不为测试单独装机) | 平衡成本与置信度 |
| JSON 序列化 | 模型**纯 POCO 零注解**:net5 消费方 Newtonsoft(大小写不敏感)天然兼容 camelCase;net8 内部用 `JsonSerializerOptions.Web` | 双序列化器兼容且不引入注解漂移 |
| 依赖策略 | SDK 最小依赖:`Microsoft.Extensions.Http` / `StackExchange.Redis` /(Auth 包)`Microsoft.AspNetCore.Authentication`——net5.0 版本线对齐消费方可用版本 | 避免 SDK 拖动消费方依赖升级 |
> 未决事项升级:若决定 GrowthHealth 先升级 .NET 8 再接入,可砍掉 net5.0 目标线(见 9.3 裁决 #1)。
## 三、仓库脚手架(从空文件夹开始)
### 3.1 目录结构
```
yh-platform-net/ # 独立仓库(与 Java 不混仓)
├── Directory.Build.props # 共享编译属性(多目标 TFM、LangVersion、确定性构建)
├── Directory.Packages.props # 集中包版本管理(= Java BOM 等价物)
├── yh-platform-net.sln
├── src/
│ ├── Yh.Platform.Abstractions/ # 模型与信封(对应 yh-utils-core)
│ │ └── Yh.Platform.Abstractions.csproj
│ ├── Yh.Platform.OpenApi/ # /oapi 客户端(对应 yh-openapi-client)
│ ├── Yh.Platform.AppAuth/ # HMAC 签名 + 防重放(对应 yh-app-auth-starter)
│ └── Yh.Platform.Auth/ # CAS SSO + 用户上下文(对应 yh-auth-starter)
├── tests/
│ ├── Yh.Platform.Abstractions.Tests/
│ ├── Yh.Platform.OpenApi.Tests/ # 含契约 fixture 对拍
│ ├── Yh.Platform.AppAuth.Tests/ # 含共享测试向量对拍
│ └── Yh.Platform.Auth.Tests/
├── samples/
│ └── Yh.Platform.Sample.Web/ # net8.0 示例应用(平台验收载体)
├── contract/ # 契约工件(单一事实源派生物)
│ ├── openapi-oapi.yaml # /oapi OpenAPI 3 spec
│ ├── sign-protocol.md # 签名协议规范
│ └── fixtures/
│ ├── sign-vectors.json # 共享签名测试向量
│ └── envelope-*.json # 信封解析 fixture(与 Java 对拍)
├── .gitignore # dotnet 标准模板
├── .gitea/workflows/ci.yml # 对齐 UserBase/Notice 的 Gitea CI 惯例
└── README.md
```
### 3.2 初始化命令序列
```powershell
# 前置:已安装 .NET SDK 8.0(dotnet --version >= 8.0)
mkdir yh-platform-net; cd yh-platform-net
git init
dotnet new gitignore
dotnet new sln -n yh-platform-net
# 四包 + 测试 + 示例(Abstractions 为 classlib,其余按依赖逐个建)
dotnet new classlib -n Yh.Platform.Abstractions -o src/Yh.Platform.Abstractions -f net8.0
dotnet new classlib -n Yh.Platform.OpenApi -o src/Yh.Platform.OpenApi -f net8.0
dotnet new classlib -n Yh.Platform.AppAuth -o src/Yh.Platform.AppAuth -f net8.0
dotnet new classlib -n Yh.Platform.Auth -o src/Yh.Platform.Auth -f net8.0
dotnet new xunit -n Yh.Platform.OpenApi.Tests -o tests/Yh.Platform.OpenApi.Tests -f net8.0
dotnet new web -n Yh.Platform.Sample.Web -o samples/Yh.Platform.Sample.Web -f net8.0
# 引用关系(镜像 Java 模块依赖图)
dotnet add src/Yh.Platform.OpenApi/src reference src/Yh.Platform.Abstractions/src # openapi → abstractions
dotnet add src/Yh.Platform.AppAuth/src reference src/Yh.Platform.Abstractions/src # appauth → abstractions
dotnet add src/Yh.Platform.Auth/src reference src/Yh.Platform.Abstractions/src # auth → abstractions
dotnet add src/Yh.Platform.OpenApi.Tests/src reference src/Yh.Platform.OpenApi/src
# ... 其余同理
# 多目标改写:手工把各 csproj 的 <TargetFramework> 改为
# <TargetFrameworks>net5.0;net8.0</TargetFrameworks>
# 并在 Directory.Build.props 固化公共属性
foreach f in (sln 里所有项目) { dotnet sln add $f }
dotnet build # 双目标全绿即脚手架完成
```
### 3.3 集中版本管理(BOM 等价物)
`Directory.Packages.props`(Central Package Management):
```xml
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<YhPlatformNetVersion>1.0.0</YhPlatformNetVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.7.33" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
```
> net5.0 目标线的扩展包版本(如 `Microsoft.Extensions.Http 5.0.x`)通过条件 ItemGroup 区分;`#if NET5_0_OR_GREATER` 符号处理 API 差异(预计极少——`IHttpClientFactory`/`DelegatingHandler` 自 2.1 起稳定)。
## 四、四包详细设计(对齐 Java 模块)
### 4.1 Yh.Platform.Abstractions(对应 yh-utils-core)
| C# 类型 | Java 对齐 | 说明 |
| -------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `BoaUser` | `utils.model.BoaUser` | 字段一比一;`CurrentRole` 属性类型 `BoaRoleInfo`(对齐 Java `BoaUser.currentRole`,**替代** GrowthHealth 本地 `Bsch.Entity.Custom.CurrentRole` 散落实现) |
| `BoaRoleInfo` | `utils.model.BoaRoleInfo` | roleId/roleName/schId/schName/childId/childName,`int?`/`string` |
| `BoaGroup` / `BoaModule` / `BoaApp` / `BoaAppType` | 同名 | 纯 POCO 零注解(双序列化器兼容,见 2) |
| `ApiEnvelope` | `ApiResult` | `{code,msg,data}` 信封 + `IsSuccess` 判别(对齐 `AbstractOpenApiClient.unwrap()` 语义) |
| `DException` | `utils.exception.DException` | 携带 code/msg/data;`code≠0` 时由 OpenApi 包抛出 |
| `YhErrorCode` | `ErrorCode`/`IErrorCode` | 错误码抽象 |
### 4.2 Yh.Platform.OpenApi(对应 yh-openapi-client + starter)
| C# 类型 | Java 对齐 | 说明 |
| -------------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `YschBaseOpenApi` | 门面同名 | **静态门面 + 实例构造** **`(baseUrl, appKey, appSecret)`** **双形态**:前者对齐 Java `YschBaseOpenApi` 静态入口;后者复刻 BschOpenApi `BaseOpenApi(host, appKey, appSecret)` 构造习惯,GrowthHealth 迁移时改 using 即可 |
| `IOpenApiUserClient` / `OpenApiUserClient` | `OpenApiUserClient`/`Default*` | 用户域:byId/byLoginName/byPhone/roles/identity current+switch/password/groupState/transferToSchool 等,端点清单从 Java 接口 javadoc 反推 |
| `IOpenApiGroupClient` / `OpenApiGroupClient` | 同名 | 组域:tree/byId/list/add/edit/remove |
| `IOpenApiAppClient` / `OpenApiAppClient` | 同名 | 应用域:byId/list |
| `OpenApiClientBase` | `AbstractOpenApiClient` | **信封感知解析单点**:成功裸数据 → 反序列化;HTTP 200 + `code≠0` → 抛 `DException`(携带 code/msg/data);`PREFIX="/oapi"` |
| `OpenApiOptions` | `OpenApiProperties` | `yh:open:` 配置节(base-host/base-path/app-key/app-secret/超时,默认值对齐 Java:connect 1500ms / read 5000ms) |
| `ServiceCollectionExtensions.AddYhOpenApi()` | `OpenApiAutoConfiguration` | `IHttpClientFactory` 注册 + typed client + `DelegatingHandler` 挂签名 |
### 4.3 Yh.Platform.AppAuth(对应 yh-app-auth-starter)
| C# 类型 | Java 对齐 | 说明 |
| ----------------- | ----------------------- | --------------------------------------------------------------------------- |
| `SignUtil` | `utils.crypto.SignUtil` | `HmacSHA256` 静态方法集,输出格式逐一对齐(见契约 `sign-protocol.md` 裁决) |
| `AppSignHandler` | `AppSignInterceptor` | `DelegatingHandler`:自动附加 `X-App-Key/X-Timestamp/X-Signature` 头(对齐 Java 头契约) |
| `SignKeyResolver` | `SignKeyResolver` | 三层:Redis → IMemoryCache → UserBase `/oapi/internal/sign-key/{appKey}` 兜底 |
| `ReplayGuard` | 防重放 | nonce + 5min 窗口,Redis 键空间与 Java 约定一致 |
| `AppAuthOptions` | `yh.app-auth.*` | 配置节 |
### 4.4 Yh.Platform.Auth(对应 yh-auth-starter)
| C# 类型 | Java 对齐 | 说明 |
| -------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `CasAuthenticationHandler` | `AddCasSso()` 用到的本地 dll | 自研 CAS 远程认证 handler(AuthenticationHandler 基类),替换 `AspNetCore.Security.CAS.dll`:CasServerUrlBase、service ticket 校验 `/serviceValidate` |
| `BoaUserContext` | `UserContextHolder` | `AsyncLocal<BoaUser>`(替代 Java ThreadLocal 语义) |
| `IUserDetailsProvider` | `UserDetailsProvider` SPI | 默认实现走 `IOpenApiUserClient` 换取 `BoaUser` |
| `AddYhAuth()` | `YhAuthAutoConfiguration` | Cookie + CAS 认证注册 + 用户上下文中间件 |
**聚合入口**:`AddYhPlatform()` 一次带齐(等价 Java `yh-spring-boot-starter`),也可按包拆引。
## 五、契约工件(MVP 前置门禁,承 v1 第六章)
1. `contract/sign-protocol.md`:权威签名方案(数据级 `data|ts` 小写 hex vs 请求级 `METHOD\nurl\nts` Base64——需裁决)、验签大小写、时间戳毫秒、5min 窗口;
2. `contract/openapi-oapi.yaml`:UserBase `controller.oapi` 四域全端点(springdoc 导出 + 人工补 `oneOf` 错误信封),排除 `/oapi/internal/**`;
3. `contract/fixtures/sign-vectors.json`:≥10 组签名向量(**由 Java** **`SignUtil`** **实际运行产出基线**,含空 body/超长参数/时间戳边界);
4. `contract/fixtures/envelope-*.json`:信封样例(成功裸数据/失败 code=4/5),双端解析对拍。
## 六、构建、CI 与发布
| 环节 | 方案 |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| 本地构建 | `dotnet build`(双目标);`dotnet test`(net8.0 全量) |
| 契约对拍 | 测试内加载 `contract/fixtures/*.json`(`CopyToOutputDirectory`),断言 C# 输出与 fixture 逐字节一致 |
| CI(Gitea,对齐 UserBase/Notice 惯例) | `dotnet build -c Release` → `dotnet test` → `dotnet pack -c Release -o artifacts`;Java 侧 CI 增加一步"重算签名向量 fixture"防漂移 |
| 发布 | `dotnet nuget push` 到 Nexus 3 NuGet 仓库(`nexus.yunhaixingtong.com`),与 Maven 同私服 |
| 版本策略 | SemVer;包版本统一随 `YhPlatformNetVersion` 走(对齐 BOM 集中钉版本);兼容矩阵登记 `yh-platform ↔ yh-platform-net ↔ /oapi 契约` |
## 七、分阶段实施步骤(每步有完成标志)
| 阶段 | 内容 | 完成标志 |
| ------------------- | -------------------------------------------------------------------------------- | ------------------------------------------- |
| **P0 环境与裁决** | 装 .NET SDK 8;裁决 9.3 的 #1(net5.0 目标线去留)与 #2(签名方案);Java 侧跑出 `sign-vectors.json` 基线 | `dotnet --version ≥ 8`;裁决记录进本文档 9.3 |
| **P1 契约提纯** | 产出第五节四件契约工件 | 自检清单 A 全勾 |
| **P2 脚手架** | 第三节命令序列跑通,空壳四包 + 测试 + sample 编译绿 | `dotnet build` 双目标零 warning(新增代码零 warning) |
| **P3 Abstractions** | 全模型 + 信封 + 异常;用 `envelope-*.json` 做反序列化对拍测试 | Abstractions.Tests 绿 |
| **P4 AppAuth** | `SignUtil` + `AppSignHandler` + `SignKeyResolver`;`sign-vectors.json` 100% 对拍 | AppAuth.Tests 绿,含向量逐条断言 |
| **P5 OpenApi** | 三域 client + 门面双形态 + `AddYhOpenApi()`;对接 UserBase 测试环境端到端 | sample 应用成功取到 `BoaUser`(含 currentRole)与角色列表 |
| **P6 Auth** | CAS handler + `BoaUserContext` + `AddYhAuth()` | sample 应用 CAS 登录 → 上下文注入 `BoaUser` |
| **P7 打包发布** | `dotnet pack` + Nexus push + CI 固化 + 兼容矩阵登记 | Nexus 可 `dotnet add package` 拉到 |
| **P8 消费方就绪文档** | 《.NET 消费方接入指南》(appKey 注册、配置样例、从 BschOpenApi 迁移对照表) | 指南成文并评审 |
## 八、完成后自检清单(Self-Check)
> 用法:**SDK 建完(P0\~P6 收尾)后,一次性照单跑一遍**。全部打勾即合格;任何一项不勾,修完再勾,不留尾巴。
### 8.1 工程自检(有没有问题)
* [ ] `git clone` 空机器 + `dotnet build` 一次通过(含 net5.0;net8.0 双目标,前提是裁决保留了 net5.0 线)
* [ ] `dotnet test` 全绿(含契约 fixture 对拍)
* [ ] 无 `#if` 之外的 TFM 差异残留;无 TODO/FIXME 遗留
* [ ] 包引用关系正确:OpenApi/AppAuth/Auth → Abstractions,无反向依赖
* [ ] `Directory.Packages.props` 全部包版本集中管理,csproj 内无散落版本号
* [ ] NuGet 包元数据完整(作者、描述、仓库地址、license)
* [ ] `dotnet pack` 产物可被空控制台项目 `dotnet add package` 正常引用还原
### 8.2 契约自检(和 Java 侧对不对得上)
* [ ] `sign-vectors.json` 全部向量 C# 输出逐字节一致(100% 对拍)
* [ ] `envelope-*.json` 全部样例:成功裸数据反序列化正确;失败信封抛 `DException` 且携带 code/msg/data
* [ ] 模型字段与 Java `BoaUser/BoaRoleInfo` 等一比一(含 `BoaUser.CurrentRole: BoaRoleInfo` 嵌套)
* [ ] 同一份 fixture JSON 用 Newtonsoft(net5 形态)与 STJ(net8 形态)都能正确反序列化
* [ ] `X-App-Key/X-Timestamp/X-Signature` 头名与 Java 逐字一致
### 8.3 运行自检(跑起来对不对)
* [ ] sample 应用签名调 /oapi 成功取到 `BoaUser`(含 currentRole)
* [ ] CAS 登录 → `BoaUserContext` 注入正确;请求结束上下文清空
* [ ] 超时默认 connect 1500ms / read 5000ms 生效
* [ ] 防重放:同一签名 5 分钟内二次请求被拒
## 九、审计核对(功能逻辑逐项验证)
> 用法:**自检全勾后**,按功能逐项核对"输入 → 预期行为",确认逻辑与 Java 侧等价。核对方式:跑 sample/单测实际验证,不靠读代码。每项给结论:通过 / 不通过(说明原因)。
### 9.1 核对表
| # | 功能 | 核对内容(输入 → 预期) | 方式 | 结果 |
| -- | --------- | ----------------------------------------------------------------------------- | -------------- | ------------------------------------------------- |
| 1 | HMAC 签名生成 | 固定 `(data, ts, secret)` → 输出与 Java `SignUtil` 完全一致 | 测试向量对拍 | ✅ 通过(向量 100% 对拍 + 独立实现抽检 6/6) |
| 2 | 签名请求头附加 | 任意经 SDK 发出的 HTTP 请求 → 自动带三头,值与手算一致 | 抓包/单测 | ✅ 通过(P1 修复后回归:handler 对 fullUrl 签名与 Java 基线逐字节一致) |
| 3 | 信封成功分支 | /oapi 返回裸数据/裸数组 → 正确反序列化为模型;数组含单元素与多元素 | fixture 单测 | ✅ 通过 |
| 4 | 信封失败分支 | HTTP 200 + `{code:4/5, msg, data}` → 抛 `DException`,code/msg/data 原样携带 | fixture 单测 | ✅ 通过(data 载体更名 `EnvelopeData`,P2 修复) |
| 5 | 业务异常等价性 | Java 侧 `catch(DException)` 的代码,C# 侧 `catch(DException)` 能拿到同样的 code/msg | 对照 | ✅ 通过 |
| 6 | 用户查询三入口 | byId / byLoginName / byPhone → 同一测试用户返回字段一致的 `BoaUser` | sample 实调 | ⏳ 待环境(UserBase 测试环境 + appKey 注册) |
| 7 | 多角色读取 | `getUserRoleList` → 角色列表字段完整(roleId/roleName/schId/schName/childId/childName) | sample 实调 | ⏳ 待环境 |
| 8 | 当前身份 | `getCurrentIdentity` → 与 UserBase `sys_user_current_role` 状态一致;未切换时回退主身份 | sample 实调 | ⏳ 待环境 |
| 9 | 身份切换(若需) | `switchIdentity` → 白名单 appKey 通过、非白名单拒绝、三元组非法拒绝 | sample 实调 | ⏳ 待环境 |
| 10 | 防重放 | 同 timestamp+nonce 二次请求 → 拒绝;5 分钟前时间戳 → 拒绝 | 实调 | ✅ 通过(逻辑单测:MemoryReplayGuard + 双向偏差窗口) |
| 11 | 密钥解析链 | Redis 有值 → 不请求 UserBase;Redis 空 → 兜底取到并回填缓存 | 断点/日志 | ✅ 通过(代码级核对,兜底实调待环境) |
| 12 | CAS 登录 | sample 走完 CAS 重定向 + ticket 校验 → 登录态建立、`BoaUser` 注入 | 浏览器实走 | ⏳ 待环境(CAS server 联通) |
| 13 | 用户上下文生命周期 | 请求内任何层可取 `BoaUserContext.Current`;请求结束不串到下一请求(并发压一下) | 单测/实调 | ✅ 通过(单测) |
| 14 | 错误传播 | UserBase 不可达/超时 → 抛超时类异常而非挂死;HTTP 非 2xx → 异常带状态码 | 实调(断 UserBase) | ✅ 通过(FakeHandler 模拟 500/异常路径) |
| 15 | 双形态门面 | `YschBaseOpenApi` 静态门面与 `new (baseUrl, appKey, appSecret)` 实例,行为一致 | 单测 | ✅ 通过 |
### 9.2 审计结论记录
> 全部核对完成后填写;"不通过"项须附原因与修复决定。
| 日期 | 核对人 | 通过 / 不通过项 | 遗留问题与决定 |
| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------- |
| 2026-08-28 | 架构验收(工具辅助) | **通过 11 项**(#1 签名向量、#3/#4 信封双分支、#5 异常等价、#10 防重放逻辑、#11 密钥链代码、#14 错误传播、#15 双形态门面等);**不通过 1 项(#2)**;待环境 3 项(#6\~#9、#12 实调类) | 见下方 2026-08-28 验收详情 |
**2026-08-28 验收详情(BasePlatformDotnet,测试 67/67 绿)**:
* **不通过 #2(P1 阻断)**:`AppSignHandler` 签名 target 未剥离 basePath。实证:Java 客户端 `AppSignInterceptor` 签名前 `stripBasePath`(配置 base-path=/userbase),服务端 `AppAuthFilter.buildSignedTarget` 剥 `request.getContextPath()`——两端验签口径为 `/oapi/**`;而 C# `AppSignHandler` 用 `uri.AbsolutePath`(含 `/userbase` 前缀)。UserBase 挂 context-path `/userbase` 时 C# 出站签名必被服务端拒绝。**根因连带**:`sign-vectors.json` 请求级向量的 `canonicalTarget` 含 `/userbase` 前缀(生成时 Java 侧 basePath 未配置),向量与 C# 实现"自洽"但均偏离服务端口径。**修复决定**:AppSignHandler 增加 basePath 剥离参数;同步修正请求级向量(canonicalTarget 去 `/userbase` 前缀)并在 `sign-protocol.md` 写明剥离规则;补一个"剥离后与 Java 拦截器同 URL 签名一致"的对拍测试。
* **P2**:`DException.Data` 遮蔽 `Exception.Data`(编译警告 CS0114,违反"零 warning"自检项)。`Exception.Data` 是 IDictionary 语义,被 `object?` 遮蔽后框架代码按基类契约访问会异常。决定:改名 `EnvelopeData`(或至少显式 `new` + 文档化),消除警告。
* **P3 观察项**:path 解码差异——Java 拦截器 `URLDecoder`(`+`→空格)vs C# `AbsolutePath`(保留 `+`);pack/Nexus push 未实际执行(P7 范畴);8.3 运行自检整组待 UserBase 测试环境(appKey 注册、网络可达)后补验 #6\~#9、#12。
* **独立验证**:签名向量经第三方实现(独立 HMACSHA256 脚本)抽样 6/6 MATCH,确认拼接顺序/编码/大小写跨实现一致。
**2026-08-28 修复执行记录(P1/P2 已闭环,回归 83/83 绿、双目标构建零 warning)**:
* **P1 basePath 剥离**:`SignCanonical` 新增 `StripBasePath`(逐行对齐 Java `stripBasePath`,含尾斜杠归一化与前缀不完整匹配不剥离边界)与 `BuildTarget(Uri, basePath)` 重载;`AppSignHandler` 增加 basePath 参数(`NormalizeBasePath` 归一化,空=挂根不剥离),DI 扩展与 `BaseOpenApi` 均已接线传入 `OpenApiOptions.BasePath`;时钟源统一走 `SignUtil.NowMillis`(可注入,测试可固定 ts 与基线逐字节对拍)。
* **P1 向量修正**:`sign-vectors.json` 六个请求级用例 canonicalTarget 去 `/userbase` 前缀并以独立 HMAC 实现重算期望值;新增 `fullUrl`/`basePath` 字段复现剥离前 URL;`requestLevel.note` 写明剥离口径。
* **P1 对拍测试**:新增 `AppSignHandlerTests`(6 个向量驱动的 handler 对 fullUrl 签名与 Java 基线逐字节一致、剥离+query 排序、未配置 basePath 不剥离、7 组 StripBasePath 边界、BuildTarget 双形态)。
* **P2**:`DException.Data` → `EnvelopeData`(消除 CS0114 对 `Exception.Data` 的遮蔽),`sign-protocol.md` 2.2 节补 basePath 剥离规则。
* **协议文档**:`sign-protocol.md` 2.2 新增"basePath 剥离(客户端与服务端共同口径)"条款。
* 遗留:P3 观察项(path `+` 解码差异)与待环境项(#6\~#9、#12、Nexus push)不变。
**2026-08-28 双端同步闭环(契约快照对拍机制建成)**:
* **流程补课**:UserBase 加 `/oapi` 文档分组(`SwaggerConfig.oapiGroup`:pathsToMatch /oapi/\*\* + pathsToExclude /oapi/internal/\*\*;prod 用 `SPRINGDOC_API_DOCS_ENABLED=false` 关闭)→ 真实导出 `contract/openapi-oapi.exported.json` 入库作为权威基线 → C# 侧 `ContractSnapshotTests`(15 个用例)对拍,双端同步机制正式生效。此前"手写 yaml 自称权威源"的流程偏差就此纠正。
* **对拍机制**:①端点对拍——SDK 路由表 ⊆ 快照(严格失败,UserBase 删改端点立刻红);快照多出的端点须显式登记白名单否则失败(防无声失联);②模型对拍——13 个 C# POCO 属性集(camelCase)⇔ 快照 schema properties 键集双向差额为零(反射驱动,UserBase 改字段立刻红)。
* **首战战果(快照暴露 4 个 SDK 真漂移,全部已修)**:① `SysUserRequest.Roles` → `RoleIds`(序列化键 `roles` 服务端收不到,会实际跑坏);② `SysUserRequest` 补 `SchoolId`;③ `SysGroupRequest` 补 `Id`;④ 新增 `GetAvailableIdentitiesAsync`(/oapi/identity/available,多角色配套端点此前缺失)。另修正手写 yaml 4 处文档偏差(byId id / appId 类型、clientType required、roles→roleIds+schoolId)。
* **基线更新流程**(写入测试 xml doc):UserBase 契约变更 → 起服务 `GET /v3/api-docs/oapi` 覆盖 exported.json → 跑测试,红了即漂移,同步修 SDK 或登记白名单。
* 回归:测试 98/98 绿(+15)。
### 9.3 方案合规核对(建设前依据,承 v2 方案审计)
| 审计项 | 结论 | 依据 |
| ------------------------ | -- | ----------------------------------------------------- |
| 是否避免为 .NET 引入额外常驻运行单元 | 通过 | 无 sidecar、无网关耦合 |
| 是否与既有 yh-platform 架构风格一致 | 通过 | 四包 1:1 镜像;"core 纯契约 + 装配扩展"分层同源 |
| 是否与既有 BOM/私服治理一致 | 通过 | CPM = BOM 等价物;Nexus 3 同私服承载 NuGet |
| 是否复用既有基础设施 | 通过 | springdoc 已在 UserBase;Redis/CasSso 复用;模型编译期共享(Java 侧) |
| 影响面是否最小化 | 通过 | yh-platform 零改动;UserBase 仅配置级(spec 分组) |
| **兼容旧栈**(v2 新增) | 通过 | 多目标 net5.0;Newtonsoft 兼容模型;`BaseOpenApi` 构造形态复刻 |
| **可从空文件夹直接落地**(v2 新增) | 通过 | 第三节命令序列 + 第七节分阶段步骤,每步有完成标志 |
| 淘汰路径 | 通过 | 阶段化交付可独立回退;最坏降级方案 C(sidecar)过渡 |
### 9.4 消费方代码实证(2026-08-28,建设依据)
| 核查对象 | 核查点 | 结果 |
| ----------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `GrowthHealth/service/Bsch.Web/Bsch.Web.csproj` | 目标框架/依赖 | 实证:net5.0;Newtonsoft 5.0.2 适配器;本地 dll 引用 BschOpenApi/AspNetCore.Security.CAS/log4net |
| `Bsch.Common.csproj` | 基础设施栈 | 实证:Redis 缓存 2.2.0(旧式)、Newtonsoft 13.0.1、Serilog 4.1.0 |
| `Bsch.Repository/Repository/UserInfo.cs` | 旧 SDK 用法 | 实证:`new BaseOpenApi(UserCenterHost, appKey, appSecret)`;`BoaUser` 来自 `BschBaseOpenApi` 命名空间;CurrentRole 为本地实体+请求头拼装 |
| `ControllerAbstract.cs` | 控制器基类消费模式 | 实证:每个控制器 new 一个 BaseOpenApi(无 DI/无连接复用)——新 SDK 需以 IHttpClientFactory 修正此反模式 |
| `ServiceCollectionExtensions.cs`(AddCasSso) | CAS 认证形态 | 实证:Cookie + `AddCAS()`(本地 dll);CasServerUrlBase 取自配置 |
| `appsettings.json` | 配置契约 | 实证:`AppTicket.AppKey=100476`、UserCenterHost/SsoHost/SchClassHost——新 SDK Options 节需给出从旧键迁移的映射 |
| `Dockerfile` | 部署形态 | 实证:sdk:5.0/aspnet:5.0,aspnet:5.0 基础镜像已 EOL(升级建议列入消费方指南) |
| Java 侧核查(承 v1) | SignUtil 双规则/信封/springdoc/模型共享 | 结论不变:内容合格、形式待提纯;详见 v1 审计表 |
### 9.5 未决事项(待裁决)
| # | 事项 | 裁决人 | 截止 | v2 备注 |
| - | ------------------------------------------------------------ | ------------------- | ---- | -------------------------------------------------------- |
| 1 | 保留 `net5.0` 目标线,还是要求消费方先升 .NET 8 | 平台负责人 | P0 | 保留=SDK 复杂度+双目标;砍掉=GrowthHealth 必须先升级(含 EF Core/基础镜像连带升级) |
| 2 | 权威签名方案:数据级(`data\|ts` 小写 hex)还是请求级(`METHOD\nurl\nts` Base64) | 平台负责人 | P1 前 | 旧 BschOpenApi 实际用哪套可作为裁决输入(反编译或抓包确认) |
| 3 | 请求级验签大小写敏感是否设计意图 | yh-platform 维护者 | P1 前 | — |
| 4 | C# 模型手写 vs NSwag/Kiota 生成 | yh-platform-net 负责人 | P3 前 | v2 倾向手写(模型量小、需 Newtonsoft 零注解兼容) |
| 5 | 双签名规则长期收敛 | 平台负责人 | 长期 | 不阻塞 |
### 9.6 变更记录
| 日期 | 版本 | 说明 |
| ---------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-08-28 | v1.0 | 初版:平台工件建设定位,方案选型、架构、影响面实证、自检清单、审计 |
| 2026-08-28 | v2.0 | 基于消费方实证(GrowthHealth net5.0 + BschOpenApi.dll 考古):新增目标框架兼容策略(net5.0;net8.0 多目标)、仓库脚手架命令序列(Directory.Build.props/CPM)、四包详细设计(含 `YschBaseOpenApi` 双形态复刻 BaseOpenApi 构造习惯)、分阶段实施步骤、自检清单 B 工程门禁、审计新增兼容旧栈与空文件夹可落地两项 |
| 2026-08-28 | v3.0 | 自检/审计简化落地:多阶段门禁改为"完成后一次性自检"(工程/契约/运行三组)+ "功能逻辑审计核对表"(15 项输入→预期核对,含审计结论记录表);原方案审计/代码实证降级为建设依据附录 |