perf: 规格连号助手优化 — 流式查询+宽松regex+Redis缓存

## base_service.py get_latest_specs
- .all()全量→with_entities().limit(10000) 防OOM
- regex放宽: 支持 LICA-3000/M3x12 等格式(旧版仅匹配OPT12046)
- 移除OPT硬编码→SUB_CATEGORY_PREFIXES可配置集合
- Redis缓存(1h): 首次查询后缓存,后续命中直接返回

## base.py 缓存失效
- 新增 _invalidate_specs_cache() 辅助函数
- create/update/delete 成功后清除缓存
This commit is contained in:
yueli
2026-07-16 18:15:43 +08:00
parent d647af6bc3
commit 2e903cff2c
2 changed files with 76 additions and 26 deletions

View File

@ -52,6 +52,16 @@ def get_current_user_permissions():
return perms return perms
def _invalidate_specs_cache():
"""规格连号缓存失效(新增/修改/删除基础信息时调用)"""
try:
from app.extensions import redis_client
if redis_client:
redis_client.delete('inventory:specs:grouped')
except Exception:
pass
def filter_item_by_permissions(item_dict, user_permissions): def filter_item_by_permissions(item_dict, user_permissions):
"""根据用户权限过滤字段,无权限的字段值置为 None""" """根据用户权限过滤字段,无权限的字段值置为 None"""
if 'material_list:*' in user_permissions: if 'material_list:*' in user_permissions:
@ -299,6 +309,7 @@ def create():
filtered_data[key] = value filtered_data[key] = value
MaterialBaseService.create_material(filtered_data) MaterialBaseService.create_material(filtered_data)
_invalidate_specs_cache()
return jsonify({"code": 200, "msg": "新增成功"}) return jsonify({"code": 200, "msg": "新增成功"})
except ValueError as e: except ValueError as e:
# 捕获业务逻辑验证错误 (如名称为空) # 捕获业务逻辑验证错误 (如名称为空)
@ -359,6 +370,7 @@ def update(id):
filtered_data[key] = value filtered_data[key] = value
# 使用过滤后的数据调用服务 # 使用过滤后的数据调用服务
MaterialBaseService.update_material(id, filtered_data) MaterialBaseService.update_material(id, filtered_data)
_invalidate_specs_cache()
return jsonify({"code": 200, "msg": "修改成功"}) return jsonify({"code": 200, "msg": "修改成功"})
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
@ -378,6 +390,7 @@ def update(id):
def delete(id): def delete(id):
try: try:
material_name = MaterialBaseService.delete_material(id) material_name = MaterialBaseService.delete_material(id)
_invalidate_specs_cache()
return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name}) return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name})
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()

View File

@ -928,59 +928,96 @@ class MaterialBaseService:
traceback.print_exc() traceback.print_exc()
raise e raise e
# 支持二级分类的前缀集合(如 OPT1, OPT2, LICA1 等子系列分组)
SUB_CATEGORY_PREFIXES = {'OPT', 'LICA', 'M', 'UAV', 'CF', 'GPS'}
@staticmethod @staticmethod
def get_latest_specs(): def get_latest_specs():
""" """
获取所有规格型号的分组统计,按规则聚合后返回 规格连号助手 — 智能分组统计v2: 流式读取 + Redis缓存 + 宽松regex
- 前缀统一大写处理
- 匹配模式(前缀)(单数字二级分类位)(纯数字部分),如 OPT12046 -> OPT, 1, 2046 匹配模式: PREFIX[-_]?NUMBERS[SUFFIX], 如:
- OPT 系列:使用 前缀+二级分类位 作为分组 Key OPT1, OPT2 OPT12046 → OPT, 1, 2046
- 其他前缀:直接使用前缀作为分组 Key LICA-3000 → LICA, 3000, ''
- 返回每个分组的数量、最大号、完整规格名 M3x12 → M, 3, 'x12'
分组规则: 前缀在 SUB_CATEGORY_PREFIXES 中 → 前缀+首位数字作为key
其他 → 只用前缀作为key
""" """
import re import re
import json as json_module
from collections import defaultdict from collections import defaultdict
# 1. 查询所有不为空的规格型号 CACHE_KEY = 'inventory:specs:grouped'
specs = MaterialBase.query.filter( CACHE_TTL = 3600
MaterialBase.spec_model.isnot(None),
MaterialBase.spec_model != ''
).all()
# 2. 按分组收集所有数字 # ── Redis 缓存 ──
try:
from app.extensions import redis_client
if redis_client:
cached = redis_client.get(CACHE_KEY)
if cached:
return json_module.loads(cached)
except Exception:
pass # Redis 不可用时降级
# ── 流式查询yield_per 分批 + limit 防 OOM ──
pattern = re.compile(r'^([A-Za-z]+)[-_]?(\d+)(.*)$')
groups = defaultdict(list) groups = defaultdict(list)
for material in specs: rows = MaterialBase.query.with_entities(
spec = material.spec_model MaterialBase.id, MaterialBase.spec_model
).filter(
MaterialBase.spec_model.isnot(None),
MaterialBase.spec_model != ''
).limit(10000).all()
for row in rows:
spec = row.spec_model
if not spec: if not spec:
continue continue
base_spec = spec.split('/')[0] base_spec = spec.split('/')[0]
match = re.match(r'^([A-Za-z]+)(\d)(\d+)$', base_spec) match = pattern.match(base_spec)
if not match: if not match:
continue continue
prefix, sub_cat, num_str = match.groups() prefix = match.group(1).upper()
prefix = prefix.upper() num_str = match.group(2)
num = int(num_str) suffix = match.group(3)
if not num_str:
continue
num = int(num_str)
sub_cat = num_str[0] # 首位数字作为子分类
# 分组 key
if prefix in MaterialBaseService.SUB_CATEGORY_PREFIXES and sub_cat:
key = f"{prefix}_{sub_cat}"
else:
key = prefix
# OPT 系列使用 前缀+单数字二级分类 作为 Key
key = f"{prefix}{sub_cat}" if prefix == 'OPT' else prefix
groups[key].append((num, spec)) groups[key].append((num, spec))
# 3. 生成展示用的统计数据 # ── 生成结果 ──
result = [] result = []
for key, items in groups.items(): for key, items in groups.items():
sorted_items = sorted(items, key=lambda x: x[0]) items.sort(key=lambda x: x[0])
max_num, max_spec = sorted_items[-1] max_num, max_spec = items[-1]
result.append({ result.append({
'group': key, 'group': key,
'count': len(sorted_items), 'count': len(items),
'latest': max_spec, 'latest': max_spec,
'max_num': max_num 'max_num': max_num
}) })
# 4. 按数量降序,再按分组名升序排列
result.sort(key=lambda x: (-x['count'], x['group'])) result.sort(key=lambda x: (-x['count'], x['group']))
# ── 写入 Redis 缓存 ──
try:
from app.extensions import redis_client
if redis_client:
redis_client.setex(CACHE_KEY, CACHE_TTL, json_module.dumps(result))
except Exception:
pass
return result return result