# LicenseManager 发布及部署指南（v1.0）

> 读者：厂商发布工程师 / 实施工程师。覆盖从源码到客户可用环境的完整链路：
> **厂商侧发布（publish + 封签）→ 交付 → 客户侧部署 → 部署授权导入 → 上线验证**。
> 平台架构、功能说明见《LicenseManager-Platform-Deployment-Guide-v1.0.md》；本文档只讲"怎么发、怎么装"。

---

## 1. 角色与环境总览

| 角色 | 环境 | 工具 | 职责 |
| --- | --- | --- | --- |
| 厂商发布工程师 | 厂商内网（Windows） | `tools/LicenseIssuer`（GUI / CLI） | 密钥保管、构建发布包、封签、签发部署授权 |
| 实施工程师 | 客户机房 / 云主机 | 部署包 + 配置 | 安装运行时、落盘配置、启动服务、导入部署授权 |

发布物一览：

```
publish/
├── api/                     # 主平台 API（LicenseManager.Api）+ integrity.manifest
├── server/                  # 浮动服务器（LicenseManager.Server，可选）+ integrity.manifest
├── web-admin/               # 管理端前端（web/admin 构建产物，部署到 nginx）
└── deployment-license.json  # 部署授权文件（厂商 issue 签发，按客户档位）
```

---

## 2. 发布前置条件（厂商侧）

1. **构建环境**：.NET 8 SDK、Node.js 20+（前端构建）、PowerShell / bash。
2. **厂商私钥就绪**：`keys/deployment_private.pem`（签发授权与发布封签共用）。
   - 私钥**离线保管**，严禁进入仓库与发布包；丢失只能换钥重签 + 发版（见部署手册 §17.2）。
   - 公钥须已更新到 `DeploymentLicensePublicKey.Encoded` 内置常量（首次 genkeys 后做一次，换钥才需要重做）。
3. **质量门禁**（发布前必须全绿）：
   - 后端全量回归：`dotnet test`（基线 2434 例）。
   - 前端：`node admin/scripts/check-i18n-dups.mjs`（7 站语言包 0 重复）+ admin/portal E2E。
4. **发布完整性自检已启用**：`AssemblyIntegrity:FailClosed` 默认 `true`——**发布后必须执行封签，否则客户生产环境无法启动**（见 §4.6 故障排查）。

---

## 3. 厂商侧发布流程

### 3.1 GUI 一键发布（推荐）

```bash
dotnet run --project tools/LicenseIssuer
# 或直接运行编译产物 LicenseIssuer.exe（无参数即启动图形界面）
```

在「一键发布」标签页：

1. **源码目录**：选择仓库根目录（含 `src/`）——系统自动探测 Api / Server / lmcli 项目路径与输出目录，可手动改。
2. **包版本**：自动从版本号唯一来源 `Directory.Build.props` 的 `<Version>` 读取（如 `1.0.0-lmc.1`，三段数字 + 可选预发布后缀）。
   允许手动修改——发布时若与来源不一致会**先校验并写回 `Directory.Build.props` 再执行 publish**，
   保证程序集版本与分发包文件名一致；手动修改后切换源码目录不再自动覆盖。
3. **发布目标**（可多选）：
   - **主平台 API**：publish → .NET Reactor 混淆（Release）→ 封签（integrity.manifest）；
   - **浮动服务器**：同上，独立部署形态；
   - **lmcli 命令行工具**：win-x64 / linux-x64 / osx-arm64 三 RID 框架依赖**单文件**发布
     （客户侧需 .NET 8 运行时，与服务端约定一致）；CLI 不含完整性自检，不做封签；
   - **客户端 SDK 分发包**：`sdk/` 下 csharp / nodejs / python / go / rust 源码目录打包
     （自动排除 `bin` / `obj` / `node_modules` / `dist` / `target` / `__pycache__` / `.venv` 等构建产物，
     缺失的语言目录自动跳过），并 `dotnet pack` 额外产出 C# SDK NuGet 包。
