签入版本
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
# 完整代码清单
|
||||
|
||||
本章汇总友链(Link)模块涉及的全部文件,便于开发时快速查阅和复制。
|
||||
|
||||
## 后端文件清单
|
||||
|
||||
### 1. 数据模型 `src/modules/link/models.py`
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链数据模型层
|
||||
# | 说明: 定义友链表结构,继承 BaseModel 获得通用字段与序列化能力
|
||||
# +======================================================================
|
||||
"""友链数据模型。"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, text
|
||||
|
||||
from config.database import DB_PREFIX
|
||||
from core.base_model import BaseModel
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
"""友情链接模型"""
|
||||
__tablename__ = DB_PREFIX + "link"
|
||||
__table_args__ = {"comment": "友情链接表"}
|
||||
|
||||
# 链接名称
|
||||
name = Column(String(255), nullable=False, index=True, comment="链接名称")
|
||||
# 链接类型
|
||||
type = Column(String(50), default="", comment="链接类型")
|
||||
# 链接来源
|
||||
form = Column(String(50), default="", comment="链接来源")
|
||||
# 链接图片
|
||||
image = Column(String(500), default="", comment="链接图片")
|
||||
# 链接地址
|
||||
url = Column(String(500), default="", comment="链接地址")
|
||||
# 状态:0-正常 1-停用
|
||||
status = Column(Integer, default=0, server_default=text("0"), comment="状态")
|
||||
# 排序
|
||||
sort = Column(Integer, default=0, server_default=text("0"), comment="排序")
|
||||
```
|
||||
|
||||
### 2. 表单验证 `src/modules/link/schemas.py`
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链请求校验模型层
|
||||
# | 说明: 定义友链创建/编辑与状态更新的 Pydantic 校验模型
|
||||
# +======================================================================
|
||||
"""友链请求校验模型。"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.base_schema import BaseSchema
|
||||
|
||||
|
||||
class LinkSchema(BaseSchema):
|
||||
"""友链创建/编辑表单"""
|
||||
# 链接名称
|
||||
name: str = Field(..., min_length=1, max_length=150, description="链接名称")
|
||||
# 链接类型
|
||||
type: Optional[str] = Field(default="", max_length=50, description="链接类型")
|
||||
# 链接来源
|
||||
form: Optional[str] = Field(default="", max_length=50, description="链接来源")
|
||||
# 链接图片
|
||||
image: Optional[str] = Field(default="", max_length=500, description="链接图片")
|
||||
# 链接地址
|
||||
url: Optional[str] = Field(default="", max_length=500, description="链接地址")
|
||||
# 状态
|
||||
status: Optional[int] = Field(default=0, ge=0, le=1, description="状态")
|
||||
# 排序
|
||||
sort: Optional[int] = Field(default=0, ge=0, le=99999, description="排序")
|
||||
|
||||
|
||||
class LinkStatusSchema(BaseModel):
|
||||
"""友链状态更新表单"""
|
||||
# 记录ID
|
||||
id: int = Field(..., gt=0, description="记录ID")
|
||||
# 状态
|
||||
status: int = Field(..., ge=0, le=1, description="状态")
|
||||
```
|
||||
|
||||
### 3. 数据访问层 `src/modules/link/repository.py`
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链数据访问层
|
||||
# | 说明: 定义友链仓储,继承 BaseRepository 获得通用 CRUD 与软删除能力
|
||||
# +======================================================================
|
||||
"""友链数据访问。"""
|
||||
|
||||
from core.base_repository import BaseRepository
|
||||
from modules.link.models import Link
|
||||
|
||||
|
||||
class LinkRepository(BaseRepository):
|
||||
"""友链数据仓库"""
|
||||
model = Link
|
||||
```
|
||||
|
||||
### 4. 业务逻辑层 `src/modules/link/service.py`
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链业务逻辑层
|
||||
# | 说明: 定义友链服务,声明差异点(file_fields 自动处理图片),
|
||||
# | 最后导出服务单例
|
||||
# +======================================================================
|
||||
"""友链业务逻辑。"""
|
||||
|
||||
from core.base_service import BaseService
|
||||
from modules.link.models import Link
|
||||
from modules.link.repository import LinkRepository
|
||||
from modules.link.schemas import LinkSchema, LinkStatusSchema
|
||||
|
||||
|
||||
class LinkService(BaseService):
|
||||
"""友链服务"""
|
||||
|
||||
# ============================================================
|
||||
# 基础声明
|
||||
# ============================================================
|
||||
model = Link
|
||||
repo_cls = LinkRepository
|
||||
|
||||
# ============================================================
|
||||
# 差异点声明
|
||||
# ============================================================
|
||||
create_schema = LinkSchema
|
||||
update_schema = LinkSchema
|
||||
status_schema = LinkStatusSchema
|
||||
# 分页查询字段与排序
|
||||
page_like_fields = ("name",)
|
||||
page_eq_fields = ("type", "form", "status")
|
||||
page_order_by = (("sort", "asc"),)
|
||||
# 枚举显示名映射
|
||||
serialize_maps = {
|
||||
"type": "link_type",
|
||||
"form": "link_form",
|
||||
}
|
||||
# 文件字段:add/update 时自动 save_file,serialize 时自动补全 URL
|
||||
file_fields = ("image",)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 服务单例
|
||||
# ======================================================================
|
||||
link_service = LinkService()
|
||||
```
|
||||
|
||||
### 5. 请求处理器 `src/modules/link/handlers.py`
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链请求处理器层
|
||||
# | 说明: 承接友情链接相关的 HTTP 请求,校验权限/演示模式并记录操作日志后,
|
||||
# | 转交友链服务层处理(分页、详情、增删改、状态、批量删除等)
|
||||
# +======================================================================
|
||||
"""友链请求处理器。"""
|
||||
|
||||
from common.middleware.access_decorators import check_demo, permission_required
|
||||
from common.middleware.operation_log import operation_log
|
||||
from core import response as R
|
||||
from core.base_handler import BaseHandler
|
||||
from modules.link.service import link_service
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 查询友链分页数据
|
||||
# ======================================================================
|
||||
class LinkPageHandler(BaseHandler):
|
||||
"""查询友链分页数据。"""
|
||||
|
||||
@permission_required("sys:link:list")
|
||||
async def get(self):
|
||||
"""GET 请求:返回友链分页列表。"""
|
||||
return await link_service.get_page(self)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 查询友链详情
|
||||
# ======================================================================
|
||||
class LinkDetailHandler(BaseHandler):
|
||||
"""查询友链详情。"""
|
||||
|
||||
@permission_required("sys:link:detail")
|
||||
async def get(self, id):
|
||||
"""GET 请求:按 ID 返回友链详情。"""
|
||||
return R.ok(self, data=link_service.get_detail(id))
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 添加友链
|
||||
# ======================================================================
|
||||
class LinkAddHandler(BaseHandler):
|
||||
"""添加友链。"""
|
||||
|
||||
@permission_required("sys:link:add")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "添加")
|
||||
async def post(self):
|
||||
"""POST 请求:新增友链。"""
|
||||
return await link_service.add(self)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 更新友链
|
||||
# ======================================================================
|
||||
class LinkUpdateHandler(BaseHandler):
|
||||
"""更新友链。"""
|
||||
|
||||
@permission_required("sys:link:update")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "更新")
|
||||
async def put(self):
|
||||
"""PUT 请求:更新友链信息。"""
|
||||
return await link_service.update(self)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 删除友链
|
||||
# ======================================================================
|
||||
class LinkDeleteHandler(BaseHandler):
|
||||
"""删除友链。"""
|
||||
|
||||
@permission_required("sys:link:delete")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "删除")
|
||||
async def delete(self, id):
|
||||
"""DELETE 请求:按 ID 删除友链(软删除)。"""
|
||||
return await link_service.delete(self, id)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 设置友链状态
|
||||
# ======================================================================
|
||||
class LinkStatusHandler(BaseHandler):
|
||||
"""设置友链状态。"""
|
||||
|
||||
@permission_required("sys:link:status")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "设置状态")
|
||||
async def put(self):
|
||||
"""PUT 请求:修改友链状态。"""
|
||||
return await link_service.update_status(self)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 批量删除友链
|
||||
# ======================================================================
|
||||
class LinkBatchDeleteHandler(BaseHandler):
|
||||
"""批量删除友链。"""
|
||||
|
||||
@permission_required("sys:link:delete")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "批量删除")
|
||||
async def post(self):
|
||||
"""POST 请求:批量删除友链(软删除)。"""
|
||||
return await link_service.batch_delete(self)
|
||||
```
|
||||
|
||||
### 6. 路由定义 `src/api/v1/link.py`
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链路由层
|
||||
# | 说明: 提供友情链接的增删改查、批量删除、状态管理等 API 接口
|
||||
# +======================================================================
|
||||
"""友链相关路由。"""
|
||||
|
||||
from modules.link.handlers import (
|
||||
LinkAddHandler, LinkBatchDeleteHandler, LinkDeleteHandler,
|
||||
LinkDetailHandler, LinkPageHandler, LinkStatusHandler,
|
||||
LinkUpdateHandler,
|
||||
)
|
||||
|
||||
routes = [
|
||||
# 查询友链分页数据
|
||||
(r"/link/page", LinkPageHandler),
|
||||
# 查询友链详情
|
||||
(r"/link/detail/(\d+)", LinkDetailHandler),
|
||||
# 添加友链
|
||||
(r"/link/add", LinkAddHandler),
|
||||
# 更新友链
|
||||
(r"/link/update", LinkUpdateHandler),
|
||||
# 删除友链
|
||||
(r"/link/delete/(\d+)", LinkDeleteHandler),
|
||||
# 设置友链状态
|
||||
(r"/link/status", LinkStatusHandler),
|
||||
# 批量删除友链
|
||||
(r"/link/batchDelete", LinkBatchDeleteHandler),
|
||||
]
|
||||
```
|
||||
|
||||
### 7. 路由注册 `src/api/v1/router.py`
|
||||
|
||||
```python
|
||||
# 在 _RESOURCE_MODULES 列表中追加 link 模块
|
||||
from api.v1 import link
|
||||
|
||||
_RESOURCE_MODULES = [
|
||||
# ... 已有模块 ...
|
||||
link,
|
||||
]
|
||||
```
|
||||
|
||||
## 前端文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|:-----|:-----|
|
||||
| `ui/src/api/system/link.js` | 接口请求封装 |
|
||||
| `ui/src/views/system/link/index.vue` | 列表页面 |
|
||||
| `ui/src/views/system/link/edit.vue` | 编辑弹窗 |
|
||||
| `ui/src/views/system/link/columns.js` | 表格列定义 |
|
||||
| `ui/src/views/system/link/querySchemas.js` | 查询条件定义 |
|
||||
|
||||
## 文件关系图
|
||||
|
||||
```
|
||||
src/modules/link/
|
||||
├── models.py ← 数据模型(继承 BaseModel)
|
||||
├── schemas.py ← 请求校验(继承 BaseSchema)
|
||||
├── repository.py ← 数据访问(继承 BaseRepository)
|
||||
├── service.py ← 业务逻辑(继承 BaseService)
|
||||
└── handlers.py ← 请求处理器(继承 BaseHandler)
|
||||
|
||||
src/api/v1/
|
||||
├── link.py ← 路由定义(routes 列表)
|
||||
└── router.py ← 路由注册(_RESOURCE_MODULES)
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
# 示例模块(Example)
|
||||
|
||||
:::tip 说明
|
||||
`src/modules/example/` 是一个完整的示例模块,演示了标准 CRUD 模块的六文件结构和开发模式。开发者可以此为模板快速创建新业务模块。
|
||||
:::
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/modules/example/
|
||||
├── __init__.py # 模块声明
|
||||
├── models.py # 数据模型
|
||||
├── schemas.py # 参数验证
|
||||
├── repository.py # 数据访问层
|
||||
├── service.py # 业务逻辑层
|
||||
└── handlers.py # 请求处理器
|
||||
```
|
||||
|
||||
## 各文件说明
|
||||
|
||||
### models.py — 数据模型
|
||||
|
||||
```python
|
||||
from sqlalchemy import Column, String, Integer, text
|
||||
from core.base_model import BaseModel
|
||||
|
||||
class Example(BaseModel):
|
||||
"""示例模型"""
|
||||
__tablename__ = "example"
|
||||
__table_args__ = {"comment": "示例表"}
|
||||
|
||||
name = Column(String(255), nullable=False, comment="名称")
|
||||
status = Column(Integer, default=0, server_default=text("0"), comment="状态")
|
||||
sort = Column(Integer, default=0, server_default=text("0"), comment="排序")
|
||||
```
|
||||
|
||||
### schemas.py — 参数验证
|
||||
|
||||
```python
|
||||
from pydantic import Field
|
||||
from core.base_schema import BaseSchema, BaseStatusSchema
|
||||
|
||||
class ExampleSchema(BaseSchema):
|
||||
"""示例表单"""
|
||||
name: str = Field(..., min_length=1, max_length=150, description="名称")
|
||||
status: int = Field(default=0, ge=0, le=1, description="状态")
|
||||
sort: int = Field(default=0, ge=0, le=99999, description="排序")
|
||||
|
||||
class ExampleStatusSchema(BaseStatusSchema):
|
||||
"""状态更新表单"""
|
||||
pass
|
||||
```
|
||||
|
||||
### repository.py — 数据访问层
|
||||
|
||||
```python
|
||||
from core.base_repository import BaseRepository
|
||||
from modules.example.models import Example
|
||||
|
||||
class ExampleRepository(BaseRepository):
|
||||
"""示例数据仓库"""
|
||||
model = Example
|
||||
```
|
||||
|
||||
### service.py — 业务逻辑层
|
||||
|
||||
```python
|
||||
from core.base_service import BaseService
|
||||
from modules.example.models import Example
|
||||
from modules.example.repository import ExampleRepository
|
||||
from modules.example.schemas import ExampleSchema, ExampleStatusSchema
|
||||
|
||||
class ExampleService(BaseService):
|
||||
"""示例服务"""
|
||||
model = Example
|
||||
repo_cls = ExampleRepository
|
||||
create_schema = ExampleSchema
|
||||
update_schema = ExampleSchema
|
||||
status_schema = ExampleStatusSchema
|
||||
page_like_fields = ("name",)
|
||||
page_eq_fields = ("status",)
|
||||
page_order_by = (("sort", "asc"),)
|
||||
|
||||
example_service = ExampleService()
|
||||
```
|
||||
|
||||
### handlers.py — 请求处理器
|
||||
|
||||
```python
|
||||
from common.middleware.access_decorators import check_demo, permission_required
|
||||
from common.middleware.operation_log import operation_log
|
||||
from core import response as R
|
||||
from core.base_handler import BaseHandler
|
||||
from modules.example.service import example_service
|
||||
|
||||
class ExamplePageHandler(BaseHandler):
|
||||
@permission_required("sys:example:list")
|
||||
async def get(self):
|
||||
return await example_service.get_page(self)
|
||||
|
||||
class ExampleDetailHandler(BaseHandler):
|
||||
@permission_required("sys:example:detail")
|
||||
async def get(self, id):
|
||||
return R.ok(self, data=example_service.get_detail(id))
|
||||
|
||||
class ExampleAddHandler(BaseHandler):
|
||||
@permission_required("sys:example:add")
|
||||
@check_demo
|
||||
@operation_log("示例管理", "添加")
|
||||
async def post(self):
|
||||
return await example_service.add(self)
|
||||
|
||||
class ExampleUpdateHandler(BaseHandler):
|
||||
@permission_required("sys:example:update")
|
||||
@check_demo
|
||||
@operation_log("示例管理", "更新")
|
||||
async def put(self):
|
||||
return await example_service.update(self)
|
||||
|
||||
class ExampleDeleteHandler(BaseHandler):
|
||||
@permission_required("sys:example:delete")
|
||||
@check_demo
|
||||
@operation_log("示例管理", "删除")
|
||||
async def delete(self, id):
|
||||
return await example_service.delete(self, id)
|
||||
|
||||
class ExampleStatusHandler(BaseHandler):
|
||||
@permission_required("sys:example:status")
|
||||
@check_demo
|
||||
@operation_log("示例管理", "设置状态")
|
||||
async def put(self):
|
||||
return await example_service.update_status(self)
|
||||
|
||||
class ExampleBatchDeleteHandler(BaseHandler):
|
||||
@permission_required("sys:example:delete")
|
||||
@check_demo
|
||||
@operation_log("示例管理", "批量删除")
|
||||
async def post(self):
|
||||
return await example_service.batch_delete(self)
|
||||
```
|
||||
|
||||
## 路由注册
|
||||
|
||||
```python
|
||||
# src/api/v1/example.py
|
||||
from modules.example.handlers import (
|
||||
ExamplePageHandler, ExampleDetailHandler, ExampleAddHandler,
|
||||
ExampleUpdateHandler, ExampleDeleteHandler, ExampleStatusHandler,
|
||||
ExampleBatchDeleteHandler,
|
||||
)
|
||||
|
||||
routes = [
|
||||
(r"/example/page", ExamplePageHandler),
|
||||
(r"/example/detail/(\d+)", ExampleDetailHandler),
|
||||
(r"/example/add", ExampleAddHandler),
|
||||
(r"/example/update", ExampleUpdateHandler),
|
||||
(r"/example/delete/(\d+)", ExampleDeleteHandler),
|
||||
(r"/example/status", ExampleStatusHandler),
|
||||
(r"/example/batchDelete", ExampleBatchDeleteHandler),
|
||||
]
|
||||
```
|
||||
|
||||
```python
|
||||
# src/api/v1/router.py — 在 _RESOURCE_MODULES 中追加
|
||||
from api.v1 import example
|
||||
_RESOURCE_MODULES = [..., example]
|
||||
```
|
||||
|
||||
## 开发新模块的步骤
|
||||
|
||||
基于 Example 模块创建新模块的流程:
|
||||
|
||||
```
|
||||
1. 复制 src/modules/example/ 为新目录
|
||||
2. 修改 models.py 中的表名和字段
|
||||
3. 修改 schemas.py 中的表单验证
|
||||
4. 修改 repository.py 中的 model 引用
|
||||
5. 修改 service.py 中的配置项
|
||||
6. 修改 handlers.py 中的权限字符串和日志描述
|
||||
7. 复制 src/api/v1/example.py 为新路由文件
|
||||
8. 在 router.py 中注册新路由
|
||||
9. 在菜单管理中配置权限节点
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Example 模块是标准 CRUD 模块的参考实现,包含 models → schemas → repository → service → handlers → 路由六层结构。开发新模块时以此为模板,修改差异点即可快速完成。
|
||||
@@ -0,0 +1,169 @@
|
||||
# 常见问题与排错
|
||||
|
||||
本章汇总模块开发过程中的常见问题和解决方案。
|
||||
|
||||
## 后端问题
|
||||
|
||||
### Q1: 启动报错 `Table 'xxx.tornado_position' doesn't exist`
|
||||
|
||||
**原因**:模型定义后未执行建表。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 方式一:脚本自动建表
|
||||
python scripts/init_db.py
|
||||
|
||||
# 方式二:Alembic 迁移
|
||||
alembic revision --autogenerate -m "add position table"
|
||||
alembic upgrade head
|
||||
|
||||
# 方式三:手动建表(参考 model.md 中的 DDL)
|
||||
```
|
||||
|
||||
### Q2: 接口返回 `{"code": 1, "msg": "权限不足"}`
|
||||
|
||||
**原因**:权限节点未配置或未分配给当前用户角色。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认菜单管理中已添加权限节点,权限标识与代码中 `@permission_required` 完全一致
|
||||
2. 确认权限节点已分配给当前用户的角色
|
||||
3. 使用 admin 账号(ID=1)测试,admin 自动跳过权限校验
|
||||
|
||||
### Q3: 唯一性校验不生效
|
||||
|
||||
**原因**:`unique_fields` 配置错误。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认 key 是模型字段名(如 `name`),不是中文名
|
||||
2. 确认 Service 类中声明了 `unique_fields`
|
||||
3. 确认调用的是 `service.add()` 或 `service.update()`,而非直接操作 repo
|
||||
|
||||
### Q4: 分页查询返回空数据
|
||||
|
||||
**原因**:分页参数名不匹配或字段配置错误。
|
||||
|
||||
**排查步骤**:
|
||||
1. 前端传参名应为 `pageNo` 和 `pageSize`(驼峰)
|
||||
2. 确认 `page_like_fields` 和 `page_eq_fields` 中的字段名与模型一致
|
||||
3. 检查数据库中是否有数据(`is_delete=0` 的记录)
|
||||
|
||||
### Q5: 删除时报错 "存在引用"
|
||||
|
||||
**原因**:`_before_delete` 钩子检测到关联数据。
|
||||
|
||||
**解决**:先调整引用该记录的其他数据,再执行删除。如岗位被用户引用时,需先修改用户的岗位字段。
|
||||
|
||||
### Q6: `batch_delete` 端点报 500 错误
|
||||
|
||||
**原因**:Handler 方法未正确继承 `BaseHandler`。
|
||||
|
||||
**解决**:批量删除 Handler 必须继承 `BaseHandler` 并使用 `async def`:
|
||||
|
||||
```python
|
||||
class PositionBatchDeleteHandler(BaseHandler):
|
||||
@permission_required("sys:position:delete")
|
||||
@check_demo
|
||||
async def post(self):
|
||||
return await position_service.batch_delete(self)
|
||||
```
|
||||
|
||||
## 前端问题
|
||||
|
||||
### Q7: 页面白屏,控制台报 `Failed to fetch dynamically imported module`
|
||||
|
||||
**原因**:路由组件路径配置错误。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认路由中的 `component` 路径与实际文件路径一致
|
||||
2. 确认后端菜单的 `component` 字段值正确(如 `system/position/index`)
|
||||
3. 检查文件名大小写是否匹配
|
||||
|
||||
### Q8: 搜索功能不生效
|
||||
|
||||
**原因**:搜索表单的 `submit` 事件未正确绑定。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认 `BasicForm` 绑定了 `@submit="handleSearch"` 事件
|
||||
2. 确认 `handleSearch` 中调用了 `reload({ searchInfo: values })`
|
||||
3. 确认后端 `page_like_fields` 和 `page_eq_fields` 配置了对应字段
|
||||
|
||||
### Q9: 编辑弹窗不回填数据
|
||||
|
||||
**原因**:`openModal` 未传入记录数据。
|
||||
|
||||
**解决**:
|
||||
|
||||
```javascript
|
||||
// 正确:第一个参数 true 表示编辑模式,第二个参数是记录数据
|
||||
openModal(true, record);
|
||||
```
|
||||
|
||||
### Q10: 操作按钮不显示
|
||||
|
||||
**原因**:`v-perm` 权限标识与后端不一致。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认 `v-perm` 中的权限字符串与后端 `@permission_required` 完全一致
|
||||
2. 确认当前用户拥有对应权限
|
||||
3. 检查 `TableAction` 中的 `auth` 字段是否正确
|
||||
|
||||
## 通用问题
|
||||
|
||||
### Q11: 接口返回 401 Unauthorized
|
||||
|
||||
**原因**:JWT Token 过期或未携带。
|
||||
|
||||
**解决**:
|
||||
1. 确认请求头包含 `Authorization: Bearer <token>`
|
||||
2. Token 过期后需调用刷新接口或重新登录
|
||||
3. 前端 `defHttp` 自动处理 Token 注入,检查是否正确引入
|
||||
|
||||
### Q12: 接口返回 403 Forbidden(演示模式)
|
||||
|
||||
**原因**:演示模式下写操作被拦截。
|
||||
|
||||
**解决**:将 `.env` 中的 `TORNADO_DEMO` 设为 `False`,或移除端点上的 `@check_demo` 装饰器(仅开发阶段)。
|
||||
|
||||
### Q13: 数据库字段中文乱码
|
||||
|
||||
**原因**:数据库或表的字符集不是 UTF-8。
|
||||
|
||||
**解决**:
|
||||
- MySQL:确保数据库和表使用 `utf8mb4` 字符集
|
||||
- PostgreSQL:建库时指定 `ENCODING 'UTF8'`
|
||||
|
||||
## 开发调试技巧
|
||||
|
||||
### 查看 SQL 日志
|
||||
|
||||
在 `.env` 中开启 SQL 调试:
|
||||
|
||||
```bash
|
||||
DB_DEBUG=True
|
||||
```
|
||||
|
||||
重启后所有 SQL 查询都会输出到控制台。
|
||||
|
||||
### 使用 curl 调试
|
||||
|
||||
使用 curl 或 Postman 等工具直接测试接口:
|
||||
|
||||
```bash
|
||||
# 登录获取 Token
|
||||
curl -X POST http://127.0.0.1:8041/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"123456","code":"xxxx","key":"xxxx"}'
|
||||
|
||||
# 携带 Token 调用业务接口
|
||||
curl http://127.0.0.1:8041/api/v1/position/page \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### 检查权限配置
|
||||
|
||||
如果不确定权限是否配置正确,可以用 admin 账号测试。admin(ID=1)自动跳过所有权限校验。
|
||||
|
||||
## 总结
|
||||
|
||||
模块开发常见问题主要集中在:字段类型不匹配、唯一性校验遗漏、软删除过滤缺失、分页参数命名不一致、权限节点未配置等。遇到问题时优先检查后端日志,确认接口入参和返回值是否符合预期。
|
||||
@@ -0,0 +1,185 @@
|
||||
# 前端 API 层
|
||||
|
||||
前端 API 层负责封装后端接口的 HTTP 请求。每个业务模块对应一个 API 文件,使用项目封装的 Axios 工具发送请求。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
ui/src/api/system/position.ts
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* @description: 岗位列表(分页)
|
||||
*/
|
||||
export function getPositionList(params?) {
|
||||
return http.request({
|
||||
url: '/position/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部岗位列表(无分页,用于下拉选择)
|
||||
* @param params 参数
|
||||
* @returns 返回结果
|
||||
*/
|
||||
export function getPositionAllList(params?) {
|
||||
return http.request({
|
||||
url: '/position/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 根据ID获取详情
|
||||
*/
|
||||
export function getPositionDetail(positionId) {
|
||||
return http.request({
|
||||
url: '/position/detail/' + positionId,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 添加岗位
|
||||
*/
|
||||
export function positionAdd(data: any) {
|
||||
return http.request({
|
||||
url: '/position/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 更新岗位
|
||||
*/
|
||||
export function positionUpdate(data: any) {
|
||||
return http.request({
|
||||
url: '/position/update',
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 删除岗位
|
||||
*/
|
||||
export function positionDelete(positionId) {
|
||||
return http.request({
|
||||
url: '/position/delete/' + positionId,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 批量删除岗位
|
||||
*/
|
||||
export function positionBatchDelete(data: any) {
|
||||
return http.request({
|
||||
url: '/position/batchDelete',
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### HTTP 客户端
|
||||
|
||||
```typescript
|
||||
import { http } from '@/utils/http/axios';
|
||||
```
|
||||
|
||||
`http` 是项目封装的 Axios 实例,自动处理:
|
||||
- 请求拦截:添加 JWT Token(`Authorization: Bearer xxx`)
|
||||
- 响应拦截:统一处理错误码、弹出提示
|
||||
- 基础路径:自动拼接 `/api/v1` 前缀
|
||||
|
||||
### 请求方式
|
||||
|
||||
```typescript
|
||||
http.request({
|
||||
url: '/position/page', // 接口路径(不含 /api/v1 前缀)
|
||||
method: 'GET', // HTTP 方法
|
||||
params, // GET 参数(拼接到 URL 查询字符串)
|
||||
})
|
||||
```
|
||||
|
||||
| 参数 | 用途 |
|
||||
|:----|:----|
|
||||
| `url` | 接口路径,与后端 router 定义的 path 对应 |
|
||||
| `method` | HTTP 方法:GET / POST / PUT / DELETE |
|
||||
| `params` | GET 请求参数(拼接到 URL) |
|
||||
| `data` | POST/PUT/DELETE 请求体(JSON) |
|
||||
|
||||
### 函数命名规范
|
||||
|
||||
| 模式 | 示例 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `get{Module}List` | `getPositionList` | 分页列表查询 |
|
||||
| `get{Module}AllList` | `getPositionAllList` | 全量列表查询 |
|
||||
| `get{Module}Detail` | `getPositionDetail` | 详情查询 |
|
||||
| `{module}Add` | `positionAdd` | 新增 |
|
||||
| `{module}Update` | `positionUpdate` | 更新 |
|
||||
| `{module}Delete` | `positionDelete` | 单条删除 |
|
||||
| `{module}BatchDelete` | `positionBatchDelete` | 批量删除 |
|
||||
|
||||
### 参数传递区别
|
||||
|
||||
```typescript
|
||||
// GET:参数通过 params 传递(拼接到 URL 查询字符串)
|
||||
http.request({ url: '/position/page', method: 'GET', params })
|
||||
|
||||
// POST/PUT/DELETE:参数通过 data 传递(放入请求体 JSON)
|
||||
http.request({ url: '/position/add', method: 'POST', data })
|
||||
|
||||
// 路径参数:直接拼接到 URL
|
||||
http.request({ url: '/position/delete/' + positionId, method: 'DELETE' })
|
||||
```
|
||||
|
||||
## 页面中的调用方式
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
||||
|
||||
// 分页查询
|
||||
const result = await getPositionList({ pageNo: 1, pageSize: 10, name: '测试' });
|
||||
|
||||
// 详情查询
|
||||
const detail = await getPositionDetail(1);
|
||||
|
||||
// 新增
|
||||
await positionAdd({ name: '新岗位', status: 1, sort: 0 });
|
||||
|
||||
// 更新
|
||||
await positionUpdate({ id: 1, name: '修改后', status: 1, sort: 1 });
|
||||
|
||||
// 单条删除
|
||||
await positionDelete(1);
|
||||
|
||||
// 批量删除
|
||||
await positionBatchDelete([1, 2, 3]);
|
||||
</script>
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **一个模块一个文件**:放在 `ui/src/api/{group}/` 目录下
|
||||
2. **URL 不含 `/api/v1` 前缀**:`http` 自动拼接
|
||||
3. **GET 用 `params`,POST/PUT/DELETE 用 `data`**:参数传递方式不同
|
||||
4. **路径参数直接拼接到 URL**:如 `/position/delete/` + id
|
||||
5. **批量删除的参数是数组**:`data: [1, 2, 3]`
|
||||
|
||||
## 总结
|
||||
|
||||
前端 API 层使用封装后的 `VAxios` 发送请求,URL 使用相对路径(不含 `/api/v1` 前缀),函数命名采用 `名词+动词` 格式。GET 请求用 `params`,POST/PUT/DELETE 用 `data`,响应拦截器自动处理错误。
|
||||
@@ -0,0 +1,139 @@
|
||||
# 前端路由与菜单
|
||||
|
||||
前端路由定义页面的访问路径,菜单是路由的可视化展示。项目的路由和菜单由后端动态返回,前端只需注册路由组件映射。
|
||||
|
||||
## 路由配置
|
||||
|
||||
### 文件位置
|
||||
|
||||
路由配置文件按模块分组:
|
||||
|
||||
```
|
||||
ui/src/router/routes/system.ts
|
||||
```
|
||||
|
||||
### 添加路由
|
||||
|
||||
在 `system.ts` 中添加岗位管理的路由:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
component: Layout,
|
||||
meta: { title: '系统管理', icon: 'setting' },
|
||||
children: [
|
||||
{
|
||||
path: 'position',
|
||||
name: 'Position',
|
||||
component: () => import('/@/views/system/position/index.vue'),
|
||||
meta: { title: '岗位管理' },
|
||||
},
|
||||
// ... 其他系统管理路由
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### 路由字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|:----|:----|
|
||||
| `path` | 路由路径(子路由不带 `/`) |
|
||||
| `name` | 路由名称(唯一标识) |
|
||||
| `component` | 组件引用(懒加载) |
|
||||
| `meta.title` | 菜单显示标题 |
|
||||
| `meta.icon` | 菜单图标 |
|
||||
|
||||
### 路由懒加载
|
||||
|
||||
```typescript
|
||||
component: () => import('/@/views/system/position/index.vue')
|
||||
```
|
||||
|
||||
使用动态 `import()` 实现路由懒加载,访问时才加载组件代码,优化首屏加载速度。
|
||||
|
||||
## 后端菜单配置
|
||||
|
||||
路由的实际注册由后端菜单数据驱动。在「系统管理 → 菜单管理」中添加菜单节点:
|
||||
|
||||
| 字段 | 值 |
|
||||
|:----|:----|
|
||||
| 菜单名称 | 岗位管理 |
|
||||
| 菜单类型 | 菜单(type=1) |
|
||||
| 路由路径 | `/system/position` |
|
||||
| 组件路径 | `system/position/index` |
|
||||
| 菜单图标 | 选择合适图标 |
|
||||
| 排序号 | 设置显示顺序 |
|
||||
| 状态 | 启用 |
|
||||
|
||||
### 菜单数据返回格式
|
||||
|
||||
后端接口 `/api/v1/menu/list` 返回的菜单数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"id": 10,
|
||||
"name": "系统管理",
|
||||
"parent_id": 0,
|
||||
"path": "/system",
|
||||
"component": "Layout",
|
||||
"type": 1,
|
||||
"icon": "setting",
|
||||
"children": [
|
||||
{
|
||||
"id": 101,
|
||||
"name": "岗位管理",
|
||||
"parent_id": 10,
|
||||
"path": "/system/position",
|
||||
"component": "system/position/index",
|
||||
"type": 1,
|
||||
"icon": "post"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 前端路由生成
|
||||
|
||||
前端在登录后请求菜单数据,动态生成路由:
|
||||
|
||||
```
|
||||
登录成功 → 请求 /api/v1/menu/list → 生成动态路由 → 注册到 Vue Router → 渲染菜单
|
||||
```
|
||||
|
||||
## 菜单与权限的关系
|
||||
|
||||
```
|
||||
菜单管理
|
||||
├── 系统管理(目录)
|
||||
│ ├── 岗位管理(菜单)
|
||||
│ │ ├── 岗位查询(权限)→ sys:position:page
|
||||
│ │ ├── 岗位新增(权限)→ sys:position:add
|
||||
│ │ ├── 岗位编辑(权限)→ sys:position:update
|
||||
│ │ └── 岗位删除(权限)→ sys:position:delete
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
| 节点类型 | type | 作用 |
|
||||
|:----|:----|:----|
|
||||
| 目录 | 0 | 菜单分组,不对应页面 |
|
||||
| 菜单 | 1 | 对应一个页面路由 |
|
||||
| 权限 | 2 | 对应一个操作权限,不在菜单中显示 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **路由文件按模块分组**:`system.ts`、`content.ts` 等
|
||||
2. **组件使用懒加载**:`() => import(...)`
|
||||
3. **菜单数据由后端驱动**:前端只需注册组件映射
|
||||
4. **组件路径与后端 `component` 字段对应**:如 `system/position/index`
|
||||
5. **新增模块只需在对应分组路由文件中追加子路由**
|
||||
|
||||
## 总结
|
||||
|
||||
前端路由使用 Vue Router 4,菜单路由由后端动态返回,前端通过 `addRoute` 动态注册。路由守卫负责权限校验和登录状态检查,未登录自动跳转登录页。
|
||||
@@ -0,0 +1,512 @@
|
||||
# 前端页面视图
|
||||
|
||||
前端页面视图是用户直接交互的界面。项目使用 Vue3 + ElementPlus + Vite 构建。一个完整的 CRUD 页面由 4 个文件组成:
|
||||
|
||||
```
|
||||
ui/src/views/system/position/
|
||||
├── index.vue # 主页面:搜索 + 表格 + 操作
|
||||
├── edit.vue # 编辑弹窗:新增/编辑表单
|
||||
├── columns.ts # 表格列定义
|
||||
└── querySchemas.ts # 搜索表单 Schema
|
||||
```
|
||||
|
||||
## 1. 主页面 `index.vue`
|
||||
|
||||
主页面是模块的入口页面,包含搜索表单、数据表格和操作按钮。
|
||||
|
||||
### 完整代码
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
ref="tableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['sys:position:add']">
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<PlusOutlined />
|
||||
</el-icon>
|
||||
</template>
|
||||
添加岗位
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleDelete()"
|
||||
:disabled="!selectionData.length"
|
||||
v-perm="['sys:position:batchDelete']"
|
||||
>
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</template>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
|
||||
<editDialog
|
||||
v-if="editVisible"
|
||||
:positionId="positionId"
|
||||
v-model:visible="editVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { ColProps } from 'element-plus';
|
||||
import { schemas } from './querySchemas';
|
||||
import { useForm } from '@/components/Form/index';
|
||||
import { TableAction } from '@/components/Table';
|
||||
import { getPositionList, positionDelete, positionBatchDelete } from '@/api/system/position';
|
||||
import { columns } from './columns';
|
||||
import { PlusOutlined } from '@vicons/antd';
|
||||
import { message, confirm } from '@/utils/auth';
|
||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||||
const positionId = ref(0);
|
||||
const editVisible = ref(false);
|
||||
const selectionData = ref([]);
|
||||
const tableRef = ref();
|
||||
|
||||
/**
|
||||
* 定义查询参数
|
||||
*/
|
||||
const formParams = reactive({
|
||||
name: '',
|
||||
status: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* 定义操作栏
|
||||
*/
|
||||
const actionColumn = reactive({
|
||||
width: 200,
|
||||
label: '操作',
|
||||
prop: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
icon: 'Edit',
|
||||
type: 'warning',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['sys:position:update'],
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'Delete',
|
||||
type: 'danger',
|
||||
onClick: handleDelete.bind(null, record),
|
||||
auth: ['sys:position:delete'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载数据列表
|
||||
*/
|
||||
const loadDataTable = async (res: any) => {
|
||||
const result = await getPositionList({ ...formParams, ...res });
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新数据列表
|
||||
*/
|
||||
function reloadTable(noRefresh = '') {
|
||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册搜索表单
|
||||
*/
|
||||
const [register, {}] = useForm({
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 } as ColProps,
|
||||
submitOnReset: true,
|
||||
schemas,
|
||||
});
|
||||
|
||||
/**
|
||||
* 执行提交表单
|
||||
*/
|
||||
function handleSubmit(values: Recordable) {
|
||||
handleReset();
|
||||
for (const key in values) {
|
||||
formParams[key] = values[key];
|
||||
}
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行重置
|
||||
*/
|
||||
function handleReset() {
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行添加
|
||||
*/
|
||||
const handleAdd = async () => {
|
||||
positionId.value = 0;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行编辑
|
||||
*/
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
positionId.value = record.row.id;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行删除(单条 + 批量)
|
||||
*/
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
}
|
||||
await confirm('确定要删除?');
|
||||
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 选项发生变化
|
||||
*/
|
||||
function onSelectionChange(value) {
|
||||
selectionData.value = value;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 代码解析
|
||||
|
||||
**页面结构**:`PageWrapper` 包裹两个 `el-card`,上方是搜索表单,下方是数据表格。
|
||||
|
||||
**搜索表单**:通过 `useForm` 注册,Schema 从 `querySchemas.ts` 导入。`submitOnReset: true` 表示重置时自动触发查询。
|
||||
|
||||
**数据表格**:通过 `BasicTable` 组件渲染,`request` 属性绑定数据加载函数,`columns` 从 `columns.ts` 导入。
|
||||
|
||||
**操作栏**:通过 `actionColumn` 的 `render` 函数使用 `h()` 渲染 `TableAction`,支持权限控制(`auth`)。
|
||||
|
||||
**编辑弹窗**:使用 `defineAsyncComponent` 懒加载 `edit.vue`,通过 `v-model:visible` 控制显示,`positionId` 传入编辑记录 ID(0 表示新增)。
|
||||
|
||||
**删除逻辑**:单条删除传入 `record`,批量删除从 `selectionData` 取已选 ID。共用同一个 `handleDelete` 方法。
|
||||
|
||||
## 2. 编辑弹窗 `edit.vue`
|
||||
|
||||
编辑弹窗是独立组件,负责新增和编辑操作的表单展示与提交。
|
||||
|
||||
### 完整代码
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="props.visible"
|
||||
:title="props.positionId ? '编辑' : '新增'"
|
||||
width="500"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="dialogClose"
|
||||
>
|
||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
||||
<el-form-item
|
||||
label="岗位名称"
|
||||
prop="name"
|
||||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||||
>
|
||||
<el-input class="ls-input" v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位状态" prop="status">
|
||||
<el-radio-group v-model="formData.status" name="status">
|
||||
<el-radio :value="1">正常</el-radio>
|
||||
<el-radio :value="2">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogClose">取消</el-button>
|
||||
<el-button :loading="subLoading" type="primary" @click="submit"> 确定 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||||
import { onMounted, reactive, shallowRef } from 'vue';
|
||||
import { message } from '@/utils/auth';
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
const formRef = shallowRef<FormInstance>();
|
||||
|
||||
/**
|
||||
* 定义表单参数
|
||||
*/
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
status: 1,
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* 定义接收的参数
|
||||
*/
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
positionId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 执行提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||||
message('操作成功');
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭窗体
|
||||
*/
|
||||
const dialogClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||||
|
||||
/**
|
||||
* 设置表单数据(编辑模式)
|
||||
*/
|
||||
const setFormData = async () => {
|
||||
const data = await getPositionDetail(props.positionId);
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
formData[key] = data[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 钩子函数
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (props.positionId) {
|
||||
setFormData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 代码解析
|
||||
|
||||
**Props 接收**:
|
||||
- `visible`:控制弹窗显示/隐藏,通过 `v-model:visible` 双向绑定
|
||||
- `positionId`:编辑记录的 ID,为 0 时表示新增模式
|
||||
|
||||
**表单数据**:使用 `reactive` 定义,字段与后端 Schema 对应。
|
||||
|
||||
**新增/编辑判断**:`props.positionId ? positionUpdate : positionAdd`,通过 ID 是否为 0 区分。
|
||||
|
||||
**防重复提交**:`useLockFn` 包装提交函数,点击后按钮变为 loading 状态,防止重复提交。
|
||||
|
||||
**数据回填**:`onMounted` 时判断如果是编辑模式,调用 `getPositionDetail` 获取数据并填充表单。
|
||||
|
||||
**事件通信**:
|
||||
- `emit('update:visible', false)`:关闭弹窗
|
||||
- `emit('success')`:通知父页面刷新表格
|
||||
|
||||
## 3. 表格列定义 `columns.ts`
|
||||
|
||||
定义表格的列配置,包括列名、字段名、宽度和自定义渲染。
|
||||
|
||||
### 完整代码
|
||||
|
||||
```typescript
|
||||
import { h } from 'vue';
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
label: 'ID',
|
||||
prop: 'id',
|
||||
fixed: 'left',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
label: '岗位名称',
|
||||
prop: 'name',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '岗位状态',
|
||||
prop: 'status',
|
||||
minWidth: 100,
|
||||
render(record) {
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: record.row.status == 1 ? 'success' : 'danger',
|
||||
},
|
||||
{
|
||||
default: () => (record.row.status == 1 ? '正常' : '停用'),
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
prop: 'sort',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUser',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 代码解析
|
||||
|
||||
**选择列**:`type: 'selection'` 自动添加复选框列,配合 `@selection-change` 事件实现批量选择。
|
||||
|
||||
**固定列**:`fixed: 'left'` 固定在左侧,`fixed: 'right'` 固定在右侧(操作列)。
|
||||
|
||||
**自定义渲染**:`render(record)` 使用 Vue 的 `h()` 函数创建虚拟 DOM。状态列将 `1/2` 渲染为绿色/红色标签。
|
||||
|
||||
**宽度设置**:
|
||||
- `width`:固定宽度
|
||||
- `minWidth`:最小宽度,可自适应拉伸
|
||||
|
||||
## 4. 搜索表单 `querySchemas.ts`
|
||||
|
||||
定义搜索表单的字段配置,使用项目封装的 `FormSchema` 类型。
|
||||
|
||||
### 完整代码
|
||||
|
||||
```typescript
|
||||
import { FormSchema } from '@/components/Form/index';
|
||||
export const schemas: FormSchema[] = [
|
||||
{
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
label: '岗位名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入岗位名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
component: 'Select',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
clearable: true,
|
||||
options: [
|
||||
{
|
||||
label: '正常',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 代码解析
|
||||
|
||||
**FormSchema 字段**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|:----|:----|
|
||||
| `field` | 字段名,与后端查询参数对应 |
|
||||
| `component` | 表单组件类型:`Input` / `Select` / `DatePicker` 等 |
|
||||
| `label` | 标签文本 |
|
||||
| `componentProps` | 组件属性,透传给 ElementPlus 组件 |
|
||||
|
||||
**Select 组件**:`options` 定义下拉选项,`clearable: true` 允许清空选择。
|
||||
|
||||
## 文件关系图
|
||||
|
||||
```
|
||||
index.vue(主页面)
|
||||
├── import schemas from './querySchemas' ← 搜索表单配置
|
||||
├── import columns from './columns' ← 表格列配置
|
||||
├── import editDialog from './edit.vue' ← 编辑弹窗组件
|
||||
└── import API from '@/api/system/position' ← 接口请求
|
||||
|
||||
edit.vue(编辑弹窗)
|
||||
└── import API from '@/api/system/position' ← 接口请求
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **一个模块一个目录**:放在 `ui/src/views/{group}/{module}/` 下
|
||||
2. **4 个文件各司其职**:主页面、编辑弹窗、列定义、搜索 Schema
|
||||
3. **编辑弹窗是独立组件**:通过 Props 接收 ID,通过 Emit 通知父页面
|
||||
4. **列定义使用 `h()` 渲染复杂内容**:如状态标签、操作按钮
|
||||
5. **搜索 Schema 使用 `FormSchema` 类型**:配置化定义表单字段
|
||||
6. **新增/编辑通过 `positionId` 区分**:0 为新增,非 0 为编辑
|
||||
7. **防重复提交使用 `useLockFn`**:提交时按钮 loading
|
||||
|
||||
## 总结
|
||||
|
||||
前端页面统一使用 `PageWrapper` + `BasicForm` + `BasicTable` + `TableAction` 四件套,分页由 `BasicTable` 内置处理。每个模块包含 `index.vue`(列表页)、`edit.vue`(编辑弹窗)、`columns.ts`(列定义)、`querySchemas.ts`(搜索配置)四个文件。
|
||||
@@ -0,0 +1,157 @@
|
||||
# 请求处理器(Handler)
|
||||
|
||||
Handler 层是 HTTP 接口层,负责接收请求、校验权限、记录操作日志,然后委托给 Service 层处理。每个业务模块对应一个 `handlers.py` 文件,所有 Handler 继承 `BaseHandler`。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/link/handlers.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from common.middleware.access_decorators import check_demo, permission_required
|
||||
from common.middleware.operation_log import operation_log
|
||||
from core import response as R
|
||||
from core.base_handler import BaseHandler
|
||||
from modules.link.service import link_service
|
||||
|
||||
|
||||
class LinkPageHandler(BaseHandler):
|
||||
"""查询友链分页数据。"""
|
||||
|
||||
@permission_required("sys:link:list")
|
||||
async def get(self):
|
||||
return await link_service.get_page(self)
|
||||
|
||||
|
||||
class LinkDetailHandler(BaseHandler):
|
||||
"""查询友链详情。"""
|
||||
|
||||
@permission_required("sys:link:detail")
|
||||
async def get(self, id):
|
||||
return R.ok(self, data=link_service.get_detail(id))
|
||||
|
||||
|
||||
class LinkAddHandler(BaseHandler):
|
||||
"""添加友链。"""
|
||||
|
||||
@permission_required("sys:link:add")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "添加")
|
||||
async def post(self):
|
||||
return await link_service.add(self)
|
||||
|
||||
|
||||
class LinkUpdateHandler(BaseHandler):
|
||||
"""更新友链。"""
|
||||
|
||||
@permission_required("sys:link:update")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "更新")
|
||||
async def put(self):
|
||||
return await link_service.update(self)
|
||||
|
||||
|
||||
class LinkDeleteHandler(BaseHandler):
|
||||
"""删除友链。"""
|
||||
|
||||
@permission_required("sys:link:delete")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "删除")
|
||||
async def delete(self, id):
|
||||
return await link_service.delete(self, id)
|
||||
|
||||
|
||||
class LinkStatusHandler(BaseHandler):
|
||||
"""设置友链状态。"""
|
||||
|
||||
@permission_required("sys:link:status")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "设置状态")
|
||||
async def put(self):
|
||||
return await link_service.update_status(self)
|
||||
|
||||
|
||||
class LinkBatchDeleteHandler(BaseHandler):
|
||||
"""批量删除友链。"""
|
||||
|
||||
@permission_required("sys:link:delete")
|
||||
@check_demo
|
||||
@operation_log("友链管理", "批量删除")
|
||||
async def post(self):
|
||||
return await link_service.batch_delete(self)
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### Handler 类
|
||||
|
||||
```python
|
||||
class LinkPageHandler(BaseHandler):
|
||||
```
|
||||
|
||||
每个 Handler 继承 `BaseHandler`(即 `tornado.web.RequestHandler`),负责一个 HTTP 端点。与 Tornado 的 Endpoint 函数不同,Tornado 使用类来组织处理器。
|
||||
|
||||
### 装饰器说明
|
||||
|
||||
| 装饰器 | 作用 | 位置 |
|
||||
|:----|:----|:----|
|
||||
| `@permission_required("sys:link:list")` | RBAC 权限校验 | 类方法上 |
|
||||
| `@check_demo` | 演示模式拦截(仅写操作) | 权限装饰器之后 |
|
||||
| `@operation_log("模块", "动作")` | 操作日志记录 | 最内层 |
|
||||
|
||||
**装饰器顺序必须严格遵守**:权限 → 演示模式 → 操作日志。
|
||||
|
||||
### 路径参数
|
||||
|
||||
```python
|
||||
async def get(self, id):
|
||||
```
|
||||
|
||||
Tornado 的路径参数通过正则捕获组传入方法参数。路由定义 `(r"/link/detail/(\d+)", LinkDetailHandler)` 中的 `(\d+)` 对应 `id` 参数。
|
||||
|
||||
### 响应封装
|
||||
|
||||
- **Service 返回 R 对象的**(如 `add`、`update`、`delete`):直接 `return`
|
||||
- **Service 返回原始数据的**(如 `get_detail`):用 `R.ok(self, data=...)` 包装
|
||||
|
||||
### HTTP 方法映射
|
||||
|
||||
| 操作 | HTTP 方法 | Handler 方法 |
|
||||
|:----|:----|:----|
|
||||
| 分页查询 | GET | `async def get(self)` |
|
||||
| 详情查询 | GET | `async def get(self, id)` |
|
||||
| 新增 | POST | `async def post(self)` |
|
||||
| 更新 | PUT | `async def put(self)` |
|
||||
| 删除 | DELETE | `async def delete(self, id)` |
|
||||
| 状态变更 | PUT | `async def put(self)` |
|
||||
| 批量删除 | POST | `async def post(self)` |
|
||||
|
||||
### 权限字符串命名规范
|
||||
|
||||
```
|
||||
sys:{module}:{action}
|
||||
```
|
||||
|
||||
| 动作 | HTTP 方法 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `list` | GET | 分页/列表查询 |
|
||||
| `detail` | GET | 详情查询 |
|
||||
| `add` | POST | 新增 |
|
||||
| `update` | PUT | 更新 |
|
||||
| `delete` | DELETE | 单条/批量删除 |
|
||||
| `status` | PUT | 状态变更 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **Handler 层不含业务逻辑**:仅做权限校验、日志记录和请求分发
|
||||
2. **装饰器顺序**:权限 → 演示模式 → 操作日志
|
||||
3. **写操作必须加 `@check_demo`**:防止演示环境被修改
|
||||
4. **写操作建议加 `@operation_log`**:记录操作日志用于审计
|
||||
5. **所有方法都是 `async def`**:Tornado Handler 方法默认异步
|
||||
|
||||
## 总结
|
||||
|
||||
Handler 层是 HTTP 请求的入口,负责权限校验、操作日志记录和请求分发,不含业务逻辑。装饰器顺序固定为权限 → 演示模式 → 操作日志,写操作必须加 `@check_demo`,所有逻辑委托 Service 处理。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 数据模型(Model)
|
||||
|
||||
数据模型是模块开发的第一步,用于定义数据库表结构。项目使用 `SQLAlchemy ORM`,模型类通过继承 `BaseModel` 自动获得通用字段和方法。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/link/models.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 友链数据模型层
|
||||
# | 说明: 定义友链表结构,继承 BaseModel 获得通用字段与序列化能力
|
||||
# +======================================================================
|
||||
"""友链数据模型。"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, text
|
||||
|
||||
from config.database import DB_PREFIX
|
||||
from core.base_model import BaseModel
|
||||
|
||||
|
||||
class Link(BaseModel):
|
||||
"""友情链接模型"""
|
||||
__tablename__ = DB_PREFIX + "link"
|
||||
__table_args__ = {"comment": "友情链接表"}
|
||||
|
||||
# 链接名称
|
||||
name = Column(String(255), nullable=False, index=True, comment="链接名称")
|
||||
# 链接类型
|
||||
type = Column(String(50), default="", comment="链接类型")
|
||||
# 链接来源
|
||||
form = Column(String(50), default="", comment="链接来源")
|
||||
# 链接图片
|
||||
image = Column(String(500), default="", comment="链接图片")
|
||||
# 链接地址
|
||||
url = Column(String(500), default="", comment="链接地址")
|
||||
# 状态:0-正常 1-停用
|
||||
status = Column(Integer, default=0, server_default=text("0"), comment="状态")
|
||||
# 排序
|
||||
sort = Column(Integer, default=0, server_default=text("0"), comment="排序")
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类继承
|
||||
|
||||
```python
|
||||
class Link(BaseModel):
|
||||
```
|
||||
|
||||
| 基类 | 来源 | 提供能力 |
|
||||
|:----|:----|:----|
|
||||
| `BaseModel` | `core/base_model.py` | 通用字段:id、create_user、create_time、update_user、update_time、is_delete;`to_dict()` 序列化方法(自动排除 password/salt/is_delete) |
|
||||
|
||||
:::tip BaseModel 说明
|
||||
`BaseModel` 是所有业务模型的基类,提供6个通用审计字段和 `to_dict()` 序列化方法。模型实例不暴露 `save()/delete()` 方法,数据操作通过 Repository 层进行。
|
||||
:::
|
||||
|
||||
### 表名配置
|
||||
|
||||
```python
|
||||
__tablename__ = DB_PREFIX + "link"
|
||||
```
|
||||
|
||||
`DB_PREFIX` 从 `.env` 读取(默认 `tornado_`),最终表名为 `tornado_link`。所有业务表都使用此前缀,便于区分不同应用的同库表。
|
||||
|
||||
### 表注释
|
||||
|
||||
```python
|
||||
__table_args__ = {"comment": "友情链接表"}
|
||||
```
|
||||
|
||||
使用 `__table_args` 字典设置表注释,同步到数据库元数据。
|
||||
|
||||
### 字段定义
|
||||
|
||||
```python
|
||||
name = Column(String(255), nullable=False, index=True, comment="链接名称")
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `String(255)` | 字段类型,VARCHAR(255) |
|
||||
| `nullable=False` | 非空约束 |
|
||||
| `index=True` | 创建索引,加速查询 |
|
||||
| `comment` | 字段注释,同步到数据库 |
|
||||
|
||||
```python
|
||||
status = Column(Integer, default=0, server_default=text("0"), comment="状态")
|
||||
```
|
||||
|
||||
`default=0` 是 Python 端默认值,`server_default=text("0")` 是数据库端默认值。两者都设置可确保无论通过 ORM 还是直接 SQL 插入,都能正确赋默认值。
|
||||
|
||||
### 枚举值约定
|
||||
|
||||
状态字段使用 `0` 和 `1` 而非 `1` 和 `2`:
|
||||
|
||||
| 字段 | 值 | 含义 |
|
||||
|:----|:----|:----|
|
||||
| `status` | `0` | 正常 |
|
||||
| `status` | `1` | 停用 |
|
||||
| `is_delete` | `0` | 未删除 |
|
||||
| `is_delete` | `1` | 已删除 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **所有模型继承 `BaseModel`**:自动获得通用字段和序列化能力
|
||||
2. **表名必须加 `DB_PREFIX` 前缀**:`DB_PREFIX + "表名"`
|
||||
3. **使用 `__table_args__` 设置表注释**:字典格式 `{"comment": "表注释"}`
|
||||
4. **查询频繁的字段设置索引**:`index=True`
|
||||
5. **同时设置 `default` 和 `server_default`**:ORM 和直 SQL 都能正确赋默认值
|
||||
6. **模型不暴露 save/delete 方法**:数据操作通过 Repository 层进行
|
||||
|
||||
## 总结
|
||||
|
||||
数据模型通过继承 `BaseModel` 获得通用审计字段和序列化能力。表名使用 `DB_PREFIX` 前缀,字段定义同时设置 Python 端和数据库端默认值。模型只定义表结构,不包含业务逻辑。
|
||||
@@ -0,0 +1,97 @@
|
||||
# 概述与目标
|
||||
|
||||
本章以「岗位管理(Position)」模块为完整案例,手把手演示从零开发一个业务模块的全流程。学完本章后,你将掌握项目中任意模块的开发方式。
|
||||
|
||||
## 你将学到什么
|
||||
|
||||
通过本章的实战演练,你将完整经历以下开发环节:
|
||||
|
||||
| 环节 | 涉及文件 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| 数据模型 | `models.py` | 定义数据库表结构 |
|
||||
| 请求校验 | `schemas.py` | Pydantic 表单验证 |
|
||||
| 数据访问 | `repository.py` | 封装数据库查询 |
|
||||
| 业务逻辑 | `service.py` | 实现增删改查与业务规则 |
|
||||
| API 端点 | `handlers.py` | 定义 HTTP 接口 |
|
||||
| 路由注册 | `router.py` | 挂载路由到应用 |
|
||||
| 权限配置 | 菜单管理 | 配置权限节点 |
|
||||
| 前端对接 | API + 页面 + 路由 | 前端完整开发 |
|
||||
|
||||
## 案例模块:岗位管理
|
||||
|
||||
选择「岗位管理」作为案例的原因:
|
||||
|
||||
1. **结构典型**:包含完整的 CRUD + 状态管理 + 删除校验,覆盖大多数业务场景
|
||||
2. **复杂度适中**:字段少(name / status / sort),逻辑清晰,适合入门
|
||||
3. **依赖简单**:仅被 User 模块引用,无复杂关联关系
|
||||
4. **代码精简**:后端 4 个文件 + 前端 3 个文件,总计约 500 行代码
|
||||
|
||||
## 模块文件总览
|
||||
|
||||
```
|
||||
后端(src/)
|
||||
├── modules/position/
|
||||
│ ├── __init__.py # 模块初始化(导出 routes)
|
||||
│ ├── models.py # 数据模型
|
||||
│ ├── schemas.py # 表单验证
|
||||
│ ├── repository.py # 数据访问层
|
||||
│ ├── service.py # 业务逻辑层
|
||||
│ └── handlers.py # 请求处理器层
|
||||
└── api/v1/
|
||||
└── position.py # 路由定义(导出 routes 列表)
|
||||
|
||||
前端(ui/src/)
|
||||
├── api/system/
|
||||
│ └── position.ts # API 请求封装
|
||||
├── views/system/
|
||||
│ └── position/
|
||||
│ ├── index.vue # 主页面:搜索 + 表格 + 操作
|
||||
│ ├── edit.vue # 编辑弹窗:新增/编辑表单
|
||||
│ ├── columns.ts # 表格列定义
|
||||
│ └── querySchemas.ts # 搜索表单 Schema
|
||||
└── router/routes/
|
||||
└── system.ts # 路由配置(追加)
|
||||
```
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
岗位表 `tornado_position` 结构如下:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| id | int | 主键,自增 |
|
||||
| name | varchar(255) | 岗位名称 |
|
||||
| status | int | 状态:1-在用 2-停用 |
|
||||
| sort | int | 排序号 |
|
||||
| create_user | varchar(50) | 创建人 |
|
||||
| create_time | datetime | 创建时间 |
|
||||
| update_user | varchar(50) | 更新人 |
|
||||
| update_time | datetime | 更新时间 |
|
||||
| is_delete | int | 软删除标记:0-正常 1-已删除 |
|
||||
|
||||
:::tip 温馨提示
|
||||
其中 `id`、`create_user`、`create_time`、`update_user`、`update_time`、`is_delete` 由基类 `BaseModel` 自动提供,业务字段仅需定义 `name`、`status`、`sort`。
|
||||
:::
|
||||
|
||||
## 开发顺序
|
||||
|
||||
建议按以下顺序逐步开发,每完成一步即可验证:
|
||||
|
||||
```
|
||||
1. models.py → 建表,确认数据库中有 tornado_position 表
|
||||
2. schemas.py → 定义验证规则(无独立验证方式,后续端点调用时自动校验)
|
||||
3. repository.py → 数据访问层(通常只需声明类,无需写代码)
|
||||
4. service.py → 业务逻辑层(声明差异点 + 自定义方法)
|
||||
5. handlers.py → 请求处理器(声明路由 + 权限装饰器)
|
||||
6. api/v1/position.py → 注册路由(导出 routes 列表,在 router.py 中自动聚合)
|
||||
7. 权限配置 → 在菜单管理中添加权限节点
|
||||
8. 前端对接 → API → 页面 → 路由
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
本章每一步都附有完整代码和详细注释。建议对照项目中 `src/modules/position/` 目录下的实际源码一起阅读,加深理解。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
模块开发实战以岗位管理(position)为例,完整演示从 Model → Schema → Repository → Service → Handler → 路由注册 → 权限配置 → 前端 API → 前端页面的全流程。每一步都附有完整代码和详细注释,建议对照实际源码一起阅读。
|
||||
@@ -0,0 +1,105 @@
|
||||
# 权限节点配置
|
||||
|
||||
后端代码中的 `@permission_required("sys:position:page")` 装饰器定义了接口所需的权限字符串,但权限节点本身需要在数据库中配置,才能在「菜单管理」中分配给角色。
|
||||
|
||||
## 权限字符串格式
|
||||
|
||||
```
|
||||
sys:{module}:{action}
|
||||
```
|
||||
|
||||
| 部分 | 说明 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| `sys` | 系统前缀(固定) | `sys` |
|
||||
| `module` | 模块名(小写) | `position` |
|
||||
| `action` | 操作名 | `page` / `add` / `update` / `delete` |
|
||||
|
||||
## Position 模块的权限节点
|
||||
|
||||
| 权限字符串 | 说明 | HTTP 方法 | 路径 |
|
||||
|:----|:----|:----|:----|
|
||||
| `sys:position:page` | 分页查询 | GET | `/api/v1/position/page` |
|
||||
| `sys:position:detail` | 详情查询 | GET | `/api/v1/position/detail/{id}` |
|
||||
| `sys:position:list` | 列表查询 | GET | `/api/v1/position/getPositionList` |
|
||||
| `sys:position:add` | 新增岗位 | POST | `/api/v1/position/add` |
|
||||
| `sys:position:update` | 更新岗位 | PUT | `/api/v1/position/update` |
|
||||
| `sys:position:delete` | 删除岗位 | DELETE | `/api/v1/position/delete/{id}` |
|
||||
| `sys:position:batchDelete` | 批量删除 | POST | `/api/v1/position/batchDelete` |
|
||||
| `sys:position:status` | 状态变更 | PUT | `/api/v1/position/status` |
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 第一步:添加菜单节点
|
||||
|
||||
登录管理后台,进入「系统管理 → 菜单管理」:
|
||||
|
||||
1. 找到或创建「岗位管理」菜单项
|
||||
2. 在该菜单下添加「权限」类型子节点
|
||||
3. 填写以下信息:
|
||||
|
||||
| 字段 | 值 |
|
||||
|:----|:----|
|
||||
| 菜单类型 | 权限 |
|
||||
| 权限标识 | `sys:position:page` |
|
||||
| 菜单名称 | 岗位查询 |
|
||||
|
||||
### 第二步:批量添加权限节点
|
||||
|
||||
为每个接口重复上述步骤,添加全部 8 个权限节点。
|
||||
|
||||
:::tip 温馨提示
|
||||
实际开发中,查询类权限(page、detail、list)通常合并为一个节点 `sys:position:list`,减少配置工作量。写操作(add、update、delete、batchDelete、status)建议独立配置,便于细粒度控制。
|
||||
:::
|
||||
|
||||
### 第三步:分配给角色
|
||||
|
||||
权限节点创建并启用后,需将其分配给角色,用户才能通过角色继承相应权限。
|
||||
|
||||
操作路径:「系统管理 → 角色管理」
|
||||
|
||||
1. 在角色列表中找到目标角色,点击「编辑」进入权限配置页面。
|
||||
2. 在权限树中展开「岗位管理」模块,勾选需要分配的权限节点(如「新增」「编辑」「删除」等)。
|
||||
3. 点击「保存」提交配置,权限即生效。
|
||||
|
||||
:::warning 特别说明
|
||||
注意:权限分配后,拥有该角色的用户需重新登录或刷新页面才能获取最新权限。若权限未生效,请检查用户是否已正确关联该角色。
|
||||
:::
|
||||
|
||||
## 权限校验流程
|
||||
|
||||
```
|
||||
用户请求 → login_required(JWT 校验) → permission_required(权限校验)
|
||||
├── 用户 ID = 1(admin)→ 跳过权限校验,直接放行
|
||||
└── 其他用户 → 查询角色权限列表 → 匹配权限字符串 → 放行 / 拒绝
|
||||
```
|
||||
|
||||
## SQL 直接插入权限节点
|
||||
|
||||
如果需要通过 SQL 直接插入权限节点(如初始化脚本),参考以下语句:
|
||||
|
||||
```sql
|
||||
-- 假设岗位管理菜单 ID 为 100
|
||||
INSERT INTO tornado_menu (name, parent_id, path, component, perms, type, icon, sort, create_time, is_delete)
|
||||
VALUES
|
||||
('岗位查询', 100, NULL, NULL, 'sys:position:page', '1', NULL, 0, NOW(), 0),
|
||||
('岗位新增', 100, NULL, NULL, 'sys:position:add', '1', NULL, 0, NOW(), 0),
|
||||
('岗位编辑', 100, NULL, NULL, 'sys:position:update', '1', NULL, 0, NOW(), 0),
|
||||
('岗位删除', 100, NULL, NULL, 'sys:position:delete', '1', NULL, 0, NOW(), 0),
|
||||
('岗位批量删除', 100, NULL, NULL, 'sys:position:batchDelete', '1', NULL, 0, NOW(), 0),
|
||||
('岗位状态', 100, NULL, NULL, 'sys:position:status', '1', NULL, 0, NOW(), 0);
|
||||
```
|
||||
|
||||
:::danger 特别说明
|
||||
其中 `type='1'` 表示权限类型节点(非菜单)。
|
||||
:::
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. 权限字符串必须与代码中的 `@permission_required` 完全一致。
|
||||
2. admin 用户(ID=1)自动跳过权限校验:开发阶段可用 admin 测试。
|
||||
3. 权限节点是菜单的子节点:type 为 `1`(权限),不是 `0`(菜单)。
|
||||
4. 查询权限可适当合并:减少配置复杂度。
|
||||
5. 写操作权限建议独立:便于细粒度控制。
|
||||
## 总结
|
||||
|
||||
权限节点是菜单的子节点(type=1),命名规范为 `sys:{模块}:{操作}`。后端通过 `@permission_required` 装饰器校验,前端通过 `v-perm` 指令控制按钮显示。查询权限可适当合并,写操作权限建议独立以实现细粒度控制。
|
||||
@@ -0,0 +1,102 @@
|
||||
# Repository 层
|
||||
|
||||
Repository 层是数据访问层,封装所有数据库查询操作。项目通过 `BaseRepository` 基类提供通用 CRUD 能力,子类通常只需声明类名,无需编写任何代码。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/position/repository.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 岗位数据访问层
|
||||
# | 说明: 定义岗位仓储,继承 BaseRepository 获得通用 CRUD 与软删除能力
|
||||
# +======================================================================
|
||||
"""岗位数据访问。"""
|
||||
|
||||
from core.base_repository import BaseRepository
|
||||
from modules.position.models import Position
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 岗位仓储
|
||||
# ======================================================================
|
||||
class PositionRepository(BaseRepository):
|
||||
"""岗位仓储。"""
|
||||
|
||||
# ============================================================
|
||||
# 绑定的模型类
|
||||
# ============================================================
|
||||
model = Position
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类继承
|
||||
|
||||
```python
|
||||
class PositionRepository(BaseRepository(Position]):
|
||||
```
|
||||
|
||||
`BaseRepository(Position]` 是泛型基类,传入模型类型后自动获得以下能力:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|:----|:----|
|
||||
| `get_by_id(id)` | 根据 ID 查询单条(自动过滤软删除) |
|
||||
| `get_by_ids(ids)` | 根据 ID 列表查询多条 |
|
||||
| `get_one(**kwargs)` | 按等值条件查询单条(跳过 None 值) |
|
||||
| `all()` | 按等值条件查询全部(自动过滤软删除) |
|
||||
| `exists_by_field(field, value)` | 判断字段值是否存在 |
|
||||
| `count(**kwargs)` | 按条件计数 |
|
||||
| `filter(*criterion)` | 原始 SQLAlchemy 条件查询(不自动过滤软删除) |
|
||||
| `filter_by(**kwargs)` | 等值条件查询(自动过滤软删除,跳过 None) |
|
||||
| `create(**kwargs)` | 创建记录 |
|
||||
| `update(id, **kwargs)` | 更新记录 |
|
||||
| `paginate(page, limit)` | 分页查询 |
|
||||
| `batch_delete(ids)` | 批量软删除 |
|
||||
|
||||
### Service 层使用方式
|
||||
|
||||
Repository 不需要手动创建单例。Service 层通过 `repo_cls = PositionRepository` 声明关联的 Repository 类,`BaseService.__init__` 会自动实例化:
|
||||
|
||||
```python
|
||||
# service.py 中的使用方式
|
||||
class PositionService(BaseService):
|
||||
model = Position
|
||||
repo_cls = PositionRepository # 传入类,基类自动实例化
|
||||
```
|
||||
|
||||
### 何时需要自定义方法
|
||||
|
||||
大多数简单 CRUD 模块(如 Position、Level、Category)的 Repository 只需 `pass`。当需要以下场景时,才在子类中添加自定义方法:
|
||||
|
||||
- **复杂联表查询**:如 User 需要关联 Dept、Role 查询
|
||||
- **特殊聚合查询**:如统计、分组
|
||||
- **非标准查询条件**:如区间查询、模糊 + 精确组合
|
||||
|
||||
示例(UserRepository 中的自定义方法):
|
||||
|
||||
```python
|
||||
class UserRepository(BaseRepository(User]):
|
||||
def get_by_username(self, username: str):
|
||||
"""根据用户名查询用户(含角色信息)"""
|
||||
return self.filter(
|
||||
User.username == username,
|
||||
User.is_delete == 0
|
||||
).first()
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **简单模块只需声明 `model`**:继承基类即可获得完整 CRUD 能力
|
||||
2. **Service 层通过 `repo_cls` 关联 Repository 类**:基类自动实例化,无需手动创建单例
|
||||
3. **`filter()` 不自动过滤软删除**:需要手动追加 `Model.is_delete == 0` 条件
|
||||
4. **`filter_by()` / `get_one()` / `all()` 自动过滤软删除**:且跳过 None 值,推荐优先使用
|
||||
5. **Repository 层不做响应封装**:返回 SQLAlchemy 查询结果,由 Service 层处理响应
|
||||
|
||||
## 总结
|
||||
|
||||
Repository 层继承 `BaseRepository` 获得通用 CRUD 能力,等值查询方法(`get_one`/`get_all`/`filter_by`)自动过滤软删除且跳过 None 值,`filter()` 为原始出口需手动追加软删除条件。Repository 层不做响应封装,由 Service 层处理。
|
||||
@@ -0,0 +1,125 @@
|
||||
# 路由注册
|
||||
|
||||
路由注册是将模块的 Handler 挂载到应用的过程。所有模块的路由在 `src/api/v1/router.py` 中统一注册。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/api/v1/router.py
|
||||
src/api/v1/{module}.py # 每个模块的路由定义
|
||||
```
|
||||
|
||||
## 注册步骤
|
||||
|
||||
### 第一步:定义模块路由
|
||||
|
||||
在 `src/api/v1/position.py` 中定义 routes 列表:
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 岗位路由层
|
||||
# | 说明: 提供岗位管理的增删改查、批量删除、状态管理等 API 接口
|
||||
# +======================================================================
|
||||
"""岗位相关路由。"""
|
||||
|
||||
from modules.position.handlers import (
|
||||
PositionAddHandler, PositionBatchDeleteHandler, PositionDeleteHandler,
|
||||
PositionDetailHandler, PositionGetListHandler, PositionPageHandler,
|
||||
PositionStatusHandler, PositionUpdateHandler,
|
||||
)
|
||||
|
||||
routes = [
|
||||
# 查询岗位分页数据
|
||||
(r"/position/page", PositionPageHandler),
|
||||
# 查询岗位详情
|
||||
(r"/position/detail/(\d+)", PositionDetailHandler),
|
||||
# 获取岗位下拉列表
|
||||
(r"/position/getPositionList", PositionGetListHandler),
|
||||
# 添加岗位
|
||||
(r"/position/add", PositionAddHandler),
|
||||
# 更新岗位
|
||||
(r"/position/update", PositionUpdateHandler),
|
||||
# 删除岗位
|
||||
(r"/position/delete/(\d+)", PositionDeleteHandler),
|
||||
# 设置岗位状态
|
||||
(r"/position/status", PositionStatusHandler),
|
||||
# 批量删除岗位
|
||||
(r"/position/batchDelete", PositionBatchDeleteHandler),
|
||||
]
|
||||
```
|
||||
|
||||
### 第二步:在 router.py 中注册
|
||||
|
||||
在 `src/api/v1/router.py` 的 `_RESOURCE_MODULES` 列表中追加模块:
|
||||
|
||||
```python
|
||||
# src/api/v1/router.py
|
||||
from api.v1 import (auth, index, upload, user,
|
||||
user_role, role, role_menu, menu,
|
||||
dept, dict, dict_item, config,
|
||||
config_item, level, position, city,
|
||||
notice, link, category, article,
|
||||
param, file_template, job, generator,
|
||||
operation_log, login_log, job_log, example)
|
||||
|
||||
from api.v1 import position # 新增
|
||||
|
||||
_RESOURCE_MODULES = [
|
||||
auth, index, upload, user,
|
||||
# ... 已有模块 ...
|
||||
position, # 新增
|
||||
]
|
||||
```
|
||||
|
||||
:::tip 注册规则
|
||||
新模块只需两步:
|
||||
1. 在 `src/api/v1/` 下创建路由文件,定义 `routes` 列表
|
||||
2. 在 `router.py` 的 `_RESOURCE_MODULES` 中追加模块导入
|
||||
|
||||
路由前缀 `/api/v1` 由 `build_routes()` 自动拼接,无需手动添加。
|
||||
:::
|
||||
|
||||
## 最终 API 路径
|
||||
|
||||
注册后,Position 模块的接口路径为:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| GET | `/api/v1/position/page` | 分页查询 |
|
||||
| GET | `/api/v1/position/detail/(\d+)` | 详情查询 |
|
||||
| GET | `/api/v1/position/getPositionList` | 下拉列表 |
|
||||
| POST | `/api/v1/position/add` | 新增 |
|
||||
| PUT | `/api/v1/position/update` | 更新 |
|
||||
| DELETE | `/api/v1/position/delete/(\d+)` | 单条删除 |
|
||||
| POST | `/api/v1/position/batchDelete` | 批量删除 |
|
||||
| PUT | `/api/v1/position/status` | 状态变更 |
|
||||
|
||||
## URL 命名规范
|
||||
|
||||
| 规则 | 示例 |
|
||||
|:----|:----|
|
||||
| 使用小写英文 | `/position` 而非 `/Position` |
|
||||
| 使用单数形式 | `/position` 而非 `/positions` |
|
||||
| 多单词用驼峰 | `/fileTemplate` 或 `/file-template` |
|
||||
| 路径参数用正则 | `(\d+)` 匹配数字 ID |
|
||||
|
||||
## build_routes 工作原理
|
||||
|
||||
```python
|
||||
# src/api/v1/router.py
|
||||
V1_PREFIX = "/api/v1"
|
||||
|
||||
def build_routes() -> list:
|
||||
"""构建全部路由(含 /api/v1 前缀与健康检查)。"""
|
||||
routes = []
|
||||
for module in _RESOURCE_MODULES:
|
||||
routes.extend([(V1_PREFIX + path, handler) for path, handler in module.routes])
|
||||
routes.append((V1_PREFIX + "/health", HealthHandler))
|
||||
return routes
|
||||
```
|
||||
|
||||
`build_routes()` 遍历所有模块的 `routes` 列表,为每条路由拼接 `/api/v1` 前缀,最后追加健康检查端点。
|
||||
|
||||
## 总结
|
||||
|
||||
路由注册采用声明式模式:每个模块在 `src/api/v1/{module}.py` 中定义 `routes` 列表,在 `router.py` 的 `_RESOURCE_MODULES` 中追加导入即可。路由前缀 `/api/v1` 自动拼接,无需手动配置。
|
||||
@@ -0,0 +1,125 @@
|
||||
# Pydantic Schema
|
||||
|
||||
Schema 层负责请求数据的校验。项目使用 Pydantic v2 的 `BaseModel` 定义表单类,通过 `Field` 约束字段格式,Tornado 在接收请求体时自动执行校验。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/position/schemas.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from core.base_schema import BaseSchema
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位表单类
|
||||
# ============================================================
|
||||
class PositionForm(BaseSchema):
|
||||
"""岗位创建/编辑表单"""
|
||||
name: str = Field(..., min_length=1, max_length=150, description="岗位名称")
|
||||
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
|
||||
sort: int = Field(..., ge=0, le=99999, description="岗位排序")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 设置状态表单类
|
||||
# ============================================================
|
||||
class PositionStatusForm(BaseModel):
|
||||
"""岗位状态更新表单"""
|
||||
id: int = Field(..., gt=0, description="岗位ID")
|
||||
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类选择
|
||||
|
||||
| 基类 | 用途 | 包含字段 |
|
||||
|:----|:----|:----|
|
||||
| `BaseSchema` | 创建/编辑通用表单 | 自带可选 `id` 字段(`Optional[int]`),编辑时传入 |
|
||||
| `BaseModel` | 特定操作表单 | 无额外字段,按需定义 |
|
||||
|
||||
创建和编辑共用 `PositionForm`:创建时不传 `id`,编辑时传入 `id`。`BaseSchema` 的 `id` 字段定义为 `Optional[int] = Field(None, gt=0)`,创建时可省略。
|
||||
|
||||
### Field 约束
|
||||
|
||||
```python
|
||||
name: str = Field(..., min_length=1, max_length=150, description="岗位名称")
|
||||
```
|
||||
|
||||
| 参数 | 含义 |
|
||||
|:----|:----|
|
||||
| `...` | 必填(Ellipsis) |
|
||||
| `min_length=1` | 最小长度 1,不允许空字符串 |
|
||||
| `max_length=150` | 最大长度 150 |
|
||||
| `description` | Swagger 文档中的字段说明 |
|
||||
|
||||
```python
|
||||
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
|
||||
```
|
||||
|
||||
| 参数 | 含义 |
|
||||
|:----|:----|
|
||||
| `ge=1` | 大于等于(Greater Equal) |
|
||||
| `le=2` | 小于等于(Less Equal) |
|
||||
|
||||
```python
|
||||
sort: int = Field(..., ge=0, le=99999, description="岗位排序")
|
||||
```
|
||||
|
||||
排序字段允许 0 到 99999 的范围。
|
||||
|
||||
### 状态更新单独建类
|
||||
|
||||
```python
|
||||
# ============================================================
|
||||
# 设置状态表单类
|
||||
# ============================================================
|
||||
class PositionStatusForm(BaseModel):
|
||||
"""岗位状态更新表单"""
|
||||
id: int = Field(..., gt=0, description="岗位ID")
|
||||
status: int = Field(..., ge=1, le=2, description="岗位状态:1-在用 2-停用")
|
||||
```
|
||||
|
||||
状态更新接口只需要 `id` 和 `status` 两个字段,与创建/编辑表单分开定义,接口语义更清晰。
|
||||
|
||||
## 如何在 Service 中使用
|
||||
|
||||
Schema 在 Service 层通过 `create_schema` / `update_schema` / `status_schema` 声明,`BaseService` 的 `add()` / `update()` / `update_status()` 方法自动执行校验:
|
||||
|
||||
```python
|
||||
class PositionService(BaseService):
|
||||
model = Position
|
||||
repo_cls = PositionRepository
|
||||
create_schema = PositionSchema # 新增/编辑时校验
|
||||
update_schema = PositionSchema # 编辑时校验
|
||||
status_schema = PositionStatusSchema # 状态变更时校验
|
||||
```
|
||||
|
||||
校验由 `BaseService.validate()` 方法内部调用 `schema_cls.model_validate(data)` 完成。校验失败时抛出 `ValidationError`,由 `BaseHandler.write_error` 统一转换为标准错误响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"data": null,
|
||||
"msg": "name: String should have at least 1 character",
|
||||
"ok": false
|
||||
}
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **创建/编辑共用一个表单类**:继承 `BaseSchema`,利用其内置的可选 `id` 字段
|
||||
2. **特殊操作单独建类**:如状态更新、排序调整等,只包含必要的字段
|
||||
3. **合理设置约束**:`min_length` / `max_length` / `ge` / `le` / `gt` 等,让框架自动校验
|
||||
4. **description 建议填写**:用于字段说明,方便团队理解字段含义
|
||||
|
||||
## 总结
|
||||
|
||||
Pydantic Schema 通过 `BaseSchema` 继承公共字段(id),使用 `Field(...)` 声明约束。在 Service 层通过 `create_schema` / `update_schema` / `status_schema` 声明,`BaseService` 自动执行校验。特殊操作单独建类。
|
||||
@@ -0,0 +1,145 @@
|
||||
# Service 层
|
||||
|
||||
Service 层是业务逻辑层,负责实现具体的业务规则。简单 CRUD 模块继承 `BaseService` 基类,只需声明差异点即可获得完整的增删改查能力。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/position/service.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
# +======================================================================
|
||||
# | 模块: 岗位业务服务层
|
||||
# | 说明: 继承 BaseService 获得通用 CRUD,声明差异点 + 覆盖删除前钩子
|
||||
# +======================================================================
|
||||
"""岗位业务服务。"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from common.utils.helpers import parse_id_list
|
||||
from core.base_service import BaseService
|
||||
from modules.position.models import Position
|
||||
from modules.position.repository import PositionRepository
|
||||
from modules.user.models import User
|
||||
from modules.user.repository import UserRepository
|
||||
|
||||
|
||||
class PositionService(BaseService(Position]):
|
||||
"""岗位业务服务类,继承基础服务获得通用 CRUD 能力。"""
|
||||
|
||||
# ============================================================
|
||||
# 数据访问与模型
|
||||
# ============================================================
|
||||
repo_cls = PositionRepository
|
||||
model = Position
|
||||
|
||||
# ============================================================
|
||||
# 分页差异点
|
||||
# ============================================================
|
||||
page_like_fields = ('name',)
|
||||
page_eq_fields = ('status',)
|
||||
page_order_by = (('sort', 'asc'),)
|
||||
|
||||
# ============================================================
|
||||
# 唯一性校验
|
||||
# ============================================================
|
||||
unique_fields = {'name': '岗位名称不能重复'}
|
||||
|
||||
# ============================================================
|
||||
# 删除前校验
|
||||
# ============================================================
|
||||
def _before_delete(self, ids) -> Optional[str]:
|
||||
"""删除前校验:存在用户引用该岗位时禁止删除。"""
|
||||
id_list = parse_id_list(str(ids))
|
||||
if id_list:
|
||||
user_repo = UserRepository()
|
||||
if user_repo.filter(User.position_id.in_(id_list), User.is_delete == 0).first():
|
||||
return "存在用户引用该岗位,请先调整用户岗位"
|
||||
return None
|
||||
|
||||
|
||||
# 模块级单例
|
||||
position_service = PositionService()
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 必须声明的差异点
|
||||
|
||||
```python
|
||||
repo_cls = PositionRepository # 关联的 Repository 类(不是实例)
|
||||
model = Position # 关联的模型类
|
||||
```
|
||||
|
||||
:::tip 注意
|
||||
`repo_cls` 是 **类属性**,值为 Repository **类**(不是实例)。`BaseService.__init__` 会自动实例化它。
|
||||
:::
|
||||
|
||||
### 分页查询配置
|
||||
|
||||
```python
|
||||
page_like_fields = ('name',) # 模糊查询字段(LIKE)
|
||||
page_eq_fields = ('status',) # 精确查询字段(=)
|
||||
page_order_by = (('sort', 'asc'),) # 排序规则
|
||||
```
|
||||
|
||||
当调用 `get_page(handler)` 时,基类自动:
|
||||
1. 从 `handler` 的查询参数读取 `pageNo`、`pageSize`
|
||||
2. 对 `page_like_fields` 中的字段执行 `LIKE %keyword%` 查询
|
||||
3. 对 `page_eq_fields` 中的字段执行精确匹配
|
||||
4. 按 `page_order_by` 排序
|
||||
5. 返回分页数据
|
||||
|
||||
### 唯一性校验
|
||||
|
||||
```python
|
||||
unique_fields = {'name': '岗位名称不能重复'}
|
||||
```
|
||||
|
||||
字典的 key 是模型字段名,value 是校验失败时的错误提示。在 `add()` 和 `update()` 时,基类自动查询数据库判断是否重复。
|
||||
|
||||
### 生命周期钩子
|
||||
|
||||
```python
|
||||
def _before_delete(self, ids) -> Optional[str]:
|
||||
```
|
||||
|
||||
`BaseService` 提供以下钩子方法,子类可按需覆盖:
|
||||
|
||||
| 钩子 | 触发时机 | 返回值 |
|
||||
|:----|:----|:----|
|
||||
| `_before_delete(ids)` | 删除操作前 | 返回错误字符串则中止删除,返回 None 则继续 |
|
||||
| `_before_add(handler, data)` | 新增操作前 | 可修改 data(原地修改) |
|
||||
| `_before_update(handler, data)` | 更新操作前 | 可修改 data(原地修改) |
|
||||
| `_serialize(item)` | 列表序列化时 | 自定义返回字段 |
|
||||
| `_serialize_detail(obj)` | 详情序列化时 | 自定义返回字段 |
|
||||
|
||||
## 基类提供的通用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|:----|:----|
|
||||
| `get_page(handler)` | 分页查询,返回 R 对象 |
|
||||
| `get_detail(obj_id)` | 查询详情,返回序列化字典或 None |
|
||||
| `add(handler)` | 新增记录,返回 R 对象 |
|
||||
| `update(handler)` | 更新记录,返回 R 对象 |
|
||||
| `delete(handler, ids)` | 单条/批量删除(软删除),返回 R 对象 |
|
||||
| `batch_delete(handler)` | 批量删除(请求体为 ID 数组),返回 R 对象 |
|
||||
| `update_status(handler)` | 更新状态,返回 R 对象 |
|
||||
| `get_options(handler)` | 下拉列表查询,返回 R 对象 |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **必须声明 `repo_cls` 和 `model`**:基类所有方法依赖这两个属性
|
||||
2. **`repo_cls` 是类而非实例**:传入 Repository 类,基类自动实例化
|
||||
3. **分页配置按需声明**:`page_like_fields` / `page_eq_fields` / `page_order_by`
|
||||
4. **唯一性校验通过 `unique_fields` 配置**:基类自动处理,无需手写查询
|
||||
5. **删除前校验用 `_before_delete` 钩子**:返回错误字符串中止操作
|
||||
6. **必须创建模块级单例**:`xxx_service = XxxService()`
|
||||
7. **所有方法接收 `handler`(Tornado Handler 实例)**:非 request 对象
|
||||
|
||||
## 总结
|
||||
|
||||
Service 层继承 `BaseService` 获得通用 CRUD 能力,只需声明差异点(`repo_cls`/`model`/`page_like_fields`/`unique_fields`/`serialize_maps`)并覆写钩子方法。必须创建模块级单例,所有方法接收 Tornado Handler 实例。
|
||||
Reference in New Issue
Block a user