# LicenseManager Platform - 部署与运维手册

| 项目 | 内容 |
| --- | --- |
| 文档版本 | v1.0 |
| 文档类型 | 部署与运维手册 (Deployment & Operations Guide) |
| 关联文档 | [RDP v1.0.1](./LicenseManager-Platform-RDP-v1.0.1.md) · [TAD v1.0](./LicenseManager-Platform-TAD-v1.0.md) · [DB Design](./LicenseManager-Platform-Database-Design-v1.0.md) · [API Spec](./LicenseManager-Platform-API-Spec-v1.0.md) |
| 目标读者 | SRE、DevOps、运维工程师、部署架构师 |
| 文档日期 | 2026-07-23 |

---

## 0. 修订历史

| 版本 | 日期 | 修订内容 |
| --- | --- | --- |
| v1.0 | 2026-07-23 | 初稿 |
| v1.1 | 2026-09 | 按代码实测同步:删除不存在的 `dotnet LicenseManager.Api.dll migrate/setup-admin/webhook replay/maintenance/db check` CLI 子命令(改用 `dotnet ef` 与管理 API);部署清单与端口表对齐实际(后台任务为 API 进程内 BackgroundService,无独立 worker/scheduler/notifier 进程,无 RabbitMQ;数据库默认 MySQL,`docker-compose.prod.yml` 示例用 PostgreSQL 16;Redis 可选);开发端口统一 8080 |
| v1.2 | 2026-09 | 按第四轮审查(R4-B/R4-C)修正:§6.4/§6.5 `lmcli` 描述修正为实际状态——`LicenseManager.Tools` 仅实现 `hardware-id` 与 `keygen` 两条命令,`activation`/`refresh` 子命令**不存在**(此前"已注册但无处理器"的表述亦高估);§8.1.2 指标表逐项标注"已实现/规划中"(当前仅 `lm_http_requests_total`、`lm_http_request_duration_seconds` 两项有产出点);§8.3 告警规则注明所引指标多为规划中、当前不会触发 |
| v1.3 | 2026-09 | 按代码核对修正:§1.2/§3.1 浮动 server 端口未在代码中固定(Kestrel 默认 5000,健康检查 `/healthz`),原"8083"无代码依据;§6.4 更正管理端离线激活路径为 `/v1/admin/offline-activation/requests` 并补全新增离线激活端点(`GET /v1/keys`、`/v1/customer/licenses/revocations`、`airgap-sync`、`/v1/portal/offline/*`);§14.2 Webhook 重放路径更正为 `/v1/admin/webhooks/deliveries/{deliveryId}/replay`;§4.2.3 补充生产必配键 `Jwt:Secret` 与 `Auth:AllowPublicActivation`、`RateLimit:PerMinute` 说明 |
| v1.4 | 2026-10 | 新增 §16 系统特性开关配置（`system_config` 表 + 管理端「系统配置」页面 + 匿名端点 `GET /v1/config/features`）；补记短信验证码登录（§16.4 配置项 `Sms:Provider`）、SCIM 2.0 目录同步（`Scim:BearerToken`）与公开状态页；5 库迁移 `SystemConfigTable`、`AddUserPhone`、`AddSsoSamlFields` |
| v1.5 | 2026-10 | 新增 §17 部署授权（自举授权）：档位/配额目录（Free 10/5/10k、Pro 100/50/100k、Enterprise 不限）、授权文件签发（厂商 RSA-2048 私钥，公钥内置程序集，无客户自签发路径；`tools/LicenseIssuer`）、导入方式（管理端卡片 / `DeploymentLicense:FilePath`）、防篡改机制（公钥指纹自检 / 时钟回拨证据 HMAC 签名 / SDK 计数 HMAC 签名 / 防篡改密钥自动生成加密持久化 / FailClosed / 公开路径配额封堵 / 连续导入失败告警）、相关配置键（`DeploymentLicense:FilePath` / `ClockSkewToleranceHours` / `FailClosed` / `TamperSecret`）、浮动服务器无数据库独立部署说明 |
| v1.6 | 2026-10-08 | 商业化裁剪：移除 PSP/支付集成章节，资金流移交外部销售系统 |
| v1.8 | 2026-10-18 | 后台域名由 `demo.licensemanager.cn` 切换为 `dash.licensemanager.cn`（nginx 配置 `dash.licensemanager.cn.conf`，站点目录 `wwwroot/licensemanager.cn/dash`）；同步官网 `www`、API `api`、文档 `docs` 三个子域名的解析与发布；管理员密码修正为实测值 `Admin@123456` |
| v1.9 | 2026-10-18 | 接入方式调整：因 Cloudflare 免费版国内路由绕美西（LAX，实测 50% 丢包、TTFB 10-20s），全部子域名改为 DNS-only 直连源站；证书由 Cloudflare Origin CA 换为 Let's Encrypt（acme.sh HTTP-01 签发，SAN 覆盖 apex/www/api/docs/dash 五域名，`--install-cert` 直写 nginx 挂载路径并自动 reload，cron 自动续期；原 Origin 证书留档 `*.cloudflare-origin.bak`）。直连 TTFB 从 10-20s 降至 ~0.8s。注意：直连后源站 IP 公开暴露 |

---

## 1. 概述

本手册覆盖 LicenseManager Platform 的所有部署形态、初始化流程、备份恢复、监控告警、灾备演练、升级与故障排查。

### 1.1 部署形态

| 形态 | 适用 | 复杂度 |
| --- | --- | --- |
| **单实例 (Docker Compose)** | 演示、POC、小客户 | 低 |
| **HA (Helm/K8s)** | 中大型客户 | 中 |
| **气隙 (Offline Installer)** | 政府、军工、医疗 | 高 |
| **云托管 (Multi-region)** | 厂商运营 | 高 |

### 1.2 部署清单(所有形态)

> 与旧版差异:异步/定时/通知任务均为 `lm-api` **进程内** `BackgroundService`(如 `WebhookDeliveryWorker`),**无独立 worker/scheduler/notifier 进程**;**无 RabbitMQ**(事件与 Webhook 投递在进程内完成,外置 MQ 列路线图)。

| 组件 | 必选 | 说明 |
| --- | --- | --- |
| `lm-api` | ✅ | ASP.NET Core API(含进程内后台任务),端口 8080 |
| `lm-floating-server` | 可选 | 浮动 server(M44,独立进程 `LicenseManager.Server`,端口未在代码中固定(Kestrel 默认 5000,可经 `ASPNETCORE_URLS`/`--urls` 指定,如 8083),健康检查端点 `/healthz`;**座席按产品隔离计数**,checkout 请求须带 `ProductId`,配额经 `FloatingLicensing:ProductSeatLimits` 按产品配置(未配置的产品使用全局 `MaxSeats`);**租约持久化**:`StorageProvider=sqlite`(默认,落盘 `StoragePath`,重启后租约自动恢复)或 `redis`(配置 `RedisConnectionString`,checkout/心跳/释放经 Lua 脚本原子执行,支持多实例 active-active 部署不超卖),`memory` 仅进程内存(重启后客户端需重新 checkout)。须配置 `FloatingLicensing:ApiKey`,未配置时全部租约端点返回 503(fail-closed);其余配置:`MaxSeats`(默认 10)、`MaxLeaseDurationSeconds`(默认 86400)、`SweepIntervalSeconds`(默认 30)。认证方式:请求头 `X-Api-Key`) |
| `lm-admin` | 可选 | 管理控制台静态 SPA(`web/admin`,经 Nginx 托管,非独立 BFF) |
| `lm-doc` | 可选 | 文档站(静态) |
| `lm-site` | 可选 | 营销官网(静态) |
| `lm-status` | 可选 | 状态页(静态) |
| MySQL 8 | ✅(默认) | 主数据库,`Database:Provider=MySql`;仅 MySQL 走 EF Migrate 自动建库。也可选 PostgreSQL(代码默认提供器,`docker-compose.prod.yml` 即 PG 16 示例;非 MySQL 提供器走 EnsureCreated) |
| Redis 7 | 可选 | 配置 `ConnectionStrings:Redis` 后启用;用于分布式限流(`RateLimit:Distributed=true`)等,未配置时回退进程内 `IMemoryCache` |
| 对象存储(S3 兼容) | 可选 | Release 包/导出(以代码配置为准) |
| RabbitMQ | ❌ | **当前版本不使用**;外置消息队列列路线图 |
| KMS/HSM | 推荐 | 签名密钥(当前版本未集成,见 §7.2.2) |
| Nginx/HAProxy | 推荐 | 反向代理 |
| Prometheus + Grafana | 推荐 | 监控 |
| Loki/ELK | 推荐 | 日志 |
| Jaeger | 可选 | 分布式追踪 |
| Sentry | 推荐 | 错误监控 |