4. **下载包**：勾选「Windows (.zip)」/「Linux (.tar.gz)」决定打包格式；lmcli 的 osx-arm64 包跟随 Linux 勾选一起出 `.tar.gz`。
5. **构建配置**：选 `Release`。
6. **厂商私钥（封签）**：指向 `keys/deployment_private.pem`。仅勾选 lmcli / SDK 时可省略（不执行封签）。
7. 点击 **一键发布部署（publish + seal）**——依次执行 `dotnet publish` 与目录封签，日志区实时输出。

#### 分发包输出（对齐 website/download.html）

全部目标完成后，分发包统一输出在 `<输出目录>/downloads/`，文件名与下载中心一一对应：

| 发布目标 | 分发包 |
| --- | --- |
| 主平台 API | `licensemanager-server-{版本}-win-x64.zip`、`licensemanager-server-{版本}-linux-x64.tar.gz` |
| 浮动服务器 | `licensemanager-floating-{版本}-win-x64.zip`、`licensemanager-floating-{版本}-linux-x64.tar.gz` |
| lmcli | `lmcli-{版本}-win-x64.zip`、`lmcli-{版本}-linux-x64.tar.gz`、`lmcli-{版本}-osx-arm64.tar.gz` |
| 客户端 SDK | `licensemanager-sdk-csharp/nodejs-{版本}.zip`、`licensemanager-sdk-python/go/rust-{版本}.tar.gz`、`LicenseManager.Client.Sdk.{版本}.nupkg` |

同目录自动生成 `SHA256SUMS.txt`（标准 `<sha256>  <filename>` 格式，与 Linux `sha256sum -c` 兼容；
覆盖全部 `.zip` / `.tar.gz`，NuGet 包以 nuget.org 校验为准），下载页 checksum 链接指向该文件。
发布目录（`publish/api`、`publish/server`）连同 `integrity.manifest` 一并随包分发。

### 3.2 CLI / CI 发布（等价命令）

```bash
# 1. 构建 + 发布（Release 自动执行 .NET Reactor 混淆：publish 后对自有程序集
#    依次字符串加密 + 控制流混淆 + 防篡改，公共类型不改名以保证 JSON/EF/MVC 兼容）
dotnet publish src/LicenseManager.Api/LicenseManager.Api.csproj    -c Release -o publish/api    --nologo
dotnet publish src/LicenseManager.Server/LicenseManager.Server.csproj -c Release -o publish/server --nologo   # 可选

# 2. 封签（必须；对已混淆产物封签）
dotnet run --project tools/LicenseIssuer -- seal --dir publish/api    --private-key keys/deployment_private.pem
dotnet run --project tools/LicenseIssuer -- seal --dir publish/server --private-key keys/deployment_private.pem
```

#### lmcli 与客户端 SDK（等价命令，可配合 GUI 使用）

> lmcli 与客户端 SDK 目标目前由 GUI 一键发布提供（含源码包过滤与打包）；CI 中可按下方等价命令手工执行。

```bash
# 3. lmcli：三 RID 框架依赖单文件发布（客户侧需 .NET 8 运行时；不做封签）
dotnet publish src/LicenseManager.Tools/LicenseManager.Tools.csproj -c Release \
    -r win-x64 --self-contained false -p:PublishSingleFile=true -p:DebugType=None -p:DebugSymbols=false \
    -o publish/lmcli/lmcli-win-x64 --nologo
# linux-x64 / osx-arm64 同理，替换 -r 参数；随后按下载中心格式打包：
#   win-x64 → .zip；linux-x64 / osx-arm64 → tar -czf downloads/lmcli-{版本}-{rid}.tar.gz -C publish/lmcli lmcli-{rid}

# 4. 客户端 SDK：C# NuGet 包（版本号来自 Directory.Build.props）
dotnet pack sdk/csharp/LicenseManager.Client.Sdk.csproj -c Release -o publish/downloads --nologo

# 5. 各语言 SDK 源码包（csharp/nodejs → .zip，python/go/rust → .tar.gz）：
#    复制 sdk/<语言>/ 至暂存目录（排除 bin/obj/node_modules/dist/target/__pycache__/.venv 等）
#    后打包为 downloads/licensemanager-sdk-<语言>-{版本}.zip / .tar.gz —— GUI 一键发布可自动完成
```

