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

43 KiB
Raw Permalink Blame History

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) 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、SchClassService.cs:22) yh-platform-net = BschOpenApi 的重生。需提供同构构造函数形态 (host, appKey, appSecret) 降低迁移摩擦;BoaUser 类型名保留
CAS 认证 本地 dll AspNetCore.Security.CAS.dll + Cookie scheme(AddCasSso(),ServiceCollectionExtensions.cs:158-184) 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 初始化命令序列

# 前置:已安装 .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):

<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 项输入→预期核对,含审计结论记录表);原方案审计/代码实证降级为建设依据附录