14 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
DjangoAdmin Tornado+EleVue is a modular enterprise admin framework. This repo is the backend only — the Tornado REST API server. The Vue2.x + ElementUI frontend ("EleVue") is a separate project not in this repo (static/ and templates/ are mostly empty). Code comments are written in Chinese.
Key stack: Tornado 6.x, SQLAlchemy 2.0, Redis, JWT auth, Pydantic v2 (settings + validation schemas), bcrypt (double-salt password hashing). Python ≥3.10, managed via Poetry. The code lives under src/ (src is added to sys.path by the root app.py entry and by pytest's pythonpath).
Multi-database support: default driver is MySQL (pymysql, installed by default). PostgreSQL / SQL Server / SQLite / Oracle are supported via optional drivers — poetry install -E postgresql (psycopg2-binary) / -E mssql (pymssql) / -E oracle (oracledb); SQLite needs no extra install. Select via DB_DRIVER in .env (aliases: postgres=postgresql, sqlserver=mssql, sqlite3=sqlite); see .env for per-DB connection examples. When DB_DRIVER=sqlite, DB_DATABASE is the DB file path (host/port/user/password ignored); when DB_DRIVER=oracle, DB_DATABASE is the SERVICE_NAME. Engine/connection-pool options are dialect-aware in src/core/database.py (_connect_args, SQLite uses NullPool + check_same_thread=False). document/djangoadmin.tornado.elevue.sql is MySQL-only — for non-MySQL DBs create the schema via alembic upgrade head (Base.metadata.create_all) and seed via scripts/seed.py. The three join queries that used raw SQL+backticks (permission/menu/user services) were rewritten with SQLAlchemy ORM to stay dialect-agnostic.
⚠️ Two files named
app.pyexist intentionally: rootapp.pyis the thin entry script (runs as__main__),src/app.pyis the app factory (create_app()). Neverimport appfrom a context where the project root issys.path[0](e.g. pytest) — usefrom src.app import create_appthere.
Common Commands
poetry install # 安装依赖(含 dev)
poetry run python app.py --port=8041 # 启动服务(默认端口 8041)
poetry run pytest # 全部测试(集成测试无服务时自动跳过)
poetry run pytest tests/unit # 仅单元测试
poetry run flake8 src tests # 代码规范检查
poetry run mypy src # 类型检查
poetry run ruff check --fix src tests
poetry run alembic upgrade head # 数据库迁移
python scripts/seed_dict.py # 初始化业务字典(dict/dict_item,幂等)
python scripts/migrate_db.py --help # 跨库数据迁移(MySQL → PG/MSSQL/SQLite/Oracle,参数显式给出,不依赖 .env)
make install / dev / test / lint # Makefile 封装
Gotchas:
- Configuration comes from
.envvia Pydantic Settings (src/config/settings.py). Keys:TORNADO_*,DB_*,REDIS_*,MAIL_*,JWT_*,CAPTCHA_*,UPLOAD_*,LOGIN_*,LOG_*.TORNADO_DEMO=Trueblocks all write endpoints.JWT_SECRET必须配置(≥32 字节强随机,缺失/过弱启动即 fail-close 中止;生成:python -c "import secrets; print(secrets.token_urlsafe(48))")。上传白名单/上限:UPLOAD_ALLOWED_EXTS(逗号分隔带点号)、UPLOAD_MAX_SIZE_MB。登录防爆破:LOGIN_FAIL_MAX/LOGIN_LOCK_SECONDS。 - 业务枚举字典(性别/友链/通知来源等)统一由
scripts/seed_dict.py写入dict/dict_item表,业务代码经common/utils/dict_util.py读取(Redis 缓存,字典维护时自动失效)。config/constants.py不再承载业务枚举,仅保留SERIALIZE_EXCLUDE_FIELDS。 - The database schema is created from
document/djangoadmin.tornado.elevue.sql(import into MySQL). Alembic is wired up (alembic/env.pyaggregatessrc/modules.*.models) and can autogenerate. requirements.txtis a plain UTF-8 fallback; Poetry (pyproject.toml+poetry.lock) is the source of truth.
Architecture
Layered design. A request flows: Tornado handler (src/modules/<name>/handlers.py) → business logic (service.py) → repository (repository.py) → SQLAlchemy model (models.py). Input is validated with Pydantic schemas (schemas.py).
Startup: root app.py → src/bootstrap.py:main() (parses --port, installs SIGINT/SIGTERM graceful shutdown) → src/app.py:create_app() builds the tornado.web.Application with routes from src/api/v1/router.py:build_routes().
src/ packages
src/config/—settings.py(Pydantic Settings, computesDATABASE_URL/UPLOAD_DIR/SECRET_KEY),constants.py(仅SERIALIZE_EXCLUDE_FIELDS;业务枚举已迁至 dict 表,经common/utils/dict_util.py读取),messages.py(含PAGE_LIMIT/MAX_PAGE_LIMIT),logging.py(JSON structured logs).src/core/— infrastructure:response.py(R.ok/R.failed/R.response →{code, data, msg, ok};Codesclass: SUCCESS=0/FAILED=1/UNAUTHORIZED=401/FORBIDDEN=403/…; 写完响应并把 body 存到self._api_result供日志装饰器解析,函数返回None),exceptions.py(BaseAppError hierarchy +write_error记录异常日志),jwt.py(JWT 签发/解析/黑名单/刷新),database.py(writer + optional reader engine, thread-localscoped_sessiondb,Base),redis.py,logger.py,password.py(double-salt bcrypt),context.py(current_user_id/current_username/current_realname/current_trace_idcontextvars), and base classes:BaseHandler(on_finish统一关闭共享 DB 会话),BaseRepository(generic CRUD + soft delete + audit; see conventions),BaseService(template-method CRUD — see below).src/modules/— the ~20 business modules (auth, user, role, menu, dept, dict, config, level, position, city, notice, link, category, article, param, file_template, job, upload, operation_log, login_log). Each is a six-file package:models.py(SQLAlchemy,core.base_model.BaseModelgivesid/create_user(String50)/create_time/update_user/update_time/is_delete+to_dict()which excludespassword/salt/is_deleteand is timezone-defensive),schemas.py(PydanticXxxSchema/XxxStatusSchema,extra="ignore"),repository.py(extendsBaseRepository, setsmodel),service.py(aXxxService(BaseService)subclass +xxx_serviceinstance; only auth/upload keep fully bespoke functions — login, file upload),handlers.py(Tornado handlers extendingBaseHandler, delegating toxxx_service),__init__.py.dictbundles Dict+DictItem,configbundles Config+ConfigItem+ConfigWeb,rolebundles RoleMenu,userbundles UserRole and the profile/index handlers.operation_log/login_logare full audit-log modules:@operation_logrecords write ops (rich fields + param redaction + IP 归属地) and@login_logrecords login success/failure/logout; both expose list/detail/delete endpoints under/api/v1/operationlogand/api/v1/loginlog. Permission nodes (menu rows withtype=1) are resolved bysrc/common/permission.py:get_permissions_list(Redis 缓存,角色/菜单变更按版本号失效).src/common/—middleware/(authentication/cors/operation_log/login_log/trace_id, plusaccess_decorators.pyholding@check_demo+@permission_required),utils/(crypto bcrypt, captcha, file [mkdir+ 魔术字节校验verify_file_magic], rich_text [save_file/get_file_url/save_content,XSS 白名单清洗 + 路径穿越防护,图片/文件保存统一入口], ip2region [IP→省市区,日志归属地], dict_util [字典 Redis 缓存读取], helpers [parse_id_list/detect_os/detect_browser/detect_source/redact_params], validators),permission.py(跨模块权限解析).src/extensions/— re-exports core singletons (sqlalchemy.py→Base/db/SessionLocal/engine/reader_engine/ReaderSession;redis.py→redis) and placeholderswagger.py/sentry.py.src/api/v1/— one route module per resource (auth.py,users.py,roles.py, …), each exportingroutes;router.py:build_routes()prefixes all with/api/v1and adds/api/v1/health.
Request lifecycle / auth
- Every business handler subclasses
src/core/base_handler.py:BaseHandler. Itsprepare()is wrapped with@login_required, so every request except/api/v1/login,/api/v1/captcha, andOPTIONSrequires a valid JWT inAuthorization: Bearer <token>. The authenticateduserIdis stored incurrent_user_id(a contextvar) viasrc/common/middleware/authentication.py. - Per-handler authorization uses
@permission_required("sys:user:list")(src/common/middleware/access_decorators.py), which checks the user's permission nodes viacommon.permission.get_permissions_list(Redis 缓存,角色/菜单/用户角色变更时按版本号失效). User id1(admin) bypasses all permission checks. - Handlers are thin: mutating endpoints are wrapped with
@check_demo(src/common/middleware/access_decorators.py), which blocks writes with a unified message whenTORNADO_DEMO=True; handlers then delegate to the module's service. Responses go throughR.ok/R.failed. - 登录/登出安全:登录做防爆破(同一 IP+用户名 连续失败
LOGIN_FAIL_MAX次后锁定LOGIN_LOCK_SECONDS秒,Redis 计数);登出把当前 token 加入黑名单立即失效;POST /api/v1/token/refresh可轮换签发新 token(旧 token 拉黑)。/api/v1/dict/data/{code}提供字典下拉数据。
BaseService (template-method CRUD)
Simple modules subclass src/core/base_service.py:BaseService and only declare difference points to get standard CRUD for free: model/repo_cls, Pydantic create_schema/update_schema/status_schema, page_like_fields/page_eq_fields/page_order_by (pagination filters & sort), unique_fields ({field: message} uniqueness check), serialize_maps ({field: dict 字面映射或字典编码字符串};str 值优先按 dict_util 字典编码解析,回落 config.constants 常量名;取值时兼容 int/str key), serialize_extra/detail_serialize (plain callables (item) -> dict for extra fields), options_filter (dropdown-list equality filter). Hooks to override for bespoke logic: _before_add/_before_update/_before_delete, _build_create_fields/_build_update_fields (update excludes None values so optional fields aren't NULLed). Standard methods: get_page(_parse_pagination 限制 page≥1、limit∈[1,MAX_PAGE_LIMIT]), get_detail, get_options, add, update, delete, update_status (all take the Tornado handler, return R objects). Heavily-custom modules (user, menu) override add/update/get_page/_serialize_detail/_build_page_query to handle roles/city/permission-nodes; only auth/upload keep fully bespoke functions.
Service / repository conventions
- Repositories (
src/core/base_repository.py): soft-delete convention (is_delete=0= active). Convenience methods (get_by_id(ID-robust),get_by_ids,get_one,all,count,exists,exists_by_field(field, value, exclude_id),filter_by,paginate,create,update,soft_delete,batch_delete("1,2,3")) auto-append the soft-delete filter;filter(*criterion)is the raw escape hatch (no soft filter) for complex conditions.create/updateauto-fill audit fields (create_user/update_user) fromcurrent_realname(操作者姓名). BaseServicestandard methods wrap intry/finally: repo.close(); bespoke services calldb.close()in afinallyblock.- List handlers use
page/limitquery args (defaultPAGE_LIMIT=10); deletion endpoints accept comma-separated IDs; all deletes are soft (is_delete=1). - Passwords are always salted via the double-salt bcrypt scheme in
src/core/password.py(generate_salt/encrypt_password/verify_password); the salt is stored in the usersaltcolumn.src/common/utils/crypto.pyis the facade:set_password(password) -> (salt, hashed)andcheck_password(password, salt, hashed)for setting/verifying, plusverify_user_password/is_legacy_hashfor the login path. Legacy no-salt bcrypt hashes are accepted on login and auto-upgraded to the salt scheme (modules/auth/service.py:_verify_login_password). MD5 is intentionally not supported. BaseModel.to_dict()excludespassword/salt/is_delete(config.constants.SERIALIZE_EXCLUDE_FIELDS) — never return raw modelto_dict()output containing secrets to the API. Model instances do not exposesave()/delete()— data access goes through repositories.
API / URL conventions
- All routes are under
/api/v1. Handler naming:XxxListHandler,XxxDetailHandler,XxxAddHandler,XxxUpdateHandler,XxxDeleteHandler,XxxStatusHandler,XxxGetListHandler(last returns an option list for dropdowns). Route conventions preserved from the original app:/list?,/detail/(\d+),/add,/update,/delete/(\d+),/status,/getXxxList. - To add a new feature: copy an existing simple module (e.g.
link) across the six files, register a route module insrc/api/v1/, and import it inrouter.py.
Tests
tests/unit covers core (jwt, response, crypto, BaseService, repository, serialization-security) and module schemas. tests/integration (health/captcha/DB/Redis) runs only when MySQL and Redis are reachable — verified via a real SELECT 1/ping in tests/integration/conftest.py, otherwise skipped. The root conftest.py was removed; pytest resolves src via [tool.pytest.ini_options].pythonpath = ["src"], and integration tests import the factory as from src.app import create_app.