### 1.3 演示环境（dash）

线上演示环境为**单实例 Docker Compose 部署**（§4 形态），托管信息如下：

| 项目 | 值 |
| --- | --- |
| 服务器 | `43.153.48.120`（ubuntu，SSH 密钥 `C:\sshkey\ssh_silicon_valley.pem`，见 `scripts/publish-downloads.sh` 的 `SERVER_HOST`/`SERVER_KEY`） |
| 演示地址（管理后台） | `https://dash.licensemanager.cn`（管理端 SPA，nginx 容器反代；2026-10-18 由 `demo.licensemanager.cn` 切换，旧域名已下线） |
| API 地址 | `https://dash.licensemanager.cn/v1/...`（同域反代至 `127.0.0.1:8080` 的 `licensemanager-api` 容器） |
| 官网 / API / 文档 | `https://www.licensemanager.cn`（静态站）、`https://api.licensemanager.cn`（API 反代）、`https://docs.licensemanager.cn`（Markdown 文档站），均由 `licensemanager.cn.conf` 承载 |
| nginx 部署方式 | Docker 容器（host 网络），配置挂载自 `/data/appData/docker/nginx/conf.d/`（后台 `dash.licensemanager.cn.conf`；官网/API/文档 `licensemanager.cn.conf`） |
| 数据库 | MySQL 8.4 容器（`mysql84`，宿主机映射端口 `31001`，库 `licenseManagerDB`，用户 `licenseManagerUser`） |
| 健康检查 | `https://dash.licensemanager.cn/v1/healthz` |

**演示环境管理员账号**：

| 邮箱 | 密码 | 说明 |
| --- | --- | --- |
| `admin@licensemanager.cn` | `Admin@123456` | 演示环境管理员（2026-10-18 实测可用；库中另有 `admin@example.com` 同密码种子账号） |

> - 密码哈希格式为 `pbkdf2$600000$base64(salt16)$base64(hash32)`（PBKDF2-HMAC-SHA256，见 `PasswordHasher.cs`；历史 210000 迭代哈希仍可校验，登录后自动升级）；如需重置，生成新哈希后直接更新 `user_account.PasswordHash` 即可，无需重启 API。
> - **该密码仅用于演示环境**；生产部署切勿在文档/配置中明文存放凭据。
> - 常见登录失败排查：登录接口 `POST /v1/auth/login` 请求体字段为 `email`/`password`（非 `username`）；失败记录可在 `login_attempt` 表查 `FailureReason`。

> - **重新发布 API 的前置条件**：当前构建内置发布完整性自检（§17.4），`dotnet publish` 产物必须经厂商私钥执行 `dotnet run --project tools/LicenseIssuer -- seal --dir <发布目录> --private-key keys/deployment_private.pem` 生成 `integrity.manifest` 后才能启动；未封签的发布目录会以退出码 87 拒绝启动（容器表现为反复重启）。私钥离线保管于厂商侧密钥库（本机 `/Users/glen/keystore/LicenseManager`），不在仓库与发布包中。2026-10-18 已完成密钥轮换（新公钥指纹 `8cb08445…10f1d0`）并用新钥封签上线演示环境；因旧库迁移链与重新脚手架化的迁移 ID 不一致（旧 `InitialCreate` 为本地未提交迁移），演示库已按新迁移链重建（原库备份于服务器 `/data/appData/mysql/backups/licenseManagerDB-pre-rebuild-20261018.sql`，内容仅种子数据）。
> - 封签发布包经 tar 分发时须排除 macOS 扩展属性（`tar --no-xattrs`），否则解包产生的 `._*.dll` AppleDouble 文件会被自检判为「未封签程序集」而拒绝启动。

---

## 2. 硬件与系统要求

### 2.1 单实例(演示/小客户)

| 资源 | 最低 | 推荐 |
| --- | --- | --- |
| CPU | 4 vCPU | 8 vCPU |
| 内存 | 8 GB | 16 GB |
| 磁盘 | 100 GB SSD | 500 GB SSD |
| 网络 | 100 Mbps | 1 Gbps |
| OS | Linux(CentOS Stream / Ubuntu LTS / RHEL) | - |

### 2.2 HA(中型客户)

| 资源 | API × 2 | Worker × 2 | DB 主从 | Redis | RabbitMQ |
| --- | --- | --- | --- | --- | --- |
| CPU | 8 vCPU | 4 vCPU | 8 vCPU | 4 vCPU | 4 vCPU |
| 内存 | 16 GB | 8 GB | 32 GB | 8 GB | 8 GB |
| 磁盘 | 100 GB | 50 GB | 1 TB SSD | 100 GB | 100 GB |
| 网络 | 1 Gbps | 1 Gbps | 10 Gbps | 1 Gbps | 1 Gbps |

### 2.3 HA(大型客户,> 100w 授权)

| 资源 | API × 4 | Worker × 3 | DB 主 + 2 从 | Redis Cluster(3 节点) | RabbitMQ Cluster(3 节点) |
| --- | --- | --- | --- | --- | --- |
| CPU | 16 vCPU | 8 vCPU | 16 vCPU | 8 vCPU | 8 vCPU |
| 内存 | 32 GB | 16 GB | 64 GB | 16 GB | 16 GB |
| 磁盘 | 200 GB | 100 GB | 2 TB SSD | 200 GB | 200 GB |
| 网络 | 10 Gbps | 10 Gbps | 25 Gbps | 10 Gbps | 10 Gbps |

### 2.4 操作系统支持

- **生产**:RHEL 8/9、Rocky Linux 8/9、Ubuntu 22.04 LTS
- **测试**:任何 Linux
- **不支持**:Windows Server(可选,Win 客户端 SDK 支持)

### 2.5 依赖服务版本

| 服务 | 版本 | 说明 |
| --- | --- | --- |
| .NET | 8.0 LTS | |
| MySQL | 8.x | 默认数据库(`Database:Provider=MySql`) |
| PostgreSQL | 16.x | 可选数据库(`docker-compose.prod.yml` 示例) |
| Redis | 7.x | 可选 |
| Nginx | 1.24+ | |
| Docker | 24+ | |
| Kubernetes | 1.28+ | |

---

## 3. 网络与端口

### 3.1 端口规划

> 仅列出当前版本真实存在的进程/端口;开发环境 API 固定 `http://localhost:8080`(`launchSettings.json`),前端 dev server 经 vite 代理指向 8080。

| 服务 | 端口(内部) | 端口(对外) | 说明 |
| --- | --- | --- | --- |
| `lm-api` | 8080 | 443 | HTTP API(开发环境直接暴露 8080) |
| `lm-floating-server` | 5000(默认) | 自定 | 浮动 server(独立进程,可选;代码未固定端口,默认 Kestrel 5000,经 `ASPNETCORE_URLS`/`--urls` 指定) |
| `lm-admin` | 80(Nginx) | 443 | 管理控制台静态 SPA(`docker-compose.prod.yml` 映射 80) |
| MySQL | 3306 | ❌ 内网 | 默认数据库 |
| PostgreSQL | 5432 | ❌ 内网 | 可选数据库(compose 示例) |
| Redis | 6379 | ❌ 内网 | 可选 |

> 旧版端口表中的 `lm-admin:8081` / `lm-portal:8082`(BFF)、`lm-doc:8090` / `lm-site:8091` / `lm-status:8092`、`lm-pprof:9090` / `lm-metrics:9091`、RabbitMQ `5672/15672` 在当前版本无对应独立进程(前端为静态 SPA、指标随 API 进程暴露),以代码为准。

### 3.2 防火墙规则

| 源 | 目标 | 端口 | 协议 |
| --- | --- | --- | --- |
| 用户 | Nginx | 443 | TCP |
| Nginx | lm-api | 8080 | TCP |
| DB | MySQL / PostgreSQL | 3306 / 5432 | TCP |
| 内部 | Redis(可选) | 6379 | TCP |

### 3.3 TLS 终结

- **推荐**:Nginx / HAProxy / Cloud LB 终结 TLS
- **证书**:Let's Encrypt / 内部 CA / 商业 CA
- **最低版本**:TLS 1.2(推荐 TLS 1.3)
- **可选 mTLS**:API 客户端证书认证(高安全场景)

---

## 4. 单实例部署(Docker Compose)

