From 2e903cff2cebb4a400d39c750602fe941cd26ed8 Mon Sep 17 00:00:00 2001 From: yueli Date: Thu, 16 Jul 2026 18:15:43 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E8=A7=84=E6=A0=BC=E8=BF=9E=E5=8F=B7?= =?UTF-8?q?=E5=8A=A9=E6=89=8B=E4=BC=98=E5=8C=96=20=E2=80=94=20=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E6=9F=A5=E8=AF=A2+=E5=AE=BD=E6=9D=BEregex+Redis?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 成功后清除缓存 --- inventory-backend/app/api/v1/inbound/base.py | 13 +++ .../app/services/inbound/base_service.py | 89 +++++++++++++------ 2 files changed, 76 insertions(+), 26 deletions(-) diff --git a/inventory-backend/app/api/v1/inbound/base.py b/inventory-backend/app/api/v1/inbound/base.py index e15ff58..0698d31 100644 --- a/inventory-backend/app/api/v1/inbound/base.py +++ b/inventory-backend/app/api/v1/inbound/base.py @@ -52,6 +52,16 @@ def get_current_user_permissions(): 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): """根据用户权限过滤字段,无权限的字段值置为 None""" if 'material_list:*' in user_permissions: @@ -299,6 +309,7 @@ def create(): filtered_data[key] = value MaterialBaseService.create_material(filtered_data) + _invalidate_specs_cache() return jsonify({"code": 200, "msg": "新增成功"}) except ValueError as e: # 捕获业务逻辑验证错误 (如名称为空) @@ -359,6 +370,7 @@ def update(id): filtered_data[key] = value # 使用过滤后的数据调用服务 MaterialBaseService.update_material(id, filtered_data) + _invalidate_specs_cache() return jsonify({"code": 200, "msg": "修改成功"}) except Exception as e: traceback.print_exc() @@ -378,6 +390,7 @@ def update(id): def delete(id): try: material_name = MaterialBaseService.delete_material(id) + _invalidate_specs_cache() return jsonify({"code": 200, "msg": "删除成功", "material_name": material_name}) except Exception as e: traceback.print_exc() diff --git a/inventory-backend/app/services/inbound/base_service.py b/inventory-backend/app/services/inbound/base_service.py index 0e02570..c30e6b4 100644 --- a/inventory-backend/app/services/inbound/base_service.py +++ b/inventory-backend/app/services/inbound/base_service.py @@ -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 \ No newline at end of file