Compare commits
2 Commits
b9c25ff4c5
...
3.0AI添加
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ba1ff37f0 | |||
| 1450e6c1de |
@ -131,6 +131,15 @@ def get_records():
|
||||
search_type = request.args.get('search_type', 'all')
|
||||
|
||||
res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword, search_type=search_type)
|
||||
|
||||
# ★ service 层异常时:code==500 的字典(带 traceback),需要直通到前端,便于排查
|
||||
if isinstance(res, dict) and res.get('code') == 500:
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'msg': res.get('msg', '服务内部错误'),
|
||||
'trace': res.get('trace', '')
|
||||
}), 500
|
||||
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if res.get('items'):
|
||||
|
||||
@ -6,7 +6,7 @@ from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
from app.models.base import MaterialBase
|
||||
from sqlalchemy import desc, func, nullslast, asc, or_, and_
|
||||
from sqlalchemy import desc, func, nullslast, asc, or_, and_, case
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
|
||||
@ -325,9 +325,51 @@ class TransService:
|
||||
|
||||
@staticmethod
|
||||
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all'):
|
||||
q = TransBorrow.query
|
||||
"""
|
||||
获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize)
|
||||
|
||||
# 如果有关键词,需要联表搜索物料名称和规格型号
|
||||
实现思路(三步走):
|
||||
步骤 1: 构造 GROUP BY borrow_no 的"单号维度视图" subquery
|
||||
(包含 borrow_no + sort_key + 状态聚合,全部聚合都在这里完成)
|
||||
步骤 2: 用一个【纯净的列查询】从 subquery 中分页得到 page_borrow_nos
|
||||
→ SELECT 只有 borrow_no 一列,【主查询无 GROUP BY】
|
||||
→ 避免触发 PG "column must appear in GROUP BY" 严格模式
|
||||
步骤 3: 用 page_borrow_nos 拉明细 + 预加载 material_name
|
||||
|
||||
状态过滤按"单号聚合"判定:
|
||||
- borrowed: 单号下至少有一条 is_returned=False
|
||||
- returned: 单号下所有明细 is_returned=True
|
||||
"""
|
||||
try:
|
||||
# ====================================================================
|
||||
# 步骤 1a:构造"单号维度"基础子查询(GROUP BY borrow_no 在这里完成)
|
||||
# ====================================================================
|
||||
# 单号 + 排序键(最早 expected_return_time)—— 这一层只含 2 列 + GROUP BY
|
||||
order_subq = (
|
||||
db.session.query(
|
||||
TransBorrow.borrow_no.label('borrow_no'),
|
||||
func.min(TransBorrow.expected_return_time).label('sort_key')
|
||||
)
|
||||
.group_by(TransBorrow.borrow_no)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# 状态聚合子查询(也是 GROUP BY borrow_no)
|
||||
status_subq = (
|
||||
db.session.query(
|
||||
TransBorrow.borrow_no.label('borrow_no'),
|
||||
func.sum(
|
||||
case((TransBorrow.is_returned == False, 1), else_=0)
|
||||
).label('unreturned_count')
|
||||
)
|
||||
.group_by(TransBorrow.borrow_no)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# ====================================================================
|
||||
# 步骤 1b:构造关键词命中单号子查询(保留原全部 search_type 逻辑)
|
||||
# ====================================================================
|
||||
keyword_conditions = None
|
||||
if keyword:
|
||||
# 根据 search_type 构建不同的搜索条件
|
||||
if search_type == 'all':
|
||||
@ -471,30 +513,82 @@ class TransService:
|
||||
|
||||
keyword_conditions = TransBorrow.id.in_(all_matches)
|
||||
|
||||
else:
|
||||
keyword_conditions = None
|
||||
else:
|
||||
keyword_conditions = None
|
||||
|
||||
# 把"命中的单号"独立成 subquery,供主查询做 IN 过滤
|
||||
keyword_borrow_nos_subq = None
|
||||
if keyword_conditions is not None:
|
||||
q = q.filter(keyword_conditions)
|
||||
keyword_borrow_nos_subq = (
|
||||
db.session.query(TransBorrow.borrow_no)
|
||||
.filter(keyword_conditions)
|
||||
.distinct()
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# ====================================================================
|
||||
# 步骤 2:纯净列查询分页(SELECT 只有 order_subq.c.borrow_no 一列)
|
||||
# ====================================================================
|
||||
borrow_no_q = db.session.query(order_subq.c.borrow_no)
|
||||
|
||||
# 关键词过滤
|
||||
if keyword_borrow_nos_subq is not None:
|
||||
borrow_no_q = borrow_no_q.filter(
|
||||
order_subq.c.borrow_no.in_(keyword_borrow_nos_subq)
|
||||
)
|
||||
|
||||
# 状态过滤(按"单号聚合"判定)
|
||||
if status == 'borrowed':
|
||||
q = q.filter(TransBorrow.is_returned == False)
|
||||
# 单号下至少一条未还
|
||||
borrow_no_q = borrow_no_q.filter(
|
||||
order_subq.c.borrow_no.in_(
|
||||
db.session.query(status_subq.c.borrow_no)
|
||||
.filter(status_subq.c.unreturned_count > 0)
|
||||
)
|
||||
)
|
||||
elif status == 'returned':
|
||||
q = q.filter(TransBorrow.is_returned == True)
|
||||
# 单号下所有明细都已归还
|
||||
borrow_no_q = borrow_no_q.filter(
|
||||
order_subq.c.borrow_no.in_(
|
||||
db.session.query(status_subq.c.borrow_no)
|
||||
.filter(status_subq.c.unreturned_count == 0)
|
||||
)
|
||||
)
|
||||
|
||||
# 使用 distinct 防止跨表查询产生重复记录
|
||||
q = q.distinct()
|
||||
# 排序(单号维度的 sort_key ASC)
|
||||
borrow_no_q = borrow_no_q.order_by(nullslast(asc(order_subq.c.sort_key)))
|
||||
|
||||
q = q.order_by(nullslast(asc(TransBorrow.expected_return_time)))
|
||||
pagination = q.paginate(page=page, per_page=limit, error_out=False)
|
||||
# 分页(基准 = borrow_no 单号数)
|
||||
pagination = borrow_no_q.paginate(page=page, per_page=limit, error_out=False)
|
||||
# ★ pagination.items 是 SQLAlchemy Row 对象,psycopg2 无法直接 adapt Row
|
||||
# 用 isinstance(row, tuple) 不够(2.x 的 Row 不一定继承 tuple)
|
||||
# 用 hasattr(row, '_mapping') 兜底,强制提取 row[0] 拿到纯字符串
|
||||
page_borrow_nos = [
|
||||
row[0] if isinstance(row, tuple) or hasattr(row, '_mapping') else row
|
||||
for row in pagination.items
|
||||
]
|
||||
total_orders = pagination.total # ★ 单号总数(修复前是明细数,分页错乱根因)
|
||||
|
||||
if not page_borrow_nos:
|
||||
return {
|
||||
'items': [],
|
||||
'total': total_orders,
|
||||
'page': page,
|
||||
'limit': limit
|
||||
}
|
||||
|
||||
# ====================================================================
|
||||
# 步骤 3:按当前页 borrow_no 集合一次性拉出所有明细
|
||||
# ====================================================================
|
||||
detail_records = (
|
||||
TransBorrow.query
|
||||
.filter(TransBorrow.borrow_no.in_(page_borrow_nos))
|
||||
.order_by(TransBorrow.borrow_no.asc(), TransBorrow.id.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# ★ 批量预加载物料名称(两步:收集ID → 批量 JOIN → 内存拼装)
|
||||
# ★ 批量预加载物料名称(三步:收集ID → 批量JOIN → SKU兜底)
|
||||
# ============================================================
|
||||
items_with_names = []
|
||||
items = pagination.items
|
||||
items = detail_records
|
||||
if items:
|
||||
# 步骤 1:收集所有 (source_table, stock_id) 对
|
||||
stock_ids_by_table = {'stock_buy': set(), 'stock_semi': set(), 'stock_product': set()}
|
||||
@ -553,13 +647,21 @@ class TransService:
|
||||
item_dict['material_name'] = material_name
|
||||
items_with_names.append(item_dict)
|
||||
|
||||
items_data = items_with_names
|
||||
else:
|
||||
items_data = []
|
||||
|
||||
return {
|
||||
'items': items_data,
|
||||
'total': pagination.total,
|
||||
'items': items_with_names,
|
||||
'total': total_orders,
|
||||
'page': page,
|
||||
'limit': limit
|
||||
}
|
||||
except Exception as e:
|
||||
# ★ 捕鼠器:把任何 SQL/运行时错误以 500 + traceback 返回,避免静默吞噬
|
||||
import traceback
|
||||
return {
|
||||
'code': 500,
|
||||
'msg': str(e),
|
||||
'trace': traceback.format_exc(),
|
||||
'items': [],
|
||||
'total': 0,
|
||||
'page': page,
|
||||
'limit': limit
|
||||
}
|
||||
|
||||
@ -239,7 +239,7 @@ const handleLogout = () => {
|
||||
<footer v-if="!isLoginPage" class="app-footer">
|
||||
<span class="version-tag">
|
||||
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||
当前版本:V3.48
|
||||
当前版本:V3.49
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user