### 4.1 适用场景

- 演示 / POC
- 10w 授权以内的小客户
- 内网测试环境

### 4.2 部署步骤

#### 4.2.1 准备工作

```bash
# 系统要求
- Linux x86_64
- Docker 24+ / Docker Compose v2
- 8 GB+ RAM
- 50 GB+ 磁盘

# 创建工作目录
mkdir -p /opt/license-manager
cd /opt/license-manager

# 下载离线包
wget https://releases.license-manager.com/v1.0.0/license-manager-offline-1.0.0.tar.gz
tar xzf license-manager-offline-1.0.0.tar.gz
cd license-manager-1.0.0
```

#### 4.2.2 目录结构

> 以下为发布包目标形态;当前仓库自带 `docker-compose.prod.yml`(api + admin + postgres + redis 四个服务)可直接使用。`worker.json`/`scheduler.json`、RabbitMQ 数据目录在当前版本无对应组件(任务进程内化,见 §1.2)。

```
/opt/license-manager/license-manager-1.0.0/
├── docker-compose.yml
├── .env.example
├── config/
│   ├── api.json
│   ├── nginx.conf
│   └── license-manager.crt
├── init/
│   ├── 01-init-db.sql
│   ├── 02-seed-data.sql
│   └── 03-prepare-keydir.sh   # 预分配签名密钥目录(密钥本身由 lm-api 首启自动生成)
├── data/                  # 持久化数据
│   ├── mysql/             # 或 postgres/(取决于 Database:Provider)
│   └── redis/             # 可选
├── logs/                  # 日志
├── backups/               # 备份
└── README.md
```

#### 4.2.3 配置 `.env`

```bash
# 复制模板
cp .env.example .env
vim .env
```

```bash
# .env
LM_VERSION=1.0.0
LM_DOMAIN=license.example.com
LM_ADMIN_EMAIL=admin@licensemanager.cn
LM_ADMIN_PASSWORD=<strong-password>

# 数据库(二选一;默认 MySQL)
# 方式 A: MySQL(默认提供器,对应配置键 Database:Provider=MySql)
MYSQL_USER=lm_admin
MYSQL_PASSWORD=<strong-db-password>
MYSQL_DATABASE=license_manager
# 方式 B: PostgreSQL(docker-compose.prod.yml 示例,Database:Provider=PostgreSQL)
POSTGRES_USER=lm_admin
POSTGRES_PASSWORD=<strong-db-password>
POSTGRES_DB=license_manager

# Redis(可选,配置后可用于分布式限流等)
REDIS_PASSWORD=<strong-redis-password>

# 签名密钥文件(license 签名 RSA 密钥,首启自动生成,多版本共存)
# 映射到配置键 License:SigningKeyPath;文件包含 RSA 私钥,须限制权限(chmod 600)
License__SigningKeyPath=/etc/license-manager/license-signing-keys.json

# KMS(规划中,当前版本未实现,密钥文件由文件系统权限保护)
KMS_PROVIDER=
KMS_KEY_ID=

# 对象存储(可选,留空则使用本地 MinIO)
S3_PROVIDER=minio
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY=<minio-access>
S3_SECRET_KEY=<minio-secret>
S3_BUCKET=license-manager

# 邮件
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=<smtp-password>
SMTP_FROM=noreply@example.com

# 监控
GRAFANA_PASSWORD=<strong-grafana-password>
```

> 除 `.env` 外,以下生产关键配置键(以环境变量 `__` 映射,如 `Jwt__Secret`,或写入配置文件)须在部署时确认(以 `appsettings.json` 为准):
>
> - `Jwt:Secret`:生产环境**必配**(强随机 ≥32 字节)。未配置或仍为内置占位符时,生产环境解析 JWT 密钥将直接抛出异常,登录/签发不可用(`JwtSecretResolver`)。
> - `Auth:AllowPublicActivation`:默认 `true`(激活/验证等客户端 SDK 生命周期端点保持公开);生产环境建议显式设为 `false` 启用强认证。
> - `RateLimit:PerMinute`:代码内回退默认 **100/分钟**,生产务必显式配置(配置模板示例 600);反向代理部署可开启 `RateLimit:TrustXForwardedFor=true`(默认 false,直连保持关闭以防伪造绕过限流)。

#### 4.2.4 初始化密钥

> 当前版本**无需手工生成签名密钥**:`lm-api` 首次启动时由 `SigningKeyStore` 自动生成 RSA-2048 密钥并原子写入 `License:SigningKeyPath` 指定的 JSON 文件(多版本共存,轮换后旧版本保留以验证既有签名)。文件权限自动限制为 0600(Unix)。

```bash
# 验证(启动 lm-api 一次后)
cat /etc/license-manager/license-signing-keys.json | jq '.activeVersion'

# 预分配目录与权限(容器以 lm 用户运行时)
install -d -m 700 -o lm -g lm /etc/license-manager
```

#### 4.2.5 启动服务

```bash
# 启动所有服务
docker compose up -d

# 查看状态
docker compose ps

# 查看日志
docker compose logs -f lm-api

# 等待就绪
until curl -sf http://localhost:8080/v1/healthz; do
  sleep 5
done
echo "LicenseManager is ready"
```

#### 4.2.6 初始化数据库

> **不存在** `dotnet LicenseManager.Api.dll migrate` / `setup-admin` 等 CLI 子命令——`LicenseManager.Api` 无任何命令行子命令。初始化按以下方式进行:

```bash
# 1. 数据库迁移:
#    - MySQL(默认提供器):lm-api 首次启动时自动执行 EF Migrate,无需手工干预;
#    - 其他提供器(PostgreSQL 等):启动时走 EnsureCreated;
#    - 需要手工迁移时,在源码环境使用 EF 工具:
dotnet ef database update --project src/LicenseManager.Infrastructure

# 2. 管理员账号:由内置 Seed 在首启自动创建
#    (配置键 Seed:AdminEmail / Seed:AdminPassword,docker-compose.prod.yml 中经
#     ADMIN_EMAIL / ADMIN_PASSWORD 环境变量注入)。
```

#### 4.2.7 验证

```bash
# 健康检查
curl http://localhost:8080/v1/healthz
# {"status":"ok"}

# 就绪检查(响应字段以实际输出为准)
curl http://localhost:8080/v1/readyz

# OpenAPI 文档(仅 Development 环境挂载;不存在 /v1/openapi.json)
curl http://localhost:8080/swagger/v1/swagger.json | head -100

# 访问控制台
open https://license.example.com/admin
```

### 4.3 常用操作

```bash
# 停止
docker compose stop

# 启动
docker compose start

# 重启单个服务
docker compose restart lm-api

# 查看日志
docker compose logs -f --tail=200 lm-api

# 进入容器
docker compose exec lm-api bash

# 更新镜像
docker compose pull
docker compose up -d

# 清理
docker compose down  # 停止
docker compose down -v  # 停止并清理数据
```

---

## 5. HA 部署(Helm/Kubernetes)

> 本章为目标形态:Helm chart 及独立 `lm-worker`/`lm-scheduler`/`lm-notifier` 副本在当前版本未随附(后台任务进程内化,见 §1.2);当前版本多实例部署时请注意:限流需配置 Redis 并开启 `RateLimit:Distributed=true`,幂等缓存为进程内 `IMemoryCache`(多实例不共享),外置 MQ/集群调度列路线图。

### 5.1 适用场景

- 中大型客户
- 需要 99.9%+ 可用性
- 100w+ 授权

### 5.2 部署架构

```
                       ┌─→ lm-api (Pod A) ─┐
        Ingress/Nginx ─┤                    ├─→ PostgreSQL Primary
                       └─→ lm-api (Pod B) ─┘     │
                                                  ├─→ Read Replica 1
                       ┌─→ lm-worker (Pod A) ─┐   └─→ Read Replica 2
                       └─→ lm-worker (Pod B) ─┘
                       ┌─→ lm-scheduler × N   → Redis Sentinel
                       └─→ lm-notifier × N      → RabbitMQ Cluster
                                                → S3 兼容存储
```

### 5.3 Helm Chart

#### 5.3.1 添加仓库

```bash
helm repo add license-manager https://charts.license-manager.com
helm repo update
```

#### 5.3.2 安装

