perf: 导出OOM修复(yield_per流式) + 采购模糊匹配安全防护

## base_service.py export_excel
- query.all()全量→yield_per(2000)分块流式读取
- all_rows[]内存收集→write_only=True直写Excel
- all_rows.sort()→ORDER BY数据库排序
- 删除~160行旧Excel样式/脱敏死代码
- 内存: 全量→2000行分块+输出流

## purchase_service.py
- 模糊匹配安全防护:
  len(n.strip())>=2 过滤空格/单字符
  .limit(500) 防全表返回
This commit is contained in:
yueli
2026-07-16 13:08:45 +08:00
parent 347f20497c
commit 3290f206c6
2 changed files with 113 additions and 287 deletions

View File

@ -205,29 +205,69 @@ class PurchaseService:
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
# ★ 批量预取 MaterialBase:三段式查询(base_id 精确 → name 精确 → name 模糊)
purchase_items = list(pagination.items)
base_ids = {p.base_id for p in purchase_items if p.base_id}
names_no_base = [p.name for p in purchase_items if p.name and not p.base_id]
material_map = {} # base_id → MaterialBase
name_map = {} # (name, spec) or (name, '__fallback__') → MaterialBase
# 阶段1: base_id 精确预取
if base_ids:
materials = MaterialBase.query.filter(MaterialBase.id.in_(base_ids)).all()
material_map = {m.id: m for m in materials}
# 阶段2: name 精确预取(name.in_)
if names_no_base:
exact_matches = MaterialBase.query.filter(
MaterialBase.name.in_(names_no_base),
MaterialBase.is_enabled == True
).all()
for m in exact_matches:
name_map[(m.name, m.spec_model or '')] = m
# 阶段3: name 模糊预取(仅>=2字符 + LIMIT 500 防爆炸)
unmatched_names = []
for p in purchase_items:
if p.base_id and p.base_id in material_map:
continue
if p.name and (p.name, p.spec_model or '') in name_map:
continue
n = (p.name or '').strip()
if len(n) >= 2: # ★ 安全阈值:至少2字符才做模糊匹配
unmatched_names.append(n)
if unmatched_names:
fuzzy_conditions = [
MaterialBase.name.ilike(f'%{n}%') for n in unmatched_names
]
fuzzy_matches = MaterialBase.query.filter(
db.or_(*fuzzy_conditions),
MaterialBase.is_enabled == True
).order_by(MaterialBase.id.desc()).limit(500).all() # ★ 硬上限:最多500条
for m in fuzzy_matches:
key = (m.name, '__fuzzy__')
if key not in name_map:
name_map[key] = m
# 内存匹配(O(1) 字典查找)
items = []
for p in pagination.items:
for p in purchase_items:
item = p.to_dict()
material = None
# 附加物料基础信息
# 1. base_id 精确
if p.base_id:
# 优先走 base_id 硬关联
material = db.session.get(MaterialBase, p.base_id)
material = material_map.get(p.base_id)
# 回退匹配:历史采购单没有 base_id,通过 name + spec_model 模糊匹配
# 2. name + spec 精确
if not material and p.name:
material = MaterialBase.query.filter(
MaterialBase.name == p.name,
MaterialBase.spec_model == (p.spec_model or ''),
MaterialBase.is_enabled == True
).first()
# 如果精确匹配失败,仅按 name 模糊匹配(取最新一条)
if not material:
material = MaterialBase.query.filter(
MaterialBase.name.ilike(f'%{p.name}%'),
MaterialBase.is_enabled == True
).order_by(MaterialBase.id.desc()).first()
material = name_map.get((p.name, p.spec_model or ''))
# 3. name 模糊回退
if not material and p.name:
material = name_map.get((p.name, '__fuzzy__'))
if material:
item['material'] = {