Files
2026-09-12 17:39:13 +08:00

16 KiB
Raw Permalink Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

DjangoAdmin FastAPI+EleVue is a backend admin framework built with FastAPI (Python), Vue2.x + ElementUI (frontend, separate repo), and MySQL (default; switchable to PostgreSQL / SQL Server / SQLite / Oracle via DB_DRIVER). It provides RBAC-based user/role/menu management, dictionary/config management, and content modules (notices, articles, links, etc.). The UI language and code comments are in Chinese.

Running the Application

# Install dependencies
pip install -r requirements.txt

# Run dev server (default: 127.0.0.1:8031)
python src/main.py

# Run via uvicorn directly (must use --app-dir src, since src/main.py imports
# are src/-relative, e.g. `from core.app import create_app`)
uvicorn main:app --app-dir src --host 127.0.0.1 --port 8031

# Docker
docker-compose up --build

Requires a database (MySQL by default; set DB_DRIVER=postgresql/mssql/sqlite/oracle to switch — see .env for per-driver examples) and Redis running. Configure connection details in .env (see .env for all variables). The database table prefix is fastapi_ (set via DB_PREFIX).

Tests: pytest (contract snapshot + unit + integration). Contract snapshot tests (tests/contract/golden) guard route paths, permission strings, and table names; run with pytest (integration tests require real MySQL/Redis and are skipped by default, see pytest.ini). No linter or formatter configuration exists.

Architecture

Request Flow