```bash
# 创建命名空间
kubectl create namespace license-manager

# 创建密钥
kubectl create secret generic lm-secrets \
  --from-literal=postgres-password=$POSTGRES_PASSWORD \
  --from-literal=redis-password=$REDIS_PASSWORD \
  --from-literal=rabbitmq-password=$RABBITMQ_PASSWORD \
  --from-literal=smtp-password=$SMTP_PASSWORD \
  --from-literal=signing-key=$(cat /etc/license-manager/license-signing-keys.json) \
  -n license-manager

# 准备 values.yaml
cat > values.yaml <<EOF
global:
  imageRegistry: registry.license-manager.com
  imageTag: 1.0.0
  domain: license.example.com

api:
  replicaCount: 2
  resources:
    requests: { cpu: 1, memory: 2Gi }
    limits: { cpu: 4, memory: 4Gi }
  
worker:
  replicaCount: 2
  
scheduler:
  replicaCount: 1
  
floatingServer:
  enabled: true
  replicaCount: 1

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  tls:
    - secretName: lm-tls
      hosts: [license.example.com]

postgresql:
  enabled: true
  architecture: replication
  primary:
    resources:
      requests: { cpu: 4, memory: 8Gi }
      limits: { cpu: 8, memory: 16Gi }
  readReplicas:
    replicaCount: 2

redis:
  enabled: true
  architecture: sentinel
  sentinel:
    quorum: 2

rabbitmq:
  enabled: true
  replicaCount: 3

s3:
  enabled: true
  bucket: license-manager
  # 留空则使用外部 S3
  external:
    endpoint: s3.amazonaws.com
    region: us-east-1

monitoring:
  prometheus:
    enabled: true
  grafana:
    enabled: true
EOF

# 安装
helm install lm license-manager/license-manager \
  --namespace license-manager \
  --values values.yaml
```

#### 5.3.3 验证

```bash
# 查看 Pod
kubectl get pods -n license-manager

# 查看 Service
kubectl get svc -n license-manager

# 端口转发(临时)
kubectl port-forward -n license-manager svc/lm-api 8080:8080

# 健康检查
curl http://localhost:8080/v1/healthz
```

### 5.4 升级

```bash
# 升级镜像版本
helm upgrade lm license-manager/license-manager \
  --namespace license-manager \
  --set global.imageTag=1.1.0

# 滚动升级(默认)
# 不会中断服务
```

### 5.5 回滚

```bash
# 查看历史
helm history lm -n license-manager

# 回滚到上一版本
helm rollback lm -n license-manager

# 回滚到指定版本
helm rollback lm 3 -n license-manager
```

---

## 6. 离线 / 气隙部署

### 6.1 适用场景

- 完全物理隔离
- 政府、军工、医疗
- 工业控制

### 6.2 离线安装包

> 以下清单为目标形态;当前版本实际交付镜像以发布物为准——`lm-worker`/`lm-scheduler`/`lm-notifier`/`lm-keygen`/RabbitMQ 在当前版本无对应组件(见 §1.2),`lm-admin`/`lm-portal` 为静态 SPA 镜像。

`license-manager-offline-1.0.0.tar.gz` 包含:

```
license-manager-offline-1.0.0/
├── docker-images/             # 所有 Docker 镜像 tar
│   ├── lm-api-1.0.0.tar
│   ├── lm-floating-server-1.0.0.tar
│   ├── lm-admin-1.0.0.tar
│   ├── lm-portal-1.0.0.tar
│   ├── lm-doc-1.0.0.tar
│   ├── lm-site-1.0.0.tar
│   ├── lm-status-1.0.0.tar
│   ├── mysql-8.tar            # 或 postgres-16.tar(按 Database:Provider)
│   ├── redis-7.tar            # 可选
│   ├── minio.tar
│   ├── nginx.tar
│   ├── prometheus.tar
│   ├── grafana.tar
│   └── ...
├── installers/                # 离线安装 RPM/DEB
│   ├── lm-tools-1.0.0.rpm
│   └── ...
├── packages/                  # OS 包
├── air-gap/                   # 气隙专用工具
│   ├── activation-file-tool
│   ├── refresh-file-tool
│   └── shortcode-tool
├── helm/                      # Helm chart 离线版
├── sql/                       # 数据库初始化
├── seed/                      # 种子数据
├── README.md                  # 离线安装说明
└── CHECKSUMS                  # 校验
```

### 6.3 安装步骤

```bash
# 1. 在联网机器上下载
# (在客户内网用 U 盘 / 一次性导入介质)
tar xzf license-manager-offline-1.0.0.tar.gz
cd license-manager-offline-1.0.0

# 2. 验证完整性
sha256sum -c CHECKSUMS

# 3. 加载 Docker 镜像
for img in docker-images/*.tar; do
  docker load -i "$img"
done

# 4. 启动(无外网)
docker compose up -d

# 5. 验证
curl http://localhost:8080/v1/healthz
```

### 6.4 气隙激活(完全无网络)

> 以下 `lmcli` 命令行为**目标形态(规划中)**;当前版本 `LicenseManager.Tools`(`lmcli`)仅实现两条命令:`hardware-id`(输出硬件标识)与 `keygen`(生成 RSA 签名密钥对),**`activation` 与 `refresh` 子命令均不存在**(以 `src/LicenseManager.Tools/Program.cs` 为准)。现阶段气隙激活请使用离线激活相关 API 或控制台 UI(「离线激活」页面)完成:管理端 `POST /v1/admin/offline-activation/requests`(创建/查询/履行离线激活请求);客户侧 `POST /v1/customer/licenses/airgap-activate`、`GET /v1/customer/licenses/revocations`(吊销列表)、`POST /v1/customer/licenses/{licenseId}/airgap-sync`(离线消费回传);Portal 侧 `POST /v1/portal/offline/airgap-activate` 与 `POST /v1/portal/offline/upload-req`;公钥分发 `GET /v1/keys`(离线客户端按密钥版本选公钥本地验签)。

#### 6.4.1 文件模式

```bash
# 在终端(可能有网络)
# 1. 生成激活请求
lmcli activation request \
  --activation-key "XXXX-XXXX-XXXX" \
  --hardware-id "$(lmcli hardware-id)" \
  --output activation.req

# 2. 复制 activation.req 到 U 盘

# 在控制台/门户(联网)
# 3. 管理员上传 .req,生成 .resp
lmcli activation respond \
  --input /path/to/activation.req \
  --output activation.resp

# 4. 复制 .resp 到 U 盘

# 在终端
# 5. 应用激活
lmcli activation apply \
  --input /path/to/activation.resp
```

#### 6.4.2 短码模式(无文件介质)

```bash
# 在终端
lmcli activation request --shortcode
# 输出: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
# (Base32 编码)

# 在控制台(键入短码)
lmcli activation respond --shortcode XXXX-XXXX-XXXX-XXXX-...
# 输出: YYYY-YYYY-YYYY-YYYY-YYYY-YYYY-YYYY-YYYY

# 在终端
lmcli activation apply --shortcode YYYY-YYYY-...
```

### 6.5 离线 Refresh

> 同样为**目标形态**:`lmcli refresh` 子命令当前未实现,离线 Refresh 请使用客户 API `POST /v1/customer/licenses/refresh-file` 完成。

```bash
# 终端
lmcli refresh request --license-key XXXX --output refresh.req

# 控制台
lmcli refresh respond --input refresh.req --output refresh.resp

# 终端
lmcli refresh apply --input refresh.resp
```

---

## 7. 初始化与首次配置

### 7.1 部署后清单

- [ ] 密钥生成(系统签名密钥)
- [ ] 密钥存入 KMS/HSM 或加密文件
- [ ] 数据库初始化 + 迁移
- [ ] 种子数据(内置角色、错误码字典)
- [ ] 管理员账号创建
- [ ] TLS 证书配置
- [ ] 域名 + DNS
- [ ] 邮件 SMTP 配置
- [ ] 备份策略启用
- [ ] 监控配置
- [ ] 日志收集
- [ ] 状态页部署
- [ ] Webhook 端点配置(自托管)
- [ ] 第一次 license 签发测试
- [ ] 第一次激活测试
- [ ] 第一次离线激活测试
- [ ] Webhook 投递测试
- [ ] 备份恢复演练

### 7.2 密钥管理

#### 7.2.1 签名密钥管理

签名密钥由 `lm-api` 内置的 `SigningKeyStore` 管理,**首启自动生成**并持久化到 `License:SigningKeyPath`(默认 `data/license-signing-keys.json`),支持多版本轮换(旧版本保留以验证既有签名)。持久化文件格式:

```json
{
  "activeVersion": 1,
  "keys": [
    {
      "version": 1,
      "publicKey": "<base64(RSAPublicKey)>",
      "privateKey": "<base64(RSAPrivateKey)>"
    }
  ]
}
```