> **混淆说明**：`LicenseManagerObfuscate` 默认 Release=true / Debug=false；工具路径可用属性
> `DotNetReactorPath` 覆盖（默认 `C:\Program Files (x86)\Eziriz\.NET Reactor\dotNET_Reactor.Console.exe`）；
> 未安装时构建给出 Warning 并跳过（不阻断）。GUI「一键发布」同样自动包含混淆（Release 配置下）。
> 厂商签发工具自身（LicenseIssuer）的 Release 发布产物亦被混淆。

### 3.3 前端构建（7 站按需）

```bash
cd web/admin && npm ci && npm run build   # 产物 dist/，其余站点同理
```

### 3.4 签发部署授权（按客户档位）

```bash
dotnet run --project tools/LicenseIssuer -- issue --tier Pro --licensee "客户名称" \
    --domains app.customer.com --expires 2027-10-07 \
    --private-key keys/deployment_private.pem --out publish/deployment-license.json
```

GUI 在「签发授权」页操作。档位与配额说明见部署手册 §17.1（对齐营销首页价格模板）。

### 3.5 发布前检查清单

- [ ] 全量测试通过（基线见 §7 版本记录与 CI 报告）
- [ ] `publish/api/integrity.manifest` 已生成且随包分发
- [ ] 部署授权文件已按客户档位签发并放入发布包
- [ ] `keys/` 目录与任何 PEM 文件**不在**发布包中
- [ ] 分发包（server / floating / lmcli / SDK）与 `SHA256SUMS.txt` 已生成，文件名与 `website/download.html` 一致
- [ ] 版本号已通过一键发布写回 `Directory.Build.props`（唯一来源），CHANGELOG 已更新

---

## 4. 客户环境部署

### 4.1 环境要求

| 项 | 要求 |
| --- | --- |
| 操作系统 | Windows Server 2016+ / Linux（Ubuntu 22.04+ 等主流发行版） |
| 运行时 | .NET 8 Runtime（ASP.NET Core） |
| 数据库 | MySQL 8 / PostgreSQL / SQL Server / Oracle 19c+ / SQLite（小规模）五选一 |
| 反向代理 | nginx / IIS / Kestrel 直连均可（建议 TLS 终止在代理层） |

### 4.2 主平台部署步骤

1. 解压 `publish/api/` 到目标目录（如 `/opt/licensemanager/api`），**保持 `integrity.manifest` 在同目录**。
2. 创建 `appsettings.Production.json`（关键配置，缺失将 fail-fast）：

```jsonc
{
  "Database": {
    "Provider": "MySql",                          // MySql / PostgreSql / SqlServer / Oracle / Sqlite
    "ServerVersion": "8.0.36"                     // MySQL 需要显式版本
  },
  "ConnectionStrings": {
    "Default": "Server=...;Database=licensemanager;User=...;Password=...;"
  },
  "Jwt": { "Secret": "至少32字节强随机串" },
  "Seed": { "AdminPassword": "首次启动管理员初始密码（必须显式配置）" },
  "DeploymentLicense": { "FilePath": "data/deployment-license.json" },
  "Auth": { "AllowPublicActivation": true }       // 按销售策略开启匿名 SDK 激活
}
```

3. 生产环境数据保护密钥：设置环境变量 `LICENSEMANAGER_DATA_PROTECTION_KEY`（缺失会拒绝启动）。
4. 启动：`dotnet LicenseManager.Api.dll`（建议注册 systemd / Windows 服务）。
5. 首次启动自动执行数据库迁移、初始化 `system_config`、`data/` 运行时文件。

### 4.3 前端部署

nginx 静态托管各站点 `dist/`，`/v1` 反代到 API（示例）：

```nginx
location /v1/ { proxy_pass http://127.0.0.1:8080; }
location /admin/ { root /opt/licensemanager/web; try_files $uri /admin/index.html; }
```

### 4.4 浮动服务器部署（可独立、无数据库）

