Files
KCGL/inventory-backend/app/api/v1/my_requests.py

189 lines
7.6 KiB
Python
Raw Normal View History

feat(my-requests): 跨模块聚合端点,一个页面看全部申请 动机 ---- 出库/借库/报废三个模块各有一套审批流,申请人此前没有统一入口。 新增只读聚合视图,把三类单据合并返回。 为什么单独建蓝图 ---------------- 权限模型不同:审批端点是「管理视角」,本端点是「申请人视角」。 把两者塞进同一端点(if not privileged: applicant_id = me)会让管理逻辑 与用户逻辑混流,一旦 is_privileged_viewer() 判定出错即越权。 本模块从设计上就没有「看别人」的分支 —— applicant_id 硬编码为当前用户。 只读保证 -------- 本模块只做查询,不修改任何数据。撤回等写操作仍由各模块自己的端点承担 (因为三者释放逻辑不同:出库/借库已接入预占,报废尚未接入)。 把风险锁在只读层,即使聚合逻辑有 bug 也不会破坏业务数据。 字段归一化 ---------- 三个模块的 items_json 存在差异,统一在服务端抹平: · 数量字段:报废用 scrap_qty,出库/借库用 quantity → 统一为 quantity · 库位字段:报废用 location → 统一为 warehouse_location 这专门避免「报废行的数量列显示空白」这类不报错的隐性 bug。 健壮性 ------ 单个模块查询失败时记日志并跳过,其余模块照常返回 (例如某张表尚未迁移时,其它两类仍可用)。 响应中附带 withdraw_endpoint 字段,前端据此分发撤回请求, 无需硬编码三个模块的 URL 映射。 实测 ---- 普通员工(INBOUND 角色,无任何审批权限)访问 → 200,18 单 三类单据齐全,type_label 正确 报废明细数量字段归一化成功(quantity=1.0) B 看不到 A 的单 → 0 条 type=scrap 过滤 → 全部为报废
2026-09-10 15:37:05 +08:00
# inventory-backend/app/api/v1/my_requests.py
"""
「我的申请单」跨模块聚合接口(申请人视角)。
设计动机
--------
出库 / 借库 / 报废三个模块各有一套审批流,申请人此前**没有查看自己单据的
入口** —— 审批页是管理视角(需 xxx_approval 权限),普通员工进不去。
本模块提供一个只读聚合视图:一个页面看全部申请。
为什么单独建一个蓝图,而不是在三个模块各加一个端点
--------------------------------------------------
权限模型不同:审批端点是「管理视角」,本端点是「申请人视角」。
把两者塞进同一个端点(如 `if not privileged: applicant_id = me`)
会让管理逻辑与用户逻辑混流,一旦 is_privileged_viewer() 判定出错即越权。
此处从设计上就没有「看别人」的分支 —— applicant_id 硬编码为当前登录用户。
只读保证
--------
本模块**只做查询**,不修改任何数据。撤回等写操作仍由各模块自己的端点承担
(因为三者的释放逻辑不同:出库/借库已接入预占,报废尚未接入)。
把风险锁在只读层,避免聚合逻辑的 bug 破坏业务数据。
"""
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
import traceback
my_requests_bp = Blueprint('my_requests', __name__)
# 申请类型 → (模型, 展示名, 数量字段名)
#
# 数量字段差异:出库/借库用 quantity,报废用 scrap_qty。
# 统一归一化为 quantity 后返回,前端只认一个字段名,避免出现
# 「报废行的数量列显示空白」这类不报错的隐性 bug。
_MODULE_SPECS = (
('outbound', 'outbound', '出库申请', 'quantity'),
('borrow', 'borrow', '借库申请', 'quantity'),
('scrap', 'scrap_approval', '报废申请', 'scrap_qty'),
)
def _load_models():
"""延迟导入三个模型,避免模块级循环依赖"""
from app.models.outbound import OutboundApproval
from app.models.borrow import BorrowApproval
from app.models.scrap_approval import ScrapApproval
return {
'outbound': OutboundApproval,
'borrow': BorrowApproval,
'scrap': ScrapApproval,
}
def _normalize_items(items, qty_field):
"""
明细归一化:把各模块的字段差异抹平。
· 统一数量字段为 quantity(报废的 scrap_qty → quantity)
· 补齐 location(报废用 location,出库/借库用 warehouse_location)
"""
normalized = []
for it in items or []:
if not isinstance(it, dict):
continue
row = dict(it)
# 数量字段归一
if 'quantity' not in row or row.get('quantity') in (None, ''):
row['quantity'] = row.get(qty_field)
# 库位字段归一
if not row.get('warehouse_location'):
row['warehouse_location'] = row.get('location') or ''
normalized.append(row)
return normalized
@my_requests_bp.route('', methods=['GET'])
@jwt_required()
def get_my_requests():
"""
当前登录用户的全部申请单(出库 + 借库 + 报废)。
Query:
page : 页码,默认 1(跨三类统一分页)
limit : 每页数量,默认 10
status : 可选,按状态过滤(0待审 1已通过 2已驳回 3已完成 4已撤回)
type : 可选,outbound / borrow / scrap,只看某一类
★ 权限:仅 @jwt_required。applicant_id 硬编码为当前登录用户,
不接受任何入参覆盖 —— 普通申请人无需任何审批类权限即可使用,
且从设计上不可能查到他人单据。
"""
try:
identity = get_jwt_identity()
if not identity:
return jsonify({'code': 401, 'msg': '用户未登录'}), 401
me = int(identity)
page = max(int(request.args.get('page', 1)), 1)
limit = min(max(int(request.args.get('limit', 10)), 1), 100)
status = request.args.get('status')
status = int(status) if status not in (None, '', 'all') else None
want_type = (request.args.get('type') or 'all').strip().lower()
models = _load_models()
# ---- 收集三类单据(各自按 applicant_id 过滤)----
collected = []
for type_key, _model_name, type_label, qty_field in _MODULE_SPECS:
if want_type != 'all' and want_type != type_key:
continue
model = models.get(type_key)
if model is None:
continue
try:
q = model.query.filter(model.applicant_id == me)
if status is not None:
q = q.filter(model.status == status)
rows = q.all()
except Exception as e:
# 单模块查询失败不应让整个聚合接口挂掉(例如该表尚未迁移)
import logging
logging.getLogger(__name__).error(
f"[my-requests] {type_key} 查询失败: {type(e).__name__}: {e}"
)
continue
for r in rows:
d = r.to_dict()
collected.append({
'type': type_key,
'type_label': type_label,
'id': d.get('id'),
'request_no': d.get('request_no'),
'applicant_id': d.get('applicant_id'),
'remark': d.get('remark') or '',
'status': d.get('status'),
'reject_reason': d.get('reject_reason') or '',
'created_at': d.get('created_at'),
# 模块特有字段(缺失时为 '',前端按 type 决定是否展示)
'outbound_type': d.get('outbound_type') or '',
'borrower_name': d.get('borrower_name') or '',
'items': _normalize_items(d.get('items'), qty_field),
# 撤回端点路径:前端据此分发到各模块(写操作不在此聚合)
'withdraw_endpoint': _withdraw_path(type_key, d.get('id')),
})
# ---- 统一按创建时间倒序,再跨类分页 ----
collected.sort(key=lambda x: x.get('created_at') or '', reverse=True)
total = len(collected)
start = (page - 1) * limit
paged = collected[start:start + limit]
return jsonify({
'code': 200, 'msg': 'success',
'data': {
'items': paged,
'total': total,
'page': page,
'pageSize': limit,
}
}), 200
except Exception as e:
traceback.print_exc()
return jsonify({'code': 500, 'msg': f'获取我的申请单失败: {str(e)}'}), 500
def _withdraw_path(type_key, req_id):
"""
撤回端点路径。撤回是**写操作**,保留在各模块自己的端点里
(三者释放逻辑不同),此处只提供路径供前端分发,不在此执行。
★ 路径不带 /api 前缀:前端 axios 实例的 baseURL 是 '/api',
传完整路径会拼成 /api/api/v1/... 导致 404。
这里返回的是 baseURL 之后的部分(与其它 API 封装的写法一致)。
feat(my-requests): 跨模块聚合端点,一个页面看全部申请 动机 ---- 出库/借库/报废三个模块各有一套审批流,申请人此前没有统一入口。 新增只读聚合视图,把三类单据合并返回。 为什么单独建蓝图 ---------------- 权限模型不同:审批端点是「管理视角」,本端点是「申请人视角」。 把两者塞进同一端点(if not privileged: applicant_id = me)会让管理逻辑 与用户逻辑混流,一旦 is_privileged_viewer() 判定出错即越权。 本模块从设计上就没有「看别人」的分支 —— applicant_id 硬编码为当前用户。 只读保证 -------- 本模块只做查询,不修改任何数据。撤回等写操作仍由各模块自己的端点承担 (因为三者释放逻辑不同:出库/借库已接入预占,报废尚未接入)。 把风险锁在只读层,即使聚合逻辑有 bug 也不会破坏业务数据。 字段归一化 ---------- 三个模块的 items_json 存在差异,统一在服务端抹平: · 数量字段:报废用 scrap_qty,出库/借库用 quantity → 统一为 quantity · 库位字段:报废用 location → 统一为 warehouse_location 这专门避免「报废行的数量列显示空白」这类不报错的隐性 bug。 健壮性 ------ 单个模块查询失败时记日志并跳过,其余模块照常返回 (例如某张表尚未迁移时,其它两类仍可用)。 响应中附带 withdraw_endpoint 字段,前端据此分发撤回请求, 无需硬编码三个模块的 URL 映射。 实测 ---- 普通员工(INBOUND 角色,无任何审批权限)访问 → 200,18 单 三类单据齐全,type_label 正确 报废明细数量字段归一化成功(quantity=1.0) B 看不到 A 的单 → 0 条 type=scrap 过滤 → 全部为报废
2026-09-10 15:37:05 +08:00
"""
if req_id is None:
return ''
if type_key == 'outbound':
return f"/v1/outbound/request/{req_id}/withdraw"
feat(my-requests): 跨模块聚合端点,一个页面看全部申请 动机 ---- 出库/借库/报废三个模块各有一套审批流,申请人此前没有统一入口。 新增只读聚合视图,把三类单据合并返回。 为什么单独建蓝图 ---------------- 权限模型不同:审批端点是「管理视角」,本端点是「申请人视角」。 把两者塞进同一端点(if not privileged: applicant_id = me)会让管理逻辑 与用户逻辑混流,一旦 is_privileged_viewer() 判定出错即越权。 本模块从设计上就没有「看别人」的分支 —— applicant_id 硬编码为当前用户。 只读保证 -------- 本模块只做查询,不修改任何数据。撤回等写操作仍由各模块自己的端点承担 (因为三者释放逻辑不同:出库/借库已接入预占,报废尚未接入)。 把风险锁在只读层,即使聚合逻辑有 bug 也不会破坏业务数据。 字段归一化 ---------- 三个模块的 items_json 存在差异,统一在服务端抹平: · 数量字段:报废用 scrap_qty,出库/借库用 quantity → 统一为 quantity · 库位字段:报废用 location → 统一为 warehouse_location 这专门避免「报废行的数量列显示空白」这类不报错的隐性 bug。 健壮性 ------ 单个模块查询失败时记日志并跳过,其余模块照常返回 (例如某张表尚未迁移时,其它两类仍可用)。 响应中附带 withdraw_endpoint 字段,前端据此分发撤回请求, 无需硬编码三个模块的 URL 映射。 实测 ---- 普通员工(INBOUND 角色,无任何审批权限)访问 → 200,18 单 三类单据齐全,type_label 正确 报废明细数量字段归一化成功(quantity=1.0) B 看不到 A 的单 → 0 条 type=scrap 过滤 → 全部为报废
2026-09-10 15:37:05 +08:00
if type_key == 'borrow':
# 申请人路径:仅校验单据归属,普通员工无需 op_borrow_approval 权限
return f"/v1/transactions/borrow/request/{req_id}/withdraw"
feat(my-requests): 跨模块聚合端点,一个页面看全部申请 动机 ---- 出库/借库/报废三个模块各有一套审批流,申请人此前没有统一入口。 新增只读聚合视图,把三类单据合并返回。 为什么单独建蓝图 ---------------- 权限模型不同:审批端点是「管理视角」,本端点是「申请人视角」。 把两者塞进同一端点(if not privileged: applicant_id = me)会让管理逻辑 与用户逻辑混流,一旦 is_privileged_viewer() 判定出错即越权。 本模块从设计上就没有「看别人」的分支 —— applicant_id 硬编码为当前用户。 只读保证 -------- 本模块只做查询,不修改任何数据。撤回等写操作仍由各模块自己的端点承担 (因为三者释放逻辑不同:出库/借库已接入预占,报废尚未接入)。 把风险锁在只读层,即使聚合逻辑有 bug 也不会破坏业务数据。 字段归一化 ---------- 三个模块的 items_json 存在差异,统一在服务端抹平: · 数量字段:报废用 scrap_qty,出库/借库用 quantity → 统一为 quantity · 库位字段:报废用 location → 统一为 warehouse_location 这专门避免「报废行的数量列显示空白」这类不报错的隐性 bug。 健壮性 ------ 单个模块查询失败时记日志并跳过,其余模块照常返回 (例如某张表尚未迁移时,其它两类仍可用)。 响应中附带 withdraw_endpoint 字段,前端据此分发撤回请求, 无需硬编码三个模块的 URL 映射。 实测 ---- 普通员工(INBOUND 角色,无任何审批权限)访问 → 200,18 单 三类单据齐全,type_label 正确 报废明细数量字段归一化成功(quantity=1.0) B 看不到 A 的单 → 0 条 type=scrap 过滤 → 全部为报废
2026-09-10 15:37:05 +08:00
if type_key == 'scrap':
return f"/v1/scrap/request/{req_id}/withdraw"
feat(my-requests): 跨模块聚合端点,一个页面看全部申请 动机 ---- 出库/借库/报废三个模块各有一套审批流,申请人此前没有统一入口。 新增只读聚合视图,把三类单据合并返回。 为什么单独建蓝图 ---------------- 权限模型不同:审批端点是「管理视角」,本端点是「申请人视角」。 把两者塞进同一端点(if not privileged: applicant_id = me)会让管理逻辑 与用户逻辑混流,一旦 is_privileged_viewer() 判定出错即越权。 本模块从设计上就没有「看别人」的分支 —— applicant_id 硬编码为当前用户。 只读保证 -------- 本模块只做查询,不修改任何数据。撤回等写操作仍由各模块自己的端点承担 (因为三者释放逻辑不同:出库/借库已接入预占,报废尚未接入)。 把风险锁在只读层,即使聚合逻辑有 bug 也不会破坏业务数据。 字段归一化 ---------- 三个模块的 items_json 存在差异,统一在服务端抹平: · 数量字段:报废用 scrap_qty,出库/借库用 quantity → 统一为 quantity · 库位字段:报废用 location → 统一为 warehouse_location 这专门避免「报废行的数量列显示空白」这类不报错的隐性 bug。 健壮性 ------ 单个模块查询失败时记日志并跳过,其余模块照常返回 (例如某张表尚未迁移时,其它两类仍可用)。 响应中附带 withdraw_endpoint 字段,前端据此分发撤回请求, 无需硬编码三个模块的 URL 映射。 实测 ---- 普通员工(INBOUND 角色,无任何审批权限)访问 → 200,18 单 三类单据齐全,type_label 正确 报废明细数量字段归一化成功(quantity=1.0) B 看不到 A 的单 → 0 条 type=scrap 过滤 → 全部为报废
2026-09-10 15:37:05 +08:00
return ''