> 该文件包含 RSA 私钥明文,安全性依赖文件系统权限(Unix 0600);生产环境建议叠加磁盘/KMS 级加密。轮换密钥调用 `POST /v1/admin/system-keys/rotate`(私钥不会通过 API 返回)。

#### 7.2.2 KMS 集成(规划中,当前版本未实现)

> 以下为目标形态设计。当前版本的签名密钥保护依赖文件系统权限(0600)+ 备份加密;数据库内系统密钥(`system_key` 表)已由 `SecretProtector`(AES-GCM,密钥源 `LICENSEMANAGER_DATA_PROTECTION_KEY`)静态加密。

```bash
# 方式 1: AWS KMS
export KMS_PROVIDER=aws
export KMS_REGION=us-east-1
export KMS_KEY_ID=arn:aws:kms:us-east-1:xxx:key/yyy

# 方式 2: HashiCorp Vault
export KMS_PROVIDER=vault
export VAULT_ADDR=https://vault.example.com
export VAULT_TOKEN=xxx
export VAULT_KEY=license-manager/system-key

# 方式 3: Azure Key Vault
export KMS_PROVIDER=azure
export AZURE_KEYVAULT_URL=https://xxx.vault.azure.net/
export AZURE_KEYVAULT_KEY=system-key

# 方式 4: 阿里云 KMS
export KMS_PROVIDER=aliyun
export ALIYUN_KMS_REGION=cn-hangzhou
export ALIYUN_KMS_KEY_ID=xxx
```

#### 7.2.3 密钥轮换

```bash
# 计划性轮换(每年)——当前版本通过管理 API 完成(lmctl 见 14.2 注记)
curl -X POST "https://license.example.com/v1/admin/system-keys/rotate?keyType=license-signing" \
  -H "Authorization: Bearer <admin-token>"

# 轮换后,旧 license 仍然可验证(旧密钥版本保留于密钥文件)
# 新签发 license 使用新密钥
# 监控旧密钥使用率,待全量迁移后下线
```

### 7.3 邮件配置

```bash
# SMTP(默认)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=xxx
SMTP_FROM=noreply@example.com
SMTP_FROM_NAME=LicenseManager

# 或使用 Postmark(推荐)
EMAIL_PROVIDER=postmark
POSTMARK_API_KEY=xxx

# 或使用 SendGrid
EMAIL_PROVIDER=sendgrid
SENDGRID_API_KEY=xxx
```

### 7.4 备份策略

#### 7.4.1 启用 WAL 归档(推荐)

```bash
# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'
max_wal_senders = 10
```

#### 7.4.2 全量备份(每日)

```bash
# /etc/cron.d/lm-backup
0 2 * * * lm /opt/license-manager/scripts/backup.sh

# backup.sh
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/opt/license-manager/backups/$(date +%Y%m%d)
mkdir -p "$BACKUP_DIR"

# PostgreSQL
pg_dump -U lm_admin -Fc license_manager | gzip > "$BACKUP_DIR/db-$(date +%H%M).dump.gz"

# 上传到 S3
aws s3 cp "$BACKUP_DIR/" "s3://license-manager-backups/$BACKUP_DIR/" --recursive

# 清理 7 天前的本地备份
find /opt/license-manager/backups -type d -mtime +7 -exec rm -rf {} \;
```

#### 7.4.3 加密密钥备份

```bash
# 密钥单独备份
# 方式 1:加密存储到保险柜
gpg --symmetric --cipher-algo AES256 /etc/license-manager/license-signing-keys.json
# 存储到安全位置

# 方式 2:KMS 自动管理
# 密钥由 KMS 托管,无需手动备份
```

---

## 8. 监控

### 8.1 Prometheus 指标

#### 8.1.1 系统指标

```yaml
# /etc/prometheus/prometheus.yml
scrape_configs:
  - job_name: 'license-manager-api'
    static_configs:
      - targets: ['lm-api:9091']
    scrape_interval: 15s
```

#### 8.1.2 关键指标

> **实现现状(2026-09 第四轮审查)**:下表为规划指标全集;当前版本**仅 `lm_http_requests_total` 与 `lm_http_request_duration_seconds` 两项有实际产出点**(`MetricsCollectionMiddleware` + `InMemoryMetricsRegistry`,经 `GET /v1/metrics` 暴露),其余指标均为**规划中、暂无埋点**。部分指标随 2026-09 迭代逐步落地,以代码为准。

| 指标 | 类型 | 说明 |
| --- | --- | --- |
| `lm_http_requests_total` | Counter | HTTP 请求数(按 endpoint / status / method)——**已实现** |
| `lm_http_request_duration_seconds` | Histogram | 请求延迟——**已实现** |
| `lm_licenses_active_total` | Gauge | 活跃 license 数——规划中 |
| `lm_licenses_activated_total` | Counter | 总激活数——规划中 |
| `lm_activations_total` | Counter | 激活次数——规划中 |
| `lm_feature_consumption_total` | Counter | 特性消耗总量——规划中 |
| `lm_floating_seats_in_use` | Gauge | 浮动席位使用中(规划中,当前版本 `lm-floating-server` 未暴露 Prometheus 指标) |
| `lm_webhook_deliveries_total` | Counter | Webhook 投递数——规划中 |
| `lm_webhook_delivery_duration_seconds` | Histogram | 投递延迟——规划中 |
| `lm_subscriptions_active` | Gauge | 活跃订阅数——规划中 |
| `lm_subscriptions_renewal_failed_total` | Counter | 续费失败数——规划中 |
| `lm_db_pool_active` | Gauge | DB 连接池活跃——规划中 |
| `lm_db_query_duration_seconds` | Histogram | DB 查询延迟——规划中 |
| `lm_cache_hits_total` / `lm_cache_misses_total` | Counter | 缓存命中/未命中——规划中 |
| `lm_queue_size` | Gauge | 队列长度——规划中 |
| `lm_jobs_running` | Gauge | 运行中 Job——规划中 |
| `lm_jobs_failed_total` | Counter | 失败 Job——规划中 |

### 8.2 Grafana 仪表盘

提供官方仪表盘:
- `lm-overview.json` - 平台总览
- `lm-licenses.json` - 授权监控
- `lm-api.json` - API 性能
- `lm-infrastructure.json` - 基础设施

### 8.3 告警规则

> **注意(2026-09 第四轮审查)**:以下规则引用的指标多为**规划中**(见 §8.1.2),当前仅 `APIHighErrorRate` 与 `APIHighLatency` 两条基于已实现指标;其余规则在当前版本**不会触发**(表达式恒为空),埋点补齐后方可生效。另 `lm_db_pool_size`、`lm_webhook_deliveries_suspended`、`lm_floating_seats_total` 未出现在指标表中,同为规划中。

```yaml
# prometheus-alerts.yml
groups:
  - name: license-manager-critical
    rules:
      - alert: APIHighErrorRate
        expr: |
          sum(rate(lm_http_requests_total{status=~"5.."}[5m]))
          / sum(rate(lm_http_requests_total[5m])) > 0.01
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: API 5xx 错误率 > 1%
          description: 错误率 {{ $value | humanizePercentage }}
      
      - alert: APIHighLatency
        expr: |
          histogram_quantile(0.95, rate(lm_http_request_duration_seconds_bucket[5m])) > 0.5
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: API P95 > 500ms
      
      - alert: DatabaseConnectionsExhausted
        expr: lm_db_pool_active / lm_db_pool_size > 0.8
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: DB 连接池使用 > 80%
      
      - alert: WebhookDeliveriesSuspended
        expr: lm_webhook_deliveries_suspended > 100
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: Webhook 挂起数 > 100
      
      - alert: SubscriptionRenewalFailures
        expr: rate(lm_subscriptions_renewal_failed_total[1h]) > 10
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: 订阅续费失败率突增
      
      - alert: FloatingSeatsExhausted
        expr: lm_floating_seats_in_use / lm_floating_seats_total > 0.95
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: 浮动席位使用 > 95%
```

### 8.4 日志

#### 8.4.1 结构化日志(JSON)

```json
{
  "@timestamp": "2026-07-23T12:00:00.000Z",
  "level": "Information",
  "category": "License.Activation",
  "message": "License activated",
  "request_id": "...",
  "trace_id": "...",
  "tenant_id": "...",
  "user_id": "...",
  "license_id": "...",
  "duration_ms": 120
}
```

#### 8.4.2 Loki / ELK

