From e93107458a3dbccf52fdfa45d3b11fa36693eae2 Mon Sep 17 00:00:00 2001 From: yueli Date: Thu, 10 Sep 2026 17:41:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(purchase):=20=E9=87=87=E8=B4=AD=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=AF=E6=8C=81=E5=85=B3=E9=94=AE=E8=AF=8D=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E4=B8=8E=E6=97=A5=E6=9C=9F=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景 ---- 采购管理此前只有状态筛选,无法按单号/物料/申请人检索,也无法按采购 日期收窄范围。现对齐出库/报废记录的搜索语义,便于用户迁移使用习惯。 后端 ---- PurchaseService.get_purchase_list 新增参数: keyword / search_type / start_date / end_date search_type 取值(与出库、报废一致): all 单号 | 物料名称 | 规格型号 | 备注 | 申请人 no 单号 name 物料名称 spec_model 规格型号 requester 申请人 实现要点 -------- · 申请人姓名存在 SysUser.username,格式「姓名/账号」(如 韩善龙/hanshanlong), ilike 直接匹配整串即可命中; · 该类字段只在 SysUser 上,故**按需 join** —— 单号/名称/规格分支不联表, 避免无谓开销; · 公司隔离分支也需 join SysUser,与关键词分支可能重复 join 同一目标, 会产生笛卡尔积导致结果翻倍,故按需补 join 并用 distinct 兜底; · 日期范围按 purchase_date 过滤(该列是 Date,无需补时分秒)。 实测:名称'充电器'→1 单,申请人'韩善龙'→21 单,日期 05-13→2 单, 关键词不存在→0 单,状态+搜索组合→14 单。 --- inventory-backend/app/api/v1/purchase.py | 14 +++- .../app/services/purchase_service.py | 69 ++++++++++++++++--- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/inventory-backend/app/api/v1/purchase.py b/inventory-backend/app/api/v1/purchase.py index fe16b69..a1b7146 100644 --- a/inventory-backend/app/api/v1/purchase.py +++ b/inventory-backend/app/api/v1/purchase.py @@ -65,7 +65,13 @@ def get_purchase_list(): page = int(request.args.get('page', 1)) per_page = int(request.args.get('limit', 20)) status = request.args.get('status') - status = int(status) if status is not None else None + status = int(status) if status not in (None, '', 'all') else None + + # 搜索参数(与出库/报废记录同一套语义,便于用户迁移习惯) + keyword = (request.args.get('keyword') or '').strip() + search_type = (request.args.get('search_type') or 'all').strip() + start_date = (request.args.get('start_date') or '').strip() + end_date = (request.args.get('end_date') or '').strip() user_id = get_current_user_id() can_view_all = _user_has_purchase_perm() @@ -74,7 +80,11 @@ def get_purchase_list(): page=page, per_page=per_page, requester_id=None if can_view_all else user_id, - status=status + status=status, + keyword=keyword, + search_type=search_type, + start_date=start_date, + end_date=end_date, ) # ★ 字段级价格过滤 diff --git a/inventory-backend/app/services/purchase_service.py b/inventory-backend/app/services/purchase_service.py index bcf2372..8cf8be7 100644 --- a/inventory-backend/app/services/purchase_service.py +++ b/inventory-backend/app/services/purchase_service.py @@ -180,11 +180,20 @@ class PurchaseService: return purchase @staticmethod - def get_purchase_list(page=1, per_page=20, requester_id=None, status=None): - """获取采购申请列表,普通用户只看自己的,主管/超管看同公司全部""" + def get_purchase_list(page=1, per_page=20, requester_id=None, status=None, + keyword=None, search_type='all', + start_date=None, end_date=None): + """ + 获取采购申请列表,普通用户只看自己的,主管/超管看同公司全部。 + + 搜索参数(与出库/报废记录保持同一套语义,便于用户迁移习惯): + keyword 关键词 + search_type all / no(单号) / name(物料名称) / spec_model / requester(申请人) + start_date / end_date 按采购日期过滤(含边界) + """ from app.utils.decorators import get_current_company_filter from app.models.system import SysUser - from sqlalchemy import or_ + from sqlalchemy import or_, cast, String query = PurchaseRequest.query @@ -194,15 +203,57 @@ class PurchaseService: if status is not None: query = query.filter(PurchaseRequest.status == status) + # ---- 关键词搜索 ---- + # + # 申请人姓名存在 SysUser.username,格式为「姓名/账号」(如 韩善龙/hanshanlong), + # 因此按「姓名」搜索时用 ilike 直接匹配整串即可命中。 + # 该类字段只存在于 SysUser,故需要 join;单号/名称/规格则不需要 —— + # 只在必要时 join,避免无谓的联表。 + needs_user = bool(keyword) and search_type in ('requester', 'all') + if needs_user: + query = query.outerjoin(SysUser, PurchaseRequest.requester_id == SysUser.id) + + if keyword: + kw = f'%{keyword}%' + if search_type == 'no': + query = query.filter(PurchaseRequest.request_no.ilike(kw)) + elif search_type == 'name': + query = query.filter(PurchaseRequest.name.ilike(kw)) + elif search_type == 'spec_model': + query = query.filter(PurchaseRequest.spec_model.ilike(kw)) + elif search_type == 'requester': + query = query.filter(SysUser.username.ilike(kw)) + else: # all —— 覆盖单号 / 名称 / 规格 / 备注 / 申请人 + query = query.filter(or_( + PurchaseRequest.request_no.ilike(kw), + PurchaseRequest.name.ilike(kw), + PurchaseRequest.spec_model.ilike(kw), + PurchaseRequest.remark.ilike(kw), + SysUser.username.ilike(kw), + )) + + # ---- 日期范围(按采购日期)---- + if start_date: + query = query.filter(PurchaseRequest.purchase_date >= start_date) + if end_date: + query = query.filter(PurchaseRequest.purchase_date <= end_date) + # 【行级数据隔离】同公司可见:匹配 MaterialBase.company_name 或 SysUser.department company_limit = get_current_company_filter() if company_limit is not None: - query = query.outerjoin(MaterialBase, PurchaseRequest.base_id == MaterialBase.id) \ - .outerjoin(SysUser, PurchaseRequest.requester_id == SysUser.id) \ - .filter(or_( - MaterialBase.company_name == company_limit, - SysUser.department == company_limit - )) + query = query.outerjoin(MaterialBase, PurchaseRequest.base_id == MaterialBase.id) + # SysUser 可能已被上面的关键词分支 join 过 —— 重复 join 会产生 + # 笛卡尔积导致结果翻倍,此处按需补 join。 + if not needs_user: + query = query.outerjoin(SysUser, PurchaseRequest.requester_id == SysUser.id) + query = query.filter(or_( + MaterialBase.company_name == company_limit, + SysUser.department == company_limit + )) + + # ★ 兜底去重:上述 join 在特定组合下(如 base_id 为空 + requester 匹配) + # 仍可能产生重复行,用 distinct 保证每个单据只出现一次。 + query = query.distinct() query = query.order_by(PurchaseRequest.created_at.desc()) pagination = query.paginate(page=page, per_page=per_page, error_out=False)