Metadata-Version: 2.1
Name: hframe
Version: 0.1.7
Summary: 分层架构后端框架 — Handler → Service → Repository
Author-email: Huey <421425339@qq.com>
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: typing-extensions >=4.0
Requires-Dist: pyyaml >=6.0
Provides-Extra: all
Requires-Dist: hframe[django,infra] ; extra == 'all'
Provides-Extra: dev
Requires-Dist: black ; extra == 'dev'
Requires-Dist: mypy ; extra == 'dev'
Provides-Extra: django
Requires-Dist: Django >=2.0 ; extra == 'django'
Provides-Extra: infra
Requires-Dist: DBUtils >=3.1.2 ; extra == 'infra'
Requires-Dist: pymysql >=1.1.0 ; extra == 'infra'
Requires-Dist: redis >=4.3.6 ; extra == 'infra'
Requires-Dist: sshtunnel ==0.3.2 ; extra == 'infra'
Requires-Dist: requests >=2.28 ; extra == 'infra'
Provides-Extra: mysql
Requires-Dist: DBUtils >=3.1.2 ; extra == 'mysql'
Requires-Dist: pymysql >=1.1.0 ; extra == 'mysql'
Requires-Dist: sshtunnel ==0.3.2 ; extra == 'mysql'
Provides-Extra: redis
Requires-Dist: redis >=4.3.6 ; extra == 'redis'
Requires-Dist: sshtunnel ==0.3.2 ; extra == 'redis'
Provides-Extra: test
Requires-Dist: pytest >=7.0 ; extra == 'test'
Provides-Extra: wechat
Requires-Dist: requests >=2.28 ; extra == 'wechat'

# hframe

> **H**uey's **Frame**work — 分层架构后端框架  
> 版本：0.1.5 | 作者：Huey | Python >= 3.8 | MIT 协议

---

## 目录

