Files
DjangoAdmin_Tornado_EleVue/CLAUDE.md
T
gitadmin fda0fe1a53
CI / lint (push) Canceled after 0s
CI / test (push) Canceled after 0s
签入版本
2026-09-16 10:08:07 +08:00

14 KiB
Raw Blame History

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.py exist intentionally: root app.py is the thin entry script (runs as __main__), src/app.py is the app factory (create_app()). Never import app from a context where the project root is sys.path[0] (e.g. pytest) — use from src.app import create_app there.

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 .env via Pydantic Settings (src/config/settings.py). Keys: TORNADO_*, DB_*, REDIS_*, MAIL_*, JWT_*, CAPTCHA_*, UPLOAD_*, LOGIN_*, LOG_*. TORNADO_DEMO=True blocks 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.py aggregates src/modules.*.models) and can autogenerate.
  • requirements.txt is 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, computes DATABASE_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}; Codes class: 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-local scoped_session db, Base), redis.py, logger.py, password.py (double-salt bcrypt), context.py (current_user_id/current_username/current_realname/current_trace_id contextvars), 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.BaseModel gives id/create_user(String50)/create_time/update_user/update_time/is_delete + to_dict() which excludes password/salt/is_delete and is timezone-defensive), schemas.py (Pydantic XxxSchema/XxxStatusSchema, extra="ignore"), repository.py (extends BaseRepository, sets model), service.py (a XxxService(BaseService) subclass + xxx_service instance; only auth/upload keep fully bespoke functions — login, file upload), handlers.py (Tornado handlers extending BaseHandler, delegating to xxx_service), __init__.py. dict bundles Dict+DictItem, config bundles Config+ConfigItem+ConfigWeb, role bundles RoleMenu, user bundles UserRole and the profile/index handlers. operation_log / login_log are full audit-log modules: @operation_log records write ops (rich fields + param redaction + IP 归属地) and @login_log records login success/failure/logout; both expose list/detail/delete endpoints under /api/v1/operationlog and /api/v1/loginlog. Permission nodes (menu rows with type=1) are resolved by src/common/permission.py:get_permissions_list(Redis 缓存,角色/菜单变更按版本号失效).
  • src/common/ — middleware/ (authentication/cors/operation_log/login_log/trace_id, plus access_decorators.py holding @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 placeholder swagger.py/sentry.py.
  • src/api/v1/ — one route module per resource (auth.py, users.py, roles.py, …), each exporting routes; router.py:build_routes() prefixes all with /api/v1 and adds /api/v1/health.

Request lifecycle / auth

  • Every business handler subclasses src/core/base_handler.py:BaseHandler. Its prepare() is wrapped with @login_required, so every request except /api/v1/login, /api/v1/captcha, and OPTIONS requires a valid JWT in Authorization: Bearer <token>. The authenticated userId is stored in current_user_id (a contextvar) via src/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 via common.permission.get_permissions_list(Redis 缓存,角色/菜单/用户角色变更时按版本号失效). User id 1 (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 when TORNADO_DEMO=True; handlers then delegate to the module's service. Responses go through R.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/update auto-fill audit fields (create_user/update_user) from current_realname(操作者姓名).
  • BaseService standard methods wrap in try/finally: repo.close(); bespoke services call db.close() in a finally block.
  • List handlers use page/limit query args (default PAGE_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 user salt column. src/common/utils/crypto.py is the facade: set_password(password) -> (salt, hashed) and check_password(password, salt, hashed) for setting/verifying, plus verify_user_password/is_legacy_hash for 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() excludes password/salt/is_delete (config.constants.SERIALIZE_EXCLUDE_FIELDS) — never return raw model to_dict() output containing secrets to the API. Model instances do not expose save()/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 in src/api/v1/, and import it in router.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.