部署总览
2024/6/5大约 6 分钟
部署总览
前后端分离部署:前端是纯静态资源(nginx / IIS 托管),后端是 ASP.NET Core 8 应用。若启用商城 C 端,后端会有两个宿主——内网后台 ZR.Admin.WebApi 与公网 C 端 ZR.Front.WebApi。
一、部署形态选择
| 方式 | 适用场景 | 说明 |
|---|---|---|
| Linux + nginx + systemd | 生产推荐 | 进程托管交给 systemd,开机自启、崩溃重启、日志进 journald |
| Linux + nginx + supervisor | 生产(已有 supervisor 的机器) | 配置见 supervisor 部署 |
| Docker Compose | 容器化环境 | 仓库自带 Dockerfile + compose.yaml(含两个宿主) |
| Windows + IIS | 已有 Windows 服务器 | 配置最繁琐,见 IIS 部署 |
| Windows 服务 | 不想装 IIS | 用 sc.exe / NSSM 托管 dotnet 进程 |
二、运行环境
| 项 | 要求 |
|---|---|
| 运行时 | ASP.NET Core Runtime 8.0(不必装 SDK),dotnet --list-runtimes 可查 |
| 数据库 | SQL Server / MySQL / PostgreSQL / Oracle / SQLite(驱动见快速开始) |
| Redis | 多实例部署必装;单实例可选(见下文「多实例」) |
| 反向代理 | nginx(推荐)或 IIS |
| 时区 | 容器/系统建议 Asia/Shanghai,否则日志与时间字段会偏 8 小时 |
三、目录规划(Linux)
/data/zradmin/ # 内网后台宿主
├── app/ # dotnet publish 产物
├── logs/ # NLog 输出:all.log / error.log / sql.log
├── DataProtection/ # 数据保护密钥环,必须持久化
└── uploads/ # 本地存储模式的附件(app/wwwroot/uploads 软链或直接改 localSavePath)
/data/zradmin-front/ # 公网 C 端宿主(可选)
├── app/
├── logs/
└── DataProtection/
/data/zradmin-web/ # 前端静态资源(dist 内容)
mkdir -p /data/zradmin/{app,logs,uploads,DataProtection}
useradd -r -s /sbin/nologin zradmin # 建议用非 root 账号运行
chown -R zradmin:zradmin /data/zradmin
DataProtection目录不持久化的后果:每次重启密钥环重建,已签发的 Cookie / 防伪令牌全部失效;多副本还必须共享同一密钥环。
四、上线前必做的 5 件事
- 配置密钥:
JwtSettings:SecretKey必填且大于 16 位,缺失时AddJwt()直接抛JwtSettings获取失败,项目起不来;公网宿主另需MemberJwt:SecretKey(未配置则会员令牌拒绝签发)。 - 两个宿主的
workId必须不同(如内网1、公网2):生产环境不会自动 +1,相同会导致雪花 ID 冲突。 - 确认必备文件随发布存在(见下方清单),
ai.json缺失会直接启动失败。 - 决定上传去向:本地存储(
wwwroot/uploads+ nginx 映射)或对象存储(OssProvider=s3/aliyun)。 - 只在单实例执行一次数据库初始化:
--initdb会做全量迁移 + 种子数据 + 模块初始化,多副本同时执行会互相覆盖。
五、必备文件清单
dotnet publish 产物中以下文件不能删:
| 文件 | 缺失后果 |
|---|---|
ai.json | AddJsonFile(..., optional: false) → 启动异常退出 |
iprate.json | 限流规则加载失败(规则为空,限流形同未启用) |
ip2region_v4.xdb | IP 归属地库缺失,启动抛 FileNotFoundException |
NLog.config | 日志不落文件,排障只能看控制台 |
appsettings.json | 无配置,启动失败 |
appsettings.Production.json | 可选,但生产覆盖项(urls、corsUrls)通常放这里 |
wwwroot/data.xlsx | 种子数据缺失,--initdb 会跳过种子导入(仅建表) |
wwwroot/(export、Generatecode) | 导出文件与代码生成模板不可用 |
csproj已用Content Update把ai.json、ip2region_v4.xdb、wwwroot子目录纳入发布,正常publish不会丢;手工拷贝bin/目录才容易漏。
六、密钥与敏感配置注入
生产不要把这些写进仓库里的 appsettings.json(模板中的密钥是示例值,必须替换):
| 配置项 | 环境变量写法 | 说明 |
|---|---|---|
JwtSettings:SecretKey | JwtSettings__SecretKey | 后台令牌,>16 位 |
MemberJwt:SecretKey | MemberJwt__SecretKey | 仅公网宿主,会员令牌 |
WechatPay:MerchantCertificatePrivateKey | WechatPay__MerchantCertificatePrivateKey | 微信支付证书私钥 |
WechatPay:MerchantV3Key | WechatPay__MerchantV3Key | APIv3 密钥 |
WechatPay:NotifyUrl | WechatPay__NotifyUrl | 公网 HTTPS 回调地址 |
dbConfigs:0:Conn | dbConfigs__0__Conn | 数组用下标 |
RedisServer:Cache | RedisServer__Cache | 启用 Redis 时 |
S3:SecretKey / ALIYUN_OSS:SECRET | S3__SecretKey / ALIYUN_OSS__SECRET | 对象存储 |
注入方式:
- systemd:
EnvironmentFile=/etc/zradmin/admin.env(一行一个KEY=VALUE,权限600); - Docker:
compose.yaml里env_file:; - IIS:
web.config的<environmentVariables>; - 双下划线
__对应配置层级(A:B→A__B)。
七、初始化与升级
首次上线
cd /data/zradmin/app
export ASPNETCORE_ENVIRONMENT=Production
dotnet ZR.Admin.WebApi.dll --initdb # 建表 + 种子 + 模块初始化,执行完退出,不启动 Web
--initdb没有环境限制,生产也能执行;它的实质是"全量迁移 + 种子导入 +ModuleInit开关对应的模块初始化",因此只应在新库或确认需要时执行一次。
日常升级
1. 备份数据库(迁移只增不减,但 DDL 仍建议先备份)
2. 停服务(systemctl stop / docker compose down)
3. 替换 app/ 目录(保留 appsettings.Production.json、logs、uploads、DataProtection)
4. 启动服务 —— 启动期自动执行三步幂等迁移:
历史表名重命名 → 系统库补 TenantId 列 → 租户库补业务表/列
5. 冒烟验证:登录、菜单、上传、WebSocket、对象存储下载
需要先预演差异时,把 DbMigration:ReportOnly 置 true 启动一次,日志会输出待执行项而不真正执行 DDL。
八、双宿主(商城 C 端)
启用商城时建议拆成两个宿主,公网只暴露 C 端接口:
内网宿主 ZR.Admin.WebApi | 公网宿主 ZR.Front.WebApi | |
|---|---|---|
| 端口 | 8888 | 8889(容器内统一 8888 映射) |
| 暴露范围 | 仅内网 / 办公网 | 公网,仅 [PublicApi] 接口 |
| 令牌 | 后台 JWT | 会员令牌(mall-token) |
workId | 1 | 2(必须不同) |
| 数据库配置 | 同一套库,dbConfigs 必须一致 | |
| 多租户开关 | TenantSettings:UseTenant 两边必须一致 |
详见公网宿主。
九、多实例(负载均衡 / 多副本)
| 依赖点 | 无 Redis 时的后果 |
|---|---|
| 限流计数 | 各节点独立计数,实际放行倍数 = 节点数 |
单设备登录(SingleLogin) | 只在单节点生效,用户仍可多端同时在线 |
| Quartz 触发防重 | 占位只在进程内,同一任务会在每个节点各跑一次 |
| 第三方登录 state | A 节点发起、B 节点回调 → 报"授权已失效" |
| 2FA 失败计数 / 锁定 | 各节点独立累计,锁定被稀释 |
结论:多实例必须启用 Redis(RedisServer:open = 1),并保证各节点时间同步(NTP)。
十、日志与排障
| 现象 | 先看哪里 |
|---|---|
| 启动即退出、无 Web 日志 | 控制台输出 + logs/all.log:多为 ai.json 缺失或 SecretKey 未配置 |
| 登录报"JwtSettings获取失败" | JwtSettings:SecretKey 为空 |
| 登录报"租户信息不匹配" | 请求头 tenantId / 域名租户与 token 租户不一致(多租户) |
| 上传 413 | nginx client_max_body_size 小于 Upload:requestLimitSize |
| 上传成功但图片 403 | Upload:uploadUrl 或 S3 domainUrl(自建必须带桶名)配置错 |
| 实时消息连不上 | nginx 未给 /msgHub 开 WebSocket(Upgrade 头) |
| 日志里 IP 全是反代地址 | nginx 未传 X-Forwarded-For / X-Forwarded-Proto |
日志文件(NLog.config):
logs/all.log 全量
logs/error.log 异常
logs/sql.log SQL(ShowDbLog=true 时)
logs/bak/ 按大小归档