1. [项目概述](#1-项目概述)
2. [安装](#2-安装)
3. [快速开始](#3-快速开始)
4. [配置管理](#4-配置管理)
5. [架构详解](#5-架构详解)
   - 5.1 [分层架构总览](#51-分层架构总览)
   - 5.2 [Registry 注册表](#52-registry-注册表)
   - 5.3 [约定优于配置](#53-约定优于配置)
6. [Repository 层](#6-repository-层)
   - 6.1 [BaseRepository](#61-baserepository)
   - 6.2 [ListFilters 结构化查询](#62-listfilters-结构化查询)
   - 6.3 [VersionRepository](#63-versionrepository)
7. [Service 层](#7-service-层)
   - 7.1 [GenericService](#71-genericservice)
   - 7.2 [ServiceConfig 配置](#72-serviceconfig-配置)
   - 7.3 [ServiceHooks 钩子体系](#73-servicehooks-钩子体系)
   - 7.4 [工厂方法 new_service()](#74-工厂方法-new_service)
8. [Handler 层](#8-handler-层)
   - 8.1 [GenericHandler](#81-generichandler)
   - 8.2 [HandlerConfig 配置](#82-handlerconfig-配置)
   - 8.3 [级联关系](#83-级联关系)
   - 8.4 [引用解析](#84-引用解析)
   - 8.5 [级联展开深度控制](#85-级联展开深度控制)
   - 8.6 [HandlerHooks](#86-handlerhooks)
   - 8.7 [工厂方法 new_handler()](#87-工厂方法-new_handler)
   - 8.8 [响应数据映射与裁剪](#88-响应数据映射与裁剪)
9. [View 层（Django 可选）](#9-view-层django-可选)
   - 9.1 [BaseView](#91-baseview)
   - 9.2 [ModuleView 与自动装配](#92-moduleview-与自动装配)
   - 9.3 [CRUD 视图](#93-crud-视图)
   - 9.4 [H5 视图](#94-h5-视图)
10. [基础设施层](#10-基础设施层)
    - 10.1 [MySQL（PyMySQL）](#101-mysqlpymysql)
    - 10.2 [Redis](#102-redis)
    - 10.3 [微信](#103-微信)
11. [工具层](#11-工具层)
12. [异常体系](#12-异常体系)
13. [日志](#13-日志)
14. [完整示例：用户管理模块](#14-完整示例用户管理模块)
15. [公共 API](#15-公共-api)

---

## 1. 项目概述

hframe 是一个通用后端分层框架，核心理念是 **Handler → Service → Repository** 三层架构：

| 层 | 职责 | 特点 |
|----|------|------|
| **Handler** | HTTP 无关的业务编排 | 级联、事务、权限、引用解析 |
| **Service** | 单实体生命周期 | before/do/after 钩子管线、审计字段、唯一性校验、版本管理 |
| **Repository** | 数据访问唯一出口 | 标准 CRUD、结构化查询、软/硬删除 |

**核心能力：**

| 能力 | 说明 |
|------|------|
| **工厂模式** | `GenericHandler.new_handler("user")` 一行创建整条链路 |
| **Registry 注册表** | 全局注册实体配置，驱动工厂自动装配 |
| **View 自动装配** | View 中 `self.sn = "user"` 即可自动创建 Handler |
| **级联操作** | 创建/删除时自动处理子表联动 |
| **引用解析** | Get/List 时自动解析外键为完整对象 |
| **钩子管线** | 每个操作 before → do → after，可任意覆写 |
| **版本管理** | 内置版本字段映射，支持发布/回滚 |
| **Django 可选** | View 层依赖 Django，核心层无 Web 框架绑定 |

**设计原则：**

- **HTTP 无关**：Handler/Service/Repository 不依赖任何 Web 框架，可用于 Django、FastAPI、CLI 等
- **配置驱动**：行为通过 Config 数据类控制，而非硬编码
- **钩子优先**：用 `ServiceHooks` / `HandlerHooks` 覆写行为，不用继承

---

## 2. 安装

```bash
# 核心层（无第三方依赖）
pip install hframe

# 带 Django View 层
pip install hframe[django]

# 带 MySQL + Redis 基础设施
pip install hframe[mysql]
pip install hframe[redis]

# 全部安装
pip install hframe[all]
```

**可选依赖分组：**

| 分组 | 包 | 用途 |
|------|-----|------|
| `django` | Django >= 2.0 | View 层 |
| `mysql` | DBUtils, PyMySQL, sshtunnel | MySQL 数据库 |
| `redis` | redis, sshtunnel | Redis 缓存 |
| `wechat` | requests | 微信 API |
| `infra` | 以上全部 | 完整基础设施 |
| `all` | django + infra | 全量安装 |

---

## 3. 快速开始

### 3.1 极简模式：5 行代码写 CRUD

```python
import hframe
from hframe import Registry, GenericHandler, ListView, EditView, DeleteView

# 1. 初始化基础设施
from hframe.infra import MySQLWithSSH, RedisClient
dao = MySQLWithSSH(mysql_host="localhost", mysql_user="root", 
                   mysql_password="pass", mysql_database="my_db")
cache = RedisClient(redis_host="localhost")

# 2. 注册全局 DAO/Cache
Registry.set_defaults(dao=dao, cache=cache)

# 3. 注册实体（table 默认等于 name）
Registry.register("sys_user")

# 4. 工厂创建 Handler
handler = GenericHandler.new_handler("sys_user")

# 5. CRUD
handler.create({"username": "zhangsan", "email": "zhangsan@test.com"})
count, users = handler.list({"search_dict": {}, "size": 20, "offset": 0})
handler.update(1, {"email": "new@test.com"})
handler.delete([1])
```

### 3.2 Django View 模式

```python
# views.py
from hframe import ListView, EditView, DetailView, DeleteView

class UserListView(ListView):
    sn = "user"  # 一行配置，自动创建 Handler

class UserDetailView(DetailView):
    sn = "user"

class UserEditView(EditView):
    sn = "user"

class UserDeleteView(DeleteView):
    sn = "user"

# urls.py
from django.urls import path
from .views import UserListView, UserEditView, UserDetailView, UserDeleteView

urlpatterns = [
    path('api/user/list',   UserListView.as_view()),
    path('api/user/detail', UserDetailView.as_view()),
    path('api/user/edit',   UserEditView.as_view()),
    path('api/user/delete', UserDeleteView.as_view()),
]
```

---

## 4. 配置管理

框架通过 `Config` 类集中管理配置，支持从 YAML 文件加载并深度合并默认值。

### 4.1 Config 类

```python
from hframe import Config

# 读取配置
secret = Config.BASE_INFO['secret_key']
db_host = Config.MYSQL_INFO['host']

# 快捷取值（支持点号路径）
secret = Config.get('BASE_INFO.secret_key')

# 从 YAML 加载（覆盖默认值）
Config.load('config.yaml')

# 重置为默认
Config.reset()
```

**配置分区：**

| 分区 | 用途 |
|------|------|
| `BASE_INFO` | 应用基础信息（密钥、Token过期时间、管理员角色等） |
| `MYSQL_INFO` | MySQL 连接配置 |
| `REDIS_INFO` | Redis 连接配置 |
| `SSH_INFO` | SSH 隧道配置 |
| `WECHAT_INFO` | 微信应用配置 |

### 4.2 config.yaml

在项目根目录创建 `config.yaml`：

```yaml
BASE_INFO:
  secret_key: "your-secret-key"
  auth_token_expire_time: 7200
  admin_role_id: 1

MYSQL_INFO:
  host: "127.0.0.1"
  port: 3306
  user: "db_user"
  password: "db_password"
  database: "my_database"
  pool_size: 5

REDIS_INFO:
  host: "127.0.0.1"
  port: 6379
  password: "redis-password"
  db: 0
  max_connections: 50

SSH_INFO:
  host: "10.0.0.1"
  port: 22
  username: "root"
  password: "ssh-password"

WECHAT_INFO:
  applet:
    app_id: "wx_applet_appid"
    app_secret: "wx_applet_secret"
  public_account:
    app_id: "wx_pa_appid"
    app_secret: "wx_pa_secret"
```

**加载时机**：在应用入口调用 `Config.load()` 或由 View 层自动加载。

---

## 5. 架构详解

### 5.1 分层架构总览

```
请求 → View(可选) → Handler → Service → Repository → DB
         ↑              ↑          ↑          ↑
      HTTP I/O      业务编排    实体生命周期   数据访问
      认证/权限     级联/事务    钩子/校验    CRUD/查询
```

**数据流向：**

- **写操作**：`raw_data → Handler.before → Handler.do(构造CrudRequest + 调用Service + 级联) → Handler.after → result`
- **读操作**：`query → Handler.before → Handler.do(调用Service + 引用解析) → Handler.after → result`
- **Service 内部**：`before → do(审计字段 + 校验 + 持久化) → after`

### 5.2 Registry 注册表

Registry 是全局实体注册表，供工厂方法使用：

```python
from hframe import Registry, ServiceConfig, HandlerConfig

# 注册实体
Registry.register("user", table="sys_user",
    service_config=ServiceConfig(
        enable_unique_validation=True,
        unique_fields=[["username"]],
        create_time_field="create_time",
        update_time_field="update_time",
    ),
    handler_config=HandlerConfig(
        entity_name="user",
        cascades=[...],
        references=[...],
    )
)

# 设置全局默认
Registry.set_defaults(
    dao=mysql_client,
    cache=redis_client,
    service_config=ServiceConfig(       # 全局 ServiceConfig 默认值
        create_time_field="create_time",
        update_time_field="update_time",
        create_user_field="create_user_id",
        update_user_field="update_user_id",
    ),
    table_prefix="shop_",               # 全局表名前缀，用于命名推断
)

# 查询
cfg = Registry.get("user")       # EntityConfig 或 None
Registry.has("user")             # bool
Registry.all_names()             # ["user", ...]

# 测试清理
Registry.clear()
```

**EntityConfig 字段：**

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `name` | str | 必填 | 实体名（唯一标识） |
| `table` | str | 等于 name | 数据库表名 |
| `service_config` | ServiceConfig | None | Service 配置 |
| `handler_config` | HandlerConfig | None | Handler 配置 |
| `repo_class` | type | BaseRepository | 自定义 Repository 类 |
| `service_class` | type | GenericService | 自定义 Service 类 |
| `handler_class` | type | GenericHandler | 自定义 Handler 类 |

### 5.3 约定优于配置

hframe 通过 **全局默认值** 和 **命名规则推断** 两大机制减少重复配置，让符合约定的实体用最少的代码完成注册。

#### 5.3.1 全局 ServiceConfig 默认值

**问题**：每个实体的 `ServiceConfig` 都要写 `create_time_field="create_time"`、`update_time_field="update_time"`，同一个人设计的数据库字段名通常是统一的。

**方案**：在 `Registry.set_defaults()` 中设置全局 `ServiceConfig`，所有未显式配置的审计字段自动继承全局值。

```python
# bootstrap.py — 一次配置，全局生效
Registry.set_defaults(
    service_config=ServiceConfig(
        create_time_field="create_time",
        update_time_field="update_time",
        create_user_field="create_user_id",
        update_user_field="update_user_id",
    ),
)

# 各 handler — 无需再写 ServiceConfig
Registry.register("shop_category",
    handler_config=HandlerConfig(response_exclude=["is_del"]),
)

# 特殊实体 — 显式覆盖
Registry.register("legacy_table",
    service_config=ServiceConfig(
        create_time_field="created_at",   # 与全局不同，显式覆盖
    ),
)
```

**合并规则**（`ServiceConfig.apply_defaults()`）：

| 实体级值 | 行为 |
|----------|------|
| `None`（默认） | 继承全局默认值 |
| 具体值（如 `"created_at"`） | 使用实体级值，不继承 |
| `""`（空字符串） | 显式禁用该字段，不自动填充 |

**审计字段填充逻辑**：

`GenericService._set_audit_fields()` 在 create/update 时自动填充已配置的审计字段：

- `create_time_field`：create 时自动填入当前时间
- `update_time_field`：create/update 时自动填入当前时间
- `create_user_field`：create 时自动填入 `self.user_id`
- `update_user_field`：create/update 时自动填入 `self.user_id`

其中 `self.user_id` 由 `GenericHandler` 在配置了 `auth` 钩子时自动注入：

```python
# Handler.create() / Handler.update() 内部自动执行：
if user_info and hasattr(user_info, 'user_id'):
    self.svc.set_user(user_info.user_id)
```

因此，要启用 create_user/update_user 自动填充，需要：
1. 在全局 ServiceConfig 中配置字段名（如 `create_user_field="create_user_id"`）
2. 在 HandlerConfig 中配置 `auth` 钩子，让 Handler 获取当前用户信息

**重要**：`Registry.set_defaults(service_config=..., table_prefix=...)` 必须在 `Registry.register()` **之前**调用，因为 `ReferenceRelation` 等在构造时就会使用 `table_prefix` 推断字段名。

#### 5.3.2 表名前缀与命名规则推断

**问题**：`ReferenceRelation` 的 `fk_field`、`show_name`、`parent_service_name` 与 `parent_table` 存在固定的命名关系，每次都写是冗余的。

**方案**：配置全局 `table_prefix`，框架根据命名规则自动推断。

**推断规则**（以 `table_prefix="shop_"` 为例）：

| 参数 | 推断规则 | 示例（`parent_table="shop_category"`） |
|------|----------|---------------------------------------|
| 短名 | 去掉 `table_prefix` | `category` |
| `fk_field` | 短名 + `"_id"` | `category_id` |
| `show_name` | 短名 | `category` |
| `parent_service_name` | 等于 `parent_table` | `shop_category` |

**前缀不匹配时的行为**：

若 `table_prefix="shop_"` 但引用的表名不匹配此前缀（如 `sys_user`），则**保留完整表名**作为短名：

| 表名 | 全局前缀 | 短名 | `fk_field` |
|------|---------|------|-----------|
| `shop_category` | `shop_` | `category` | `category_id` |
| `sys_user` | `shop_` | `sys_user` | `sys_user_id` |

此时若希望外键字段为 `user_id`，需**显式传入**：

```python
ReferenceRelation(
    parent_table="sys_user",
    fk_field="user_id",      # 前缀不匹配，显式指定
    show_name="user",
)
```

**使用前后对比**：

```python
# 之前 — 每个字段都显式写
ReferenceRelation(
    parent_table="shop_category",
    fk_field="category_id",
    parent_pk_field="id",
    parent_columns=["id", "name"],
    show_name="category",
    parent_service_name="shop_category",
)

# 之后 — 符合命名规则的自动推断
ReferenceRelation(parent_table="shop_category")

# 不符合命名规则的字段仍需显式传入
ReferenceRelation(
    parent_table="shop_category",
    parent_columns=["id", "name", "status"],  # 自定义列
)
```

**同规则适用于其他关系类**：

| 类 | 推断字段 | 推断规则 |
|----|---------|----------|
| `CascadeRelation` | `show_name` | 子表短名 |
| | `child_service_name` | 等于 `child_table` |
| `ChildRefRelation` | `show_name` | 子表短名 |
| | `child_service_name` | 等于 `child_table` |

#### 5.3.3 其他省略规则

| 项 | 省略规则 | 示例 |
|----|---------|------|
| `table` 参数 | 默认等于 `name` | `Registry.register("shop_category")` 等价于 `Registry.register("shop_category", table="shop_category")` |
| `entity_name` | 默认使用注册名 `name` | `HandlerConfig()` 中不设 `entity_name`，工厂创建时自动设为注册名 |
| `parent_pk_field` | 默认 `"id"` | 绝大多数表的主键都是 id |
| `parent_columns` | 默认 `["id", "name"]` | 常见的展示字段 |
| `delete_field` | 默认 `"is_del"` | 常见软删除字段名 |

---

## 6. Repository 层

### 6.1 BaseRepository

所有数据库操作必须通过 Repository 层，Service 不直接操作 DAO。

```python
from hframe import BaseRepository

# 创建实例
repo = BaseRepository(dao=mysql_client, table="sys_user")

# CRUD
record = repo.get(1)                                  # 按主键查
record = repo.get_by_field("username", "zhangsan")    # 按字段查
count, records = repo.list(search_dict={"status": 1}, offset=0, size=20, order_by="id DESC")

new_id = repo.create({"username": "zhangsan"})        # 插入，返回主键
repo.batch_create([{"username": "u1"}, {"username": "u2"}])  # 批量插入
repo.update(1, {"email": "new@test.com"})             # 按主键更新
repo.update_by_fields({"status": 0}, {"role": "admin"})  # 按条件更新

repo.delete(1)          # 删除（智能判断软删除/硬删除）
repo.delete([1, 2, 3])  # 批量删除

# 辅助
repo.exists({"username": "zhangsan"})  # 是否存在
total = repo.count({"status": 1})      # 计数
pk = repo.pk_field                     # 主键字段名（延迟加载）
```

**智能删除**：如果表有 `is_del` 字段，自动执行软删除（设 `is_del=1`），否则硬删除。

### 6.2 ListFilters 结构化查询

推荐使用 `ListFilters` 代替原始 `search_dict`：

```python
from hframe import ListFilters, Filter, FilterOp

filters = ListFilters(
    page=1,
    page_size=20,
    filters=[
        Filter(field="status", op=FilterOp.EQ, value=1),
        Filter(field="age", op=FilterOp.GTE, value=18),
        Filter(field="name", op=FilterOp.LIKE, value="%张%"),
        Filter(field="department_id", op=FilterOp.IN, value=[1, 2, 3]),
        Filter(field="price", op=FilterOp.BETWEEN, value=[100, 500]),
    ],
    logic="and",
    order_by="id",
    order_dir="desc",
)

count, results = repo.list(filters=filters)
```

**FilterOp 操作符：**

| 操作符 | 值 | SQL 等价 |
|--------|-----|----------|
| `EQ` | `"eq"` | `field = value` |
| `NEQ` | `"neq"` | `field != value` |
| `LIKE` | `"like"` | `field LIKE value` |
| `GT` | `"gt"` | `field > value` |
| `GTE` | `"gte"` | `field >= value` |
| `LT` | `"lt"` | `field < value` |
| `LTE` | `"lte"` | `field <= value` |
| `IN` | `"in"` | `field IN (values)` |
| `BETWEEN` | `"between"` | `field >= v1 AND field <= v2` |

**ListFilters 到 repo.list() 的参数映射**：

传入 `repo.list(filters=filters)` 时，内部自动完成以下转换：

| ListFilters 字段 | repo.list() 参数 | 转换规则 |
|------------------|------------------|----------|
| `filters` | `search_dict` | 通过 `_build_search_dict()` 转为原始查询字典 |
| `page` + `page_size` | `offset` | `offset = (page - 1) * page_size`（page > 0 时） |
| `page_size` | `size` | 直接赋值 |
| `order_by` + `order_dir` | `order_by` | 拼接为 `"field ASC"` 或 `"field DESC"` |

### 6.3 VersionRepository

继承 `BaseRepository`，增加版本管理相关的查询方法。适用于需要版本控制的实体（如配置、规则）。

---

## 7. Service 层

### 7.1 GenericService

每个实体类型对应一个 Service 实例，负责单实体完整生命周期。

```python
from hframe import GenericService, ServiceConfig

# 手动创建
repo = BaseRepository(dao, "sys_user")
svc = GenericService(repo, ServiceConfig(
    enable_unique_validation=True,
    unique_fields=[["username"]],
    create_time_field="create_time",
    update_time_field="update_time",
))
svc.set_user(user_id=1)

# CRUD
result = svc.create({"username": "zhangsan", "email": "z@test.com"})
count, users = svc.list({"search_dict": {}, "size": 20, "offset": 0})
# 或使用 ListFilters（推荐）
# from hframe import ListFilters
# count, users = svc.list(ListFilters(page=1, page_size=20))
user = svc.get(1)
svc.update(1, {"email": "new@test.com"})
svc.delete([1])

# 版本管理（需启用 version_mode）
svc.activate(version_id)
versions = svc.list_versions("CONFIG_001")
svc.edit_version(version_id, {"version_remark": "修改说明"})
```

**操作管线**：每个 CRUD 操作都经过 `before → do → after` 三阶段：

```
create:  before_create → do_create → after_create
update:  before_update → do_update → after_update
delete:  before_delete → do_delete → after_delete
get:     before_get    → do_get    → after_get
list:    before_list   → do_list   → after_list
```

**内置 before 行为：**

| 操作 | 自动处理 |
|------|---------|
| `before_create` | 设置默认值、审计字段（create_time/create_user）、版本字段、唯一性校验 |
| `before_update` | 获取旧记录、合并数据、审计字段（update_time/update_user）、唯一性校验 |
| `before_delete` | 校验 ID 列表 |
| `do_create` | 调用 Repository.create |
| `do_update` | 调用 Repository.update（版本模式：旧行退位 + 新行插入） |
| `do_delete` | 调用 Repository.delete |

> **svc.list() 的 query 参数说明**
>
> `svc.list(query)` 的 query 支持三种类型：
>
> | query 类型 | 行为 | 示例 |
> |-----------|------|------|
> | `ListFilters` | 传入 `repo.list(filters=query)`，自动转换 `page`/`page_size` 为 `offset`/`size` | `svc.list(ListFilters(page=1, page_size=20))` |
> | `dict` | 通过 `**query` 展开为 `repo.list()` 的关键字参数，key 必须是 `search_dict`/`offset`/`size`/`order_by`/`group_by`/`having` | `svc.list({"search_dict": {"status": 1}, "size": 20})` |
> | 其他 | 调用 `repo.list()` 使用默认参数 | `svc.list()` |

### 7.2 ServiceConfig 配置

```python
from hframe import ServiceConfig, VersionFieldMapping

cfg = ServiceConfig(
    # 唯一性校验
    enable_unique_validation=True,
    unique_fields=[["username"], ["email"]],  # 每组内字段组合唯一

    # 审计字段（自动填充）
    # None = 未配置（使用 Registry 全局默认值）
    # "" = 显式禁用
    create_time_field="create_time",
    update_time_field="update_time",
    create_user_field="create_user_id",
    update_user_field="update_user_id",

    # 软删除
    delete_field="is_del",

    # 版本管理
    version_mode=True,
    version_fields=VersionFieldMapping(
        ulid_field="ulid",
        code_field="code",
        version_field="version_code",
        current_field="is_current",
        status_field="version_status",
        parent_field="parent_ulid",
    ),

    # 操作日志
    enable_op_log=True,
    entity_name="用户",
)
```

### 7.3 ServiceHooks 钩子体系

通过钩子覆写任意阶段的行为，无需继承：

```python
from hframe import GenericService, ServiceHooks

hooks = ServiceHooks()

# 覆写 before_create：添加自定义校验
def my_before_create(service, input_list):
    for item in input_list:
        if len(item.get("username", "")) < 3:
            raise ValidationException("用户名至少3个字符")
    return input_list

hooks.before_create = my_before_create

# 覆写 after_create：发送通知
def my_after_create(service, result):
    for record in result:
        send_notification(record)
    return result

hooks.after_create = my_after_create

# 注入
svc = GenericService(repo, config)
svc.set_hooks(hooks)
```

**钩子签名约定：**

```python
before_xxx(service, *args) → 处理后的参数
do_xxx(service, *args) → 执行结果
after_xxx(service, result, *args) → 最终返回
```

### 7.4 工厂方法 new_service()

从 Registry 自动创建 Repository + Service：

```python
from hframe import GenericService

# 前提：Registry 已注册
Registry.register("user", table="sys_user",
    service_config=ServiceConfig(enable_unique_validation=True, unique_fields=[["username"]])
)

# 一行创建
svc = GenericService.new_service("user")
# 等价于：
# repo = BaseRepository(dao, "sys_user")
# svc = GenericService(repo, service_config)
```

---

## 8. Handler 层

### 8.1 GenericHandler

Handler 是 HTTP 无关的业务编排层，在 Service 之上处理跨实体逻辑。

```python
from hframe import GenericHandler, HandlerConfig

# 手动创建
svc = GenericService(repo, service_config)
handler = GenericHandler(svc, HandlerConfig(entity_name="user"))

# CRUD（自动处理认证/权限 + 钩子管线）
handler.create({"username": "zhangsan"}, context=request_context)
user = handler.get(GetRequest(id=1), context=request_context)
count, users = handler.list({"search_dict": {}, "size": 20, "offset": 0}, context=request_context)
handler.update(1, {"email": "new@test.com"}, context=request_context)
handler.delete([1], context=request_context)

# 访问底层 Service
service = handler.service()
```

> **⚠️ handler.list() 的 query 参数说明**
>
> 当 query 为 dict 时，其 key 会通过 `**query` 展开为 `BaseRepository.list()` 的关键字参数。因此 key 必须是 `search_dict`、`offset`、`size`、`order_by`、`group_by`、`having` 之一，**不支持** `page`/`page_size`。
>
> 如需分页，请使用 `offset` 和 `size`：`offset = (page - 1) * size`。
>
> 如需更便捷的分页方式，建议传入 `ListFilters` 对象代替 dict，它内部会自动将 `page`+`page_size` 转换为 `offset`+`size`：
>
> ```python
> from hframe import ListFilters
> count, users = handler.list(ListFilters(page=1, page_size=20))
> ```

**handler.create() 返回值**：始终返回 `list[dict]`（即使只创建一条记录也返回列表）。取首条记录的 ID：`result[0]["id"]`。

**handler.list_by_params()** — 约定式参数解析一站式入口

将前端 GET 参数按约定规则自动解析为结构化 query，再委托给 `handler.list()`。适用于 Django / Flask / FastAPI 等任意框架，无需手动拼 query。

```python
# Django View
count, results = handler.list_by_params(self.get_data, context=ctx)

# Flask View
from flask import request
count, results = handler.list_by_params(dict(request.args), context=ctx)

# FastAPI View
count, results = handler.list_by_params(dict(request.query_params), context=ctx)

# 自定义默认每页条数
count, results = handler.list_by_params(params, context=ctx, default_size=50)
```

前端参数约定（详见第 9 节）：

| 参数 | 说明 | 示例 |
|------|------|------|
| `_page/_size/_offset` | 分页 | `?_page=2&_size=10` |
| `_sort` | 排序（`-`前缀=DESC） | `?_sort=-id,name` |
| `_group/_having` | 分组 | `?_group=category_id&_having=COUNT(*)>5` |
| `field[]` | IN 查询 | `?status[]=1,2` |
| `field[start]/field[end]` | 范围查询 | `?age[start]=18&age[end]=65` |
| `field_like` | 模糊匹配 | `?name_like=test` |
| `field:op` | 自定义操作符 | `?price:gte=100` |
| `field=value` | 等值查询 | `?category_id=5` |

> **设计决策**：约定式参数解析下沉至 Handler 层而非 View 层，确保 Django / Flask / FastAPI 等不同框架的 View 统一调用方式。View 层只需传原始 GET 参数字典，不关心解析规则。

**context 参数**：可选的请求上下文 dict，供认证/权限钩子使用：

```python
context = {
    "user_id": 1,          # 仅供参考，Handler 不直接读取此值
    "user_info": {...},    # 仅供参考，Handler 不直接读取此值
    "auth_token": "xxx",   # auth 钩子通过此值解析用户身份
    "ip": "127.0.0.1",
    "trace": "trace-id",
    "_request_id": "A1B2C3D4E5F6",  # View 层自动生成，贯穿 Handler 日志链路
}
```

> **注意**：Handler 并不直接从 context 中提取 `user_id`。用户 ID 的注入依赖 `HandlerConfig.auth` 钩子：auth 钩子接收 context，返回 `user_info` 对象，Handler 再从 `user_info.user_id` 注入 Service。**如果未配置 auth 钩子，审计字段 `create_user_id`/`update_user_id` 不会被自动填充，且不会报错。** 如需启用，在注册时配置 auth 钩子即可：
>
> ```python
> Registry.register("user", handler_config=HandlerConfig(
>     auth=lambda ctx: get_user_from_token(ctx.get("auth_token")),
> ))
> ```

### 8.2 HandlerConfig 配置

```python
from hframe import HandlerConfig, CascadeRelation, ReferenceRelation, ChildRefRelation

config = HandlerConfig(
    entity_name="order",

    # 级联关系（向下：父→子）
    cascades=[
        CascadeRelation(
            child_table="order_item",
            parent_fk_field="items",
            child_fk_field="order_id",
            show_name="items",
            on_delete="cascade",  # cascade | soft_delete | restrict
        ),
    ],

    # 向上引用（子→父）
    references=[
        # 简化写法：fk_field/show_name/parent_service_name 自动推断
        # 需配置 Registry.set_defaults(table_prefix="sys_")
        ReferenceRelation(parent_table="sys_product"),

        # 或显式指定不符合命名规则的字段
        ReferenceRelation(
            parent_table="product",
            fk_field="product_id",
            parent_pk_field="id",
            show_name="product",
        ),
    ],

    # 向下子引用（父→子 FK 列表）
    child_refs=[
        ChildRefRelation(
            child_table="tag",
            fk_list_field="tag_ids",
            child_pk_field="id",
            show_name="tags",
        ),
    ],

    # 级联展开深度控制
    max_expand_depth=3,                          # 全局最大递归展开深度（默认 1 = 展开一层不递归）
    field_depth_limits={"product_id": 1},       # 单字段深度上限：product 只展开 1 层
    field_stop_rules={                          # 字段级截止规则
        "product_id": [
            StopRule(on_handler="product", field="category", stop=True),   # 不展开 category
        ],
    },

    # 响应数据映射与裁剪
    response_exclude=["is_deleted", "password_hash"],  # 排除敏感字段
    # response_mapper=lambda e: {"id": e["id"], "name": e["name"]},  # 可选：自定义映射

    # 认证/权限钩子
    auth=lambda ctx: get_user_from_token(ctx.get("auth_token")),
    perm=lambda user, resource, action: check_permission(user, resource, action),
)
```

### 8.3 级联关系

级联关系控制父子表联动，在 Create 和 Delete 时自动触发。

**CascadeRelation 参数：**

| 参数 | 说明 |
|------|------|
| `child_table` | 子表名 |
| `parent_fk_field` | 父表中指向子表的字段名 |
| `child_fk_field` | 子表中指向父表的外键字段名 |
| `show_name` | 返回数据中的键名 |
| `on_delete` | 删除策略：`cascade`（级联删除）/ `soft_delete`（软删除）/ `restrict`（禁止删除） |

**级联创建流程：**

```
1. handler.create(data) → Service.create() → 主表插入
2. 检查 data 中是否有 cascade.show_name 对应的子数据
3. 自动设置 child_fk_field = parent_id
4. child_handler.create(child_data)
```

**级联删除策略：**

| 策略 | 行为 |
|------|------|
| `cascade` | 子表记录一同删除 |
| `soft_delete` | 子表记录设 is_del=1 |
| `restrict` | 如果有子记录，抛出异常阻止删除 |

### 8.4 引用解析

引用解析在 Get/List 时自动将外键字段解析为完整对象。

**向上引用（ReferenceRelation）：**

```python
# 简化写法（需配置 table_prefix）
# Registry.set_defaults(table_prefix="")
references=[
    ReferenceRelation(parent_table="product")
    # 自动推断：fk_field="product_id", show_name="product", parent_service_name="product"
]

# 显式写法（不符合命名规则时使用）
references=[
    ReferenceRelation(
        parent_table="product",
        fk_field="product_id",     # 本表外键
        parent_pk_field="id",      # 父表主键
        show_name="product",       # 挂载到结果的键名
    )
]

# 查询结果自动包含：
# {"id": 1, "product_id": 5, "product": {"id": 5, "name": "商品A", ...}}
```

**向下子引用（ChildRefRelation）：**

```python
# 文章表 tag_ids: [1,3,5] → 解析为完整 tag 对象列表
child_refs=[
    ChildRefRelation(
        child_table="tag",
        fk_list_field="tag_ids",   # 本表中存放子ID列表的字段
        child_pk_field="id",
        show_name="tags",
    )
]

# 查询结果自动包含：
# {"id": 1, "tag_ids": [1,3,5], "tags": [{"id":1,"name":"Python"}, ...]}
```

### 8.5 级联展开深度控制

Handler 的引用解析（References / ChildRefs）支持**递归展开**——被解析出的父/子对象，如果自身也有引用关系，会继续递归解析。深度控制机制提供精细化的递归管理。

#### 8.5.1 三层控制机制

| 控制层 | 配置项 | 作用 |
|--------|--------|------|
| **全局深度** | `max_expand_depth` | 所有字段统一的最大递归层数 |
| **单字段深度** | `field_depth_limits` | 对特定字段设定不同的深度上限 |
| **字段截止规则** | `field_stop_rules` | 对子 Handler 中的指定字段完全跳过或展开一层后截止 |

**默认行为**：未设置任何深度控制时，展开一层不递归（`max_expand_depth=1`）。

#### 8.5.2 全局深度 — max_expand_depth

```python
from hframe import HandlerConfig

config = HandlerConfig(
    entity_name="user",
    max_expand_depth=3,  # 递归展开最多 3 层
    references=[
        ReferenceRelation(parent_table="department", fk_field="dept_ulid", show_name="dept"),
    ],
)
```

- `max_expand_depth=1`：展开一层，不递归（默认）
- `max_expand_depth=2`：展开一层，被展开的对象再递归一层
- `max_expand_depth=N`：递归 N 层

#### 8.5.3 单字段深度 — field_depth_limits

对某个字段单独限制递归层数，覆盖全局 `max_expand_depth`。

**场景**：User 通过 `dept_ulid` 引用 Department，Department 自引用 `parent_ulid`。默认会递归到顶层部门，但只需直属部门信息。

```python
from hframe import HandlerConfig

config = HandlerConfig(
    entity_name="user",
    max_expand_depth=5,                         # 全局允许 5 层
    field_depth_limits={"dept_ulid": 1},        # 但 dept 只展开 1 层
    references=[
        ReferenceRelation(parent_table="department", fk_field="dept_ulid", show_name="dept"),
    ],
)
```

**HTTP 降级覆盖**：

```
GET /api/v1/users/get?id=xxx&fdepth=dept_ulid:1
```

- 格式：`字段:深度`，多字段逗号分隔（`fdepth=a:1,b:2`）
- HTTP 的 `fdepth` 只能**降级**（≤ 服务端配置值），不能放大

#### 8.5.4 字段截止规则 — field_stop_rules

对子 Handler 中的指定字段进行截止控制。

**StopRule 结构**：

```python
from hframe import StopRule

# 完全跳过（- 前缀 = stop=True）
StopRule(on_handler="department", field="manager", stop=True)

# 展开一层后截止（无 - 前缀 = stop=False）
StopRule(on_handler="department", field="parent_id", stop=False)
```

**场景**：User 引用 Department，Department 有 `manager` 和 `parent_id` 两个引用。展开 `dept_ulid` 时：
- 跳过 `manager`（不需要经理信息）
- `parent_id` 只展开一层（拿到父部门名称即可）

```python
from hframe import HandlerConfig, StopRule, ReferenceRelation

config = HandlerConfig(
    entity_name="user",
    max_expand_depth=3,
    field_stop_rules={
        "dept_ulid": [
            StopRule(on_handler="department", field="manager", stop=True),    # 不查 manager
            StopRule(on_handler="department", field="parent_id", stop=False), # parent 只展开 1 层
        ],
    },
    references=[
        ReferenceRelation(parent_table="department", fk_field="dept_ulid", show_name="dept"),
    ],
)
```

**HTTP 覆盖**：

```
GET /api/v1/users/get?fstop=dept_ulid=-department:manager,department:parent_id
```

- 格式：`字段=规则列表`，多条规则逗号分隔
- `-handler:field` = 完全跳过
- `handler:field` = 展开一层后截止
- 可传多条 `fstop` 参数

#### 8.5.5 优先级

解析顺序（优先级从高到低）：

1. **fieldLimitMap**（父 Handler 通过 `build_field_ctx` 注入）→ 最先检查
2. **字段级 FieldDepthLimits**（当前 Handler 配置）
3. **全局 MaxExpandDepth**（当前 Handler 配置）
4. **默认值** → 展开一层不递归

#### 8.5.6 上下文传递流程

```
HTTP 请求
  │
  ├─ ?depth=N ──────────────→ inject_expand_params()  → context._expand_depth
  ├─ ?fdepth=field:depth ──→ inject_expand_params()  → context._fd_override
  └─ ?fstop=field=rules ───→ inject_expand_params()  → context._fs_override
         │
         ▼
  Handler.get() / Handler.list()
         │
         ├─ _prepare_expand_context() 解析参数注入 context
         │
         ├─ _resolve_references / _resolve_child_refs
         │      │
         │      ├─ effective_expand_depth(ctx, field, config)
         │      │      ├─ fieldLimitMap 命中 → 按 Stop 规则返回
         │      │      └─ 否则 → 使用 depth 值
         │      │
         │      └─ depth > 1 时 → build_field_ctx() 构建子 context
         │                      → 递归调用子 Handler 的 resolve 方法
         │
         └─ 返回结果
```

#### 8.5.7 完整示例

```python
from hframe import (
    Registry, HandlerConfig, ServiceConfig,
    ReferenceRelation, StopRule,
)

# 注册 User（带深度控制）
Registry.register("user", table="sys_user",
    handler_config=HandlerConfig(
        entity_name="user",
        max_expand_depth=3,
        field_depth_limits={"dept_ulid": 2},
        field_stop_rules={
            "dept_ulid": [
                StopRule(on_handler="department", field="manager", stop=True),
                StopRule(on_handler="department", field="parent_id", stop=False),
            ],
        },
        references=[
            ReferenceRelation(
                parent_table="department",
                fk_field="dept_ulid",
                parent_pk_field="ulid",
                show_name="dept",
            ),
        ],
    ),
)

# 注册 Department（带自引用）
Registry.register("department", table="sys_department",
    handler_config=HandlerConfig(
        entity_name="department",
        max_expand_depth=3,
        references=[
            ReferenceRelation(
                parent_table="user",
                fk_field="manager_ulid",
                parent_service_name="user",
                show_name="manager",
            ),
            ReferenceRelation(
                parent_table="department",
                fk_field="parent_ulid",
                parent_service_name="department",
                show_name="parent",
            ),
        ],
    ),
)

# 查询结果示例：
# GET /api/v1/users/get?id=xxx
# {
#   "id": 1, "name": "张三", "dept_ulid": "D001",
#   "dept": {                              # ← 展开 dept（2层深度）
#     "ulid": "D001", "name": "技术部",
#     "parent_ulid": "D000",
#     "parent": {                          # ← stop=False，展开 1 层
#       "ulid": "D000", "name": "总公司",
#       "parent_ulid": null
#       # ← 不再递归 parent
#     },
#     # ← manager 被跳过（stop=True）
#   }
# }
```

### 8.6 HandlerHooks

与 ServiceHooks 类似，Handler 层也支持钩子覆写：

```python
from hframe import HandlerHooks

hooks = HandlerHooks()

# 覆写 do_create：添加自定义逻辑
hooks.do_create = lambda handler, input_list: handler.do_create(input_list)

# 覆写 before_delete：删除前检查
def check_before_delete(handler, ids):
    # 检查是否允许删除
    if has_active_orders(ids):
        raise ValidationException("存在关联订单，无法删除")
    return ids

hooks.before_delete = check_before_delete

handler.set_hooks(hooks)
```

### 8.7 工厂方法 new_handler()

从 Registry 一行创建整条链路（Repository → Service → Handler）：

```python
from hframe import GenericHandler

# 前提：Registry 已注册
Registry.register("order", table="sys_order",
    handler_config=HandlerConfig(
        entity_name="order",
        cascades=[CascadeRelation(...)],
        references=[ReferenceRelation(...)],
    )
)

# 一行创建
handler = GenericHandler.new_handler("order")
```

**内部流程：**

```
Registry.get("order") → EntityConfig
  ↓
GenericService.new_service("order")
  → BaseRepository(dao, "sys_order")
  → GenericService(repo, service_config)
  ↓
GenericHandler(service, handler_config)
```

### 8.8 响应数据映射与裁剪

Handler 默认直接将 DB 实体 dict 返回给 API 消费者，但这会暴露不应泄露的字段（如 `is_deleted`、`password_hash`、`version_status`）。

hframe 提供双轨机制，在 `get()` / `list()` 末尾自动执行响应映射，**100% 向后兼容**：

| 机制 | 配置项 | 适用场景 |
|------|--------|----------|
| **声明式字段排除** | `response_exclude` | 90% 场景：移除若干敏感字段 |
| **可编程响应映射器** | `response_mapper` | 10% 场景：自定义结构、只返回概要等 |

#### 8.8.1 声明式字段排除 — response_exclude

最简单的用法，声明要从响应中移除的字段名列表：

```python
from hframe import HandlerConfig

config = HandlerConfig(
    entity_name="user",
    response_exclude=["is_deleted", "password_hash", "version_status"],
)
```

**效果**：`get()` / `list()` 返回的每条记录中，这些字段会被自动移除。

**注意**：`response_exclude` 只排除**顶层字段**，不影响嵌套的引用展开数据。如需对嵌套数据也裁剪，请使用 `response_mapper`。

#### 8.8.2 自定义响应映射器 — response_mapper

对每条记录执行自定义映射，完全控制返回结构：

```python
from hframe import HandlerConfig

# 场景 1：只返回概要字段
config = HandlerConfig(
    entity_name="site",
    response_mapper=lambda entity: {
        "code": entity.get("site_code"),
        "name": entity.get("site_name"),
    },
)

# 场景 2：映射为自定义对象
class BriefUser:
    def __init__(self, id, name):
        self.id = id
        self.name = name

config = HandlerConfig(
    entity_name="user",
    response_mapper=lambda e: BriefUser(e["id"], e["name"]),
)
```

**mapper 接收的是展开后的完整数据**——此时 References/Cascades 已经注入到 dict 中，mapper 可以访问嵌套的关联数据：

```python
# 利用展开后的 dept 数据
config = HandlerConfig(
    entity_name="user",
    response_mapper=lambda e: {
        "id": e["id"],
        "name": e["name"],
        "dept_name": e.get("dept", {}).get("name"),  # 只取部门名称
    },
)
```

#### 8.8.3 两者共存

`response_exclude` 和 `response_mapper` 可以同时配置。执行顺序：**先排除，再映射**——mapper 接收到的是已排除后的数据。

```python
config = HandlerConfig(
    entity_name="user",
    response_exclude=["is_deleted", "password_hash"],  # 先排除
    response_mapper=lambda e: {"id": e["id"], "name": e["name"]},  # 再映射
)
```

#### 8.8.4 执行位置与级联隔离

映射**仅在顶层 HTTP 入口**（`handler.get()` / `handler.list()`）执行，在 `after_get` / `after_list` 钩子之后：

```
handler.get()
  → before_get → do_get（含引用展开）→ after_get
  → _apply_response_mapper()   ← 在此执行
  → 返回
```

级联解析（`_resolve_references` / `_resolve_child_refs`）直接操作 result dict，**不走映射路径**，避免中间数据被误裁剪。

#### 8.8.5 默认行为

| 配置 | 默认值 | 行为 |
|------|--------|------|
| `response_exclude` | `[]` | 不排除任何字段 |
| `response_mapper` | `None` | 不执行映射，原样返回 |

两者均为默认值时，行为与未添加此功能前完全一致——**零破坏性变更**。

---

## 9. View 层（Django 可选）

View 层依赖 Django，仅做 HTTP I/O 适配，业务逻辑全部委托给 Handler。

### 9.1 BaseView

Django 视图基类，提供完整的请求生命周期：

```
set_config → init_utils → get_info_from_request → log_in → check_permission → 业务方法 → close_utils
```

**GET 请求：** `pre_get → do_get → ap_get`  
**POST 请求：** `pre_post → do_post → ap_post`

**核心属性：**

| 属性 | 类型 | 说明 |
|------|------|------|
| `self.trace` | str | 请求唯一标识 |
| `self._request_id` | str | 请求链路追踪 ID（注入 Handler 日志） |
| `self.user_info` | dict | 当前用户信息 |
| `self.user_id` | int | 当前用户 ID |
| `self.get_data` | dict | GET 参数 |
| `self.post_data` | dict | POST 参数 |
| `self.resp_data` | dict | 响应数据 `{code, msg, data}` |
| `self.dao` | MySQLWithSSH | 数据库客户端 |
| `self.cache` | RedisClient | Redis 客户端 |
| `self.logger` | LogUtil | 日志实例 |

**Django 兼容性：**

BaseView 重写了 `setup()` 方法，解决与 Django 5.2+ 的兼容性问题（Django 的 `View.setup()` 检查 `hasattr(self, 'request')`，与 BaseView 的 `__init__` 冲突）。使用 hframe 的开发者无需关心此细节。

**异常自动处理：**

`get()` / `post()` 方法自动捕获 `HFrameException`（如 `ValidationException`、`NotFoundException`），用其 `code` 和 `message` 返回对应的 HTTP 响应。Handler 层抛出的业务异常无需在 View 层手动处理：

```python
# Handler hook 中抛出
raise ValidationException("库存不足")

# View 层自动返回
# → {"code": 400, "msg": "库存不足", "data": {}}
```

**统一响应：**

```python
# 成功
self.resp_data["data"] = result
return self.return_success()    # → {"code": 200, "msg": "", "data": {...}}

# 失败
return self.return_error(code=400, msg="参数错误")
```

### 9.2 ModuleView 与自动装配

ModuleView 持有 Handler 实例，支持三种配置方式：

**方式一：极简模式（self.sn）**

```python
class UserListView(ListView):
    def set_config(self):
        self.sn = "user"  # 自动从 Registry 创建 Handler
```

**方式二：手动注入**

```python
class UserListView(ListView):
    def set_config(self):
        handler = GenericHandler.new_handler("user")
        self.set_handler(handler)
```

**方式三：共享 handler_map 模式（推荐）**

通过 bootstrap 初始化共享的 handler_map，所有 View 复用同一组 Handler 实例（连接池复用，级联正常），避免每个请求重建 DAO：

```python
# bootstrap.py — 初始化一次，所有视图共享
from hframe import Registry, GenericHandler

def bootstrap():
    # ... 初始化 DAO、注册实体 ...
    handler_map = {
        "user":     GenericHandler.new_handler("user"),
        "order":    GenericHandler.new_handler("order"),
        "order_item": GenericHandler.new_handler("order_item"),
    }
    # 注入 handler_registry（级联支持）
    for h in handler_map.values():
        h.set_handler_registry(handler_map)
    return dao, handler_map

# views.py — 声明 sn + handler_map 类变量
class UserListView(ListView):
    sn = "user"                          # 实体注册名
    handler_map = None                   # 由启动入口注入

# main.py — 启动时注入
_, handler_map = bootstrap()
UserListView.handler_map = handler_map   # 类变量，所有实例共享
```

**自动装配流程：**

```
ModuleView.initialize()
  → set_config()
  → if self.sn and not self.handler and self.handler_map:
      self.handler = self.handler_map.get(self.sn)  ← 优先从共享 map 取
  → if self.sn and not self.handler:
      GenericHandler.new_handler(self.sn)            ← 没有共享 map 时才工厂创建
  → if self.handler_map:
      # 共享模式：只初始化 logger，跳过重建 DAO
      self.logger = LogUtil(name="view")
      self.get_info_from_request(request)
  else:
      # 传统模式：完整初始化
      init_utils() → get_info_from_request() → log_in() → ...
```

### 9.3 CRUD 视图

#### ListView — 列表查询

ListView 默认委托给 `handler.list_by_params()` 进行约定式参数解析，View 层只需传递原始 GET 参数，无需手动拼 query。

**属性/钩子：**

| 属性/方法 | 用途 | 说明 |
|-----------|------|------|
| `default_order_by` | 类变量声明默认排序 | 如 `"sort ASC, id ASC"`，前端未传 `_sort` 时生效 |
| `default_page_size` | 类变量声明默认每页条数 | 默认 20，子类可覆写如 `50` |
| `build_query()` | 完全自定义 query | 返回 None 时走约定式解析；返回 dict 时走 handler.list() |

**前端参数约定（由 Handler.list_by_params 自动解析）：**

| GET 参数 | 约定 | 转换结果 |
|---------|------|---------|
| `_page` / `_size` / `_offset` | 分页 | offset = (page-1) * size（兼容无前缀 page/size） |
| `_sort` | 排序 | `-field` 表示 DESC，逗号分隔多字段 |
| `_group` / `_having` | 分组 | group_by / having |
| `field=value` | 等值查询 | `{"field": value}` |
| `field[]=values` | IN 查询 | `{"field": {"IN": [values]}}` |
| `field[start]=value` | 范围起始 | `{"field": {">=": value}}` |
| `field[end]=value` | 范围结束 | `{"field": {"<=": value}}` |
| `field_like=value` | 模糊查询 | `{"field": {"LIKE": value}}` |
| `field:op=value` | 自定义操作符 | `{"field": {op.upper(): value}}` |

**范围查询合并**：`age[start]=18` + `age[end]=65` → `{"age": {">=": 18, "<=": 65}}`

**最简用法 — 只声明 sn，前端按约定传参即可：**

```python
from hframe import ListView

class CategoryListView(ListView):
    sn = "shop_category"
    default_order_by = "sort ASC, id ASC"

# GET /shop/category/list?_page=1&_size=20&_sort=-id&status=1&name_like=手机
# → 自动解析为：
#   search_dict: {"status": 1, "name": {"LIKE": "手机"}}
#   order_by: "id DESC"
#   offset: 0, size: 20
# → {"code": 200, "data": {"count": 50, "list": [...]}}
```

无需手写任何解析逻辑 — 前端按约定传参，Handler 层自动解析。

> **设计决策**：约定式参数解析下沉至 Handler 层的 `list_by_params()` 方法，而非 View 层。这样 Django / Flask / FastAPI 等不同框架的 View 统一调用方式，View 只需传原始 GET 参数字典。

**设置默认排序和分页：**

```python
class CartListView(ListView):
    sn = "shop_cart_item"
    default_page_size = 50  # 购物车默认每页 50 条

# GET /shop/cart/list?user_id=1&_sort=-create_time
# → 约定式解析自动支持 user_id 等值筛选
```

**完全自定义 — 覆写 build_query：**

```python
class OrderListView(ListView):
    sn = "shop_order"

    def build_query(self) -> dict:
        """完全接管 query 构建（返回非 None 时走 handler.list()）"""
        return {
            "search_dict": {"user_id": int(self.get_data.get("user_id", 1))},
            "size": 20,
            "offset": 0,
            "order_by": "id DESC",
        }
```

**优先级：** `build_query()` 返回非 None → `handler.list()`；返回 None → `handler.list_by_params()`（约定式解析）

#### DetailView — 详情查询

id 通过 GET 参数 `?id=XX` 传入，不使用 RESTful 路径参数：

```python
from hframe import DetailView

class UserDetailView(DetailView):
    sn = "user"

# GET /api/user/detail?id=1
# → {"code": 200, "data": {"id": 1, "username": "zhangsan", ...}}
```

缺少 id 参数时自动返回 400 错误：

```
GET /api/user/detail  →  {"code": 400, "msg": "缺少 id 参数"}
```

#### EditView — 添加/编辑

自动根据 POST body 中是否包含主键判断是添加还是编辑：

```python
from hframe import EditView

class UserEditView(EditView):
    sn = "user"

# POST /api/user/edit  {"username": "new_user"}       → 创建
# POST /api/user/edit  {"id": 1, "username": "张三"}   → 更新
```

可在 `ap_post` 中设置成功提示：

```python
class OrderCreateView(EditView):
    sn = "shop_order"

    def ap_post(self, request):
        self.resp_data["msg"] = "下单成功"
```

#### DeleteView — 删除

id 通过 POST body 传入：

```python
from hframe import DeleteView

class UserDeleteView(DeleteView):
    sn = "user"

# POST /api/user/delete  {"id": 1}
# POST /api/user/delete  {"ids": [1, 2, 3]}
```

### 9.4 H5 视图

H5 视图预置 AJAX 模式（`is_ajax=True`），适用于微信小程序/H5 页面：

```python
from hframe import H5ListView, H5EditView, H5DetailView, H5DeleteView, H5Mixin

class MyH5ListView(H5ListView):
    sn = "product"
```

---

## 10. 基础设施层

### 10.1 MySQL（PyMySQL）

`MySQLWithSSH` 是 `DBClient` 的完整实现，基于 PyMySQL + DBUtils 连接池。

```python
from hframe.infra import MySQLWithSSH

# 直连
dao = MySQLWithSSH(
    mysql_host="127.0.0.1",
    mysql_port=3306,
    mysql_user="root",
    mysql_password="password",
    mysql_database="my_db",
    pool_size=5,
)

# SSH 隧道
dao = MySQLWithSSH(
    ssh_host="10.0.0.1",
    ssh_port=22,
    ssh_username="root",
    ssh_password="ssh_pass",
    mysql_host="127.0.0.1",
    mysql_user="db_user",
    mysql_password="db_pass",
    mysql_database="my_db",
)

# 带 Redis 缓存表结构
dao = MySQLWithSSH(..., redis_client=redis_client)
```

**基础 CRUD：**

```python
# 查询单条
user = dao.get_info("sys_user", search_dict={"id": 1})

# 查询列表（分页）
count, users = dao.get_list("sys_user", search_dict={"status": 1}, 
                             offset=0, size=20, order_by="id DESC")

# 插入
dao.upsert("sys_user", {"username": "zhangsan", "status": 1})

# 更新
dao.update("sys_user", data={"status": 0}, search_dict={"id": 1})

# 删除
dao.delete("sys_user", search_dict={"id": 1})

# 原生 SQL
rows = dao.query("SELECT * FROM sys_user WHERE status = %s", (1,))
row = dao.query_one("SELECT * FROM sys_user WHERE id = %s", (1,))
count = dao.query_value("SELECT COUNT(*) FROM sys_user")
affected = dao.execute("UPDATE sys_user SET status = 0 WHERE id = %s", (1,))
```

**search_dict 条件筛选：**

```python
dao.get_list("sys_user", search_dict={
    "status": 1,                        # 等于
    "department_id": [1, 2, 3],         # IN 查询
    "name": "like:张三",                # LIKE
    "created_at": ">=:2024-01-01",      # >=
    "id": "!=:100",                     # !=
    "price": "between:100|500",         # BETWEEN
    "email": "llike:%@qq.com",          # 左 LIKE
    "phone": "rlike:138%",              # 右 LIKE
    "title": "not_like:%test%",         # NOT LIKE
})

# OR 条件
dao.get_list("sys_user", search_dict={
    "status": 1,
    ":or": {"name": "张三", "email": "z@qq.com"}
})
```

**批量操作与事务：**

```python
# 批量插入
dao.batch_insert("sys_user", ["name", "age"], [
    {"name": "张三", "age": 25},
    {"name": "李四", "age": 30},
], batch_size=500)

# 事务
with dao.transaction() as conn:
    conn.execute("UPDATE account SET balance = balance - 100 WHERE id = 1")
    conn.execute("UPDATE account SET balance = balance + 100 WHERE id = 2")
```

**表结构缓存：**

```python
columns = dao.get_table_structure("sys_user")  # 自动缓存
valid_cols = dao.filter_columns("sys_user", ["name", "age", "unknown"])  # ["name", "age"]
dao.reset_table_structure()  # 重置缓存
```

### 10.2 Redis

`RedisClient` 是 `CacheClient` 的完整实现。

```python
from hframe.infra import RedisClient

# 创建
cache = RedisClient(
    redis_host="127.0.0.1",
    redis_port=6379,
    redis_password="password",
    redis_db=0,
    max_connections=50,
)

# SSH 隧道
cache = RedisClient(
    ssh_host="10.0.0.1", ssh_username="root", ssh_password="pass",
    redis_host="127.0.0.1", redis_port=6379,
)
```

**基础操作：**

```python
cache.set("key", "value", ex=3600)     # 设置，带过期时间
cache.set("key", {"a": 1})             # 自动序列化
result = cache.get("key")              # 自动反序列化
cache.delete("key")
cache.exists("key")
cache.expire("key", 7200)
cache.ttl("key")
cache.incr("counter")
cache.decr("counter", 5)
```

**Hash / List / Set / Sorted Set：**

```python
# Hash
cache.hset("user:1001", "name", "张三")
name = cache.hget("user:1001", "name")
all_fields = cache.h_get_all("user:1001")
cache.hm_set("user:1001", {"name": "张三", "age": "25"})

# List
cache.l_push("queue", "task1")
cache.r_push("queue", "task2")
items = cache.lrange("queue", 0, -1)

# Set
cache.s_add("tags", "python", "redis")
members = cache.s_members("tags")

# Sorted Set
cache.z_add("leaderboard", {"user_a": 100, "user_b": 85})
top10 = cache.z_range("leaderboard", 0, 9, desc=True)
```

**管道与批量：**

```python
with cache.pipeline(transaction=True) as pipe:
    pipe.set("key1", "val1")
    pipe.set("key2", "val2")

values = cache.mget(["key1", "key2", "key3"])
cache.mset({"key1": "val1", "key2": "val2"})
keys = cache.get_keys("user:*")
```

**@cached 装饰器：**

```python
@cache.cached(ttl=300, prefix="data")
def get_user_list(page):
    return dao.get_list("sys_user", offset=(page-1)*20, size=20)

# 自定义 key
@cache.cached(key_func=lambda *args: f"user:{args[0]}", ttl=600)
def get_user(user_id):
    return dao.get_info("sys_user", search_dict={"id": user_id})
```

**统计与维护：**

```python
stats = cache.get_stats()     # 命令数、命中率等
cache.ping()                  # 连接检查
info = cache.info()           # Redis INFO
cache.clear_db()              # 清空
cache.close()                 # 关闭
```

### 10.3 微信

```python
from hframe.infra import WechatUtil

wechat = WechatUtil(app_id="wx_app_id", app_secret="app_secret", redis=cache)

# 公众号 OAuth2.0
result = wechat.log_by_code(code, client_type="public_account")
# → {"openid": "xxx", "access_token": "xxx", ...}

# 小程序 jscode2session
result = wechat.log_by_code(code, client_type="applet")
# → {"openid": "xxx", "session_key": "xxx", "unionid": "xxx"}

# 获取手机号
phone_info = wechat.get_phone_info(code)

# 发送客服消息
wechat.send_text({"touser": "OPENID", "msgtype": "text", "text": {"content": "你好"}})
```

---

## 11. 工具层

```python
from hframe import crypto, data, format, transform, yaml_util
from hframe import UserService, CodeService
```

| 模块 | 函数 | 说明 |
|------|------|------|
| `crypto` | `md5(s)` | MD5 哈希 |
| | `crypt(password)` | 加盐加密（用于密码） |
| | `make_id()` | 唯一 ID 生成 |
| `data` | `build_tree(flat_data, sort_field)` | 扁平列表转树形结构 |
| | `copy_dict(source, keys, filter_keys)` | 选择性复制字段 |
| | `check_data_type(data, type_map)` | 批量类型检查 |
| `format` | `to_snake_case("UserName")` | → "user_name" |
| | `to_camel_case("user_name")` | → "userName" |
| | `json_dumps(data)` | 支持 datetime/Decimal 的 JSON 序列化 |
| | `trans_key(data, "camel")` | 递归转换所有键的命名风格 |
| `transform` | `is_integer("123")` | → True |
| | `set_default_int("abc", 0)` | → 0（安全 int 转换） |
| | `trans_str_to_arr("1,2,3")` | → ["1", "2", "3"] |
| `yaml_util` | `read_yaml_file(path)` | 读取 YAML 文件 |
| | `write_yaml_file(data, path)` | 写入 YAML 文件 |
| `auth` | `UserService` | 用户登录/注册/身份服务（静态方法） |
| `verify` | `CodeService` | 验证码服务 |

---

## 12. 异常体系

hframe 提供类型化异常体系，每个异常携带业务码和消息：

```python
from hframe import (
    HFrameException,           # 基础异常（code=500）
    ValidationException,       # 数据校验失败（400）
    UniqueValidationException, # 唯一性校验失败（409）
    NotFoundException,        # 资源不存在（404）
    UnauthorizedException,    # 未认证（401）
    PermissionException,      # 权限不足（403）
    ServiceException,         # 业务异常（500）
    ConflictException,        # 状态冲突（409）
    ForbiddenOperationException, # 操作被禁止（403）
    RepositoryException,      # 数据访问异常（500）
    InfrastructureException,  # 基础设施异常（500）
    is_exception,             # 异常类型匹配
    get_exception_code,       # 获取异常码
)
```

**使用示例：**

```python
# 抛出
raise ValidationException("用户名不能为空")
raise NotFoundException(f"用户不存在: {user_id}")

# 匹配（类比 Go 的 errors.Is）
try:
    handler.delete([1])
except Exception as e:
    if is_exception(e, NotFoundException):
        return "未找到"
    elif is_exception(e, PermissionException):
        return "无权限"
    raise

# 获取码
code = get_exception_code(e)  # 404 / 403 / 500
```

**View 层自动处理：**

当 View 层（`BaseView.get()` / `BaseView.post()`）捕获到 `HFrameException` 时，自动使用异常的 `code` 和 `message` 作为 HTTP 响应，无需在 View 层手动 try/except：

| 异常类型 | 自动返回码 | 示例场景 |
|----------|------------|----------|
| `ValidationException` | 400 | 数据校验失败、库存不足 |
| `NotFoundException` | 404 | 资源不存在 |
| `UniqueValidationException` | 409 | 唯一性冲突 |
| `PermissionException` | 403 | 权限不足 |
| `ServiceException` | 500 | 业务异常 |

---

## 13. 日志

```python
from hframe import LogUtil

# 创建
logger = LogUtil(name="my_module", log_dir="logs")

# 使用
logger.info("处理完成")
logger.error("处理失败", exc_info=True)
logger.warning("警告")
logger.debug("调试信息")

# 关闭
logger.close()
```

**特点：**
- 日志文件：`logs/business_2024-01-01.log`
- 同时输出到文件和控制台
- 类级别缓存，同名不重复创建
- 格式：`时间 - 名称 - 级别 - 消息`

---

## 14. 完整示例：用户管理模块

### 14.1 注册实体

```python
# registry.py
from hframe import (
    Registry, ServiceConfig, HandlerConfig,
    CascadeRelation, ReferenceRelation,
)
from hframe.infra import MySQLWithSSH, RedisClient

# 初始化基础设施
dao = MySQLWithSSH(
    mysql_host="localhost", mysql_user="root",
    mysql_password="password", mysql_database="my_app",
)
cache = RedisClient(redis_host="localhost")

# 设置全局默认值（必须在注册实体之前）
Registry.set_defaults(
    dao=dao, cache=cache,
    service_config=ServiceConfig(
        create_time_field="create_time",
        update_time_field="update_time",
        create_user_field="create_user_id",
        update_user_field="update_user_id",
    ),
    table_prefix="sys_",
)

# 注册用户实体 — 无需重复写审计字段
Registry.register("sys_user",
    service_config=ServiceConfig(
        enable_unique_validation=True,
        unique_fields=[["username"], ["email"]],
    ),
    handler_config=HandlerConfig(
        entity_name="用户",
    ),
)

# 注册订单实体 — 级联 + 引用，利用命名推断
Registry.register("sys_order",
    handler_config=HandlerConfig(
        entity_name="订单",
        cascades=[
            CascadeRelation(
                child_table="order_item",
                parent_fk_field="items",
                child_fk_field="order_id",
                show_name="items",
                on_delete="cascade",
            ),
        ],
        references=[
            # fk_field="user_id"/show_name="user"/parent_service_name="sys_user" 自动推断
            ReferenceRelation(parent_table="sys_user"),
        ],
    ),
)
```

### 14.2 Django Views

```python
# views.py
from hframe import ListView, DetailView, EditView, DeleteView

class UserListView(ListView):
    sn = "sys_user"

class UserDetailView(DetailView):
    sn = "sys_user"

class UserEditView(EditView):
    sn = "sys_user"

class UserDeleteView(DeleteView):
    sn = "sys_user"
```

### 14.3 Django URLs

```python
# urls.py
from django.urls import path
from .views import UserListView, UserDetailView, UserEditView, UserDeleteView

urlpatterns = [
    path('api/user/list/', UserListView.as_view(), name='user_list'),
    path('api/user/detail/', UserDetailView.as_view(), name='user_detail'),
    path('api/user/edit/', UserEditView.as_view(), name='user_edit'),
    path('api/user/delete/', UserDeleteView.as_view(), name='user_delete'),
]
```

### 14.4 非 Django 使用（Flask / FastAPI / CLI / 任意场景）

```python
from hframe import GenericHandler, Registry

# handler 完全不依赖 Django
handler = GenericHandler.new_handler("sys_user")

# 创建
result = handler.create({"username": "zhangsan", "email": "z@test.com"})

# 查询列表 — 约定式参数解析（推荐，一键生成 query）
# Flask: dict(request.args)  |  FastAPI: dict(request.query_params)
count, users = handler.list_by_params(dict(request.args), context=ctx)
# 前端传 ?_page=1&_size=20&_sort=-id&name_like=test&status[]=1,2
# → 自动解析 search_dict/offset/size/order_by

# 查询列表 — 手动构建 query（完全自定义时使用）
count, users = handler.list({"search_dict": {}, "size": 20, "offset": 0})

# 查询列表 — 使用 ListFilters
from hframe import ListFilters
count, users = handler.list(ListFilters(page=1, page_size=20))

# 查询详情
from hframe import GetRequest
user = handler.get(GetRequest(id=1))

# 更新
handler.update(1, {"email": "new@test.com"})

# 删除
handler.delete([1, 2])
```

---

## 15. 常见踩坑

### 15.1 handler.list() / svc.list() 传入 {"page": 1} 报 TypeError

**现象**：`handler.list({"page": 1, "page_size": 20})` 报 `TypeError: list() got an unexpected keyword argument 'page'`。

**原因**：`svc.list()`/`handler.list()` 接收 dict 时，内部通过 `**query` 展开为 `BaseRepository.list()` 的关键字参数。而 `BaseRepository.list()` 不接受 `page`/`page_size`，只接受 `search_dict`/`offset`/`size`/`order_by`/`group_by`/`having`。

**推荐写法**（约定式参数解析，自动处理分页/排序/筛选）：

```python
# Django View
count, users = handler.list_by_params(self.get_data, context=ctx)

# Flask View
count, users = handler.list_by_params(dict(request.args), context=ctx)
```

**手动写法**：

```python
# 方式一：使用 offset/size（手动计算）
count, users = handler.list({"search_dict": {}, "offset": 0, "size": 20})

# 方式二：使用 ListFilters（自动计算 offset）
from hframe import ListFilters
count, users = handler.list(ListFilters(page=1, page_size=20))
```

### 15.2 handler.create() 返回值是列表而非单条 dict

**现象**：`result = handler.create({"name": "test"})` 后直接 `result["id"]` 报 `TypeError`。

**原因**：`do_create()` 始终返回 `List[Dict]`，即使只创建一条记录也返回长度为 1 的列表。

**正确写法**：

```python
result = handler.create({"name": "test"})
new_id = result[0]["id"]  # 取列表首条记录
```

### 15.3 未配置 auth 钩子导致审计字段不填充

**现象**：配置了 `create_user_field="create_user_id"`，但创建记录时该字段始终为空，且不报错。

**原因**：审计字段的用户 ID 依赖 Handler 调用 `svc.set_user(user_id)`。而 `user_id` 来源于 `HandlerConfig.auth` 钩子的返回值（`user_info.user_id`），**不是**直接从 `context["user_id"]` 读取。如果未配置 auth 钩子，`_check_auth()` 返回 None，`set_user()` 不会被调用，审计字段静默跳过。

**修复**：在注册时配置 auth 钩子：

```python
Registry.register("user", handler_config=HandlerConfig(
    auth=lambda ctx: get_user_from_token(ctx.get("auth_token")),
))
```

auth 钩子返回的对象需要有 `user_id` 属性，Handler 会自动调用 `svc.set_user(user_info.user_id)`。

### 15.4 table_prefix 不匹配导致自动推断失败

**现象**：注册了 `Registry.register("user")` 并设置了 `table_prefix="sys_"`，但创建 Handler 时报实体未注册。

**原因**：`table_prefix` 用于**命名推断**，它匹配的是 `parent_table`/`child_table` 等字段的前缀，而非实体名本身。实体注册时的 `name` 必须与实际使用的名称完全一致。

---

## 16. 公共 API

hframe 包导出的所有公共 API：

```python
from hframe import (
    # 版本
    '__version__',

    # 配置
    'Config',

    # 异常
    'HFrameException', 'ValidationException', 'UniqueValidationException',
    'NotFoundException', 'UnauthorizedException', 'PermissionException',
    'ServiceException', 'ConflictException', 'ForbiddenOperationException',
    'RepositoryException', 'InfrastructureException',
    'is_exception', 'get_exception_code',

    # 日志
    'LogUtil',

    # Repository
    'BaseRepository', 'VersionRepository', 'ListFilters', 'Filter', 'FilterOp',

    # Service
    'GenericService', 'ServiceConfig', 'ServiceHooks', 'CrudRequest', 'VersionFieldMapping',

    # Handler（核心）
    'GenericHandler', 'HandlerConfig', 'HandlerHooks',
    'CascadeRelation', 'ReferenceRelation', 'ChildRefRelation',
    'GetRequest', 'MapRequest', 'RequestFactory',
    'StopRule', 'FieldLimit',
    'inject_expand_params', 'build_field_ctx', 'effective_expand_depth', 'get_stop_cfg',

    # 注册表
    'Registry', 'EntityConfig',

    # 工具
    'UserService', 'CodeService',
    'crypto', 'data', 'format', 'transform', 'yaml_util',
    'parse_search_dict', 'parse_sort', 'parse_list_query',
)
```

**Django 可选（需安装 hframe[django]）：**

```python
from hframe import (
    'BaseView', 'ModuleView', 'ListView', 'DetailView', 'EditView', 'DeleteView',
    'H5BaseView', 'H5ModuleView', 'H5ListView', 'H5DetailView', 'H5EditView', 'H5DeleteView', 'H5Mixin',
)
```

**基础设施（需安装对应可选依赖）：**

```python
from hframe.infra import MySQLWithSSH, RedisClient, RedisUtil, WechatUtil
```
