部署
Docker Compose(推荐)
git clone https://github.com/sleep1223/fast-soy-admin
cd fast-soy-admin
docker compose up -d| 服务 | 端口 | 说明 |
|---|---|---|
| nginx | 1880 | 前端 + API 反向代理 |
| app | 9999 | FastAPI 后端 |
| redis | 6379 | 缓存层 |
| postgres | 5432(仅内网) | 主数据库(postgres:18.3-alpine,挂卷 postgres_data) |
默认随容器起一套 PostgreSQL;
app通过 compose 内部网络以postgres:5432连接,端口不对外暴露。如需用外部托管 PG,可在docker-compose.yml中删除postgres服务,并把.env.docker的DB_URL指向外部地址。
查看日志
docker compose logs -f # 所有服务
docker compose logs -f app # 仅后端
make logs # 等价的 Makefile 入口;可加 SVC=app|nginx|redis、TAIL=N更新部署
git pull
docker compose down && docker compose up -d --build # 代码变更要加 --build(或 make rebuild)
docker compose exec app uv run tortoise migrate # 若本次更新含模型变更Docker 下的数据库初始化与迁移
核心原则:
- 迁移文件(
migrations/目录)由开发者在本地生成、随 git 提交、随镜像COPY进容器;- 迁移执行(建表 / 升级表结构 / 跑初始化数据)由运维在容器里执行。
启动时不会自动迁移——新部署必须手动
initdb,后续模型变更必须手动migrate。
上线前必改的关键信息(务必修改)
模板默认值是为了开箱即用,生产环境上线前必须修改,否则等同于把数据库公开:
| 位置 | 字段 | 默认值 | 必改原因 |
|---|---|---|---|
docker-compose.yml postgres.environment | POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB | 均为 fastsoyadmin | 弱口令;任何能进入 compose 网络的容器都能直连 |
.env.docker | DB_URL | postgres://fastsoyadmin:fastsoyadmin@postgres:5432/fastsoyadmin | 必须与 compose 中的账号密码保持一致 |
.env.docker | SECRET_KEY | 模板自带 | JWT 签名密钥,泄露=任意伪造 token;用 openssl rand -hex 32 重新生成 |
.env.docker | CORS_ORIGINS | ["*"] | 生产改成具体域名白名单 |
.env.docker | APP_DEBUG | 已是 false,确认不要改回 true | 调试模式会泄露堆栈与内部细节 |
修改 PG 密码后若已经起过容器,需要
docker compose down -v清掉postgres_data卷再重启——否则会沿用首次初始化时写入的旧密码。生产更稳的做法:用${PG_PASSWORD}等占位符 + 部署机的环境变量注入,避免明文入库。
数据持久化前置检查
默认 docker-compose.yml 声明了 redis_data / static_data / postgres_data 三个卷,PostgreSQL 数据存放在 postgres_data(容器内 /var/lib/postgresql/data),docker compose down 后保留,仅 docker compose down -v 才会删除。
不使用内置 PG 而切到外部托管库时,请把 .env.docker 的 DB_URL 指向外部地址,并删除 docker-compose.yml 中的 postgres 服务以及 app.depends_on.postgres、volumes.postgres_data 块。
首次部署(initdb)
make up / docker compose up -d 不会自动建表。新库第一次启动时,后端连上去会发现表不存在(日志里能看到 no such table: ... 之类的报错)——这是预期行为,手动触发一次 initdb 即可:
make up # 起容器
docker compose exec app uv run python -m app.cli initdb # 建表 + 写入基础数据
docker compose restart app # 让后端重连到已建好的库几个容易踩的点:
initdb必须在容器里跑,不能在宿主机跑。compose 用.env.docker,DB_URL/ Redis 地址都指向容器网络(postgres:5432);宿主机的make initdb会按本地.env走,连不到容器内的 PG。initdb只能在全新库上跑一次,它不是幂等建表,表已存在会报错。之后任何模型变更一律走migrate(见下一节)。判断该
initdb还是migrate:bashdocker compose exec app uv run tortoise history报错 / 无输出 → 空库,跑
initdb;有历史但不是最新 → 跑migrate。直连 PG 排查:
bashdocker compose exec postgres psql -U fastsoyadmin -d fastsoyadmin -c '\dt'
日常模型变更(makemigrations + migrate)
推荐流程:本地生成迁移、提交 git、服务器重建并执行。
# --- 本地(开发机) ---
# 1. 改 app/**/models.py
make makemigrations # 生成 migrations/models/*.py
git add migrations/ app/
git commit -m "feat(xxx): ..."
git push
# --- 服务器 ---
git pull
docker compose up -d --build app # 重建包含新代码 + 新迁移文件的镜像
docker compose exec app uv run tortoise migrate
docker compose exec app uv run tortoise history # 确认迁移已应用为什么不在容器里 makemigrations:容器内生成的迁移文件在 docker compose down 后会随容器销毁,且不会回流到 git,下次重建会重新生成冲突。迁移文件属于代码,必须在本地生成并入库。
业务模块 init_data.init() 何时跑
不是迁移——是每次启动由 Redis leader worker 自动执行的幂等对账(菜单 / 角色 / API / 业务种子数据)。所以:
- 新增业务模块 / 修改
init_data.py后,docker compose up -d --build即可,不需要手动触发; - 但表结构变更仍然要先
migrate,init_data依赖表已存在。
启动期的完整顺序见 启动初始化与对账。
常用排错命令
docker compose exec app uv run tortoise history # 查看已应用的迁移
docker compose exec app uv run tortoise heads # 查看最新迁移头
docker compose logs -f app | grep -iE "migrate|tortoise" # 观察启动期日志
docker compose exec app sh -c 'ls -la migrations/models/' # 确认迁移文件已随镜像打包回滚
生产回滚推荐方案:
- 先从备份恢复数据库(PG 用
pg_restore/ 快照还原;外部托管库走 DBA 通道); git revert对应的代码 + 迁移提交;docker compose up -d --build。
不要试图在生产用 tortoise downgrade 回滚结构变更。建议为 postgres_data 卷配置定期备份(pg_dump cron 或宿主机快照)。
手动部署
后端
uv sync --no-dev
uvicorn app:app --host 0.0.0.0 --port 9999 --workers 4前端
cd web && pnpm install && pnpm build
# 将 dist/ 部署到 Web 服务器Nginx 配置
server {
listen 80;
root /path/to/web/dist;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:9999;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}上线前安全清单
模板默认携带方便本地开发的演示数据(预填账号、明文 123456、占位短信网关)。上线前必须逐项清理,否则等同于把后台公开。建议在第一次发版前一次性做完,并写进运维 runbook。
1. 移除登录页快捷登录与默认填充账号
文件:web/src/views/_builtin/login/modules/pwd-login.vue
需要改两处:
a. 清空 model 默认值(避免输入框预填管理员账号 / 密码):
// before
const model: FormModel = reactive({
userName: 'Soybean',
password: '123456'
});
// after
const model: FormModel = reactive({
userName: '',
password: ''
});b. 删除快捷登录按钮区(accounts 数组 + handleAccountLogin + 模板里渲染这些按钮的 NSpace)。生产环境绝不能在 UI 上暴露任何账号。
校验:
cd web && pnpm dev # 打开登录页确认账号框为空、底部无快捷登录按钮
grep -RnE "Soybean|Super|Admin|123456" web/src/views/_builtin/login # 应无业务命中2. 删除 / 禁用模板内置用户
模板的 app/system/init_data.py 在首次启动时会创建以下账号(密码全部是 123456):
| 用户名 | 角色 | 密码 | 用途 |
|---|---|---|---|
| Soybean | R_SUPER | 123456 | 演示超管 |
| Super | R_SUPER | 123456 | 演示超管 |
| Admin | R_ADMIN | 123456 | 演示管理员 |
| User | R_USER | 123456 | 演示普通用户 |
生产推荐:保留 1 个真实超管,其他全部删除。 步骤:
- 创建你自己的超管(推荐先做这一步,避免锁死后台):用任一
Soybean / Super登录 → 系统管理 → 用户管理 → 新增 → 设置强密码 → 角色勾选R_SUPER。 - 退出登录,用新账号登录,验证权限正常。
- 逐个删除模板账号:用户管理列表里删除
Soybean / Super / Admin / User(或先批量「禁用」观察一周再删)。 - 如果不打算让访客自助注册:把前端
pwd-login.vue底部的register跳转按钮一并删除;同时考虑在后端auth.py注释掉/auth/register(或加 IP 白名单 / 邀请码)。 - 如果你修改了
init_data.py删除了模板用户的种子数据,注意init_data.py仅 upsert(不会主动删数据库里已存在的用户),所以老库需要手工清理一次,新库则不会再生成。
注意:当前
init_data.py的用户种子是无条件插入的——如果你只在数据库里删了用户没改种子文件,重启后会被重新创建。建议同时把种子里的演示用户也注释掉。
3. 重新规划普通用户角色
模板自带 R_USER 角色,菜单 / API / 按钮权限是按演示场景配置的(包含一些后台演示页)。生产环境普通用户通常不应直接复用 R_USER,建议:
做法 A(推荐):新建生产专用角色,原 R_USER 留空备用 / 删除
- 系统管理 → 角色管理 → 新增角色,例如
R_BIZ_USER(中文名"业务用户")。 - 在该角色的「菜单权限」里,只勾选用户实际需要看到的页面(一般只有
home+ 你的业务模块菜单)。 - 「按钮权限」里只勾选这些菜单下需要暴露的按钮码(如
B_HR_DEPT_VIEW,但不勾B_HR_DEPT_DELETE)。 - 「接口权限」一定要把对应的
(method, path)也勾上——按钮在前端隐藏不等于后端拒绝(参见 RBAC)。 - 「数据范围」按需选
self/department/custom,不要保留默认all。 - 把所有真实业务用户从
R_USER改挂到R_BIZ_USER。
做法 B:直接修改 R_USER
如果不想新建角色,在角色管理里编辑 R_USER,按上面 (2)~(5) 重新勾权限即可。但要注意 init_data.py 里 ensure_role 是 upsert + clear-and-readd(菜单/按钮/接口),意味着重启会被种子覆盖回演示配置——做法 B 必须同步修改 init_data.py 里 R_USER 的种子定义,否则改动会被回滚。
校验:
# 用 R_BIZ_USER 用户登录后台
# - 侧边栏只剩允许的菜单
# - 直接 curl 一个未授权接口应返回 2200/PERMISSION_DENIED
curl -H "Authorization: Bearer <user_token>" http://your-host/api/v1/system/users/search -X POST -d '{}'4. 接入真实短信网关替换验证码占位
当前 app/system/services/captcha.py 的 send_captcha() 只把验证码写进 Redis 并打日志,并未真正发送。这意味着:注册 / 验证码登录 / 忘记密码三条链路在生产环境形同虚设——任何人能看到日志(或拿到 Redis)就能登录任意账号。
替换步骤(以阿里云 SMS 为例,腾讯云 / 华为云 / Twilio 思路一致):
添加依赖:
bashuv add alibabacloud_dysmsapi20170525加配置项到
.env与app/core/config.py:envSMS_PROVIDER=aliyun # aliyun / tencent / mock,留 mock 等价当前占位 SMS_ACCESS_KEY_ID=xxx SMS_ACCESS_KEY_SECRET=xxx SMS_SIGN_NAME=your-sign # 阿里云已审核的签名 SMS_TEMPLATE_CODE=SMS_xxxxx # 已审核的模板码,模板内容要含 ${code} SMS_DAILY_LIMIT_PER_PHONE=10 # 单号单日上限 SMS_COOLDOWN_SECONDS=60 # 同号两次发送冷却改写
send_captcha,把radar_log("...开发模式...")替换为真实 SDK 调用,并加频控:pythonasync def send_captcha(redis: Redis, phone: str) -> bool: cooldown_key = f"captcha_cd:{phone}" if await redis.get(cooldown_key): return False # 命中冷却 daily_key = f"captcha_daily:{phone}:{date.today()}" if int(await redis.get(daily_key) or 0) >= APP_SETTINGS.SMS_DAILY_LIMIT_PER_PHONE: return False # 命中日上限 code = _generate_code() await redis.set(f"{_CAPTCHA_PREFIX}{phone}", code, ex=_CAPTCHA_EXPIRE) ok = await _provider_send(phone, code) # 调阿里云 SDK if not ok: return False await redis.set(cooldown_key, "1", ex=APP_SETTINGS.SMS_COOLDOWN_SECONDS) await redis.incr(daily_key) await redis.expire(daily_key, 86400) return True加测试 + 灰度:先在 staging 用真实手机号验证一遍登录 / 注册 / 重置密码三条链路,再切生产。生产环境一定要把
radar_log里打印code的那一行删掉,否则验证码会进入 Radar 监控数据库,等同于明文留底。加风控:建议把
/auth/captcha、/auth/register、/auth/reset-password三个端点单独配 IP 限流(参考app/core里GUARD_*配置),防止短信轰炸。
上线前最终检查
# 后端
grep -RnE "password.*123456|Soybean|Super|Admin" app/ # 应只剩注释 / 文档
docker compose exec app uv run python -c "from app.system.models import User; ..." # 列表应为你的真实账号
# 前端
grep -RnE "userName: 'Soybean'|password: '123456'" web/src/ # 应无命中
curl -s https://your-host/login | grep -iE "soybean|123456" # 应无命中
# 短信
APP_DEBUG=false python -c "import asyncio; from app.system.services.captcha import send_captcha; ..."
# 用真实手机号收一条短信确认完成上述四项才视为生产就绪。建议把这份清单挂到 PR 模板或上线 checklist 里,避免以后新环境复发。