分层架构后端框架 — Handler → Service → Repository
Project description
hframe
Huey's Framework — 分层架构后端框架
版本:0.1.5 | 作者:Huey | Python >= 3.8 | MIT 协议
目录
- 项目概述
- 安装
- 快速开始
- 配置管理
- 架构详解
- 5.1 分层架构总览
- 5.2 Registry 注册表
- 5.3 约定优于配置
- Repository 层
- 6.1 BaseRepository
- 6.2 ListFilters 结构化查询
- 6.3 VersionRepository
- Service 层
- 7.1 GenericService
- 7.2 ServiceConfig 配置
- 7.3 ServiceHooks 钩子体系
- 7.4 工厂方法 new_service()
- Handler 层
- 8.1 GenericHandler
- 8.2 HandlerConfig 配置
- 8.3 级联关系
- 8.4 引用解析
- 8.5 级联展开深度控制
- 8.6 HandlerHooks
- 8.7 工厂方法 new_handler()
- 8.8 响应数据映射与裁剪
- 8.9 字段映射与命名转换
- View 层(Django 可选)
- 9.1 BaseView
- 9.2 ModuleView 与自动装配
- 9.3 CRUD 视图
- 9.4 H5 视图
- 基础设施层
- 10.1 MySQL(PyMySQL)
- 10.2 MongoDB(pymongo)
- 10.3 Redis
- 10.4 微信
- 工具层
- 异常体系
- 日志
- 完整示例:用户管理模块
- 公共 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. 安装
# 核心层(无第三方依赖)
pip install hframe
# 带 Django View 层
pip install hframe[django]
# 带 MySQL + Redis 基础设施
pip install hframe[mysql]
pip install hframe[mongo]
pip install hframe[redis]
# 全部安装
pip install hframe[all]
可选依赖分组:
| 分组 | 包 | 用途 |
|---|---|---|
django |
Django >= 2.0 | View 层 |
mysql |
DBUtils, PyMySQL, sshtunnel | MySQL 数据库 |
mongo |
pymongo >= 4.3 | MongoDB 数据库 |
redis |
redis, sshtunnel | Redis 缓存 |
wechat |
requests | 微信 API |
infra |
以上全部 | 完整基础设施 |
all |
django + infra | 全量安装 |
3. 快速开始
3.1 极简模式:5 行代码写 CRUD
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 模式
# 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 类
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 连接配置 |
MONGO_INFO |
MongoDB 连接配置 |
REDIS_INFO |
Redis 连接配置 |
SSH_INFO |
SSH 隧道配置 |
WECHAT_INFO |
微信应用配置 |
4.2 config.yaml
在项目根目录创建 config.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
MONGO_INFO:
host: "127.0.0.1"
port: 27017
username: ""
password: ""
database: "my_database"
auth_source: "admin"
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 是全局实体注册表,供工厂方法使用:
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,
mongo_dao=mongo_client, # MongoDB DAO(按需)
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 | 数据库表名/集合名 |
db_type |
str | "mysql" |
数据库类型:"mysql" 或 "mongo" |
service_config |
ServiceConfig | None | Service 配置 |
handler_config |
HandlerConfig | None | Handler 配置 |
repo_class |
type | 按 db_type 自动选择 | 自定义 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,所有未显式配置的审计字段自动继承全局值。
# 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_idupdate_user_field:create/update 时自动填入self.user_id
其中 self.user_id 由 GenericHandler 在配置了 auth 钩子时自动注入:
# 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 自动填充,需要:
- 在全局 ServiceConfig 中配置字段名(如
create_user_field="create_user_id") - 在 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,需显式传入:
ReferenceRelation(
parent_table="sys_user",
fk_field="user_id", # 前缀不匹配,显式指定
show_name="user",
)
使用前后对比:
# 之前 — 每个字段都显式写
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",自动适配 |
MongoDB 实体会自动使用 "_id",无需显式指定 |
parent_columns |
默认 ["id", "name"] |
常见的展示字段 |
delete_field |
默认 "is_del" |
常见软删除字段名 |
6. Repository 层
6.1 BaseRepository
所有数据库操作必须通过 Repository 层,Service 不直接操作 DAO。
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:
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 实例,负责单实体完整生命周期。
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)、唯一性校验;旧记录不存在时抛 NotFoundException |
before_delete |
校验 ID 列表 |
do_create |
调用 Repository.create |
do_update |
调用 Repository.update(版本模式:旧行退位 + 新行插入) |
do_delete |
调用 Repository.delete |
do_get |
调用 Repository.get,找不到时返回 None(由调用方决定如何处理) |
svc.list() 的 query 参数说明
svc.list(query)的 query 支持三种类型:
query 类型 行为 示例 ListFilters传入 repo.list(filters=query),自动转换page/page_size为offset/sizesvc.list(ListFilters(page=1, page_size=20))dict通过 **query展开为repo.list()的关键字参数,key 必须是search_dict/offset/size/order_by/group_by/havingsvc.list({"search_dict": {"status": 1}, "size": 20})其他 调用 repo.list()使用默认参数svc.list()
7.2 ServiceConfig 配置
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 钩子体系
通过钩子覆写任意阶段的行为,无需继承:
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)
钩子签名约定:
before_xxx(service, *args) → 处理后的参数
do_xxx(service, *args) → 执行结果
after_xxx(service, result, *args) → 最终返回
7.4 工厂方法 new_service()
从 Registry 自动创建 Repository + Service:
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 之上处理跨实体逻辑。
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: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。
# 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,供认证/权限钩子使用:
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 钩子即可:Registry.register("user", handler_config=HandlerConfig( auth=lambda ctx: get_user_from_token(ctx.get("auth_token")), ))
8.2 HandlerConfig 配置
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"]}, # 可选:自定义映射
# 字段映射与命名转换
camel_case=True, # 开启 snake↔camel 自动转换
field_map={"sort_num": "sortNum"}, # 显式字段映射,优先级高于 camelCase
# 认证/权限钩子
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):
# 简化写法(需配置 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):
# 文章表 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
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。默认会递归到顶层部门,但只需直属部门信息。
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 结构:
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只展开一层(拿到父部门名称即可)
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 优先级
解析顺序(优先级从高到低):
- fieldLimitMap(父 Handler 通过
build_field_ctx注入)→ 最先检查 - 字段级 FieldDepthLimits(当前 Handler 配置)
- 全局 MaxExpandDepth(当前 Handler 配置)
- 默认值 → 展开一层不递归
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 完整示例
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 层也支持钩子覆写:
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):
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
最简单的用法,声明要从响应中移除的字段名列表:
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
对每条记录执行自定义映射,完全控制返回结构:
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 可以访问嵌套的关联数据:
# 利用展开后的 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 接收到的是已排除后的数据。
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 |
不执行映射,原样返回 |
两者均为默认值时,行为与未添加此功能前完全一致——零破坏性变更。
8.9 字段映射与命名转换
当 Python 后端需要与 Java 前端对接时,Java 使用 Jackson 的 SNAKE_CASE 策略自动将 snake_case 转为 camelCase,并用 @JsonProperty 对部分字段做显式别名映射。hframe 提供声明式配置来匹配这一行为。
8.9.1 功能一:自动命名转换(camel_case)
开启后,输出时自动 snake→camel(如 create_time → createTime),输入时自动 camel→snake(如 typeName → type_name)。
Handler 级配置:
from hframe import HandlerConfig
config = HandlerConfig(
entity_name="product_info",
camel_case=True, # 该 handler 开启 snake↔camel 转换
)
全局默认配置:
from hframe import Registry
# 全局默认开启,所有 handler 生效,handler 级可覆盖
Registry.set_defaults(camel_case=True)
转换规则:
| DB 字段(snake_case) | API 字段(camelCase) |
|---|---|
create_time |
createTime |
type_id |
typeId |
is_del |
isDel |
cover_banner |
coverBanner |
user_id_list |
userIdList |
生效范围:
handler.get()返回的 dicthandler.list()返回的 list 中每个 dicthandler.create()返回的结果(输出方向)- 输入方向:
handler.create(data)和handler.update(id, data)时自动 camel→snake - 嵌套 dict(如引用展开的关联对象)递归处理
不转换: search_dict(查询条件由代码构造,已用 DB 字段名)、非 handler 来源的数据
8.9.2 功能二:显式字段映射(field_map)
指定字段的输出别名,优先级高于 camelCase 自动转换:
from hframe import HandlerConfig
config = HandlerConfig(
entity_name="user_info",
camel_case=True,
field_map={
"username": "name", # 输出时 username → name(不走 camelCase 变成 username)
"mobile": "phone", # 输出时 mobile → phone
"avatar_url": "avatar", # 输出时 avatar_url → avatar(不走 camelCase 变成 avatarUrl)
"sort_num": "sortNum", # 输出时 sort_num → sortNum(自定义 camelCase 形式)
},
)
8.9.3 执行顺序
输出方向(响应):
- 先查
field_map,命中则用映射值(如mobile→phone) - 未命中且
camel_case=True,则走自动转换(如create_time→createTime) - 都未匹配,原样输出
输入方向(请求):
- 先反查
field_map的 value→key(如phone→mobile) - 未命中且
camel_case=True,则 camel→snake(如typeName→type_name) - 都未匹配,原样使用
8.9.4 完整示例
from hframe import HandlerConfig, Registry
# 全局默认开启 camelCase
Registry.set_defaults(camel_case=True)
# 特定 handler 配置显式映射
config = HandlerConfig(
entity_name="product_info",
camel_case=True, # 继承全局默认,也可显式写
field_map={
"sort_num": "sortNum", # 与 Java @JsonProperty 保持一致
"cover_banner": "coverBanner",
},
)
# 输出测试
# DB 原始: {"id": 1, "create_time": 1700000000, "type_id": 5, "sort_num": 1, "title": "test"}
# 转换后: {"id": 1, "createTime": 1700000000, "typeId": 5, "sortNum": 1, "title": "test"}
# 输入测试
# 前端传入: {"typeName": "A", "sortNum": 1, "title": "test"}
# 转换后: {"type_name": "A", "sort_num": 1, "title": "test"}
8.9.5 与响应映射的关系
字段映射在 _apply_response_mapper() 中最先执行,完整执行顺序:
camel_case+field_map字段映射与命名转换response_exclude声明式字段排除response_mapper自定义响应映射器
这意味着 response_exclude 中应使用转换后的字段名(camelCase),response_mapper 接收的也是转换后的数据。
8.9.6 默认行为
| 配置 | 默认值 | 行为 |
|---|---|---|
camel_case |
None |
不开启自动转换(None 时继承全局默认,全局默认也为 None 则不开启) |
field_map |
{} |
无显式映射 |
两者均为默认值时,行为与未添加此功能前完全一致——零破坏性变更。
8.9.7 工具函数
如需在 handler 之外手动使用转换函数:
from hframe import transform_output, transform_input
# 输出方向
result = transform_output(data, camel_case=True, field_map={"sort_num": "sortNum"})
# 输入方向
result = transform_input(data, camel_case=True, field_map={"sort_num": "sortNum"})
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 层手动处理:
# Handler hook 中抛出
raise ValidationException("库存不足")
# View 层自动返回
# → {"code": 400, "msg": "库存不足", "data": {}}
统一响应:
# 成功
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)
class UserListView(ListView):
def set_config(self):
self.sn = "user" # 自动从 Registry 创建 Handler
方式二:手动注入
class UserListView(ListView):
def set_config(self):
handler = GenericHandler.new_handler("user")
self.set_handler(handler)
方式三:共享 handler_map 模式(推荐)
通过 bootstrap 初始化共享的 handler_map,所有 View 复用同一组 Handler 实例(连接池复用,级联正常),避免每个请求重建 DAO:
# 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,前端按约定传参即可:
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 参数字典。
设置默认排序和分页:
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:
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 路径参数:
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 中是否包含主键判断是添加还是编辑:
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 中设置成功提示:
class OrderCreateView(EditView):
sn = "shop_order"
def ap_post(self, request):
self.resp_data["msg"] = "下单成功"
DeleteView — 删除
id 通过 POST body 传入:
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 页面:
from hframe import H5ListView, H5EditView, H5DetailView, H5DeleteView, H5Mixin
class MyH5ListView(H5ListView):
sn = "product"
10. 基础设施层
10.1 MySQL(PyMySQL)
MySQLWithSSH 是 DBClient 的完整实现,基于 PyMySQL + DBUtils 连接池。
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:
# 查询单条
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 条件筛选:
# 基础条件
dao.get_list("sys_user", search_dict={
"status": 1, # 等于 status=1
"id": {"!=": 100}, # 不等于 id!=100
"age": {">": 18, "<": 65}, # 范围 (age>18 AND age<65)
})
# 比较操作符(符号和别名均支持)
dao.get_list("sys_user", search_dict={
"age": {">": 18}, # 大于:age>18
"age": {"gt": 18}, # 别名:age>18
"age": {">=": 18}, # 大于等于
"age": {"ge": 18}, # 别名
"age": {"<": 65}, # 小于
"age": {"lt": 65}, # 别名
"age": {"<=": 65}, # 小于等于
"age": {"le": 65}, # 别名
"id": {"!=": 100}, # 不等于
"id": {"ne": 100}, # 别名
})
# IN / NOT IN
search_dict = {
"status": {"IN": [1, 2, 3]}, # status IN (1,2,3)
"status": {"NOT IN": [4, 5]}, # status NOT IN (4,5)
}
# LIKE 模糊匹配
search_dict = {
"name": {"LIKE": "张三"}, # LIKE 双侧匹配
"name": {"NOT LIKE": "test"}, # NOT LIKE
"name": {"llike": "张三"}, # LIKE 左匹配 (LIKE '%张三')
"name": {"rlike": "张三"}, # LIKE 右匹配 (LIKE '张三%')
}
# BETWEEN
search_dict = {
"price": {"BETWEEN": [100, 500]}, # price BETWEEN 100 AND 500
}
# 字段级 AND/OR 组合(同一字段的多个条件)
search_dict = {
"age": {"AND": [{">": 18}, {"<": 65}]}, # (age>18 AND age<65)
"status": {"OR": [{"=": 1}, {"=": 2}]}, # (status=1 OR status=2)
}
# 顶层 AND/OR 组合(跨字段条件组合)
search_dict = {
"AND": [{"status": 1}, {"type": "A"}] # (status=1) AND (type='A')
}
search_dict = {
"OR": [{"status": 1}, {"status": 2}] # (status=1) OR (status=2)
}
# 混合使用:普通条件 + 顶层 AND/OR
search_dict = {
"is_del": 0, # is_del=0
"OR": [{"status": 1}, {"type": "A"}] # AND ((status=1) OR (type='A'))
}
# 顶层原生 SQL 片段
search_dict = {
"sql": ["is_del=0 AND type!=%s", ["draft"]] # is_del=0 AND type!='draft'
}
批量操作与事务:
# 批量插入
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")
表结构缓存:
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 MongoDB(pymongo)
HFrameMongoClient 封装 pymongo,MongoRepository 对齐 BaseRepository 方法签名,上层 Handler/Service 无需改动即可切换。
依赖:pip install hframe[mongo]
10.2.1 初始化与注册
from hframe.infra import HFrameMongoClient
# 从 Config 创建
from hframe import Config
Config.load('config.yaml')
mongo_dao = HFrameMongoClient(**Config.MONGO_INFO)
# 或直接创建
mongo_dao = HFrameMongoClient(
host="127.0.0.1",
port=27017,
username="admin",
password="password",
database="my_db",
auth_source="admin",
)
# 注册全局 MongoDB DAO
from hframe import Registry, GenericHandler
Registry.set_defaults(mongo_dao=mongo_dao)
# 注册 MongoDB 实体(db_type="mongo" 路由到 MongoRepository)
Registry.register("log_record", db_type="mongo")
# 工厂创建 Handler(自动路由到 MongoRepository)
handler = GenericHandler.new_handler("log_record")
混用 MySQL + MongoDB:
from hframe.infra import MySQLWithSSH, HFrameMongoClient
Registry.set_defaults(dao=mysql_dao, mongo_dao=mongo_dao)
# MySQL 实体(默认 db_type="mysql")
Registry.register("sys_user")
# MongoDB 实体
Registry.register("access_log", db_type="mongo")
10.2.2 完整 CRUD 示例
from hframe import Registry, GenericHandler, ServiceConfig, HandlerConfig
from hframe.infra import HFrameMongoClient
# 1. 初始化
mongo = HFrameMongoClient(host="localhost", port=27017, database="my_app")
Registry.set_defaults(mongo_dao=mongo)
# 2. 注册实体
Registry.register("log_record", db_type="mongo",
service_config=ServiceConfig(
create_time_field="create_time",
update_time_field="update_time",
),
handler_config=HandlerConfig(
entity_name="log_record",
response_exclude=["is_del"],
),
)
# 3. 创建 Handler
handler = GenericHandler.new_handler("log_record")
# 4. CRUD 操作 — API 与 MySQL 完全一致
result = handler.create({"level": "ERROR", "message": "出错了"})
doc = handler.get(result[0]["_id"])
count, docs = handler.list({"search_dict": {"level": "ERROR"}, "size": 20, "offset": 0})
handler.update(result[0]["_id"], {"level": "WARN"})
handler.delete([result[0]["_id"]])
# 5. 约定式参数解析 — 同样支持
from flask import request
count, docs = handler.list_by_params(dict(request.args), context=ctx)
# 前端: ?level=ERROR&message_like=出错&_page=1&_size=20&_sort=-create_time
10.2.3 search_dict 自动转换
HFrameMongoClient 会自动将 hframe search_dict 约定转为 MongoDB 查询:
# 等值查询
mongo_dao.get_list("users", search_dict={"status": 1})
# → db.users.find({"status": 1})
# 比较操作符
mongo_dao.get_list("users", search_dict={"age": {"gt": 18}})
# → db.users.find({"age": {"$gt": 18}})
# IN
mongo_dao.get_list("users", search_dict={"status": {"IN": [1, 2, 3]}})
# → db.users.find({"status": {"$in": [1, 2, 3]}})
# LIKE → 正则
mongo_dao.get_list("users", search_dict={"name": {"LIKE": "张三"}})
# → db.users.find({"name": {"$regex": "张三"}})
# BETWEEN
mongo_dao.get_list("users", search_dict={"age": {"BETWEEN": [18, 65]}})
# → db.users.find({"age": {"$gte": 18, "$lte": 65}})
# 顶层 AND/OR
mongo_dao.get_list("users", search_dict={
"AND": [{"status": 1}, {"type": "A"}]
})
# → db.users.find({"$and": [{"status": 1}, {"type": "A"}]})
# 原生 MongoDB 操作符($ 前缀直接传递)
mongo_dao.get_list("users", search_dict={"age": {"$exists": True}})
# _id 自动转换 — 字符串自动转为 ObjectId
mongo_dao.get_info("users", _id="6821a1f0e4b0f12a5c8b4567")
# → db.users.find_one({"_id": ObjectId("6821a1f0e4b0f12a5c8b4567")})
操作符映射表:
| hframe search_dict | MongoDB 查询 | 说明 |
|---|---|---|
{"field": value} |
{"field": value} |
等值 |
{"field": {"gt": v}} |
{"field": {"$gt": v}} |
大于 |
{"field": {"ge": v}} |
{"field": {"$gte": v}} |
大于等于 |
{"field": {"lt": v}} |
{"field": {"$lt": v}} |
小于 |
{"field": {"le": v}} |
{"field": {"$lte": v}} |
小于等于 |
{"field": {"ne": v}} |
{"field": {"$ne": v}} |
不等于 |
{"field": {"IN": [...]}} |
{"field": {"$in": [...]}} |
IN |
{"field": {"LIKE": "x"}} |
{"field": {"$regex": "x"}} |
模糊匹配 |
{"field": {"BETWEEN": [a,b]}} |
{"field": {"$gte": a, "$lte": b}} |
范围 |
{"AND": [...]} |
{"$and": [...]} |
跨字段 AND |
{"OR": [...]} |
{"$or": [...]} |
跨字段 OR |
{"sql": [...]} |
跳过 | MongoDB 不支持 SQL 片段 |
{"_id": "str"} |
{"_id": ObjectId("str")} |
_id 自动转换 |
10.2.4 前端约定式参数 — MongoDB 完全支持
MongoDB 实体的 handler.list_by_params() 支持与 MySQL 相同的前端参数约定:
| 前端参数 | search_dict | MongoDB 查询 | 支持 |
|---|---|---|---|
status=1 |
{"status": 1} |
{"status": 1} |
✅ |
name_like=test |
{"name": {"LIKE": "test"}} |
{"name": {"$regex": "test"}} |
✅ |
status[]=1&status[]=2 |
{"status": {"IN": [1,2]}} |
{"status": {"$in": [1,2]}} |
✅ |
age[start]=18&age[end]=65 |
{"age": {">=": 18, "<=": 65}} |
{"age": {"$gte": 18, "$lte": 65}} |
✅ |
age:gt=18 |
{"age": {">": 18}} |
{"age": {"$gt": 18}} |
✅ |
_page=1&_size=20 |
分页参数 | skip(0).limit(20) |
✅ |
_sort=-create_time |
"create_time DESC" |
[("create_time", -1)] |
✅ |
_group=type |
group_by | 静默忽略 | ❌ |
_having=... |
having | 静默忽略 | ❌ |
10.2.5 引用解析与级联 — pk_field 自动适配
当 MySQL 实体引用 MongoDB 实体,或反过来时,ReferenceRelation / ChildRefRelation 的 parent_pk_field 和 child_pk_field 会自动适配目标实体的实际主键字段:
- MySQL 实体的
repo.pk_field = "id" - MongoDB 实体的
repo.pk_field = "_id"
无需显式指定 parent_pk_field="_id",注册写法与 MySQL 实体完全一致:
# MongoDB 实体引用 MySQL 分类表 — 不需要写 parent_pk_field="_id"
Registry.register("log_record", db_type="mongo",
handler_config=HandlerConfig(
references=[
ReferenceRelation(parent_table="shop_category"), # 自动适配
],
),
)
# MySQL 实体引用 MongoDB 集合 — 同样自动适配
Registry.register("shop_product",
handler_config=HandlerConfig(
child_refs=[
ChildRefRelation(child_table="access_log"), # 自动适配 child_pk_field
],
),
)
适配逻辑:如果配置值是默认的 "id",则使用目标 Handler 的 repo.pk_field 替代;如果显式指定了非默认值,则使用显式值。
| 场景 | 配置 | 实际使用 |
|---|---|---|
| MySQL → MySQL | 默认 "id" |
"id" |
| 任意 → MongoDB | 默认 "id" |
自动用 "_id" |
| 任意 → MongoDB | 显式设 "_id" |
用 "_id" |
| 特殊 MySQL 表 | 显式设 "ulid" |
用 "ulid" |
10.2.6 软删除与 _id 处理
软删除:MongoDB 通过采样文档检测 is_del 字段是否存在,存在则走软删除($set: {is_del: 1}),否则硬删除。
MongoRepository.delete()先采样第一个文档判断MongoRepository.list()中如果文档含is_del字段,默认过滤is_del=1
_id 不可变保护:HFrameMongoClient.update() 会自动从 $set 中移除 _id,防止因 _id 不可变导致更新失败。
_id 自动转换:translate_search_dict() 会自动将 _id 的字符串值转为 ObjectId,包括 IN/$in 等操作符中的列表元素。
10.2.7 MongoDB 限制与注意事项
| 项目 | 说明 |
|---|---|
| 主键 | 固定为 _id(ObjectId),查询返回时自动转为字符串 |
| group_by / having | 传入时静默忽略 |
| 原生 SQL | execute/query 方法抛 NotImplementedError |
| set_str | MySQL 特有的 SET x=y 语法,会被忽略并记录警告 |
| 事务 | 需要 MongoDB 4.0+ 副本集 |
| VersionRepository | MongoDB 暂不支持 |
10.2.8 MongoRepository 方法签名
MongoRepository 与 BaseRepository 方法签名完全对齐,Service/Handler 层无需感知差异:
| 方法 | 说明 |
|---|---|
get(pk) |
按 _id 查询 |
get_by_field(field, value) |
按字段查询 |
list(search_dict, offset, size, order_by, ...) |
列表查询(支持 search_dict 自动转换) |
create(data) |
插入文档,返回 _id 字符串 |
batch_create(data_list) |
批量插入 |
update(pk, data) |
按 _id 更新($set) |
update_by_fields(data, search_dict) |
按条件更新 |
delete(ids) |
智能软/硬删除 |
exists(search_dict) |
是否存在 |
count(search_dict) |
计数 |
10.3 Redis
RedisClient 是 CacheClient 的完整实现。
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,
)
基础操作:
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:
# 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)
管道与批量:
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 装饰器:
@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})
统计与维护:
stats = cache.get_stats() # 命令数、命中率等
cache.ping() # 连接检查
info = cache.info() # Redis INFO
cache.clear_db() # 清空
cache.close() # 关闭
10.4 微信
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. 工具层
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 提供类型化异常体系,每个异常携带业务码和消息:
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, # 获取异常码
)
使用示例:
# 抛出
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. 日志
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 注册实体
# 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
# 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
# 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 / 任意场景)
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。
推荐写法(约定式参数解析,自动处理分页/排序/筛选):
# 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)
手动写法:
# 方式一:使用 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 的列表。
正确写法:
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 钩子:
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:
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]):
from hframe import (
'BaseView', 'ModuleView', 'ListView', 'DetailView', 'EditView', 'DeleteView',
'H5BaseView', 'H5ModuleView', 'H5ListView', 'H5DetailView', 'H5EditView', 'H5DeleteView', 'H5Mixin',
)
基础设施(需安装对应可选依赖):
from hframe.infra import MySQLWithSSH, RedisClient, RedisUtil, WechatUtil
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hframe-0.2.2.tar.gz.
File metadata
- Download URL: hframe-0.2.2.tar.gz
- Upload date:
- Size: 161.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.7.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
044cff639710218ada15fdad5835e13bb2a7151b1bb2142e9f91682d8156fd91
|
|
| MD5 |
109a55e5ab7153b5b4262f4ba81af4ff
|
|
| BLAKE2b-256 |
d08a2ff08aba4f4c63fe3c9c273a905e63a33938227cf29b26be421589268c2d
|
File details
Details for the file hframe-0.2.2-py3-none-any.whl.
File metadata
- Download URL: hframe-0.2.2-py3-none-any.whl
- Upload date:
- Size: 119.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.7.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b7a1caeecabca0b07501488e967c429b11a59e2d438d62e578d93f9ae2e6189
|
|
| MD5 |
df31e33a7fe55371b0f00c993c84016d
|
|
| BLAKE2b-256 |
74202f72e8d081e6b7e3f397814785a202ba6137278e5d3e83daee85778e43ef
|