```yaml
# promtail 配置
scrape_configs:
  - job_name: lm-api
    static_configs:
      - targets: [localhost]
        labels:
          job: lm-api
          __path__: /var/log/license-manager/api/*.log
    pipeline_stages:
      - json:
          expressions:
            level: level
            message: message
            tenant_id: tenant_id
      - labels:
          level:
```

### 8.5 分布式追踪

- OpenTelemetry SDK
- 导出到 Jaeger / Tempo
- 采样率:100%(生产可降至 10%)

### 8.6 健康检查

```yaml
# Kubernetes liveness
livenessProbe:
  httpGet:
    path: /v1/healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

# Readiness
readinessProbe:
  httpGet:
    path: /v1/readyz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3
```

---

## 9. 备份与恢复

> 本节 SQL/命令以 PostgreSQL 为例(`docker-compose.prod.yml` 形态);默认 MySQL 部署请改用 `mysqldump` / `mysql` 等价命令,列名以实际 schema(PascalCase)为准。

### 9.1 备份类型

| 类型 | 频率 | 保留 | 工具 |
| --- | --- | --- | --- |
| 数据库全量(MySQL/PostgreSQL) | 每日 | 7 天本地 + 30 天 S3 | `mysqldump` / `pg_dump` |
| PostgreSQL WAL(仅 PG 部署) | 持续 | 7 天 | `archive_command` |
| Redis(可选组件) | 每日 | 7 天 | `BGSAVE` |
| 密钥 | 每次轮换 | 永久 | KMS/HSM(当前版本未集成,用加密文件备份) |
| 配置 | 每次变更 | 12 个月 | Git |
| 审计冷存 | 每月 | 7 年 | S3 冷存储 |
| S3 对象 | 跨区域复制 | 永久 | S3 跨区域复制 |

### 9.2 恢复流程

#### 9.2.1 数据库恢复(全量)

```bash
# 1. 停止应用(当前版本服务名以 docker-compose.prod.yml 为准:api / admin / postgres / redis)
docker compose stop api

# 2. 恢复数据库
gunzip -c /opt/license-manager/backups/20260723/db-0200.dump.gz | pg_restore \
  -U lm_admin \
  -d license_manager \
  --clean \
  --if-exists

# 3. 验证
psql -U lm_admin -d license_manager -c "SELECT count(*) FROM license;"

# 4. 启动应用
docker compose start api

# 5. 验证
curl http://localhost:8080/v1/healthz
```

#### 9.2.2 PITR(恢复到指定时间)

```bash
# 1. 停止应用
docker compose stop api

# 2. 恢复基础备份
pg_basebackup -D /var/lib/postgresql/restore --checkpoint=fast

# 3. 配置 recovery.signal
touch /var/lib/postgresql/restore/recovery.signal
cat >> /var/lib/postgresql/restore/postgresql.conf <<EOF
restore_command = 'cp /var/lib/postgresql/wal_archive/%f %p'
recovery_target_time = '2026-07-23 12:00:00'
recovery_target_action = 'promote'
EOF

# 4. 启动 PostgreSQL(进入恢复模式)
systemctl start postgresql

# 5. 等待 promote
# 6. 启动应用
```

#### 9.2.3 密钥恢复

```bash
# 1. 从加密备份取回(KMS 当前版本未实现,使用 gpg 备份)
gpg --decrypt license-signing-keys.gpg > license-signing-keys.json

# 2. 部署到目标实例
cp license-signing-keys.json /etc/license-manager/license-signing-keys.json
chmod 600 /etc/license-manager/license-signing-keys.json
chown lm:lm /etc/license-manager/license-signing-keys.json
```

### 9.3 灾备演练

**每季度 1 次**:
1. 在隔离环境完整恢复
2. 测量 RTO(恢复时间目标)
3. 测量 RPO(数据丢失)
4. 验证关键功能
5. 撰写报告

**RTO/RPO 目标**:
| 部署模式 | RTO | RPO |
| --- | --- | --- |
| 单实例 | 4h | 24h |
| HA(同区域) | < 5min | < 1min |
| 多区域 | < 1h | < 5min |
| 灾备(异地) | < 24h | < 1h |

---

## 10. 升级

### 10.1 升级策略

- **滚动升级**:Kubernetes 默认
- **蓝绿**:HA 场景可选
- **金丝雀**:大版本推荐
- **数据库迁移**:向前兼容,新代码可读旧表

### 10.2 升级前检查

- [ ] 阅读 release notes
- [ ] 备份数据库
- [ ] 备份密钥
- [ ] 在测试环境验证
- [ ] 制定回滚方案
- [ ] 公告计划维护窗口

### 10.3 Docker Compose 升级

```bash
# 1. 拉取新镜像
docker compose pull

# 2. 滚动重启
docker compose up -d

# 3. 监控
docker compose logs -f --tail=200 lm-api

# 4. 验证
curl http://localhost:8080/v1/healthz
curl http://localhost:8080/v1/readyz
```

### 10.4 K8s 升级

```bash
# 1. 更新 Helm values
helm upgrade lm license-manager/license-manager \
  --namespace license-manager \
  --set global.imageTag=1.1.0

# 2. 观察滚动升级
kubectl rollout status deployment/lm-api -n license-manager
kubectl rollout status deployment/lm-worker -n license-manager

# 3. 回滚(如需)
helm rollback lm -n license-manager
```

### 10.5 数据库迁移

```bash
# MySQL(默认提供器):lm-api 启动时自动执行 EF Migrate,无需手工命令
# 其他提供器:启动时走 EnsureCreated

# 需要手工迁移时(源码环境,安装 dotnet-ef 工具后):
dotnet ef database update --project src/LicenseManager.Infrastructure

# 查看迁移状态:
dotnet ef migrations list --project src/LicenseManager.Infrastructure
```

### 10.6 重大升级(跨主版本)

1. **停机窗口** 或 **蓝绿**
2. 完整备份
3. 升级到 v(N+1)
4. 数据迁移(可能耗时)
5. 烟测
6. 切流量
7. 观察 24h
8. 旧环境保留 7 天

---

## 11. 安全运维

### 11.1 密钥管理

- **轮换**:每年 1 次
- **存储**:KMS/HSM 优先
- **访问**:最小权限
- **审计**:所有访问记录

### 11.2 漏洞管理

- **CVE 监控**:订阅 OS、.NET、PostgreSQL 安全公告
- **SCA**:CI 中集成
- **补丁窗口**:
  - 严重:24h
  - 高:1 周
  - 中:1 月
  - 低:下一版本

### 11.3 访问控制

- **SSH**:仅密钥认证,禁用密码
- **堡垒机**:所有 SSH 经堡垒机
- **审计**:所有命令记录
- **2FA**:运维账号强制

### 11.4 审计日志

- 不可变存储
- 7 年保留
- 定期导出冷存

### 11.5 合规检查

- ISO 27001 控制项
- SOC2 控制项
- 季度自检

---

## 12. 故障排查

### 12.1 常见问题

#### Q: API 502/503

```bash
# 检查容器
docker compose ps

# 查看日志
docker compose logs --tail=200 lm-api

# 检查数据库连通性(无 db check 子命令,用就绪探针)
curl http://localhost:8080/v1/readyz

# 重启
docker compose restart lm-api
```

#### Q: 激活失败

```bash
# 查看激活日志
docker compose logs lm-api | grep "Activation"

# 检查 license 状态(表名 snake_case 单数;列名为 PascalCase,以实际 schema 为准)
mysql -u lm_admin -p license_manager \
  -e "SELECT Id, Status, ExpiresAt FROM license WHERE LicenseKey='XXXX';"

# 检查激活数
mysql -u lm_admin -p license_manager \
  -e "SELECT count(*) FROM activation WHERE LicenseId='xxx';"
```

#### Q: Webhook 投递失败

```bash
# 查看 webhook 状态(列名以实际 schema 为准)
mysql -u lm_admin -p license_manager \
  -e "SELECT Id, Status, AttemptCount, LastResponseStatus FROM webhook_delivery WHERE Status='failed' ORDER BY OccurredAt DESC LIMIT 20;"

# 手动重放(无 CLI 子命令,用管理 API,成功返回 200)
curl -X POST "https://license.example.com/v1/admin/webhooks/deliveries/<delivery_id>/replay" \
  -H "Authorization: Bearer <admin-token>"
```

#### Q: 数据库慢查询

