16 KiB
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_demodecorators. ReturnsR.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 inheritingbase_model(provides id, create_user, create_time, update_user, update_time, is_delete) andbase_db(providessave()). Table name usesDB_PREFIXprefix.src/modules/{group}/{name}/repository.py- Data access layer: subclassBaseRepository[Model](fromsrc/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- PydanticBaseModelvalidation classes. Fields useField(...)for constraints (min_length,max_length,ge,le,gt). Custom validators use@field_validator('field')raisingValueError(msg). Uniqueness checks querydb.query(Model).filter(...).first()inside validators.src/modules/{group}/{name}/service.py- Business logic. Simple CRUD modules subclassBaseService[Model](fromsrc/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_filtersfor fixed filters,_serialize/_serialize_detailfor payload shaping,_build_create_fields/_build_update_fieldsfor field assembly). Complex modules (user/role/menu/dept/article/category/job/city) keep custom service functions. For delete, custom services reuseBaseService.batch_delete_with_r(repo, ids)(静态方法,软删 + 统一 R 文案) instead of callingrepo.batch_deletedirectly. 简单等值查询(唯一性校验、按字段取记录)优先用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 viadata.fieldordata.model_dump()(noawait 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 viaDB_DRIVER, pool config, active URLSQLALCHEMY_DATABASE_URL),redis.py,auth.py(JWT),captcha.py,email.py,sms.py. All loaded from.envviapython-dotenv.config/__init__.pyloads.envfirst, 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_demodecorators, from the formercore/middleware.py),logger.py(structured logging),app.py(create_app factory + lifespan). Also hostsbase_model.py/base_db.py/base_repository.py(moved from the formershared/) andbase_service.py(generic CRUD service base with overridable hooks; the 10 simple modules subclass it).src/constants/- Constants & enums (split out from the formershared/):constants.py(PAGE_SIZE/DEFAULT_PASSWORD/MESSAGE_OK/SERIALIZE_EXCLUDE_FIELDS),enums.py(enum dicts),pagination.py. Re-exported viasrc/constants/__init__.py.src/modules/- Business modules (business layer only), each a directory withmodels.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 formerrouters/+core/deps.py):__init__.py(register_router统一把全部路由挂到/api/v1前缀下:auth 路由免认证挂 app,v1 路由带login_required挂 app),deps.py(login_requiredauth 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/passwordare re-exported fromcoreviasrc/utils/__init__.pyfor back-compat.src/middleware/- Generic HTTP middlewares (all moved fromcore/middleware.py):cors.py(register_cors),rate_limit.py(per-IP Redis sliding-window limiter, opt-in viaRATE_LIMIT_ENABLED; on exceed returns standard{code:1}200 response directly, sinceBaseHTTPMiddlewareexceptions 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_middlewareempty request hook). Registered incore/app.pyin order: CORS → redis → rate_limit → db_session → operation_log → logger hook.migrations/- Alembic migrations.alembic.ini+migrations/env.py(injectssrc/, loads activeSQLALCHEMY_DATABASE_URLfromcore.config,create_app()to fillBase.metadata) +versions/0001_baseline.py(baseline =Base.metadata.create_all). Fresh installs:python scripts/init_db.py(auto-stamps head, driver-aware perDB_DRIVER) ormake db-init. Future schema changes:alembic revision --autogenerate->alembic upgrade head(see Makefilemigrate-*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 asAuthorization: Bearer <token>header. Supports token refresh and blacklist (logout) via Redis. login_requiredis a FastAPIDependsapplied globally to the v1 router. On success, it setsrequest.state.user_id,request.state.username,request.state.realname.permission_requiredis a per-endpoint decorator checking permission strings like"sys:level:list". User ID 1 (admin) bypasses all permission checks. It lazily queriesmodules.system.menu.service.get_permissions_list_sync.check_demois a per-endpoint decorator that returnsR.failed("演示环境,暂无操作权限")whenFASTAPI_DEMO=True. Apply it after@permission_requiredon 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
- Create
src/modules/{group}/{name}/models.py- model class inheritingbase_modelandbase_db(fromsrc/core/), set__tablename__ = DB_PREFIX + "{name}" - Create
src/modules/{group}/{name}/schemas.py- PydanticBaseModelvalidation classes withField(...)constraints and@field_validatorfor custom checks - Create
src/modules/{group}/{name}/repository.py- subclasscore.base_repository.BaseRepository+ module-level instance - Create
src/modules/{group}/{name}/service.py- for simple CRUD, subclasscore.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 (seesrc/modules/system/user/service.py) - Create
src/api/v1/endpoints/{name}.py- APIRouter with endpoints, apply@permission_requiredand@check_demoon write endpoints, delegate logic to the service - 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.0mapped_columnstyle) - 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
.envloading, bcrypt for password hashing, Pillow for captcha generation