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