```sql
-- 启用扩展
CREATE EXTENSION pg_stat_statements;

-- 查找慢查询
SELECT mean_exec_time, calls, query
FROM pg_stat_statements
WHERE mean_exec_time > 200
ORDER BY mean_exec_time DESC
LIMIT 20;

-- ANALYZE 更新统计
ANALYZE;
```

#### Q: 内存泄漏

```bash
# 查看进程
docker stats lm-api

# dump 堆
docker compose exec lm-api dotnet dump collect -p 1

# 分析
dotnet-symbol --port 5000
```

### 12.2 应急响应

```bash
# 1. 查看状态(含维护模式标记)
curl http://localhost:8080/v1/status
curl http://localhost:8080/v1/status/maintenance

# 2. 启用/关闭维护模式:无 CLI 子命令;
#    通过管理 API(平台运维域,见 API Spec)或数据库配置项操作,以代码为准。
```

### 12.3 事故复盘(公开)

严重事故 5 个工作日内公开 RCA(参见 RDP §4.33.3):

- **事故摘要**
- **影响范围**
- **时间线**
- **根因分析**
- **修复措施**
- **预防措施**

---

## 13. 容量规划

### 13.1 性能基线

| 指标 | 目标 |
| --- | --- |
| API P95 | < 100ms(在线验证)/ < 300ms(激活) |
| 错误率 | < 0.1% |
| 可用性 | 99.9%(HA) |
| 席位同步延迟 | < 5s |
| Webhook 投递延迟 | < 5s |
| 续费扣款成功率 | > 99% |

### 13.2 容量评估

> 以下以 PostgreSQL 为例;MySQL 部署请改用 `information_schema` 等价查询。

```bash
# 数据库
psql -U lm_admin -d license_manager -c "SELECT pg_size_pretty(pg_database_size('license_manager'));"

# 表大小
psql -U lm_admin -d license_manager -c "
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename))
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;
"
```

### 13.3 扩容策略

| 资源 | 触发 | 动作 |
| --- | --- | --- |
| CPU | > 70% 持续 30min | 增加副本 |
| 内存 | > 80% 持续 30min | 增加副本或扩容 |
| 磁盘 | > 80% | 扩容 / 清理 |
| DB 连接 | > 80% | 扩容连接池 |
| 队列 | 持续增长 | 增加 Worker |

---

## 14. 附录

### 14.1 关联文档

- [RDP v1.0.1](./LicenseManager-Platform-RDP-v1.0.1.md) - 产品需求
- [TAD v1.0](./LicenseManager-Platform-TAD-v1.0.md) - 技术架构
- [Database Design](./LicenseManager-Platform-Database-Design-v1.0.md) - 数据库设计
- [API Spec](./LicenseManager-Platform-API-Spec-v1.0.md) - API 规范
- [LicenseManager.Core Dev Guide](./LicenseManager.Core-Development-Guide-v1.0.md) - 核心库

### 14.2 关键脚本

> `lmctl` 为规划中的运维 CLI,当前版本未随附。下表对应能力现阶段通过管理 API / 内置机制完成:密钥生成由 `lm-api` 首启自动完成,密钥轮换用 `POST /v1/admin/system-keys/rotate`,Webhook 重放用 `POST /v1/admin/webhooks/deliveries/{deliveryId}/replay`,数据库迁移用 `dotnet ef database update`。

| 脚本 | 用途 |
| --- | --- |
| `lmctl init` | 初始化 |
| `lmctl backup` | 备份 |
| `lmctl restore` | 恢复 |
| `lmctl migrate` | 数据库迁移 |
| `lmctl keygen` | 密钥生成 |
| `lmctl key rotate` | 密钥轮换 |
| `lmctl webhook replay` | Webhook 重放 |
| `lmctl maintenance` | 维护模式 |
| `lmcli activation` | 激活(终端)(规划中,当前未实现) |
| `lmcli hardware-id` | 硬件 ID(已实现) |
| `lmcli keygen` | RSA 密钥对生成(已实现) |
| `lmcli refresh` | Refresh 文件(规划中,当前未实现) |

### 14.3 紧急联系

(部署文档中由客户填写)

| 角色 | 联系人 | 方式 |
| --- | --- | --- |
| SRE 主管 | | |
| 平台开发 | | |
| 数据库 DBA | | |
| 安全 | | |
| 商务支持 | | |

### 14.4 升级检查表

- [ ] 备份完成
- [ ] 密钥备份
- [ ] 测试环境验证
- [ ] 维护窗口公告
- [ ] 监控告警规则检查
- [ ] 回滚方案就绪
- [ ] 关键人员 standby
- [ ] 升级执行
- [ ] 烟测
- [ ] 状态页更新
- [ ] 通知用户
- [ ] 7 天观察

### 14.5 灾备检查表

- [ ] RTO/RPO 目标确认
- [ ] 异地存储就绪
- [ ] 灾备流程文档化
- [ ] 季度演练
- [ ] 报告存档

## 15. 支付渠道（PSP）接入配置（已废弃）

> 支付/PSP 集成已随商业化裁剪移除，资金流由外部销售系统承接。本章原内容（10 个支付渠道 + mock 的凭据配置、`POST /v1/admin/psp-configs` 管理端点、各渠道商户后台 Webhook 注册指引、`psp_config` 表与 `Payments:PublicBaseUrl` 等配置）已删除，部署时无需配置任何支付渠道凭据；本系统仅保留订阅续期（后台每小时自动扫描 + 手动触发 `POST /v1/admin/subscriptions/renewal/execute`）与出站事件通知（`subscription.renewed` / `subscription.payment_failed`）。

---

## 16. 系统特性开关配置

平台按销售市场（国内 / 欧美）区分部分系统特性的展示。特性开关存储于数据库 `system_config` 表（键值对，`Key` 唯一索引），由管理端「系统配置」页面维护，**运行时即时生效，无需重启**；代码内置默认值保证未配置时行为可预期（fail-safe）。

### 16.1 特性键目录

| 特性键 | 默认值 | 市场归属 | 作用 |
| --- | --- | --- | --- |
| `feature.sms_login` | `true` | 国内 | 登录页显示短信验证码登录 Tab（admin + portal） |
| `feature.cookie_consent` | `false` | 欧美 | 向访客展示 Cookie 使用同意横幅（GDPR/ePrivacy 合规） |
| `feature.scim` | `false` | 欧美 | 在 API Keys 页展示 SCIM 2.0 目录同步说明（配合 Okta / Entra ID 等目录同步） |

新增特性开关需在后端 `SystemConfigService.FeatureFlags` 目录登记（键 + 默认值），前端经 `GET /v1/config/features` 读取渲染。

### 16.2 管理接口

| 端点 | 认证 | 说明 |
| --- | --- | --- |
| `GET /v1/config/features` | 匿名 | 前端特性开关集合（登录页首屏即需读取；仅返回非敏感展示开关） |
| `GET /v1/admin/system-configs` | 管理员 JWT | 全部特性配置详情（当前值 / 默认值 / 最近更新时间 / 操作者） |
| `PUT /v1/admin/system-configs/{key}` | 管理员 JWT | 设置开关（body `{"value": true|false}`）；目录外键返回 404 |

### 16.3 数据库迁移

`system_config` 表由 5 库迁移 `SystemConfigTable`（Sqlite / MySql / SqlServer / PostgreSql / Oracle）创建，升级时执行 `dotnet ef database update`（或容器启动自动迁移）。表内仅存储管理员显式覆盖的值，未覆盖特性使用代码默认值。

### 16.4 相关配置键（appsettings.json）

| 配置键 | 默认 | 说明 |
| --- | --- | --- |
| `Sms:Provider` | `dev` | 短信验证码登录的服务商标识。当前构建仅内置 `dev`（开发发送器，仅记录日志不实际下发）；配置为其他值时**启动 fail-fast**，防止误以为短信通道已接通。接入真实短信服务商需扩展 `ISmsSender` 实现 |
| `Scim:BearerToken` | 空 | SCIM 2.0 目录同步（`/scim/v2`）的独立 Bearer Token，为空时该端点对所有请求 401（fail-closed）。生产必须配置 ≥32 字符随机值，仅下发给 IdP 目录同步集成使用 |

### 16.5 公开状态页

Portal 提供公开路由 `/status`（匿名访问），展示 `GET /v1/status`（服务 / 版本 / 环境 / 运行时长）与 `GET /v1/status/dependencies`（数据库 / Redis 健康与延迟），用于对客户公示服务状态（Trust/SLA 基础）。该页面不暴露任何内部信息，依赖异常详情仅写服务端日志。

## 17. 部署授权（自举授权）

