Compare commits
2 Commits
3bb3975022
...
6c20233d45
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c20233d45 | |||
| 71e5f075d2 |
@ -214,10 +214,11 @@ def get_outbound_list():
|
|||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
limit = int(request.args.get('limit', 10))
|
limit = int(request.args.get('limit', 10))
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
|
search_type = request.args.get('search_type', 'all')
|
||||||
# 如果前端传了日期范围,可以解析处理,这里暂略
|
# 如果前端传了日期范围,可以解析处理,这里暂略
|
||||||
|
|
||||||
# ★ [修改] 调用分组查询服务
|
# ★ [修改] 调用分组查询服务,支持搜索类型
|
||||||
result = OutboundService.get_grouped_list(page, limit, keyword)
|
result = OutboundService.get_grouped_list(page, limit, keyword, search_type=search_type)
|
||||||
|
|
||||||
# 字段级脱敏
|
# 字段级脱敏
|
||||||
user_permissions = get_current_user_permissions()
|
user_permissions = get_current_user_permissions()
|
||||||
|
|||||||
@ -156,8 +156,9 @@ def get_records():
|
|||||||
status = request.args.get('status', 'all')
|
status = request.args.get('status', 'all')
|
||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
|
search_type = request.args.get('search_type', 'all')
|
||||||
|
|
||||||
res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword)
|
res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword, search_type=search_type)
|
||||||
# 字段级脱敏
|
# 字段级脱敏
|
||||||
user_permissions = get_current_user_permissions()
|
user_permissions = get_current_user_permissions()
|
||||||
if res.get('items'):
|
if res.get('items'):
|
||||||
|
|||||||
@ -27,8 +27,11 @@ def _store_token_to_redis(user_id, token, token_type='access'):
|
|||||||
else:
|
else:
|
||||||
expires_delta = current_app.config.get('JWT_REFRESH_TOKEN_EXPIRES', timedelta(days=7))
|
expires_delta = current_app.config.get('JWT_REFRESH_TOKEN_EXPIRES', timedelta(days=7))
|
||||||
|
|
||||||
# 转换为秒数
|
# 转换为秒数(兼容 timedelta 和 int 类型)
|
||||||
|
if isinstance(expires_delta, timedelta):
|
||||||
expires_seconds = int(expires_delta.total_seconds())
|
expires_seconds = int(expires_delta.total_seconds())
|
||||||
|
else:
|
||||||
|
expires_seconds = int(expires_delta) # 已经是秒数(整数)
|
||||||
|
|
||||||
# 存储到 Redis
|
# 存储到 Redis
|
||||||
key = f"user_token_{user_id}"
|
key = f"user_token_{user_id}"
|
||||||
|
|||||||
@ -192,18 +192,18 @@ class OutboundService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_grouped_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None):
|
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None):
|
||||||
"""
|
"""
|
||||||
查询出库记录(按出库单号分组),包含详细物品信息
|
查询出库记录(按出库单号分组),包含详细物品信息
|
||||||
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
||||||
|
search_type: all, no, name, sku, material_name, spec_model
|
||||||
"""
|
"""
|
||||||
# 1. 构建基础查询
|
# 1. 构建基础查询
|
||||||
# 如果有关键词,需要联表搜索物料名称和规格型号
|
# 如果有关键词,需要联表搜索物料名称和规格型号
|
||||||
if keyword:
|
if keyword:
|
||||||
# 由于 TransOutbound 通过 source_table + stock_id 关联到不同库存表,再关联到 MaterialBase
|
# 根据 search_type 构建不同的搜索条件
|
||||||
# 需要使用 union 或分别查询后合并
|
if search_type == 'all':
|
||||||
# 方案:分别查询三种来源的 matching outbound_no,然后合并
|
# 原有逻辑:or_ 联表全局模糊搜索
|
||||||
|
|
||||||
# 查询 stock_buy 路径匹配的名称/规格
|
# 查询 stock_buy 路径匹配的名称/规格
|
||||||
buy_match = db.session.query(TransOutbound.outbound_no).join(
|
buy_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
StockBuy, and_(
|
StockBuy, and_(
|
||||||
@ -255,13 +255,96 @@ class OutboundService:
|
|||||||
db.session.query(product_match.c.outbound_no)
|
db.session.query(product_match.c.outbound_no)
|
||||||
).subquery()
|
).subquery()
|
||||||
|
|
||||||
# 主搜索条件:单号、领用人、SKU + 物料名称/规格匹配的单号
|
|
||||||
keyword_conditions = or_(
|
keyword_conditions = or_(
|
||||||
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
|
TransOutbound.outbound_no.ilike(f'%{keyword}%'),
|
||||||
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
|
TransOutbound.consumer_name.ilike(f'%{keyword}%'),
|
||||||
TransOutbound.sku.ilike(f'%{keyword}%'),
|
TransOutbound.sku.ilike(f'%{keyword}%'),
|
||||||
TransOutbound.outbound_no.in_(all_matches)
|
TransOutbound.outbound_no.in_(all_matches)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif search_type == 'no':
|
||||||
|
keyword_conditions = TransOutbound.outbound_no.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
elif search_type == 'name':
|
||||||
|
keyword_conditions = TransOutbound.consumer_name.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
elif search_type == 'sku':
|
||||||
|
keyword_conditions = TransOutbound.sku.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
elif search_type == 'material_name':
|
||||||
|
# 联表查询物料名称
|
||||||
|
buy_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockBuy, and_(
|
||||||
|
TransOutbound.stock_id == StockBuy.id,
|
||||||
|
TransOutbound.source_table == 'stock_buy'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.name.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
semi_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockSemi, and_(
|
||||||
|
TransOutbound.stock_id == StockSemi.id,
|
||||||
|
TransOutbound.source_table == 'stock_semi'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.name.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
product_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockProduct, and_(
|
||||||
|
TransOutbound.stock_id == StockProduct.id,
|
||||||
|
TransOutbound.source_table == 'stock_product'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.name.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
all_matches = db.session.query(buy_match.c.outbound_no).union(
|
||||||
|
db.session.query(semi_match.c.outbound_no),
|
||||||
|
db.session.query(product_match.c.outbound_no)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
keyword_conditions = TransOutbound.outbound_no.in_(all_matches)
|
||||||
|
|
||||||
|
elif search_type == 'spec_model':
|
||||||
|
# 联表查询规格型号
|
||||||
|
buy_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockBuy, and_(
|
||||||
|
TransOutbound.stock_id == StockBuy.id,
|
||||||
|
TransOutbound.source_table == 'stock_buy'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.spec_model.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
semi_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockSemi, and_(
|
||||||
|
TransOutbound.stock_id == StockSemi.id,
|
||||||
|
TransOutbound.source_table == 'stock_semi'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.spec_model.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
product_match = db.session.query(TransOutbound.outbound_no).join(
|
||||||
|
StockProduct, and_(
|
||||||
|
TransOutbound.stock_id == StockProduct.id,
|
||||||
|
TransOutbound.source_table == 'stock_product'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.spec_model.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
all_matches = db.session.query(buy_match.c.outbound_no).union(
|
||||||
|
db.session.query(semi_match.c.outbound_no),
|
||||||
|
db.session.query(product_match.c.outbound_no)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
keyword_conditions = TransOutbound.outbound_no.in_(all_matches)
|
||||||
|
|
||||||
|
else:
|
||||||
|
keyword_conditions = None
|
||||||
else:
|
else:
|
||||||
keyword_conditions = None
|
keyword_conditions = None
|
||||||
|
|
||||||
|
|||||||
@ -459,6 +459,27 @@ class PermissionService:
|
|||||||
SysRolePermission.query.filter_by(target_code=menu.code).delete()
|
SysRolePermission.query.filter_by(target_code=menu.code).delete()
|
||||||
db.session.delete(menu)
|
db.session.delete(menu)
|
||||||
|
|
||||||
|
# 第二步:清理重复菜单(同一个 code 存在多条记录,保留 ID 最小的)
|
||||||
|
# 先找出所有可能重复的 code
|
||||||
|
duplicate_check = db.session.query(
|
||||||
|
SysMenu.code,
|
||||||
|
func.count(SysMenu.id).label('cnt')
|
||||||
|
).group_by(SysMenu.code).having(func.count(SysMenu.id) > 1).all()
|
||||||
|
|
||||||
|
for code, cnt in duplicate_check:
|
||||||
|
# 获取该 code 的所有记录,按 id 排序,保留第一条,删除其余
|
||||||
|
duplicates = SysMenu.query.filter_by(code=code).order_by(SysMenu.id).all()
|
||||||
|
# 保留第一条,删除其他
|
||||||
|
for dup in duplicates[1:]:
|
||||||
|
print(f"🗑️ 清理重复菜单: {dup.code} (id={dup.id}, name={dup.name})")
|
||||||
|
SysRolePermission.query.filter_by(target_code=dup.code).delete()
|
||||||
|
SysElement.query.filter_by(menu_code=dup.code).delete()
|
||||||
|
db.session.delete(dup)
|
||||||
|
|
||||||
|
# 第三步:强制重新设置所有子菜单的 parent_id,确保没有遗漏
|
||||||
|
# 先将所有子菜单的 parent_id 设为 None,然后重新设置
|
||||||
|
SysMenu.query.filter(SysMenu.code.in_(child_codes)).update({SysMenu.parent_id: None})
|
||||||
|
|
||||||
# 创建或更新菜单
|
# 创建或更新菜单
|
||||||
menu_map = {} # code -> menu obj
|
menu_map = {} # code -> menu obj
|
||||||
|
|
||||||
|
|||||||
@ -188,14 +188,14 @@ class TransService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_records(page=1, limit=10, status='all', keyword=None):
|
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all'):
|
||||||
q = TransBorrow.query
|
q = TransBorrow.query
|
||||||
|
|
||||||
# 如果有关键词,需要联表搜索物料名称和规格型号
|
# 如果有关键词,需要联表搜索物料名称和规格型号
|
||||||
if keyword:
|
if keyword:
|
||||||
# TransBorrow 通过 source_table + stock_id 关联到不同库存表,再关联到 MaterialBase
|
# 根据 search_type 构建不同的搜索条件
|
||||||
# 需要使用 union 或分别查询后合并
|
if search_type == 'all':
|
||||||
|
# 原有逻辑:or_ 联表全局模糊搜索
|
||||||
# 查询 stock_buy 路径匹配的名称/规格
|
# 查询 stock_buy 路径匹配的名称/规格
|
||||||
buy_match = db.session.query(TransBorrow.id).join(
|
buy_match = db.session.query(TransBorrow.id).join(
|
||||||
StockBuy, and_(
|
StockBuy, and_(
|
||||||
@ -247,13 +247,100 @@ class TransService:
|
|||||||
db.session.query(product_match.c.id)
|
db.session.query(product_match.c.id)
|
||||||
).subquery()
|
).subquery()
|
||||||
|
|
||||||
# 主搜索条件:借用人、SKU、单号 + 物料名称/规格匹配
|
|
||||||
keyword_conditions = or_(
|
keyword_conditions = or_(
|
||||||
TransBorrow.borrower_name.ilike(f'%{keyword}%'),
|
TransBorrow.borrower_name.ilike(f'%{keyword}%'),
|
||||||
TransBorrow.sku.ilike(f'%{keyword}%'),
|
TransBorrow.sku.ilike(f'%{keyword}%'),
|
||||||
TransBorrow.borrow_no.ilike(f'%{keyword}%'),
|
TransBorrow.borrow_no.ilike(f'%{keyword}%'),
|
||||||
TransBorrow.id.in_(all_matches)
|
TransBorrow.id.in_(all_matches)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif search_type == 'no':
|
||||||
|
keyword_conditions = TransBorrow.borrow_no.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
elif search_type == 'name':
|
||||||
|
keyword_conditions = TransBorrow.borrower_name.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
elif search_type == 'sku':
|
||||||
|
keyword_conditions = TransBorrow.sku.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
elif search_type == 'material_name':
|
||||||
|
# 联表查询物料名称
|
||||||
|
buy_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockBuy, and_(
|
||||||
|
TransBorrow.stock_id == StockBuy.id,
|
||||||
|
TransBorrow.source_table == 'stock_buy'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.name.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
semi_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockSemi, and_(
|
||||||
|
TransBorrow.stock_id == StockSemi.id,
|
||||||
|
TransBorrow.source_table == 'stock_semi'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.name.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
product_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockProduct, and_(
|
||||||
|
TransBorrow.stock_id == StockProduct.id,
|
||||||
|
TransBorrow.source_table == 'stock_product'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.name.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
all_matches = db.session.query(buy_match.c.id).union(
|
||||||
|
db.session.query(semi_match.c.id),
|
||||||
|
db.session.query(product_match.c.id)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
keyword_conditions = TransBorrow.id.in_(all_matches)
|
||||||
|
|
||||||
|
elif search_type == 'spec_model':
|
||||||
|
# 联表查询规格型号
|
||||||
|
buy_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockBuy, and_(
|
||||||
|
TransBorrow.stock_id == StockBuy.id,
|
||||||
|
TransBorrow.source_table == 'stock_buy'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockBuy.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.spec_model.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
semi_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockSemi, and_(
|
||||||
|
TransBorrow.stock_id == StockSemi.id,
|
||||||
|
TransBorrow.source_table == 'stock_semi'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockSemi.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.spec_model.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
product_match = db.session.query(TransBorrow.id).join(
|
||||||
|
StockProduct, and_(
|
||||||
|
TransBorrow.stock_id == StockProduct.id,
|
||||||
|
TransBorrow.source_table == 'stock_product'
|
||||||
|
)
|
||||||
|
).join(
|
||||||
|
MaterialBase, StockProduct.base_id == MaterialBase.id
|
||||||
|
).filter(MaterialBase.spec_model.ilike(f'%{keyword}%')).subquery()
|
||||||
|
|
||||||
|
all_matches = db.session.query(buy_match.c.id).union(
|
||||||
|
db.session.query(semi_match.c.id),
|
||||||
|
db.session.query(product_match.c.id)
|
||||||
|
).subquery()
|
||||||
|
|
||||||
|
keyword_conditions = TransBorrow.id.in_(all_matches)
|
||||||
|
|
||||||
|
else:
|
||||||
|
keyword_conditions = None
|
||||||
|
else:
|
||||||
|
keyword_conditions = None
|
||||||
|
|
||||||
|
if keyword_conditions is not None:
|
||||||
q = q.filter(keyword_conditions)
|
q = q.filter(keyword_conditions)
|
||||||
|
|
||||||
if status == 'borrowed':
|
if status == 'borrowed':
|
||||||
|
|||||||
@ -3,13 +3,24 @@
|
|||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="listQuery.keyword"
|
v-model="listQuery.keyword"
|
||||||
placeholder="单号/姓名/SKU/名称/规格"
|
placeholder="请输入关键词"
|
||||||
style="width: 250px;"
|
style="width: 250px;"
|
||||||
class="filter-item"
|
class="filter-item"
|
||||||
clearable
|
clearable
|
||||||
@input="debouncedSearch"
|
@input="debouncedSearch"
|
||||||
@clear="handleClearSearch"
|
@clear="handleClearSearch"
|
||||||
/>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<el-select v-model="listQuery.search_type" placeholder="搜索类型" style="width: 110px" @change="debouncedSearch">
|
||||||
|
<el-option label="全部" value="all" />
|
||||||
|
<el-option label="单号" value="no" />
|
||||||
|
<el-option label="姓名" value="name" />
|
||||||
|
<el-option label="SKU" value="sku" />
|
||||||
|
<el-option label="物料名称" value="material_name" />
|
||||||
|
<el-option label="规格型号" value="spec_model" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="listQuery.dateRange"
|
v-model="listQuery.dateRange"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
@ -187,6 +198,7 @@ const listQuery = reactive({
|
|||||||
page: 1,
|
page: 1,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
keyword: '',
|
keyword: '',
|
||||||
|
search_type: 'all',
|
||||||
dateRange: []
|
dateRange: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,23 @@
|
|||||||
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
placeholder="单号/借用人/SKU/名称/规格"
|
placeholder="请输入关键词"
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
clearable
|
clearable
|
||||||
@input="debouncedSearch"
|
@input="debouncedSearch"
|
||||||
@clear="handleClearSearch"
|
@clear="handleClearSearch"
|
||||||
/>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<el-select v-model="searchType" placeholder="类型" style="width: 110px" @change="debouncedSearch">
|
||||||
|
<el-option label="全部" value="all" />
|
||||||
|
<el-option label="单号" value="no" />
|
||||||
|
<el-option label="借用人" value="name" />
|
||||||
|
<el-option label="SKU" value="sku" />
|
||||||
|
<el-option label="物料名称" value="material_name" />
|
||||||
|
<el-option label="规格型号" value="spec_model" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -171,6 +182,7 @@ const total = ref(0)
|
|||||||
// ★ 修改点:默认状态改为 'borrowed' (未归还)
|
// ★ 修改点:默认状态改为 'borrowed' (未归还)
|
||||||
const status = ref('borrowed')
|
const status = ref('borrowed')
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
|
const searchType = ref('all')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@ -183,7 +195,8 @@ const fetchData = async () => {
|
|||||||
params: {
|
params: {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
status: status.value,
|
status: status.value,
|
||||||
keyword: keyword.value
|
keyword: keyword.value,
|
||||||
|
search_type: searchType.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
list.value = res.data.items
|
list.value = res.data.items
|
||||||
|
|||||||
Reference in New Issue
Block a user