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:
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
Reference in New Issue
Block a user