部署授权控制**本部署实例**的功能档位与用量配额，是产品销售后的授权凭据：厂商按客户购买的销售档位（对齐营销首页价格模板）签发授权文件，客户导入后即时生效。未导入或授权失效时 fail-safe 降级为 **Free 档**（基础功能可用，配额收紧），不会导致业务中断。

### 17.1 档位与配额

| 档位 | 有效许可证 | 浮动席位（并发） | 每月 SDK 调用 | 可用特性 |
| --- | --- | --- | --- | --- |
| Free（默认） | 10 | 5 | 10,000 | 基础功能（无市场特性） |
| Pro | 100 | 50 | 100,000 | `feature.sms_login` |
| Enterprise | 不限 | 不限 | 不限 | 全部特性 |

授权文件可携带 `quotaOverrides` 按客户覆盖上述默认配额（null 字段沿用档位默认值）。

### 17.2 授权文件与签发

- 授权文件为 JSON（`{ payload, signature, keyVersion }`），payload 为 base64 的签发数据（被授权方、档位、配额、特性、签发/到期时间），由厂商 RSA-2048 私钥签名（SHA-256 + PKCS#1）。
- **验签公钥内置于程序集**（`DeploymentLicensePublicKey`），客户侧无私钥——授权文件仅能由厂商签发，不存在客户自签发的可能。私钥保存于厂商侧密钥库，**不随产品分发**。
- 签发工具：`tools/LicenseIssuer`（厂商侧专用）：`dotnet run --project tools/LicenseIssuer -- genkeys` 生成厂商密钥对（公钥须更新到 `DeploymentLicensePublicKey.Encoded` 内置常量后重新编译发布）；`issue` 子命令按档位 / 配额 / 有效期签发授权文件；`seal` 子命令对发布目录做完整性封签（见 §17.4「发布完整性自检」）。
- **厂商发布流程**：`dotnet publish` → 运行 `seal --dir <发布目录>` 生成 `integrity.manifest` → 将发布目录连同该文件一并分发。
- 厂商自有域名部署（白名单 `*.licensemanager.cn`，以 base64 数组内置于 `DeploymentWhitelist`）豁免全部校验：档位视为 Enterprise、配额不限、后台任务全部放行。新增豁免域名向 `EncodedPatterns` 数组追加 base64 串。

### 17.3 导入方式

| 方式 | 操作 | 说明 |
| --- | --- | --- |
| 管理端页面 | 「系统配置」页 → 部署授权卡片 → 导入授权文件（粘贴或选择本地文件） | 验签失败 / 格式错误 / 时钟回拨 → 400，错误详情即时展示 |
| 配置文件 / 环境变量 | `DeploymentLicense:FilePath` 指向授权文件（如 `data/deployment-license.json`；K8s 挂卷用 `DeploymentLicense__FilePath`） | 应用启动时加载验签；适合无人值守部署 |

导入成功后授权状态持久化于 `system_config` 表并**即时生效**（无需重启）；每小时后台任务自动重新验签处理自然到期降级。

### 17.4 防篡改与防破解机制

| 机制 | 说明 |
| --- | --- |
| 单路验签 | 授权文件必须携带厂商私钥签名，公钥内置、无自签发路径；payload 被篡改（改档位 / 改配额 / 改有效期）即验签失败 |
| 公钥指纹自检 | 内置公钥的 SHA-256 指纹硬编码在独立类型（`DeploymentLicenseIntegrity`）中，每次验签前比对——程序集被 dnSpy patch 替换公钥（塞入攻击者公钥后自签发授权）会立即校验失败（纵深防御层，根保障依赖发行包 Authenticode 代码签名） |
| 时钟回拨检测 | `system_config` 持久化历史最大时间戳（`deployment.clock.max_seen`）及其 HMAC 签名（`deployment.clock.max_seen.sig`）：回拨超过容差（`DeploymentLicense:ClockSkewToleranceHours`，默认 6h）→ 导入拒绝 / 刷新降级 Invalid；**改库篡改时间戳** → 签名不匹配 fail-closed 拒绝；签名缺失（升级遗留）→ 告警并按时间戳逻辑继续。注意：不防硬件级时钟篡改，仅防软件改期 |
| SDK 计数防篡改 | 每月 SDK 调用计数写入 `system_config` 时附带 HMAC-SHA256 签名（键 `deployment.usage.sdk.sig:yyyy-MM`），恢复时校验失败视为被篡改并拒绝恢复（重置为 0 从头累计） |
| 防篡改密钥保护 | HMAC 签名密钥由 `DeploymentTamperSecretProvider` 统一提供：显式配置 `DeploymentLicense:TamperSecret` 优先；未配置时**自动生成 32 字节随机密钥并以 DPAPI 加密持久化**到 `data/deployment-tamper-secret.json`（Windows DPAPI CurrentUser；非 Windows 依赖 0600 文件权限），避免签名密钥明文暴露在客户可读的配置文件中；文件不可写时回退 `Jwt:Secret`，最后内置兜底值 |
| FailClosed 模式 | `DeploymentLicense:FailClosed=true` 时授权非 Valid 将**拒绝业务**：许可证签发 422 `deployment_license_required`、SDK 调用 403 `deployment_license_required`（默认 false 仅降级 Free 档不拒签）。适用于按档位计费的强管控部署 |
| 公开路径配额封堵 | `Auth:AllowPublicActivation=true` 放行的 SDK 生命周期端点（activate / verify / heartbeat / deactivate / refresh-file / checkout）同样计入每月 SDK 调用配额，超限 429 `sdk_quota_exceeded`——公开路径无法绕过配额 |
| 破解尝试告警 | 授权导入连续失败 3 次及以上时日志升级为 **Error**（`DeploymentLicenseService.ConsecutiveImportFailures` 计数，成功导入即清零）——连续失败通常是破解尝试或配置错误信号，告警管道可按 Error 级别采集 |
| 发布完整性自检 | 厂商发布后用 `LicenseIssuer seal` 递归扫描发布目录全部 `*.dll` / `*.exe` 生成 SHA-256 清单并用厂商私钥签名（`integrity.manifest`，与部署授权同构信封）；运行时启动最早期（API 与浮动服务器 `Program.cs`）验签清单并逐一比对文件哈希，**任一程序集被篡改 / 清单被替换（验签失败）/ 清单缺失 / 出现未封签程序集 → fail-fast 拒绝启动（退出码 87）**。攻击者无法伪造清单（需厂商私钥），patch 校验代码本身又导致其所在程序集哈希失配。开发环境豁免；`AssemblyIntegrity:FailClosed=false` 时清单缺失/多余程序集仅告警放行 |

### 17.5 相关配置键（appsettings.json）

| 配置键 | 默认 | 说明 |
| --- | --- | --- |
| `DeploymentLicense:FilePath` | 空 | 授权文件路径；为空时启动后从 `system_config` 恢复（导入过则生效，未导入为 Free 档） |
| `DeploymentLicense:ClockSkewToleranceHours` | 6 | 时钟回拨容差（小时）；回拨超过该值拒绝导入 / 降级 Invalid |
| `DeploymentLicense:FailClosed` | `false` | `true` 时授权非 Valid 拒绝签发许可证与 SDK 调用（见 §17.4） |
| `DeploymentLicense:TamperSecret` | 自动生成（加密存储） | SDK 计数与时钟回拨证据的 HMAC 签名密钥：显式配置优先；未配置时自动生成 32 字节随机密钥并以 DPAPI 加密持久化到 `data/deployment-tamper-secret.json`（见 §17.4）；生成不可用时回退 `Jwt:Secret` → 内置兜底值。生产建议显式配置独立值 |
| `AssemblyIntegrity:FailClosed` | `true` | 发布完整性自检（见 §17.4）：清单缺失或存在未封签程序集时是否拒绝启动（程序集哈希失配/清单验签失败始终拒绝） |

### 17.6 独立部署的浮动授权服务器（无数据库）

`LicenseManager.Server`（浮动许可证服务器）可独立于主平台部署且**无数据库**：其 `Program.cs` 仅注册部署授权精简服务集（授权验签/状态缓存、SDK 使用计数器、启动/定时 HostedService），不注册依赖仓储的全量平台服务。授权状态经 `DeploymentLicense__FilePath` 指向的授权文件加载验签（K8s 挂卷场景）；未配置文件时 fail-safe 为 Free 档配额（10 许可证 / 5 席位 / 10,000 SDK 调用每月），席位超限返回 409 `DEPLOYMENT_SEAT_LIMIT`。
