feat(phase5): 解耦与收尾清理

- core/redis_client.py 移除 scheduler 耦合,lifespan 由 core/app.py 组合(Redis→调度器→Redis)
- main.py 改从 core.app 导入 create_app
- 删除 apps/、extensions/ 目录;utils 删除已迁移垫片与死代码 formatter.py
- 新增 deploy/(nginx、supervisor)、scripts/(init_db、seed)、logs/(结构化日志落盘)
- core/app.py 接入 core.logger setup_logging
- 更新 CLAUDE.md 反映 core/shared/modules 新结构

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-07 10:16:44 +08:00
co-authored by Claude
parent 64a5504396
commit 63bebedf05
99 changed files with 229 additions and 631 deletions
+16 -16
View File
@@ -53,30 +53,30 @@ async def add(request: Request, data: LevelForm):
return await level.LevelAdd(request, data)
```
Validation errors are caught by a global `RequestValidationError` handler registered in `apps/__init__.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`).
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
- **`config/`** - Configuration split into sub-modules: `app.py` (app settings, CORS, upload limits), `database.py` (DB connection, pool config, `SQLALCHEMY_MYSQL_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.
- **`extensions/`** - Framework extensions: SQLAlchemy engine/session/contextvars (`extensions/sqlalchemy.py`), custom exception class (`extensions/exception.py`), `get_db_session()` accessor.
- **`routers/v1.py`** - Central route registration. All module routers are included here with prefix paths.
- **`apps/middleware/`** - `login_required` (JWT auth dependency, ignores `/login` and `/captcha`), `permission_required` (decorator checking user's permission nodes; admin userId=1 bypasses all checks), `check_demo` (decorator that blocks write operations when `FASTAPI_DEMO=True`), `db_session` (Starlette middleware setting request-scoped DB session via contextvars).
- **`utils/`** - `response` (JSON response helpers: `R.ok()`, `R.failed()`), `jwt` (JWT create/parse/refresh/blacklist with HS256), `password` (bcrypt-based password hashing with salt), `security` (`get_user_id(request)`, `get_username(request)` — reads from `request.state` set by `login_required`), `request` (`parse_batch_ids`, `like_escape`, `like_contains` for safe LIKE queries), `captcha` (verification code generation), `file` (file operations), `redis_ctx` (Redis connection pool lifecycle and request-scoped context via `get_redis()`), `ip`, `formatter`, `rich_text`.
- **`apps/constants/`** - `constants.py` (enum dicts for gender, dept types, menu types, etc.), `message.py` (default page size, messages, `SERIALIZE_EXCLUDE_FIELDS`).
- **`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`), `middleware.py` (`login_required`/`permission_required`/`check_demo`/`db_session_middleware`/`operation_log_middleware`/`register_cros`), `deps.py` (DI), `logger.py` (structured logging), `app.py` (create_app factory + lifespan).
- **`shared/`** - Cross-module shared layer: `enums.py` (enum dicts), `constants.py` (PAGE_SIZE/DEFAULT_PASSWORD/SERIALIZE_EXCLUDE_FIELDS), `pagination.py`, `base_model.py`, `base_db.py`, `base_repository.py` (Generic repository base), `base_service.py`.
- **`modules/`** - Business modules, each a directory with `router.py` (may export multiple routers, e.g. `dict_router` + `dict_item_router`), `schemas.py`, `service.py`, `repository.py`, `models.py`. Module groups: `auth/`, `system/` (user, role, menu, dept, position, level), `dictionary/`, `configuration/`, `cms/` (item, item_cate, ad, ad_sort), `member/`, `region/`, `link/`, `notice/`, `job/` (job + job_log + scheduler + job_executor), `log/`, `upload/`, `index/`.
- **`routers/`** - `v1.py` central route registration (imports routers from `modules/`), `__init__.py` mounts login (免认证) on app and v1 behind `login_required`.
- **`utils/`** - Remaining utilities that stay here: `request` (`parse_batch_ids`, `like_escape`, `like_contains`), `captcha`, `file`, `ip`/`ip2region`, `ua`, `rich_text`, `perm_cache`. `R`/`password` are re-exported from `core` via `utils/__init__.py` for back-compat.
### Database Access
Database sessions are request-scoped via `contextvars` + Starlette middleware (`apps/middleware/db_session.py`). Services call `db = get_db_session()` to get the current request's session. Models use `self.save()` / `self.delete()` from `base_db`. The middleware auto-closes the session after each request (with rollback). Soft deletes via `is_delete` flag (0=active, 1=deleted).
Database sessions are request-scoped via `contextvars` + Starlette middleware (`core/middleware.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 `shared/base_db.py`. The middleware auto-closes the session after each request (with rollback). Soft deletes via `is_delete` flag (0=active, 1=deleted).
### Redis Access
Redis uses async client via `utils/redis_ctx.py`. Connection pool created in `redis_lifespan` (app lifespan), injected per-request via `redis_middleware` into contextvars. Business code calls `get_redis()` to get the current request's async Redis client.
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 `utils/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.
- 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.
- `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
@@ -98,12 +98,12 @@ Single delete: `DELETE /delete/{id}` where id can be comma-separated (e.g., `"1,
## Adding a New Module
1. Create `apps/models/{name}.py` - model class inheriting `base_model` and `base_db`, set `__tablename__ = DB_PREFIX + "{name}"`
2. Create `apps/forms/{name}.py` - Pydantic `BaseModel` validation classes with `Field(...)` constraints and `@field_validator` for custom checks
3. Create `apps/services/{name}.py` - CRUD service functions following the `get_level_page`/`get_level_detail`/`add_level`/`update_level`/`delete_level`/`update_level_status` pattern
4. Create `apps/views/{name}.py` - APIRouter with endpoints, apply `@permission_required` and `@check_demo` on write endpoints
5. Register the router in `routers/v1.py` - `v1.include_router({name}.router, prefix="/{prefix}", tags=["标签"])`
6. Import the view in `routers/v1.py`
1. Create `modules/{group}/{name}/models.py` - model class inheriting `base_model` and `base_db` (from `shared/`), set `__tablename__ = DB_PREFIX + "{name}"`
2. Create `modules/{group}/{name}/schemas.py` - Pydantic `BaseModel` validation classes with `Field(...)` constraints and `@field_validator` for custom checks
3. Create `modules/{group}/{name}/repository.py` - subclass `shared.base_repository.BaseRepository` + module-level instance
4. Create `modules/{group}/{name}/service.py` - CRUD service functions following the `get_level_page`/`get_level_detail`/`add_level`/`update_level`/`delete_level`/`update_level_status` pattern
5. Create `modules/{group}/{name}/router.py` - APIRouter with endpoints, apply `@permission_required` and `@check_demo` on write endpoints
6. Register the router in `routers/v1.py` - `v1.include_router({name}_router, prefix="/{prefix}", tags=["标签"])`
## Key Dependencies