签入版本
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
# 路由注册(Flask Blueprint)
|
||||
|
||||
Flask 使用 `Blueprint` 机制实现模块化路由注册。每个业务模块定义自己的 Blueprint,然后在路由注册中心统一挂载到应用。
|
||||
|
||||
## Blueprint 概念
|
||||
|
||||
`Blueprint` 是 Flask 提供的模块化路由组织方式。它允许将路由、视图函数、模板等组织在一个模块中,然后在应用中注册使用。
|
||||
|
||||
## 模块 Blueprint 定义
|
||||
|
||||
每个业务模块在 `router.py` 文件中定义自己的 Blueprint:
|
||||
|
||||
```python
|
||||
# src/modules/system/position/router.py
|
||||
|
||||
from flask import Blueprint, request
|
||||
from src.core.permission import check_permission
|
||||
from src.core.response import R
|
||||
from src.common.utils.request import parse_request_body, parse_id_list
|
||||
|
||||
# 定义 Blueprint
|
||||
# 参数说明:
|
||||
# - 'position': Blueprint 名称,用于内部标识
|
||||
# - __name__: 模块名称,用于确定静态文件等资源路径
|
||||
# - url_prefix='/position': 路由前缀,所有路由都会添加此前缀
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
|
||||
|
||||
@position_blue.route('/page', methods=['GET'])
|
||||
@check_permission('sys:position:page')
|
||||
def page():
|
||||
"""分页查询岗位列表"""
|
||||
from src.modules.system.position.service import get_position_page
|
||||
return get_position_page()
|
||||
|
||||
|
||||
@position_blue.route('/detail/<int:id>', methods=['GET'])
|
||||
@check_permission('sys:position:detail')
|
||||
def detail(id):
|
||||
"""查询岗位详情"""
|
||||
from src.modules.system.position.service import get_position_detail
|
||||
return get_position_detail(id)
|
||||
|
||||
|
||||
@position_blue.route('/add', methods=['POST'])
|
||||
@check_permission('sys:position:add')
|
||||
def add():
|
||||
"""新增岗位"""
|
||||
from src.modules.system.position.service import add_position
|
||||
from src.modules.system.position.schema import PositionForm
|
||||
data = parse_request_body(request, PositionForm)
|
||||
return add_position(data)
|
||||
|
||||
|
||||
@position_blue.route('/update', methods=['PUT'])
|
||||
@check_permission('sys:position:update')
|
||||
def update():
|
||||
"""更新岗位"""
|
||||
from src.modules.system.position.service import update_position
|
||||
from src.modules.system.position.schema import PositionForm
|
||||
data = parse_request_body(request, PositionForm)
|
||||
return update_position(data)
|
||||
|
||||
|
||||
@position_blue.route('/delete/<int:id>', methods=['DELETE'])
|
||||
@check_permission('sys:position:delete')
|
||||
def delete(id):
|
||||
"""删除岗位"""
|
||||
from src.modules.system.position.service import delete_position
|
||||
return delete_position(id)
|
||||
|
||||
|
||||
@position_blue.route('/status', methods=['PUT'])
|
||||
@check_permission('sys:position:status')
|
||||
def status():
|
||||
"""更新岗位状态"""
|
||||
from src.modules.system.position.service import update_position_status
|
||||
from src.modules.system.position.schema import PositionStatusForm
|
||||
data = parse_request_body(request, PositionStatusForm)
|
||||
return update_position_status(data)
|
||||
```
|
||||
|
||||
## 路由注册中心
|
||||
|
||||
所有模块的 Blueprint 在 `src/routes/v1.py` 中统一注册:
|
||||
|
||||
```python
|
||||
# src/routes/v1.py
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from src.modules.system.position.router import position_blue
|
||||
from src.modules.system.level.router import level_blue
|
||||
from src.modules.system.dept.router import dept_blue
|
||||
from src.modules.system.role.router import role_blue
|
||||
from src.modules.system.menu.router import menu_blue
|
||||
from src.modules.system.user.router import user_blue
|
||||
# ... 其他模块导入
|
||||
|
||||
# 全部业务蓝图(按领域分组排序)
|
||||
BLUEPRINTS = [
|
||||
# 系统管理模块
|
||||
user_blue, role_blue, menu_blue, dept_blue, position_blue, level_blue,
|
||||
# 内容管理模块
|
||||
# article_blue, category_blue, ...
|
||||
# 监控管理模块
|
||||
# job_blue, job_log_blue, ...
|
||||
# ... 其他模块
|
||||
]
|
||||
|
||||
|
||||
def register_v1(app):
|
||||
"""
|
||||
注册 API v1 路由(所有业务蓝图挂载到 /api/v1)。
|
||||
|
||||
Args:
|
||||
app: Flask 应用实例
|
||||
"""
|
||||
# 创建 v1 父 Blueprint
|
||||
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
|
||||
|
||||
# 注册所有子 Blueprint
|
||||
for blue in BLUEPRINTS:
|
||||
v1.register_blueprint(blue)
|
||||
|
||||
# 将 v1 注册到应用
|
||||
app.register_blueprint(v1)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
## 路由注册流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Flask 应用 (app) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ v1 Blueprint (prefix: /api/v1) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ position_blue │ │ level_blue │ │ dept_blue │
|
||||
│ /position │ │ /level │ │ /dept │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
/api/v1/position/* /api/v1/level/* /api/v1/dept/*
|
||||
```
|
||||
|
||||
## Blueprint 参数说明
|
||||
|
||||
```python
|
||||
position_blue = Blueprint(
|
||||
'position', # Blueprint 名称
|
||||
__name__, # 模块名称
|
||||
url_prefix='/position', # 路由前缀
|
||||
# template_folder='templates', # 模板文件夹(可选)
|
||||
# static_folder='static', # 静态文件夹(可选)
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `name` | Blueprint 名称,用于内部标识,必须唯一 |
|
||||
| `import_name` | 模块名称,通常使用 `__name__` |
|
||||
| `url_prefix` | 路由前缀,所有路由都会添加此前缀 |
|
||||
| `template_folder` | 模板文件夹路径(相对于 Blueprint) |
|
||||
| `static_folder` | 静态文件夹路径(相对于 Blueprint) |
|
||||
|
||||
## 路由装饰器
|
||||
|
||||
Flask 使用 `@blueprint.route()` 装饰器定义路由:
|
||||
|
||||
```python
|
||||
@position_blue.route('/page', methods=['GET'])
|
||||
def page():
|
||||
"""分页查询岗位列表"""
|
||||
pass
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|:----|:----|
|
||||
| `rule` | 路由规则,如 `/page`、`/detail/<int:id>` |
|
||||
| `methods` | HTTP 方法列表,如 `['GET']`、`['POST']` |
|
||||
|
||||
### 动态路由参数
|
||||
|
||||
```python
|
||||
@position_blue.route('/detail/<int:id>', methods=['GET'])
|
||||
def detail(id):
|
||||
"""查询岗位详情"""
|
||||
pass
|
||||
```
|
||||
|
||||
| 转换器 | 说明 | 示例 |
|
||||
|:----|:----|:----|
|
||||
| `<int:id>` | 整数参数 | `/detail/123` |
|
||||
| `<string:name>` | 字符串参数(默认) | `/detail/abc` |
|
||||
| `<float:price>` | 浮点数参数 | `/price/12.5` |
|
||||
| `<uuid:id>` | UUID 参数 | `/detail/550e8400-e29b-41d4-a716-446655440000` |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **每个模块定义自己的 Blueprint**:在 `router.py` 文件中定义
|
||||
2. **统一在 `v1.py` 中注册**:将所有 Blueprint 添加到 `BLUEPRINTS` 列表
|
||||
3. **使用 `check_permission` 装饰器**:实现 RBAC 权限控制
|
||||
4. **路由前缀与模块目录名保持一致**:便于代码导航
|
||||
5. **使用中文注释**:方便理解路由功能
|
||||
|
||||
## 总结
|
||||
|
||||
Flask Blueprint 是模块化路由组织的核心机制。每个业务模块定义自己的 Blueprint,在 `src/routes/v1.py` 中统一注册到 `v1` 父 Blueprint,最终挂载到 Flask 应用。通过 `url_prefix` 设置路由前缀,使用 `@blueprint.route()` 装饰器定义路由,结合 `check_permission` 实现 RBAC 权限控制。
|
||||
@@ -0,0 +1,382 @@
|
||||
# 完整代码清单
|
||||
|
||||
本章汇总 Position 岗位模块涉及的全部文件,便于开发时快速查阅和复制。
|
||||
|
||||
## 后端文件清单
|
||||
|
||||
### 1. 数据模型 `src/modules/system/position/models.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位模型模块
|
||||
=============================================================
|
||||
|
||||
岗位模型定义,包含名称、排序、状态等属性。
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Integer, text
|
||||
|
||||
from src.common.base_db import BaseDB
|
||||
from src.common.base_model import BaseModel
|
||||
from config.database import DB_PREFIX
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位模型
|
||||
# ============================================================
|
||||
class Position(BaseModel, BaseDB):
|
||||
"""岗位模型类"""
|
||||
# ============================================================
|
||||
# 表名配置
|
||||
# ============================================================
|
||||
__tablename__ = DB_PREFIX + "position"
|
||||
__table_comment__ = "岗位表"
|
||||
|
||||
# ============================================================
|
||||
# 字段定义
|
||||
# ============================================================
|
||||
# 岗位名称
|
||||
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
|
||||
# 岗位状态:1-在用 2-停用
|
||||
status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用")
|
||||
# 岗位排序
|
||||
sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序")
|
||||
|
||||
# ============================================================
|
||||
# 内置方法
|
||||
# ============================================================
|
||||
def __repr__(self):
|
||||
"""返回岗位名称作为字符串表示"""
|
||||
return '岗位:{}'.format(self.name)
|
||||
```
|
||||
|
||||
### 2. 表单验证 `src/modules/system/position/schema.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位表单验证模块
|
||||
=============================================================
|
||||
|
||||
提供岗位相关的表单验证功能,包括:
|
||||
- 岗位添加/更新表单验证
|
||||
- 岗位状态设置表单验证
|
||||
"""
|
||||
|
||||
from wtforms import ValidationError, StringField, IntegerField
|
||||
from wtforms.validators import DataRequired, Length, NumberRange, Optional
|
||||
|
||||
from src.common.forms.base_form import BaseForm
|
||||
from src.modules.system.position.repository import PositionRepository
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位表单验证(添加和更新共用)
|
||||
# ============================================================
|
||||
|
||||
class PositionForm(BaseForm):
|
||||
"""
|
||||
岗位表单验证类
|
||||
|
||||
Attributes:
|
||||
id: 岗位ID(更新时必填,添加时可选)
|
||||
name: 岗位名称(必填,最长150字符,不可重复)
|
||||
status: 岗位状态(必填,1-在用,2-停用)
|
||||
sort: 岗位排序(可选,0-99999之间的整数)
|
||||
"""
|
||||
id = IntegerField(
|
||||
label='岗位ID',
|
||||
validators=[Optional(), NumberRange(min=1, message='岗位ID必须大于0')],
|
||||
)
|
||||
name = StringField(
|
||||
label='岗位名称',
|
||||
validators=[
|
||||
DataRequired(message='岗位名称不能为空'),
|
||||
Length(max=150, message='岗位名称长度不得超过150个字符')
|
||||
],
|
||||
)
|
||||
status = IntegerField(
|
||||
label='岗位状态',
|
||||
validators=[
|
||||
DataRequired(message='岗位状态不能为空'),
|
||||
NumberRange(min=1, max=2, message='岗位状态值在1~2之间')
|
||||
],
|
||||
)
|
||||
sort = IntegerField(
|
||||
label='岗位排序',
|
||||
validators=[Optional(), NumberRange(min=0, max=99999, message='岗位排序值在0~99999之间')],
|
||||
)
|
||||
|
||||
def validate_name(self, field):
|
||||
"""岗位名称唯一性校验"""
|
||||
if not field.data:
|
||||
return
|
||||
current_id = self.id.data if self.id.data else None
|
||||
if PositionRepository.exists_by({'name': field.data}, exclude_id=current_id):
|
||||
raise ValidationError("岗位名称已存在")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 状态设置表单
|
||||
# ============================================================
|
||||
|
||||
class PositionStatusForm(BaseForm):
|
||||
"""岗位状态更新表单"""
|
||||
id = IntegerField(
|
||||
label='岗位ID',
|
||||
validators=[DataRequired(message='岗位ID不能为空'), NumberRange(min=1, message='岗位ID必须大于0')],
|
||||
)
|
||||
status = IntegerField(
|
||||
label='岗位状态',
|
||||
validators=[
|
||||
DataRequired(message='岗位状态不能为空'),
|
||||
NumberRange(min=1, max=2, message='岗位状态值在1~2之间')
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 数据访问层 `src/modules/system/position/repository.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位仓储模块
|
||||
=============================================================
|
||||
|
||||
岗位数据访问层,继承 BaseRepository 获得通用 CRUD 能力。
|
||||
"""
|
||||
|
||||
from src.common.base_repository import BaseRepository
|
||||
from src.common.constants.common import PAGE_SIZE
|
||||
from src.modules.system.position.models import Position
|
||||
|
||||
|
||||
class PositionRepository(BaseRepository):
|
||||
"""岗位数据仓储,提供岗位相关的数据库访问操作"""
|
||||
|
||||
model = Position
|
||||
|
||||
@classmethod
|
||||
def page(cls, params: dict):
|
||||
"""
|
||||
岗位分页查询
|
||||
|
||||
Args:
|
||||
params: 查询参数字典(pageNo/pageSize/name/status)
|
||||
|
||||
Returns:
|
||||
tuple: (items, total, pages)
|
||||
"""
|
||||
return cls.find_page(
|
||||
page=int(params.get('pageNo', 1)),
|
||||
page_size=int(params.get('pageSize', PAGE_SIZE)),
|
||||
filters={'status': params.get('status')},
|
||||
like_filters={'name': params.get('name')},
|
||||
order_by='sort',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def list_enabled(cls, params=None):
|
||||
"""
|
||||
查询启用的岗位列表
|
||||
|
||||
Args:
|
||||
params: 查询参数字典,可包含 name 字段
|
||||
|
||||
Returns:
|
||||
list: 启用状态的岗位实例列表
|
||||
"""
|
||||
params = params or {}
|
||||
return cls.find_list(
|
||||
filters={'status': 1},
|
||||
like_filters={'name': params.get('name')},
|
||||
order_by='sort',
|
||||
)
|
||||
```
|
||||
|
||||
### 4. 业务逻辑层 `src/modules/system/position/service.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位管理服务模块
|
||||
=============================================================
|
||||
|
||||
该模块实现了岗位管理相关的所有业务逻辑。
|
||||
"""
|
||||
|
||||
from src.common.base_service import BaseService
|
||||
from src.core.exceptions import BizError
|
||||
from src.modules.system.position.schema import PositionForm, PositionStatusForm
|
||||
from src.modules.system.position.repository import PositionRepository
|
||||
from src.modules.system.user.repository import UserRepository
|
||||
|
||||
|
||||
class PositionService(BaseService):
|
||||
"""
|
||||
岗位服务类
|
||||
|
||||
继承 BaseService 获得标准 CRUD 能力,并扩展删除前用户引用校验。
|
||||
"""
|
||||
repository = PositionRepository
|
||||
form = PositionForm
|
||||
status_form = PositionStatusForm
|
||||
model_name = "岗位"
|
||||
|
||||
@classmethod
|
||||
def before_delete(cls, ids):
|
||||
"""
|
||||
删除前校验钩子
|
||||
|
||||
检查岗位是否仍被用户引用,若被引用则拒绝删除。
|
||||
"""
|
||||
for pid in ids:
|
||||
if UserRepository.count_by({'position_id': pid}) > 0:
|
||||
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块级代理函数:保持 router 调用兼容
|
||||
# ============================================================
|
||||
|
||||
get_position_page = PositionService.get_page
|
||||
get_position_detail = PositionService.get_detail
|
||||
get_info = PositionService.get_info
|
||||
add_position = PositionService.add
|
||||
update_position = PositionService.update
|
||||
delete_positions = PositionService.remove
|
||||
update_position_status = PositionService.update_status
|
||||
get_position_list = PositionService.get_list
|
||||
```
|
||||
|
||||
### 5. 路由控制器 `src/modules/system/position/router.py`
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位管理控制器模块
|
||||
=============================================================
|
||||
|
||||
提供岗位(Position)相关的API接口。
|
||||
"""
|
||||
from flask import Blueprint, request
|
||||
|
||||
from src.middleware import check_demo, operation_log
|
||||
from src.core.permission import check_permission
|
||||
from src.modules.system.position import service as position
|
||||
from src.common.utils.request import parse_batch_ids, parse_request_body
|
||||
|
||||
from src.core import response as R
|
||||
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
|
||||
|
||||
@position_blue.route('/page', methods=["GET"])
|
||||
@check_permission("sys:position:page")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def page():
|
||||
"""查询岗位分页数据"""
|
||||
return R.ok(data=position.get_position_page(request.args))
|
||||
|
||||
|
||||
@position_blue.route('/detail/<int:id>', methods=["GET"])
|
||||
@check_permission("sys:position:detail")
|
||||
@operation_log("岗位管理", "详情")
|
||||
def detail(id):
|
||||
"""查询岗位详情"""
|
||||
data = position.get_position_detail(id)
|
||||
return R.ok(data=data)
|
||||
|
||||
|
||||
@position_blue.route('/add', methods=["POST"])
|
||||
@check_permission("sys:position:add")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "新增")
|
||||
def add():
|
||||
"""添加岗位"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.add_position(json_data)
|
||||
return R.ok(msg="添加成功")
|
||||
|
||||
|
||||
@position_blue.route('/update', methods=["PUT"])
|
||||
@check_permission("sys:position:update")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "修改")
|
||||
def update():
|
||||
"""更新岗位"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.update_position(json_data)
|
||||
return R.ok(msg="更新成功")
|
||||
|
||||
|
||||
@position_blue.route('/delete/<string:id>', methods=["DELETE"])
|
||||
@check_permission("sys:position:delete")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "删除")
|
||||
def delete(id):
|
||||
"""删除岗位"""
|
||||
count = position.delete_positions(id)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
|
||||
|
||||
@position_blue.route('/batchDelete', methods=["DELETE"])
|
||||
@check_permission("sys:position:batchDelete")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "批量删除")
|
||||
def batch_delete():
|
||||
"""批量删除岗位"""
|
||||
id_str, err = parse_batch_ids()
|
||||
if err:
|
||||
return err
|
||||
count = position.delete_positions(id_str)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
|
||||
|
||||
@position_blue.route('/status', methods=["PUT"])
|
||||
@check_permission("sys:position:status")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "状态变更")
|
||||
def status():
|
||||
"""设置岗位状态"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.update_position_status(json_data)
|
||||
return R.ok(msg="设置成功")
|
||||
|
||||
|
||||
@position_blue.route('/list', methods=["GET"])
|
||||
@check_permission("sys:position:list")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def get_position_list():
|
||||
"""获取岗位数据列表(无分页)"""
|
||||
result = position.get_position_list(request.args)
|
||||
return R.ok(data=result)
|
||||
```
|
||||
|
||||
### 6. 路由注册 `src/routes/v1.py`
|
||||
|
||||
```python
|
||||
from src.modules.system.position.router import position_blue
|
||||
# ... 其他模块导入
|
||||
|
||||
BLUEPRINTS = [
|
||||
# 系统管理模块
|
||||
user_blue, role_blue, menu_blue, dept_blue, position_blue, level_blue,
|
||||
# ... 其他模块
|
||||
]
|
||||
```
|
||||
|
||||
## API 接口清单
|
||||
|
||||
| 方法 | 路径 | 权限节点 | 说明 |
|
||||
|:---|:---|:---|:---|
|
||||
| GET | `/api/v1/position/page` | `sys:position:page` | 分页查询 |
|
||||
| GET | `/api/v1/position/detail/<id>` | `sys:position:detail` | 详情查询 |
|
||||
| GET | `/api/v1/position/list` | `sys:position:list` | 列表查询(无分页) |
|
||||
| POST | `/api/v1/position/add` | `sys:position:add` | 新增 |
|
||||
| PUT | `/api/v1/position/update` | `sys:position:update` | 更新 |
|
||||
| DELETE | `/api/v1/position/delete/<id>` | `sys:position:delete` | 单条删除 |
|
||||
| DELETE | `/api/v1/position/batchDelete` | `sys:position:batchDelete` | 批量删除 |
|
||||
| PUT | `/api/v1/position/status` | `sys:position:status` | 状态变更 |
|
||||
@@ -0,0 +1,287 @@
|
||||
# Router 层(Flask Blueprint)
|
||||
|
||||
Router 层是 HTTP 接口层,负责定义路由、权限装饰器和请求参数解析。每个业务模块对应一个 router 文件,所有业务逻辑委托给 Service 层处理。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/system/position/router.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
以岗位模块为例,`src/modules/system/position/router.py` 完整内容:
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位管理控制器模块
|
||||
=============================================================
|
||||
|
||||
提供岗位(Position)相关的API接口,包括:
|
||||
- 岗位分页查询
|
||||
- 岗位详情查询
|
||||
- 岗位添加、更新、删除
|
||||
- 岗位状态设置
|
||||
- 岗位数据列表获取
|
||||
"""
|
||||
from flask import Blueprint, request
|
||||
|
||||
from src.middleware import check_demo, operation_log
|
||||
from src.core.permission import check_permission
|
||||
from src.modules.system.position import service as position
|
||||
from src.common.utils.request import parse_batch_ids, parse_request_body
|
||||
|
||||
from src.core import response as R
|
||||
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位分页查询
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/page', methods=["GET"])
|
||||
@check_permission("sys:position:page")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def page():
|
||||
"""
|
||||
查询岗位分页数据
|
||||
|
||||
Returns:
|
||||
JSON: 岗位分页数据,包含 records(数据列表)、total(总记录数)等
|
||||
"""
|
||||
return R.ok(data=position.get_position_page(request.args))
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位详情查询
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/detail/<int:id>', methods=["GET"])
|
||||
@check_permission("sys:position:detail")
|
||||
@operation_log("岗位管理", "详情")
|
||||
def detail(id):
|
||||
"""
|
||||
查询岗位详情
|
||||
|
||||
Args:
|
||||
id: 岗位ID
|
||||
|
||||
Returns:
|
||||
JSON: 岗位详细信息
|
||||
"""
|
||||
data = position.get_position_detail(id)
|
||||
return R.ok(data=data)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位添加
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/add', methods=["POST"])
|
||||
@check_permission("sys:position:add")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "新增")
|
||||
def add():
|
||||
"""
|
||||
添加岗位
|
||||
|
||||
Returns:
|
||||
JSON: 操作结果
|
||||
"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.add_position(json_data)
|
||||
return R.ok(msg="添加成功")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位更新
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/update', methods=["PUT"])
|
||||
@check_permission("sys:position:update")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "修改")
|
||||
def update():
|
||||
"""
|
||||
更新岗位
|
||||
|
||||
Returns:
|
||||
JSON: 操作结果
|
||||
"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.update_position(json_data)
|
||||
return R.ok(msg="更新成功")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位删除
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/delete/<string:id>', methods=["DELETE"])
|
||||
@check_permission("sys:position:delete")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "删除")
|
||||
def delete(id):
|
||||
"""
|
||||
删除岗位
|
||||
|
||||
Args:
|
||||
id: 岗位ID(支持逗号分隔的多个ID)
|
||||
|
||||
Returns:
|
||||
JSON: 操作结果
|
||||
"""
|
||||
count = position.delete_positions(id)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位批量删除
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/batchDelete', methods=["DELETE"])
|
||||
@check_permission("sys:position:batchDelete")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "批量删除")
|
||||
def batch_delete():
|
||||
"""
|
||||
批量删除岗位
|
||||
|
||||
Request Body:
|
||||
[1, 2, 3] # 岗位ID数组
|
||||
|
||||
Returns:
|
||||
JSON: 操作结果
|
||||
"""
|
||||
id_str, err = parse_batch_ids()
|
||||
if err:
|
||||
return err
|
||||
count = position.delete_positions(id_str)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位状态设置
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/status', methods=["PUT"])
|
||||
@check_permission("sys:position:status")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "状态变更")
|
||||
def status():
|
||||
"""
|
||||
设置岗位状态
|
||||
|
||||
Returns:
|
||||
JSON: 操作结果
|
||||
"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.update_position_status(json_data)
|
||||
return R.ok(msg="设置成功")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位列表获取(无分页)
|
||||
# ============================================================
|
||||
|
||||
@position_blue.route('/list', methods=["GET"])
|
||||
@check_permission("sys:position:list")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def get_position_list():
|
||||
"""
|
||||
获取岗位数据列表(无分页)
|
||||
|
||||
用于下拉选择框,支持 name 模糊匹配。
|
||||
|
||||
Returns:
|
||||
JSON: 岗位列表数据
|
||||
"""
|
||||
result = position.get_position_list(request.args)
|
||||
return R.ok(data=result)
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### Blueprint 实例
|
||||
|
||||
```python
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
```
|
||||
|
||||
每个 router 文件创建一个独立的 `Blueprint` 实例,后续在 `src/routes/v1.py` 中统一注册。
|
||||
|
||||
### 装饰器说明
|
||||
|
||||
| 装饰器 | 作用 | 位置 |
|
||||
|:---|:---|:---|
|
||||
| `@position_blue.route()` | 定义 HTTP 方法和路径 | 最外层 |
|
||||
| `@check_permission('sys:position:page')` | RBAC 权限校验 | 路由装饰器之后 |
|
||||
| `@check_demo` | 演示模式拦截(写操作) | 权限装饰器之后 |
|
||||
| `@operation_log("岗位管理", "查询")` | 操作日志记录 | 最内层 |
|
||||
|
||||
**装饰器顺序必须严格遵守**:路由 → 权限 → 演示模式 → 操作日志。
|
||||
|
||||
### 权限字符串命名规范
|
||||
|
||||
```
|
||||
sys:{module}:{action}
|
||||
```
|
||||
|
||||
| 动作 | HTTP 方法 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| `page` | GET | 分页查询 |
|
||||
| `detail` | GET | 详情查询 |
|
||||
| `list` | GET | 列表查询(无分页) |
|
||||
| `add` | POST | 新增 |
|
||||
| `update` | PUT | 更新 |
|
||||
| `delete` | DELETE | 单条删除 |
|
||||
| `batchDelete` | DELETE | 批量删除 |
|
||||
| `status` | PUT | 状态变更 |
|
||||
|
||||
### 参数解析
|
||||
|
||||
```python
|
||||
# 解析请求体 JSON(POST/PUT)
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
|
||||
# 解析批量删除的 ID 数组(DELETE)
|
||||
id_str, err = parse_batch_ids()
|
||||
if err:
|
||||
return err
|
||||
|
||||
# 获取查询参数(GET)
|
||||
params = request.args # 如 request.args.get('name')
|
||||
```
|
||||
|
||||
### 响应封装
|
||||
|
||||
```python
|
||||
# 成功响应
|
||||
R.ok(data=data) # 返回数据
|
||||
R.ok(msg="添加成功") # 返回消息
|
||||
R.ok(data=data, msg="ok") # 同时返回数据和消息
|
||||
|
||||
# 失败响应
|
||||
R.failed(msg="参数错误") # 返回错误消息
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **只需两步注册**:一行导入 Blueprint,一行添加到 `BLUEPRINTS` 列表
|
||||
2. **prefix 与模块目录名保持一致**:便于代码导航
|
||||
3. **所有业务蓝图都挂在 `v1` 路由组下**:自动享受 `login_required` 认证保护
|
||||
4. **装饰器顺序**:路由 → `@check_permission` → `@check_demo` → `@operation_log`
|
||||
5. **Router 层不处理业务逻辑**:只做参数解析和响应封装,业务逻辑委托 Service 层
|
||||
|
||||
## 总结
|
||||
|
||||
Router 层通过 Flask Blueprint 定义路由,使用 `@check_permission` 实现 RBAC 权限控制,`@check_demo` 拦截演示模式写操作,`@operation_log` 自动记录操作日志。参数解析通过 `parse_request_body()` / `parse_batch_ids()` 完成,响应封装通过 `R.ok()` / `R.failed()` 统一格式。
|
||||
@@ -0,0 +1,167 @@
|
||||
# 常见问题与排错
|
||||
|
||||
本章汇总模块开发过程中的常见问题和解决方案。
|
||||
|
||||
## 后端问题
|
||||
|
||||
### Q1: 启动报错 `Table 'xxx.flask_position' doesn't exist`
|
||||
|
||||
**原因**:模型定义后未执行建表。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 方式一:脚本自动建表
|
||||
python scripts/setup_db.py
|
||||
|
||||
# 方式二:Flask-Migrate 迁移
|
||||
flask db migrate -m "add position table"
|
||||
flask db upgrade
|
||||
|
||||
# 方式三:手动建表(参考 model.md 中的 DDL)
|
||||
```
|
||||
|
||||
### Q2: 接口返回 `{"code": 1, "msg": "权限不足"}`
|
||||
|
||||
**原因**:权限节点未配置或未分配给当前用户角色。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认菜单管理中已添加权限节点,权限标识与代码中 `@check_permission` 完全一致
|
||||
2. 确认权限节点已分配给当前用户的角色
|
||||
3. 使用 admin 账号(ID=1)测试,admin 自动跳过权限校验
|
||||
|
||||
### Q3: 唯一性校验不生效
|
||||
|
||||
**原因**:Schema 中 `validate_` 方法的 `exists_by` 调用错误。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认 Schema 类中定义了 `validate_{field_name}` 方法
|
||||
2. 确认 `exists_by({'field': value}, exclude_id=current_id)` 参数正确
|
||||
3. 确认调用的是 `service.add()` 或 `service.update()`,而非直接操作 repo
|
||||
|
||||
### Q4: 分页查询返回空数据
|
||||
|
||||
**原因**:分页参数名不匹配或字段配置错误。
|
||||
|
||||
**排查步骤**:
|
||||
1. 前端传参名应为 `pageNo` 和 `pageSize`(驼峰)
|
||||
2. 确认 Repository 的 `page()` 方法中 `filters` / `like_filters` 字段名与模型一致
|
||||
3. 检查数据库中是否有数据(`is_delete=0` 的记录)
|
||||
|
||||
### Q5: 删除时报错 "存在引用"
|
||||
|
||||
**原因**:`before_delete` 钩子检测到关联数据。
|
||||
|
||||
**解决**:先调整引用该记录的其他数据,再执行删除。如岗位被用户引用时,需先修改用户的岗位字段。
|
||||
|
||||
### Q6: 批量删除报 500 错误
|
||||
|
||||
**原因**:`parse_id_list` 函数未正确导入或使用。
|
||||
|
||||
**解决**:确保正确导入并使用:
|
||||
|
||||
```python
|
||||
from src.common.utils.request import parse_batch_ids
|
||||
|
||||
@position_blue.route('/batchDelete', methods=["DELETE"])
|
||||
@check_permission("sys:position:batchDelete")
|
||||
@check_demo
|
||||
def batch_delete():
|
||||
id_str, err = parse_batch_ids()
|
||||
if err:
|
||||
return err
|
||||
count = position.delete_positions(id_str)
|
||||
return R.ok(msg=f"本次共删除{count}条数据")
|
||||
```
|
||||
|
||||
## 前端问题
|
||||
|
||||
### 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. 确认 Repository 的 `page()` 方法中 `filters` / `like_filters` 配置了对应字段
|
||||
|
||||
### Q9: 编辑弹窗不回填数据
|
||||
|
||||
**原因**:`openModal` 未传入记录数据。
|
||||
|
||||
**解决**:
|
||||
|
||||
```javascript
|
||||
// 正确:第一个参数 true 表示编辑模式,第二个参数是记录数据
|
||||
openModal(true, record);
|
||||
```
|
||||
|
||||
### Q10: 操作按钮不显示
|
||||
|
||||
**原因**:`v-perm` 权限标识与后端不一致。
|
||||
|
||||
**排查步骤**:
|
||||
1. 确认 `v-perm` 中的权限字符串与后端 `@check_permission` 完全一致
|
||||
2. 确认当前用户拥有对应权限
|
||||
3. 检查 `TableAction` 中的 `auth` 字段是否正确
|
||||
|
||||
## 通用问题
|
||||
|
||||
### Q11: 接口返回 401 Unauthorized
|
||||
|
||||
**原因**:JWT Token 过期或未携带。
|
||||
|
||||
**解决**:
|
||||
1. 确认请求头包含 `Authorization: Bearer <token>`
|
||||
2. Token 过期后需调用刷新接口或重新登录
|
||||
3. 前端 `VAxios` 自动处理 Token 注入,检查是否正确引入
|
||||
|
||||
### Q12: 接口返回 403 Forbidden(演示模式)
|
||||
|
||||
**原因**:演示模式下写操作被拦截。
|
||||
|
||||
**解决**:将 `.env` 中的 `FLASK_DEMO` 设为 `False`,或移除端点上的 `@check_demo` 装饰器(仅开发阶段)。
|
||||
|
||||
### Q13: 数据库字段中文乱码
|
||||
|
||||
**原因**:数据库或表的字符集不是 UTF-8。
|
||||
|
||||
**解决**:
|
||||
- MySQL:确保数据库和表使用 `utf8mb4` 字符集
|
||||
- PostgreSQL:建库时指定 `ENCODING 'UTF8'`
|
||||
|
||||
## 开发调试技巧
|
||||
|
||||
### 查看 SQL 日志
|
||||
|
||||
在 `.env` 中开启 SQL 调试:
|
||||
|
||||
```bash
|
||||
DB_DEBUG=True
|
||||
```
|
||||
|
||||
重启后所有 SQL 查询都会输出到控制台。
|
||||
|
||||
### 使用 Postman 调试
|
||||
|
||||
使用 Postman 或 curl 调试接口:
|
||||
1. 先调用登录接口获取 Token
|
||||
2. 在请求头中添加 `Authorization: Bearer <token>`
|
||||
3. 调用业务接口
|
||||
|
||||
### 检查权限配置
|
||||
|
||||
如果不确定权限是否配置正确,可以用 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,126 @@
|
||||
# 数据模型(Model)
|
||||
|
||||
数据模型是模块开发的第一步,用于定义数据库表结构。项目使用 `SQLAlchemy ORM`,模型类通过继承基类自动获得通用字段和方法。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/system/position/models.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from sqlalchemy import Column, String, Integer, text
|
||||
|
||||
from src.common.base_db import BaseDB
|
||||
from src.common.base_model import BaseModel
|
||||
from config.database import DB_PREFIX
|
||||
|
||||
|
||||
class Position(BaseModel, BaseDB):
|
||||
"""岗位模型类"""
|
||||
# 表名配置
|
||||
__tablename__ = DB_PREFIX + "position"
|
||||
__table_comment__ = "岗位表"
|
||||
|
||||
# 业务字段定义
|
||||
# 岗位名称
|
||||
name = Column(String(255), nullable=False, index=True, comment="岗位名称")
|
||||
# 岗位状态:1-在用 2-停用
|
||||
status = Column(Integer, default=0, server_default=text('0'), index=True, comment="岗位状态:1-在用 2-停用")
|
||||
# 岗位排序
|
||||
sort = Column(Integer, default=0, server_default=text('0'), comment="岗位排序")
|
||||
|
||||
def __repr__(self):
|
||||
"""返回岗位名称作为字符串表示"""
|
||||
return '岗位:{}'.format(self.name)
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类继承
|
||||
|
||||
```python
|
||||
class Position(BaseModel, BaseDB):
|
||||
```
|
||||
|
||||
| 基类 | 来源 | 提供能力 |
|
||||
|:----|:----|:----|
|
||||
| `BaseModel` | `src/common/base_model.py` | 通用字段:id、create_user、create_time、update_user、update_time、is_delete |
|
||||
| `BaseDB` | `src/common/base_db.py` | `save()` 持久化方法、`delete()` 删除方法、`to_dict()` 序列化方法 |
|
||||
|
||||
### 表名配置
|
||||
|
||||
```python
|
||||
__tablename__ = DB_PREFIX + "position"
|
||||
```
|
||||
|
||||
`DB_PREFIX` 从 `.env` 读取(默认 `flask_`),最终表名为 `flask_position`。所有业务表都使用此前缀,便于区分框架表与业务表。
|
||||
|
||||
### 字段定义
|
||||
|
||||
```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'), index=True, comment="岗位状态:1-在用 2-停用")
|
||||
```
|
||||
|
||||
`default=0` 是 Python 端默认值,`server_default=text('0')` 是数据库端默认值。两者都设置可确保无论通过 ORM 还是直接 SQL 插入,都能正确赋默认值。
|
||||
|
||||
### 枚举值约定
|
||||
|
||||
项目中状态字段统一使用整数枚举:
|
||||
|
||||
| 值 | 含义 |
|
||||
|:----|:----|
|
||||
| 1 | 启用 / 在用 |
|
||||
| 2 | 停用 / 禁用 |
|
||||
|
||||
软删除字段 `is_delete` 由基类提供:`0` = 正常,`1` = 已删除。
|
||||
|
||||
## 数据表 DDL(参考)
|
||||
|
||||
模型定义对应的 MySQL 建表语句:
|
||||
|
||||
```sql
|
||||
CREATE TABLE `flask_position` (
|
||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '岗位名称',
|
||||
`status` int DEFAULT '0' COMMENT '岗位状态:1-在用 2-停用',
|
||||
`sort` int DEFAULT '0' COMMENT '岗位排序',
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`create_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT (now()) COMMENT '创建时间',
|
||||
`update_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT (now()) COMMENT '更新时间',
|
||||
`is_delete` int DEFAULT '0' COMMENT '删除标识:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_flask_position_name` (`name`) USING BTREE,
|
||||
KEY `idx_flask_position_status` (`status`) USING BTREE,
|
||||
KEY `idx_flask_position_is_delete` (`is_delete`) USING BTREE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='岗位表';
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
实际开发中不需要手写 DDL。模型定义完成后,框架会通过 `Base.metadata.create_all()` 自动建表,或通过 `flask db migrate` 生成迁移脚本。
|
||||
:::
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **业务字段仅定义差异部分**:通用字段(id、时间、软删除等)由基类自动提供
|
||||
2. **表名必须加 `DB_PREFIX` 前缀**:保持命名一致性
|
||||
3. **合理使用索引**:查询频繁的字段(如 name、status)设置 `index=True`
|
||||
4. **同时设置 `default` 和 `server_default`**:确保 ORM 和直 SQL 两种场景都能正确赋默认值
|
||||
|
||||
## 总结
|
||||
|
||||
数据模型继承 `BaseModel`(公共字段)和 `BaseDB`(save/delete 方法),表名必须加 `DB_PREFIX` 前缀。查询频繁的字段设置索引,同时设置 `default` 和 `server_default` 确保 ORM 和直 SQL 两种场景都能正确赋默认值。
|
||||
@@ -0,0 +1,94 @@
|
||||
# 概述与目标
|
||||
|
||||
本章以「岗位管理(Position)」模块为完整案例,手把手演示从零开发一个业务模块的全流程。学完本章后,你将掌握项目中任意模块的开发方式。
|
||||
|
||||
## 你将学到什么
|
||||
|
||||
通过本章的实战演练,你将完整经历以下开发环节:
|
||||
|
||||
| 环节 | 涉及文件 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| 数据模型 | `models.py` | 定义数据库表结构 |
|
||||
| 请求校验 | `schema.py` | WTForms 表单验证 |
|
||||
| 数据访问 | `repository.py` | 封装数据库查询 |
|
||||
| 业务逻辑 | `service.py` | 实现增删改查与业务规则 |
|
||||
| 路由定义 | `router.py` | 定义 HTTP 接口 |
|
||||
| 路由注册 | `v1.py` | 挂载路由到应用 |
|
||||
| 权限配置 | 菜单管理 | 配置权限节点 |
|
||||
| 前端对接 | API + 页面 + 路由 | 前端完整开发 |
|
||||
|
||||
## 案例模块:岗位管理
|
||||
|
||||
选择「岗位管理」作为案例的原因:
|
||||
|
||||
1. **结构典型**:包含完整的 CRUD + 状态管理 + 删除校验,覆盖大多数业务场景
|
||||
2. **复杂度适中**:字段少(name / status / sort),逻辑清晰,适合入门
|
||||
3. **依赖简单**:仅被 User 模块引用,无复杂关联关系
|
||||
4. **代码精简**:后端 5 个文件 + 前端 3 个文件,总计约 500 行代码
|
||||
|
||||
## 模块文件总览
|
||||
|
||||
```
|
||||
后端(src/)
|
||||
├── modules/system/position/
|
||||
│ ├── models.py # 数据模型
|
||||
│ ├── schema.py # 表单验证
|
||||
│ ├── repository.py # 数据访问层
|
||||
│ ├── service.py # 业务逻辑层
|
||||
│ └── router.py # 路由定义层
|
||||
|
||||
前端(ui/src/)
|
||||
├── api/system/
|
||||
│ └── position.ts # API 请求封装
|
||||
├── views/system/
|
||||
│ └── position/
|
||||
│ ├── index.vue # 主页面:搜索 + 表格 + 操作
|
||||
│ ├── edit.vue # 编辑弹窗:新增/编辑表单
|
||||
│ ├── columns.ts # 表格列定义
|
||||
│ └── querySchemas.ts # 搜索表单 Schema
|
||||
└── router/routes/
|
||||
└── system.ts # 路由配置(追加)
|
||||
```
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
岗位表 `flask_position` 结构如下:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| id | int | 主键,自增 |
|
||||
| name | varchar(255) | 岗位名称 |
|
||||
| status | int | 状态:1-在用 2-停用 |
|
||||
| sort | int | 排序号 |
|
||||
| create_user | varchar(255) | 创建人 |
|
||||
| create_time | datetime | 创建时间 |
|
||||
| update_user | varchar(255) | 更新人 |
|
||||
| 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 → 建表,确认数据库中有 flask_position 表
|
||||
2. schema.py → 定义验证规则(无独立验证方式,后续路由调用时自动校验)
|
||||
3. repository.py → 数据访问层(通常只需声明类,无需写代码)
|
||||
4. service.py → 业务逻辑层(声明差异点 + 自定义方法)
|
||||
5. router.py → 路由定义(声明路由 + 权限装饰器)
|
||||
6. v1.py → 注册路由(一行代码)
|
||||
7. 权限配置 → 在菜单管理中添加权限节点
|
||||
8. 前端对接 → API → 页面 → 路由
|
||||
```
|
||||
|
||||
:::tip 温馨提示
|
||||
本章每一步都附有完整代码和详细注释。建议对照项目中 `src/modules/system/position/` 目录下的实际源码一起阅读,加深理解。
|
||||
:::
|
||||
|
||||
## 总结
|
||||
|
||||
模块开发实战以岗位管理(position)为例,完整演示从 Model → Schema → Repository → Service → Router → 路由注册 → 权限配置 → 前端 API → 前端页面的全流程。每一步都附有完整代码和详细注释,建议对照实际源码一起阅读。
|
||||
@@ -0,0 +1,106 @@
|
||||
# 权限节点配置
|
||||
|
||||
后端代码中的 `@check_permission("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/list` |
|
||||
| `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` | 批量删除 | DELETE | `/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 校验) → check_permission(权限校验)
|
||||
├── 用户 ID = 1(admin)→ 跳过权限校验,直接放行
|
||||
└── 其他用户 → 查询角色权限列表 → 匹配权限字符串 → 放行 / 拒绝
|
||||
```
|
||||
|
||||
## SQL 直接插入权限节点
|
||||
|
||||
如果需要通过 SQL 直接插入权限节点(如初始化脚本),参考以下语句:
|
||||
|
||||
```sql
|
||||
-- 假设岗位管理菜单 ID 为 100
|
||||
INSERT INTO flask_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. 权限字符串必须与代码中的 `@check_permission` 完全一致。
|
||||
2. admin 用户(ID=1)自动跳过权限校验:开发阶段可用 admin 测试。
|
||||
3. 权限节点是菜单的子节点:type 为 `1`(权限),不是 `0`(菜单)。
|
||||
4. 查询权限可适当合并:减少配置复杂度。
|
||||
5. 写操作权限建议独立:便于细粒度控制。
|
||||
|
||||
## 总结
|
||||
|
||||
权限节点是菜单的子节点(type=1),命名规范为 `sys:{模块}:{操作}`。后端通过 `@check_permission` 装饰器校验,前端通过 `v-perm` 指令控制按钮显示。查询权限可适当合并,写操作权限建议独立以实现细粒度控制。
|
||||
@@ -0,0 +1,196 @@
|
||||
# Repository 层
|
||||
|
||||
Repository 层是数据访问层,封装所有数据库查询操作。项目通过 `BaseRepository` 基类提供通用 CRUD 能力,子类只需声明 `model` 并按需实现 `page()` 和 `list_enabled()` 方法。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/system/position/repository.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
以岗位模块为例,`src/modules/system/position/repository.py` 完整内容:
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位仓储模块
|
||||
=============================================================
|
||||
|
||||
岗位数据访问层,继承 BaseRepository 获得通用 CRUD 能力。
|
||||
"""
|
||||
|
||||
from src.common.base_repository import BaseRepository
|
||||
from src.common.constants.common import PAGE_SIZE
|
||||
from src.modules.system.position.models import Position
|
||||
|
||||
|
||||
class PositionRepository(BaseRepository):
|
||||
"""岗位数据仓储,提供岗位相关的数据库访问操作"""
|
||||
|
||||
model = Position
|
||||
|
||||
@classmethod
|
||||
def page(cls, params: dict):
|
||||
"""
|
||||
岗位分页查询
|
||||
|
||||
支持按名称模糊搜索、按状态精确过滤,按 sort 字段升序排列。
|
||||
|
||||
Args:
|
||||
params: 查询参数字典
|
||||
- pageNo: 当前页码
|
||||
- pageSize: 每页记录数
|
||||
- name: 岗位名称(模糊匹配)
|
||||
- status: 状态(精确匹配)
|
||||
|
||||
Returns:
|
||||
tuple: (items, total, pages)
|
||||
- items: 当前页数据列表
|
||||
- total: 总记录数
|
||||
- pages: 总页数
|
||||
"""
|
||||
return cls.find_page(
|
||||
page=int(params.get('pageNo', 1)),
|
||||
page_size=int(params.get('pageSize', PAGE_SIZE)),
|
||||
filters={'status': params.get('status')},
|
||||
like_filters={'name': params.get('name')},
|
||||
order_by='sort',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def list_enabled(cls, params=None):
|
||||
"""
|
||||
查询启用的岗位列表
|
||||
|
||||
用于下拉选择场景,仅返回启用状态的岗位(status=1 为硬性条件,不可传参覆盖)。
|
||||
支持与分页一致的条件筛选(name 模糊匹配)。
|
||||
|
||||
Args:
|
||||
params: 查询参数字典,可包含 name 字段用于模糊搜索
|
||||
|
||||
Returns:
|
||||
list: 启用状态的岗位实例列表,按 sort 升序排列
|
||||
"""
|
||||
params = params or {}
|
||||
return cls.find_list(
|
||||
filters={'status': 1},
|
||||
like_filters={'name': params.get('name')},
|
||||
order_by='sort',
|
||||
)
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类继承
|
||||
|
||||
```python
|
||||
class PositionRepository(BaseRepository):
|
||||
model = Position
|
||||
```
|
||||
|
||||
`BaseRepository` 是基类,子类只需声明 `model` 即可获得通用 CRUD 能力。所有方法均为 `@classmethod`,无需创建实例。
|
||||
|
||||
### 必须实现的方法
|
||||
|
||||
| 方法 | 说明 | 何时需要 |
|
||||
|:---|:---|:---|
|
||||
| `page(params)` | 分页查询 | 所有模块(Service 的 `get_page` 依赖此方法) |
|
||||
| `list_enabled(params)` | 无分页列表查询 | 需要下拉选择的模块(Service 的 `get_list` 依赖此方法) |
|
||||
|
||||
`page()` 和 `list_enabled()` 内部调用基类的 `find_page()` 和 `find_list()` 方法,传入筛选条件和排序规则。
|
||||
|
||||
### BaseRepository 提供的通用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|:---|:---|
|
||||
| `get_by_id(obj_id)` | 按主键查询单条未删除记录 |
|
||||
| `get_by_ids(ids)` | 按主键列表批量查询未删除记录 |
|
||||
| `first_by(filters)` | 按等值条件查询单条记录 |
|
||||
| `exists_by(filters, exclude_id)` | 检查是否存在未删除记录(唯一性校验) |
|
||||
| `count_by(filters)` | 按等值条件统计未删除记录数 |
|
||||
| `exists(obj_id)` | 检查未删除记录是否存在 |
|
||||
| `count_children(parent_id)` | 统计子记录数(树形结构删除前校验) |
|
||||
| `find_page(...)` | 统一分页查询(支持等值/模糊/范围过滤) |
|
||||
| `find_list(...)` | 统一不分页查询 |
|
||||
| `create(**kwargs)` | 创建并保存新实例 |
|
||||
| `update_by_id(obj_id, data)` | 按主键更新字段 |
|
||||
| `update_status(obj_id, status)` | 单独更新状态字段 |
|
||||
| `delete_by_id(obj_id)` | 软删除单条记录 |
|
||||
| `batch_delete(ids)` | 批量软删除 |
|
||||
| `batch_restore(ids)` | 批量恢复已删除记录 |
|
||||
|
||||
### 何时需要自定义方法
|
||||
|
||||
大多数模块只需实现 `page()` 和 `list_enabled()`。当需要以下场景时,才在子类中添加自定义方法:
|
||||
|
||||
- **复杂联表查询**:如 User 需要关联 Dept、Role 查询
|
||||
- **特殊聚合查询**:如统计、分组
|
||||
- **非标准查询条件**:如区间查询、模糊 + 精确组合
|
||||
|
||||
示例(UserRepository):
|
||||
|
||||
```python
|
||||
# src/modules/system/user/repository.py
|
||||
class UserRepository(BaseRepository):
|
||||
"""用户数据仓储"""
|
||||
model = User
|
||||
|
||||
@classmethod
|
||||
def page(cls, params: dict):
|
||||
"""用户分页查询"""
|
||||
return cls.find_page(
|
||||
page=int(params.get('pageNo', 1)),
|
||||
page_size=int(params.get('pageSize', PAGE_SIZE)),
|
||||
like_filters={'realname': params.get('realname')},
|
||||
filters={'gender': params.get('gender'), 'status': params.get('status')},
|
||||
order_desc='id',
|
||||
)
|
||||
```
|
||||
|
||||
## Repository 与 Service 的关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Router 层 │
|
||||
│ - 定义路由(Blueprint) │
|
||||
│ - 权限校验(@check_permission) │
|
||||
│ - 参数解析(parse_request_body) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Service 层 │
|
||||
│ - 业务逻辑(BaseService 模板方法 + 钩子) │
|
||||
│ - 表单校验(WTForms) │
|
||||
│ - 存在性校验(Repository.exists_by) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Repository 层 │
|
||||
│ - 数据库查询(classmethod) │
|
||||
│ - CRUD 操作 │
|
||||
│ - 软删除过滤(is_delete == 0) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Model 层 │
|
||||
│ - 表结构定义(BaseModel + BaseDB) │
|
||||
│ - 字段映射 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **所有方法均为 classmethod**:无需创建实例,直接通过类调用
|
||||
2. **必须实现 `page(params)`**:Service 的 `get_page` 依赖此方法
|
||||
3. **按需实现 `list_enabled(params)`**:需要下拉选择的模块实现此方法
|
||||
4. **`find_page` / `find_list` 自动过滤软删除**:且跳过空值查询条件
|
||||
5. **`exists_by(filters, exclude_id)` 用于唯一性校验**:支持排除自身 ID(更新时校验)
|
||||
6. **Repository 层不做响应封装**:返回 SQLAlchemy 查询结果,由 Service 层处理响应
|
||||
|
||||
## 总结
|
||||
|
||||
Repository 层继承 `BaseRepository` 获得通用 CRUD 能力,所有方法均为 `classmethod`。子类只需声明 `model` 并实现 `page()` / `list_enabled()` 方法。`find_page` / `find_list` 自动过滤软删除且跳过空值条件,`exists_by` 用于唯一性校验。Repository 层不做响应封装,由 Service 层处理。
|
||||
@@ -0,0 +1,138 @@
|
||||
# 路由注册
|
||||
|
||||
路由注册是将模块的 Blueprint 挂载到应用的过程。所有模块的路由在 `src/routes/v1.py` 中统一注册。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/routes/v1.py
|
||||
```
|
||||
|
||||
## 注册步骤
|
||||
|
||||
### 第一步:导入 Blueprint
|
||||
|
||||
在 `v1.py` 文件顶部的导入区域添加:
|
||||
|
||||
```python
|
||||
from src.modules.system.position.router import position_blue
|
||||
```
|
||||
|
||||
### 第二步:挂载 Blueprint
|
||||
|
||||
在 `BLUEPRINTS` 列表中添加:
|
||||
|
||||
```python
|
||||
BLUEPRINTS = [
|
||||
# ... 其他 Blueprint
|
||||
position_blue,
|
||||
]
|
||||
```
|
||||
|
||||
### 完整上下文
|
||||
|
||||
```python
|
||||
# v1.py 中的路由注册(简化示例)
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from src.modules.system.position.router import position_blue
|
||||
from src.modules.system.level.router import level_blue
|
||||
from src.modules.system.dept.router import dept_blue
|
||||
# ... 其他模块导入
|
||||
|
||||
# 全部业务蓝图(按领域分组排序)
|
||||
BLUEPRINTS = [
|
||||
position_blue,
|
||||
level_blue,
|
||||
dept_blue,
|
||||
# ... 其他模块注册
|
||||
]
|
||||
|
||||
|
||||
def register_v1(app):
|
||||
"""
|
||||
注册 API v1 路由(所有业务蓝图挂载到 /api/v1)。
|
||||
|
||||
Args:
|
||||
app: Flask 应用实例
|
||||
"""
|
||||
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
|
||||
for blue in BLUEPRINTS:
|
||||
v1.register_blueprint(blue)
|
||||
app.register_blueprint(v1)
|
||||
return app
|
||||
```
|
||||
|
||||
## 最终 API 路径
|
||||
|
||||
注册后,Position 模块的接口路径为:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| GET | `/api/v1/position/page` | 分页查询 |
|
||||
| GET | `/api/v1/position/detail/<id>` | 详情查询 |
|
||||
| GET | `/api/v1/position/list` | 列表查询 |
|
||||
| POST | `/api/v1/position/add` | 新增 |
|
||||
| PUT | `/api/v1/position/update` | 更新 |
|
||||
| DELETE | `/api/v1/position/delete/<id>` | 单条删除 |
|
||||
| DELETE | `/api/v1/position/batchDelete` | 批量删除 |
|
||||
| PUT | `/api/v1/position/status` | 状态变更 |
|
||||
|
||||
## prefix 命名规范
|
||||
|
||||
| 规则 | 示例 |
|
||||
|:----|:----|
|
||||
| 使用小写英文 | `/position` 而非 `/Position` |
|
||||
| 使用单数形式 | `/position` 而非 `/positions` |
|
||||
| 多单词用驼峰或短横线 | `/jobLog` 或 `/job-log` |
|
||||
| 与模块目录名一致 | `modules/system/position/` → `/position` |
|
||||
|
||||
## Blueprint 定义示例
|
||||
|
||||
在模块的 `router.py` 文件中定义 Blueprint:
|
||||
|
||||
```python
|
||||
# src/modules/system/position/router.py
|
||||
from flask import Blueprint, request
|
||||
|
||||
from src.middleware import check_demo, operation_log
|
||||
from src.core.permission import check_permission
|
||||
from src.modules.system.position import service as position
|
||||
from src.common.utils.request import parse_request_body
|
||||
from src.core import response as R
|
||||
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
|
||||
|
||||
@position_blue.route('/page', methods=["GET"])
|
||||
@check_permission("sys:position:page")
|
||||
@operation_log("岗位管理", "查询")
|
||||
def page():
|
||||
"""查询岗位分页数据"""
|
||||
return R.ok(data=position.get_position_page(request.args))
|
||||
|
||||
|
||||
@position_blue.route('/add', methods=["POST"])
|
||||
@check_permission("sys:position:add")
|
||||
@check_demo
|
||||
@operation_log("岗位管理", "新增")
|
||||
def add():
|
||||
"""添加岗位"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.add_position(json_data)
|
||||
return R.ok(msg="添加成功")
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **只需两步**:一行导入,一行添加到 `BLUEPRINTS` 列表
|
||||
2. **prefix 与模块目录名保持一致**:便于代码导航
|
||||
3. **所有业务蓝图都挂在 `v1` 路由组下**:自动享受 `login_required` 认证保护
|
||||
4. **使用 `check_permission` 装饰器**:实现 RBAC 权限控制
|
||||
|
||||
## 总结
|
||||
|
||||
路由注册在 `src/routes/v1.py` 中统一管理,`BLUEPRINTS` 列表包含所有业务模块的 Blueprint。所有业务蓝图挂在 `v1` 路由组下,自动享受 `login_required` 认证保护。使用 `check_permission` 装饰器实现 RBAC 权限控制。
|
||||
@@ -0,0 +1,186 @@
|
||||
# WTForms Schema
|
||||
|
||||
Schema 层负责请求数据的校验。项目使用 `WTForms` 定义表单类,通过 `validators` 约束字段格式,框架在接收请求体时自动执行校验。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/system/position/schema.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
from wtforms import StringField, IntegerField
|
||||
from wtforms.validators import DataRequired, Length, NumberRange, Optional
|
||||
|
||||
from src.common.forms.base_form import BaseForm
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 岗位表单类
|
||||
# ============================================================
|
||||
class PositionForm(BaseForm):
|
||||
"""岗位创建/编辑表单"""
|
||||
name = StringField('岗位名称', validators=[
|
||||
DataRequired(message='岗位名称不能为空'),
|
||||
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
|
||||
])
|
||||
status = IntegerField('岗位状态', validators=[
|
||||
DataRequired(message='岗位状态不能为空'),
|
||||
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
||||
])
|
||||
sort = IntegerField('岗位排序', validators=[
|
||||
DataRequired(message='岗位排序不能为空'),
|
||||
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
|
||||
])
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 设置状态表单类
|
||||
# ============================================================
|
||||
class PositionStatusForm(BaseForm):
|
||||
"""岗位状态更新表单"""
|
||||
id = IntegerField('岗位ID', validators=[
|
||||
DataRequired(message='岗位ID不能为空'),
|
||||
NumberRange(min=1, message='岗位ID必须大于0')
|
||||
])
|
||||
status = IntegerField('岗位状态', validators=[
|
||||
DataRequired(message='岗位状态不能为空'),
|
||||
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
||||
])
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 基类选择
|
||||
|
||||
| 基类 | 用途 | 包含字段 |
|
||||
|:----|:----|:----|
|
||||
| `BaseForm` | 创建/编辑通用表单 | 自带可选 `id` 字段(`Optional`),编辑时传入 |
|
||||
| 特定操作表单 | 无额外字段,按需定义 |
|
||||
|
||||
创建和编辑共用 `PositionForm`:创建时不传 `id`,编辑时传入 `id`。`BaseForm` 的 `id` 字段定义为 `IntegerField('ID', validators=[Optional()])`,创建时可省略。
|
||||
|
||||
### Validator 约束
|
||||
|
||||
```python
|
||||
name = StringField('岗位名称', validators=[
|
||||
DataRequired(message='岗位名称不能为空'),
|
||||
Length(min=1, max=150, message='岗位名称长度必须在1-150个字符之间')
|
||||
])
|
||||
```
|
||||
|
||||
| Validator | 含义 |
|
||||
|:----|:----|
|
||||
| `DataRequired` | 必填字段,不允许空值 |
|
||||
| `Length(min=1, max=150)` | 字符串长度限制 |
|
||||
| `NumberRange(min=1, max=2)` | 数值范围限制 |
|
||||
| `Optional` | 可选字段 |
|
||||
|
||||
```python
|
||||
status = IntegerField('岗位状态', validators=[
|
||||
DataRequired(message='岗位状态不能为空'),
|
||||
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
||||
])
|
||||
```
|
||||
|
||||
| 参数 | 含义 |
|
||||
|:----|:----|
|
||||
| `min=1` | 最小值(大于等于) |
|
||||
| `max=2` | 最大值(小于等于) |
|
||||
| `message` | 校验失败时的错误信息 |
|
||||
|
||||
```python
|
||||
sort = IntegerField('岗位排序', validators=[
|
||||
DataRequired(message='岗位排序不能为空'),
|
||||
NumberRange(min=0, max=99999, message='岗位排序必须在0-99999之间')
|
||||
])
|
||||
```
|
||||
|
||||
排序字段允许 0 到 99999 的范围。
|
||||
|
||||
### 状态更新单独建类
|
||||
|
||||
```python
|
||||
# ============================================================
|
||||
# 设置状态表单类
|
||||
# ============================================================
|
||||
class PositionStatusForm(BaseForm):
|
||||
"""岗位状态更新表单"""
|
||||
id = IntegerField('岗位ID', validators=[
|
||||
DataRequired(message='岗位ID不能为空'),
|
||||
NumberRange(min=1, message='岗位ID必须大于0')
|
||||
])
|
||||
status = IntegerField('岗位状态', validators=[
|
||||
DataRequired(message='岗位状态不能为空'),
|
||||
NumberRange(min=1, max=2, message='岗位状态必须是1或2')
|
||||
])
|
||||
```
|
||||
|
||||
状态更新接口只需要 `id` 和 `status` 两个字段,与创建/编辑表单分开定义,接口语义更清晰。
|
||||
|
||||
## 如何在 Router 中使用
|
||||
|
||||
```python
|
||||
from flask import Blueprint, request
|
||||
from src.common.utils.request import parse_request_body
|
||||
from src.modules.system.position import service as position
|
||||
from src.core import response as R
|
||||
|
||||
position_blue = Blueprint('position', __name__, url_prefix='/position')
|
||||
|
||||
|
||||
@position_blue.route('/add', methods=['POST'])
|
||||
def add():
|
||||
"""新增岗位"""
|
||||
json_data, err = parse_request_body()
|
||||
if err:
|
||||
return R.failed(msg=err)
|
||||
position.add_position(json_data)
|
||||
return R.ok(msg="添加成功")
|
||||
```
|
||||
|
||||
`parse_request_body` 函数自动解析请求体 JSON。校验失败时,Service 层抛出 `BizError`,由异常处理器自动返回标准错误响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"data": null,
|
||||
"msg": "岗位名称不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义校验器
|
||||
|
||||
如果需要自定义校验逻辑,可以在表单类中定义 `validate_` 开头的方法:
|
||||
|
||||
```python
|
||||
class PositionForm(BaseForm):
|
||||
"""岗位创建/编辑表单"""
|
||||
name = StringField('岗位名称', validators=[
|
||||
DataRequired(message='岗位名称不能为空'),
|
||||
Length(max=150, message='岗位名称长度不得超过150个字符')
|
||||
])
|
||||
|
||||
def validate_name(self, field):
|
||||
"""自定义校验:检查岗位名称是否已存在"""
|
||||
from src.modules.system.position.repository import PositionRepository
|
||||
if not field.data:
|
||||
return
|
||||
current_id = self.id.data if self.id.data else None
|
||||
if PositionRepository.exists_by({'name': field.data}, exclude_id=current_id):
|
||||
raise ValidationError('岗位名称已存在')
|
||||
```
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **创建/编辑共用一个表单类**:继承 `BaseForm`,利用其内置的可选 `id` 字段
|
||||
2. **特殊操作单独建类**:如状态更新、排序调整等,只包含必要的字段
|
||||
3. **合理设置 Validator**:`DataRequired` / `Length` / `NumberRange` 等,让框架自动校验
|
||||
4. **message 必填**:用于返回给前端的错误信息,方便调试
|
||||
5. **唯一性校验**:通过自定义 `validate_` 方法实现,使用 Repository 层的 `exists_by(filters, exclude_id)` 方法
|
||||
|
||||
## 总结
|
||||
|
||||
WTForms Schema 通过 `BaseForm` 继承公共字段(id 等),使用 `validators` 声明约束,自定义 `validate_` 方法实现唯一性校验。唯一性校验使用 `Repository.exists_by({'field': value}, exclude_id=current_id)` 方法,自动过滤软删除记录并支持排除自身。特殊操作单独建类,`message` 必填用于返回给前端的错误信息。
|
||||
@@ -0,0 +1,144 @@
|
||||
# Service 层
|
||||
|
||||
Service 层是业务逻辑层,负责实现具体的业务规则。简单 CRUD 模块继承 `BaseService` 基类,只需声明差异点即可获得完整的增删改查能力。
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
src/modules/system/position/service.py
|
||||
```
|
||||
|
||||
## 完整代码
|
||||
|
||||
```python
|
||||
"""
|
||||
岗位管理服务模块
|
||||
=============================================================
|
||||
|
||||
该模块实现了岗位管理相关的所有业务逻辑。
|
||||
"""
|
||||
|
||||
from src.common.base_service import BaseService
|
||||
from src.core.exceptions import BizError
|
||||
from src.modules.system.position.schema import PositionForm, PositionStatusForm
|
||||
from src.modules.system.position.repository import PositionRepository
|
||||
from src.modules.system.user.repository import UserRepository
|
||||
|
||||
|
||||
class PositionService(BaseService):
|
||||
"""
|
||||
岗位服务类
|
||||
|
||||
继承 BaseService 获得标准 CRUD 能力,并扩展删除前用户引用校验。
|
||||
"""
|
||||
repository = PositionRepository
|
||||
form = PositionForm
|
||||
status_form = PositionStatusForm
|
||||
model_name = "岗位"
|
||||
|
||||
@classmethod
|
||||
def before_delete(cls, ids):
|
||||
"""
|
||||
删除前校验钩子
|
||||
|
||||
检查岗位是否仍被用户引用,若被引用则拒绝删除。
|
||||
|
||||
Args:
|
||||
ids: 待删除的岗位ID列表
|
||||
|
||||
Raises:
|
||||
BizError: 岗位被用户引用时抛出
|
||||
"""
|
||||
for pid in ids:
|
||||
if UserRepository.count_by({'position_id': pid}) > 0:
|
||||
raise BizError(f"岗位ID {pid} 已被用户引用,无法删除")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 模块级代理函数:保持 router 调用兼容
|
||||
# ============================================================
|
||||
|
||||
get_position_page = PositionService.get_page
|
||||
get_position_detail = PositionService.get_detail
|
||||
get_info = PositionService.get_info
|
||||
add_position = PositionService.add
|
||||
update_position = PositionService.update
|
||||
delete_positions = PositionService.remove
|
||||
update_position_status = PositionService.update_status
|
||||
get_position_list = PositionService.get_list
|
||||
```
|
||||
|
||||
## 代码解析
|
||||
|
||||
### 必须声明的属性
|
||||
|
||||
```python
|
||||
repository = PositionRepository # 关联的 Repository 类
|
||||
form = PositionForm # 新增/更新表单校验类
|
||||
status_form = PositionStatusForm # 状态更新表单校验类
|
||||
model_name = "岗位" # 业务对象名称,用于错误提示
|
||||
```
|
||||
|
||||
### 表单校验
|
||||
|
||||
`BaseService` 通过 `form` 和 `status_form` 属性自动执行表单校验:
|
||||
|
||||
- `add()` / `update()` 时使用 `form` 校验
|
||||
- `update_status()` 时使用 `status_form` 校验
|
||||
- 校验失败自动抛出 `BizError`
|
||||
|
||||
### 生命周期钩子
|
||||
|
||||
`BaseService` 提供以下钩子方法,子类可按需覆盖:
|
||||
|
||||
| 钩子 | 触发时机 | 说明 |
|
||||
|:----|:----|:----|
|
||||
| `before_add(form)` | 新增操作前 | 可返回额外写入字段 |
|
||||
| `after_add(obj)` | 新增操作后 | 用于缓存清理等 |
|
||||
| `before_update(form, obj)` | 更新操作前 | 可返回额外更新字段 |
|
||||
| `after_update(obj)` | 更新操作后 | 用于缓存清理等 |
|
||||
| `before_delete(ids)` | 删除操作前 | 抛出 BizError 中止删除 |
|
||||
| `after_delete(count)` | 删除操作后 | 记录审计日志等 |
|
||||
| `_enrich_page_records(records)` | 分页富化时 | 追加关联数据 |
|
||||
| `_detail_dict(obj)` | 详情序列化时 | 自定义返回字段 |
|
||||
|
||||
### 模块级代理函数
|
||||
|
||||
```python
|
||||
# 模块级代理函数:保持 router 调用兼容
|
||||
get_position_page = PositionService.get_page
|
||||
get_position_detail = PositionService.get_detail
|
||||
add_position = PositionService.add
|
||||
update_position = PositionService.update
|
||||
delete_positions = PositionService.remove
|
||||
update_position_status = PositionService.update_status
|
||||
get_position_list = PositionService.get_list
|
||||
```
|
||||
|
||||
模块级代理函数将 Service 类方法暴露为模块级函数,供 Router 层调用。Router 层直接导入函数,不依赖 Service 实例。
|
||||
|
||||
## 基类提供的通用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|:----|:----|
|
||||
| `get_page(params)` | 分页查询(params 包含 pageNo/pageSize 及筛选条件) |
|
||||
| `get_detail(id)` | 查询详情(返回字典) |
|
||||
| `get_info(id)` | 查询模型对象(内部使用) |
|
||||
| `add(data)` | 新增记录(含表单校验 + 钩子) |
|
||||
| `update(data)` | 更新记录(含表单校验 + 存在性校验 + 钩子) |
|
||||
| `remove(ids)` | 批量软删除(逗号分隔 ID 字符串) |
|
||||
| `update_status(data)` | 更新状态(含状态表单校验) |
|
||||
| `get_list(params)` | 无分页列表查询(用于下拉选择) |
|
||||
|
||||
## 开发要点
|
||||
|
||||
1. **必须声明 `repository`**:基类所有数据访问方法依赖此属性
|
||||
2. **声明 `form` 和 `status_form`**:用于自动表单校验
|
||||
3. **声明 `model_name`**:用于错误提示信息
|
||||
4. **删除前校验用 `before_delete` 钩子**:抛出 BizError 中止操作
|
||||
5. **必须创建模块级代理函数**:供 Router 层调用
|
||||
6. **Service 层不直接读取 request**:参数通过函数传入(dict 类型)
|
||||
|
||||
## 总结
|
||||
|
||||
Service 层继承 `BaseService` 获得通用 CRUD 能力,只需声明差异点(`repository`/`form`/`model_name`)并覆写钩子方法。必须创建模块级代理函数供 Router 层调用,Service 层不直接读取 request,入参为数据字典,出参为纯数据,业务失败通过 `raise BizError` 表达。
|
||||
Reference in New Issue
Block a user