From f1ce992d690103234cdc0f8cbcb5cb6615e503e5 Mon Sep 17 00:00:00 2001 From: shiran <2488252513@qq.com> Date: Sat, 9 May 2026 19:00:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E6=B7=BB=E5=8A=A0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=AE=A1=E7=90=86=E5=91=98=E8=87=AA=E5=8A=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 .env.example 中添加默认管理员相关配置项 - 在 docker-compose.yml 中添加默认管理员环境变量映射 - 在 server/app/core/config.py 中定义默认管理员配置 - 创建 server/app/db/bootstrap.py 文件实现默认管理员创建逻辑 - 在 server/app/main.py 的生命周期中集成默认管理员确保功能 - 更新 README.md 文档说明新的管理员配置方式 新配置项包括:DEFAULT_ADMIN_ENABLED、DEFAULT_ADMIN_PHONE、 DEFAULT_ADMIN_EMAIL、DEFAULT_ADMIN_PASSWORD、DEFAULT_ADMIN_NICKNAME 和 DEFAULT_ADMIN_SYNC_PASSWORD。 --- .env.example | 12 +++++++ docker-compose.yml | 6 ++++ server/README.md | 8 +++-- server/app/core/config.py | 7 ++++ server/app/db/bootstrap.py | 65 ++++++++++++++++++++++++++++++++++++++ server/app/main.py | 2 ++ 6 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 server/app/db/bootstrap.py diff --git a/.env.example b/.env.example index 957f980..5b574b2 100644 --- a/.env.example +++ b/.env.example @@ -69,6 +69,18 @@ SENTRY_DSN= LOG_LEVEL=INFO # 是否输出 JSON 格式日志。 LOG_JSON=false +# 是否在后端启动时自动创建默认管理员。 +DEFAULT_ADMIN_ENABLED=true +# 默认管理员手机号,可用于登录管理端。 +DEFAULT_ADMIN_PHONE=13900000001 +# 默认管理员邮箱,也可用于登录管理端。 +DEFAULT_ADMIN_EMAIL=admin@ciyuan.local +# 默认管理员密码,生产环境必须修改。 +DEFAULT_ADMIN_PASSWORD=admin123456 +# 默认管理员昵称。 +DEFAULT_ADMIN_NICKNAME=系统管理员 +# 启动时是否把已有默认管理员密码同步为 DEFAULT_ADMIN_PASSWORD。 +DEFAULT_ADMIN_SYNC_PASSWORD=true # 管理端前端构建配置 # 管理端构建阶段 Node 基础镜像。 diff --git a/docker-compose.yml b/docker-compose.yml index 800661a..6fab80a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -86,6 +86,12 @@ services: SENTRY_DSN: "${SENTRY_DSN}" LOG_LEVEL: "${LOG_LEVEL}" LOG_JSON: "${LOG_JSON}" + DEFAULT_ADMIN_ENABLED: "${DEFAULT_ADMIN_ENABLED}" + DEFAULT_ADMIN_PHONE: "${DEFAULT_ADMIN_PHONE}" + DEFAULT_ADMIN_EMAIL: "${DEFAULT_ADMIN_EMAIL}" + DEFAULT_ADMIN_PASSWORD: "${DEFAULT_ADMIN_PASSWORD}" + DEFAULT_ADMIN_NICKNAME: "${DEFAULT_ADMIN_NICKNAME}" + DEFAULT_ADMIN_SYNC_PASSWORD: "${DEFAULT_ADMIN_SYNC_PASSWORD}" expose: - "${SERVER_INTERNAL_PORT}" volumes: diff --git a/server/README.md b/server/README.md index 44ace6a..0a74ae2 100644 --- a/server/README.md +++ b/server/README.md @@ -1,6 +1,8 @@ # 后端默认管理员账号 -- 管理后台登录账号(手机号):`13900000001` -- 管理后台登录密码:`demo123456` +后端启动时会根据根目录 `.env` 自动确保默认管理员存在。 -> 说明:以上为 `seed_demo_data.py` 初始化的演示管理员账号(role=`admin`)。 +- 管理后台登录账号:`DEFAULT_ADMIN_PHONE` 或 `DEFAULT_ADMIN_EMAIL` +- 管理后台登录密码:`DEFAULT_ADMIN_PASSWORD` + +默认模板值为 `13900000001 / admin123456`。生产环境请修改根目录 `.env`。 diff --git a/server/app/core/config.py b/server/app/core/config.py index 0c4a517..74f70d9 100644 --- a/server/app/core/config.py +++ b/server/app/core/config.py @@ -34,6 +34,13 @@ class Settings(BaseSettings): LOG_JSON: bool = False LOG_LEVEL: str = "INFO" + DEFAULT_ADMIN_ENABLED: bool = True + DEFAULT_ADMIN_PHONE: str = "13900000001" + DEFAULT_ADMIN_EMAIL: str = "admin@ciyuan.local" + DEFAULT_ADMIN_PASSWORD: str = "admin123456" + DEFAULT_ADMIN_NICKNAME: str = "系统管理员" + DEFAULT_ADMIN_SYNC_PASSWORD: bool = True + class Config: env_file = _env_file diff --git a/server/app/db/bootstrap.py b/server/app/db/bootstrap.py new file mode 100644 index 0000000..14dbadf --- /dev/null +++ b/server/app/db/bootstrap.py @@ -0,0 +1,65 @@ +from sqlalchemy import or_, select +from sqlalchemy.orm import Session + +from app.core.config import settings +from app.core.security import get_password_hash, verify_password +from app.db.session import sync_engine +from app.models.user import User + + +def ensure_default_admin() -> None: + if not settings.DEFAULT_ADMIN_ENABLED: + return + + phone = settings.DEFAULT_ADMIN_PHONE.strip() or None + email = settings.DEFAULT_ADMIN_EMAIL.strip() or None + if not phone and not email: + raise RuntimeError("DEFAULT_ADMIN_PHONE or DEFAULT_ADMIN_EMAIL is required") + + filters = [] + if phone: + filters.append(User.phone == phone) + if email: + filters.append(User.email == email) + + with Session(sync_engine) as session: + user = session.execute(select(User).where(or_(*filters))).scalar_one_or_none() + + if user is None: + user = User( + phone=phone, + email=email, + password_hash=get_password_hash(settings.DEFAULT_ADMIN_PASSWORD), + nickname=settings.DEFAULT_ADMIN_NICKNAME, + identity="both", + role="admin", + is_active=True, + ) + session.add(user) + session.commit() + return + + changed = False + expected = { + "phone": phone, + "email": email, + "nickname": settings.DEFAULT_ADMIN_NICKNAME, + "identity": "both", + "role": "admin", + "is_active": True, + } + for field, value in expected.items(): + if value is not None and getattr(user, field) != value: + setattr(user, field, value) + changed = True + + if settings.DEFAULT_ADMIN_SYNC_PASSWORD and not verify_password( + settings.DEFAULT_ADMIN_PASSWORD, + user.password_hash, + ): + user.password_hash = get_password_hash(settings.DEFAULT_ADMIN_PASSWORD) + changed = True + + if changed: + session.add(user) + session.commit() diff --git a/server/app/main.py b/server/app/main.py index 464e341..903cf23 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -14,6 +14,7 @@ from app.api.v1.router import v1_router from app.core.config import settings from app.core.deps import get_current_active_user, get_db from app.core.storage import init_storage +from app.db.bootstrap import ensure_default_admin from app.db.migrations import run_startup_migrations from app.models.spot import Spot from app.models.tag import Tag @@ -44,6 +45,7 @@ logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): await asyncio.to_thread(run_startup_migrations) + await asyncio.to_thread(ensure_default_admin) app.state.redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True) app.state.storage = init_storage() yield