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

79 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
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`.