1. 解压 `publish/server/`，**保持 `integrity.manifest` 在同目录**。
2. 最小配置（appsettings.Production.json）：

```jsonc
{
  "FloatingLicensing": { "ApiKey": "强随机Key", "MaxSeats": 20, "StorageProvider": "sqlite", "StoragePath": "data/leases.db" },
  "DeploymentLicense": { "FilePath": "data/deployment-license.json" }
}
```

3. 启动 `dotnet LicenseManager.Server.dll`；健康检查 `GET /healthz` 返回 `{"status":"ok"}`。
4. 详细的档位降级行为见部署手册 §17.6。

### 4.5 部署授权导入（激活档位）

| 方式 | 操作 |
| --- | --- |
| 管理端页面（推荐） | 管理员登录 →「系统配置」→ 部署授权卡片 → 粘贴 / 上传 `deployment-license.json` → 即时生效，无需重启 |
| 配置文件 | §4.2 已配置 `DeploymentLicense:FilePath` 时，启动即加载验签 |

导入前请核对客户服务器系统时间准确（回拨超过 6 小时将拒绝导入）。

### 4.6 部署后验证

| 检查项 | 预期 |
| --- | --- |
| 启动日志无 `[FAIL-FAST] 发布完整性自检失败` | 发布包未被改动；出现则见 §5 排查 |
| `GET /healthz` | 200 `{"status":"ok"}` |
| 管理员登录（Seed 密码）后修改密码 | 正常 |
| 「系统配置」页部署授权卡片 | 显示已导入档位（Pro/Enterprise）与配额进度 |
| 一个 SDK activate 冒烟调用 | 200 且计数 +1 |

---

## 5. 故障排查

| 现象 | 原因 | 处置 |
| --- | --- | --- |
| 启动即退出，stderr 出现 `[FAIL-FAST] 发布完整性自检失败：...清单缺失` | `integrity.manifest` 未随包分发或被删 | 从发布包重新复制该文件（与程序集同目录） |
| `[FAIL-FAST] ...程序集被篡改：<文件名>` | 该 DLL/EXE 与厂商封签不符（被修改/损坏/版本混装） | 重新解压原始发布包；严禁手工替换单个文件 |
| `[FAIL-FAST] ...发现未封签程序集` | 发布目录被放入第三方 DLL | 删除该文件；确需注入组件时联系厂商重新封签 |
| `[FAIL-FAST] ...清单签名验证失败` | manifest 被改动或非本厂商签发 | 用原始发布包覆盖 |
| 启动要求 `LICENSEMANAGER_DATA_PROTECTION_KEY` | 生产环境缺数据保护密钥 | 设置环境变量后重启（见 §4.2 步骤 3） |
| 授权导入 400「时钟回拨」 | 服务器时间被回拨超过容差 | 校准系统时间后重试 |
| 授权导入 400 `deployment_license_invalid` | 文件被改 / 非该客户签发 | 向厂商重新索取授权文件 |

---

## 6. 版本升级流程

1. 厂商侧按 §3 发布新版本包（新 `integrity.manifest` 随包）。
2. 客户侧：停止服务 → **备份 `data/` 目录**（含授权状态、签名密钥、SQLite 库）→ 覆盖程序集目录（保留 `data/` 与本地配置）→ 启动。
3. 部署授权持久化于数据库（`system_config`），升级后自动恢复；若用 `FilePath` 方式则授权文件在 `data/` 中一并保留。
4. 验证 §4.6 清单。

---

## 7. 修订历史

| 版本 | 日期 | 说明 |
| --- | --- | --- |
| v1.0 | 2026-10 | 初版：GUI/CLI 一键发布（publish + seal）、封签与完整性自检、客户部署、部署授权导入、fail-fast 排查、升级流程 |
| v1.1 | 2026-10 | 一键发布：版本号唯一来源（`Directory.Build.props` 自动读取/写回）；新增 lmcli 命令行与客户端 SDK（5 语言源码包 + C# NuGet）发布目标；分发包统一输出 `downloads/` 并生成 `SHA256SUMS.txt` |