src/main.py -> src/core/app.py:create_app() -> registers CORS, Redis, routers, middleware, exception handlers -> src/api/__init__.py:register_router mounts everything under the /api/v1 prefix (login/captcha unauthenticated on app, v1 behind login_required; the frontend VITE_PROXY targets http://127.0.0.1:8031/api/v1) -> permission_required decorator checks node-level RBAC per endpoint -> check_demo decorator blocks write operations in demo mode.

Layered Module Pattern

HTTP layer and business layer are separated. Each business module (e.g., level, position, dept, role, user, menu, notice, dicts, config, etc.) follows the same pattern:

  • src/api/v1/endpoints/{name}.py - HTTP layer: FastAPI router with endpoint definitions. Uses @permission_required("sys:{module}:{action}") and @check_demo decorators. Returns R.ok()/R.failed(). Endpoints that accept a body declare a Pydantic form parameter (e.g., data: LevelForm) — FastAPI parses and validates the JSON automatically before the handler runs. Handlers delegate to {module}.service; no business logic lives here.
  • src/modules/{group}/{name}/models.py - SQLAlchemy ORM model inheriting base_model (provides id, create_user, create_time, update_user, update_time, is_delete) and base_db (provides save()). Table name uses DB_PREFIX prefix.
  • src/modules/{group}/{name}/repository.py - Data access layer: subclass BaseRepository[Model] (from src/core/base_repository.py) + module-level singleton instance {name}_repo. Common operations (get_by_id, get_by_ids, get_one, get_all, exists, exists_by_field, count, filter, filter_by, create, update, paginate, batch_delete [软删,纯数据原语,返回删除条数]) come from the base; subclasses add custom queries. 等值查询类(get_one/get_all/count/filter_by/exists_by_field)自动软删过滤且跳过 None 值;filter(*criterion) 为原始出口(不自动软删,供 in_/and_/区间等复杂条件),需调用方自行追加 is_delete==0。Repository层不做 R 封装——删除的 R 响应由 service 层 BaseService.batch_delete_with_r(repo, ids)(静态方法)统一产出。
  • src/modules/{group}/{name}/schemas.py - Pydantic BaseModel validation classes. Fields use Field(...) for constraints (min_length, max_length, ge, le, gt). Custom validators use @field_validator('field') raising ValueError(msg). Uniqueness checks query db.query(Model).filter(...).first() inside validators.
  • src/modules/{group}/{name}/service.py - Business logic. Simple CRUD modules subclass BaseService[Model] (from src/core/base_service.py) + module-level singleton {name}_service: declare differences (repo/model/page_like_fields/page_eq_fields/page_order_by/unique_fields/serialize_maps) and override hooks (_apply_page_filters for fixed filters, _serialize/_serialize_detail for payload shaping, _build_create_fields/_build_update_fields for field assembly). Complex modules (user/role/menu/dept/article/category/job/city) keep custom service functions. For delete, custom services reuse BaseService.batch_delete_with_r(repo, ids) (静态方法,软删 + 统一 R 文案) instead of calling repo.batch_delete directly. 简单等值查询(唯一性校验、按字段取记录)优先用 repo.get_one(...)/repo.exists_by_field(field, value, exclude_id)/repo.filter_by(...) 等基类方法,不再手写 repo.filter(Model.is_delete == 0, ...) 原始查询。Body params arrive as already-validated Pydantic models, so services access fields via data.field or data.model_dump() (no await request.json() in service bodies).

同步/异步约定

  • service 需要 await(Redis 缓存读写/失效、request.json()、异步文件读取等)→ 用 async def,其中 DB/磁盘等同步操作一律用 asyncio.to_thread 包裹;对应端点为 async def + await。
  • 纯 DB CRUD(BaseService 简单模块及自定义同步 service)→ 保持同步 def;FastAPI 自动在线程池执行,不阻塞事件循环。
  • 禁止在 async def 端点或 service 内直接调用同步 DB 查询(会阻塞事件循环)。
  • 批量删除端点需 await parse_batch_ids(request)(异步读请求体),故 batch_delete 端点恒为 async def,属正常差异。

Request Validation

Pydantic v2 is used for all request body validation. The pattern for an endpoint:

@router.post('/add')
@permission_required("sys:level:add")
@check_demo
async def add(request: Request, data: LevelForm):
    return await level.LevelAdd(request, data)

Validation errors are caught by a global RequestValidationError handler registered in core/app.py and converted to the standard response shape {"code": 1, "data": null, "msg": "field: message/..."} with HTTP 200. Services receive the validated Pydantic instance and use data.model_dump() (full dict), data.model_dump(exclude={'id'}) (for updates), or direct attribute access (data.id, data.status).

Key Directories

  • src/core/config/ - Configuration split into sub-modules: app.py (app settings, CORS, upload limits), database.py (DB connection, driver registry via DB_DRIVER, pool config, active URL SQLALCHEMY_DATABASE_URL), redis.py, auth.py (JWT), captcha.py, email.py, sms.py. All loaded from .env via python-dotenv. config/__init__.py loads .env first, then re-exports everything.
  • src/core/ - Core infrastructure: database.py (engine/SessionLocal/_db_ctx/get_db_session()/commit_independently/Base), redis_client.py (get_redis()/redis_lifespan/redis_middleware), jwt.py (JWT create/parse/refresh/blacklist with HS256), password.py (bcrypt hashing with salt), response.py (R.ok()/R.failed()), exceptions.py (AuthorizationException/BusinessException), security.py (get_user_id/get_username), access_decorators.py (permission_required/check_demo decorators, from the former core/middleware.py), logger.py (structured logging), app.py (create_app factory + lifespan). Also hosts base_model.py/base_db.py/base_repository.py (moved from the former shared/) and base_service.py (generic CRUD service base with overridable hooks; the 10 simple modules subclass it).
  • src/constants/ - Constants & enums (split out from the former shared/): constants.py (PAGE_SIZE/DEFAULT_PASSWORD/MESSAGE_OK/SERIALIZE_EXCLUDE_FIELDS), enums.py (enum dicts), pagination.py. Re-exported via src/constants/__init__.py.
  • src/modules/ - Business modules (business layer only), each a directory with models.py, repository.py, schemas.py, service.py. Module groups: auth/, system/ (user, role, menu, dept, position, level), dictionary/, configuration/, file_template/, category/, article/, city/, link/, notice/, param/, job/ (job + job_log + scheduler + job_executor), login_log/, operation_log/, upload/.
  • src/api/ - Route assembly (from the former routers/ + core/deps.py): __init__.py (register_router 统一把全部路由挂到 /api/v1 前缀下:auth 路由免认证挂 app,v1 路由带 login_required 挂 app), deps.py (login_required auth dependency), v1/router.py (central route registration, fixed prefixes per module), v1/endpoints/ (per-module HTTP layer).
  • src/utils/ - Utilities: request.py (parse_batch_ids, like_escape, like_contains, parse_id_list), captcha, file, ip/ip2region, ua, rich_text, perm_cache. R/password are re-exported from core via src/utils/__init__.py for back-compat.
  • src/middleware/ - Generic HTTP middlewares (all moved from core/middleware.py): cors.py (register_cors), rate_limit.py (per-IP Redis sliding-window limiter, opt-in via RATE_LIMIT_ENABLED; on exceed returns standard {code:1} 200 response directly, since BaseHTTPMiddleware exceptions bypass global handlers), db_session.py (request-scoped DB session, commits/rolls back per business success), operation_log.py (auto-records write-operation logs, lazy-imports the service), logger.py (register_middleware empty request hook). Registered in core/app.py in order: CORS → redis → rate_limit → db_session → operation_log → logger hook.
  • migrations/ - Alembic migrations. alembic.ini + migrations/env.py (injects src/, loads active SQLALCHEMY_DATABASE_URL from core.config, create_app() to fill Base.metadata) + versions/0001_baseline.py (baseline = Base.metadata.create_all). Fresh installs: python scripts/init_db.py (auto-stamps head, driver-aware per DB_DRIVER) or make db-init. Future schema changes: alembic revision --autogenerate -> alembic upgrade head (see Makefile migrate-* targets). Requirements: alembic>=1.13.0.

Database Access

Database sessions are request-scoped via contextvars + Starlette middleware (src/middleware/db_session.py: db_session_middleware). Services call db = get_db_session() (from core.database) to get the current request's session. Models use self.save() / self.delete() from src/core/base_db.py. The middleware auto-closes the session after each request (with rollback). Soft deletes via is_delete flag (0=active, 1=deleted).

Cross-Database Migration(跨库迁移)

把源库(通常 MySQL)的「结构 + 数据」整体迁到目标库(PostgreSQL / SQL Server / SQLite),补上多驱动接入后缺的数据迁移与整体流程。结构可移植已由 Base.metadata.create_all() / alembic 打通(方言 DDL),scripts/migrate_db.py 负责数据泵 + 端到端操作。

  • scripts/migrate_db.py - CLI 驱动,--src-*/--dst-* 显式连接参数(不读 .env)。流程:create_database_if_missing()(镜像 init_db.py 的维护库建库分支:mysql 无库连接 / pg 连 postgres / mssql 连 master)→ 目标端 Base.metadata.create_all() 建结构(模型即 head,含 0002 的 city.parent_code nullable)→ 泵数据 → 重置自增序列 → 逐表 COUNT(*) 校验。
  • 数据泵:拓扑序(基础表 dept/menu/role/level/position/city/category/dict/config/file_template/param/job/link 在前,依赖表 role_menu/dict_item/config_item/job_log/user/user_role/article/notice/operation_log/login_log 在后)+ 每表 SELECT * ... ORDER BY id + 分块 executemany 显式带 id 插入,保住无外键裸列引用(user.dept_id、role_menu.role_id/menu_id、menu.parent_id 等)。sanitize() 防御性把零日期 0000-00-00* 统一转 None。
  • 自增序列重置:PG setval(pg_get_serial_sequence(...));MSSQL 需 SET IDENTITY_INSERT ON/OFF 包住显式插 id + DBCC CHECKIDENT 重置;MySQL/SQLite 显式插 id 后自动跟随 MAX(id),免处理。
  • 用法:make db-migrate-dry-run ARGS="..." 干跑核对计划 → make db-migrate ARGS="... --drop-target-first"(加 --drop-target-first 可幂等重跑)。完整操作手册 + 目标端校验清单见 document/数据库迁移_runbook.md。
  • 测试:tests/unit/scripts/test_migrate_db.py(sqlite 同时充当源/目标双库,无需真实 MySQL;9 例覆盖 id/引用保真、分块边界、only-tables、dry-run、PG/MSSQL 重置 SQL、count 不一致上报)。
  • 常用选项:--dry-run / --only-tables(续传)/ --skip-existing / --single-transaction / --skip-verify / --chunk-size。

Redis Access

Redis uses async client via core/redis_client.py. Connection pool created in redis_lifespan (composed with scheduler startup in core/app.py lifespan), injected per-request via redis_middleware into contextvars. Business code calls get_redis() to get the current request's async Redis client.

Authentication & Authorization

  • JWT tokens via core/jwt.py (HS256, configurable expiry via JWT_EXPIRE_MINUTES, secret via JWT_SALT env var). Token sent as Authorization: Bearer <token> header. Supports token refresh and blacklist (logout) via Redis.
  • login_required is a FastAPI Depends applied globally to the v1 router. On success, it sets request.state.user_id, request.state.username, request.state.realname.
  • permission_required is a per-endpoint decorator checking permission strings like "sys:level:list". User ID 1 (admin) bypasses all permission checks. It lazily queries modules.system.menu.service.get_permissions_list_sync.
  • check_demo is a per-endpoint decorator that returns R.failed("演示环境,暂无操作权限") when FASTAPI_DEMO=True. Apply it after @permission_required on all write endpoints (add, update, delete, status, import, export).

API Response Format

All endpoints return JSONResponse via R.ok() or R.failed():

{"code": 0, "data": ..., "msg": "操作成功", "ok": true}   // success
{"code": 1, "data": null, "msg": "错误信息", "ok": false}  // failure

R.ok() supports **kwargs for extra fields (e.g., R.ok(data=result, count=total) adds a count field).

Pagination Pattern

List endpoints read pageNo and pageSize from request.query_params, defaulting to page 1 and PAGE_SIZE (10). Services use SQLAlchemy .limit(limit).offset((page - 1) * limit).all() and return R.ok(data=result, count=count).

Delete Pattern

Single delete: DELETE /delete/{id} where id can be comma-separated (e.g., "1,2,3"). Batch delete: DELETE /batchDelete with JSON body [1, 2, 3], parsed via parse_batch_ids(request). Both use soft delete (set is_delete = 1).

Adding a New Module

  1. Create src/modules/{group}/{name}/models.py - model class inheriting base_model and base_db (from src/core/), set __tablename__ = DB_PREFIX + "{name}"
  2. Create src/modules/{group}/{name}/schemas.py - Pydantic BaseModel validation classes with Field(...) constraints and @field_validator for custom checks
  3. Create src/modules/{group}/{name}/repository.py - subclass core.base_repository.BaseRepository + module-level instance
  4. Create src/modules/{group}/{name}/service.py - for simple CRUD, subclass core.base_service.BaseService[Model] declaring differences (repo/model/page_like_fields/page_eq_fields/page_order_by/unique_fields/serialize_maps) + module-level singleton {name}_service; override hooks for special business. For complex modules, write custom service functions instead (see src/modules/system/user/service.py)
  5. Create src/api/v1/endpoints/{name}.py - APIRouter with endpoints, apply @permission_required and @check_demo on write endpoints, delegate logic to the service
  6. Register the router in src/api/v1/router.py - v1.include_router({name}_router, prefix="/{prefix}", tags=["标签"])

Key Dependencies

  • FastAPI 0.140.0, Uvicorn 0.51.0
  • SQLAlchemy 2.0.51 (declarative style with declarative_base(), not 2.0 mapped_column style)
  • DB drivers: PyMySQL 1.2.0 (MySQL), psycopg[binary] (PostgreSQL), pymssql (SQL Server), oracledb (Oracle); Redis 8.0.1 (async), PyJWT 2.13.0
  • Pydantic 2.13.4 (for request validation via BaseModel + Field + field_validator), openpyxl (for Excel import/export)
  • python-dotenv for .env loading, bcrypt for password hashing, Pillow for captcha generation