| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- """宿主固定发现测点:取齐分页、校验矿井、按真实类型选开停和风量,不调用模型。"""
- from collections import defaultdict
- import hashlib
- import json
- import re
- import unicodedata
- import config
- from extensions.main_fan import exploration, realtime
- from extensions.main_fan.batch_models import HistoryPoint, MinePlan, ShaftPlan
- from extensions.main_fan.batch_tools import checked_plan
- from extensions.main_fan.batch_context import scope_for
- from runtime.python_sandbox import workspace_file
- def fan_name(row):
- """只识别名称明确写出的风机号;不从nodeId或一级/二级推断编号。"""
- numbers = '一二三四五六七八九十'
- number = rf'([0-9]+(?![0-9])|[{numbers}]+(?![{numbers}]))'
- found = set()
- for key in ('devName', 'nodePlace', 'nodeTypeName'):
- text = unicodedata.normalize('NFKC', str(row.get(key) or ''))
- # 前置“1号主扇”和后置“主通1”都可识别;编号后紧跟“级”时是电机级数。
- patterns = (rf'主(?:通风机|通|扇)(?:风机)?\s*{number}(?!\s*级)',
- rf'{number}\s*(?:号|#)\s*(?:主扇|主要通风机|主通风机|通风机|主风机|风机|风量)')
- for pattern in patterns:
- for value in re.findall(pattern, text):
- if len(value) == 1 and value in numbers:
- value = str(numbers.index(value) + 1)
- if value.isdigit():
- found.add(str(int(value)) + '号风机')
- return next(iter(found)) if len(found) == 1 else ''
- def shaft_name(row):
- for key in ('shaftName', 'wellName'):
- if row.get(key):
- return str(row[key]).strip()
- # 只匹配明确的井名片段;单独“通风机”或风机号均不构成通风井身份。
- names = set()
- for key in ('devFixPos', 'nodePlace', 'devName'):
- text = str(row.get(key) or '')
- names.update(re.findall(r'(?:[东西南北中](?:翼)?[一二三四五0-9]*|[一二三四五0-9]+号)(?:回风立井|回风斜井|回风井|通风井|风井)', text))
- return next(iter(names)) if len(names) == 1 else ''
- def plan_from_rows(mine, safety_rows, equipment_rows):
- groups, seen = defaultdict(list), {}
- for system, rows in (('safety', safety_rows), ('equipment', equipment_rows)):
- for row in rows:
- if not isinstance(row, dict):
- raise ValueError('实时列表包含非对象记录')
- selected = (str(row.get('sensorTypeId', '')) == '1010' if system == 'safety'
- else str(row.get('nodeTypeNo', '')) == '30101')
- if not selected:
- continue
- for field, expected in (('deptId', mine['dept_id']), ('mineId', mine['mine_id'])):
- if row.get(field) is not None and str(row[field]) != str(expected):
- raise ValueError('实时测点的' + field + '与本矿不一致')
- if not row.get('nodeId'):
- raise ValueError('目标类型测点缺少nodeId')
- point = HistoryPoint(system=system, node_id=str(row['nodeId']),
- role='fan_state' if system == 'safety' else 'flow', fan_id=fan_name(row),
- label=str((row.get('nodePlace') if system == 'safety' else row.get('nodeTypeName')) or ''),
- unit=str(row.get('sensorUnit') or ''), mapping_confirmed=False,
- mapping_reason='按接口原名称保留编号;一级/二级分别保留,跨系统物理绑定未确认')
- name = shaft_name(row)
- key = (system, point.node_id)
- signature = (name, point.model_dump())
- if key in seen:
- if seen[key] != signature:
- raise ValueError('分页中同一测点的类型、名称或归属冲突:' + point.node_id)
- continue
- seen[key] = signature
- groups[name].append(point)
- if not groups:
- raise ValueError('本矿未取得安全监控1010开停点或重大设备30101风量点')
- shafts, limitations = [], []
- for name, points in sorted(groups.items()):
- fans = sorted({point.fan_id for point in points if point.fan_id})
- notes = ['风机号来自测点名称,跨系统仅按原编号作时间对照,未证明物理绑定。']
- if not name:
- notes.append('接口缺少明确井名,保留矿级待确认测点集合;不将1号、2号当作两个通风井。')
- if len(fans) != 2 or any(not point.fan_id for point in points):
- notes.append('部分风机编号或主备配对不足,不能据此宣称已覆盖全部主备系统。')
- shafts.append(ShaftPlan(shaft_id='shaft-' + hashlib.sha256(name.encode()).hexdigest()[:12] if name else 'unresolved-main',
- shaft_name=name or '主通风机测点集合(通风井归属待确认)',
- identity_status='confirmed' if name else 'candidate',
- identity_reason='实时列表明确井名原文:' + name if name else '两个实时列表未提供明确通风井归属',
- fan_pair=fans if len(fans) == 2 else [], points=points, limitations=notes))
- limitations.extend(notes)
- return MinePlan(coverage='partial', shafts=shafts, limitations=list(dict.fromkeys(limitations)))
- async def discover(identifier, user, session):
- scope = scope_for(user, session)
- pages = {}
- size = min(100, config.get_int('MAIN_FAN_REALTIME_MAX_PAGE_SIZE', 1000))
- for system in ('safety', 'equipment'):
- rows, page, visited = [], 1, set()
- while page:
- if page in visited:
- raise ValueError('实时列表分页循环,不能声称已取齐')
- visited.add(page)
- if system == 'safety':
- receipt = await realtime.query_sensors(identifier, scope.mine['dept_id'], page, size, ['1010'], user, session)
- else:
- receipt = await realtime.query_equipment(identifier, scope.mine['dept_id'], page, size, user, session)
- body = json.loads(workspace_file(user, session, receipt['data_path']).read_text())['payload']
- if not isinstance(body, dict) or str(body.get('code')) not in {'0', '200'} or body.get('success') is False:
- raise ValueError('实时列表未返回成功业务响应:' + system)
- if not receipt['list_recognized'] or (receipt['has_more'] and not receipt['next_page']):
- raise ValueError('实时列表结构或分页不完整:' + system)
- container = body['rows'] if 'rows' in body else body.get('data')
- rows.extend(container['rows'] if isinstance(container, dict) else container)
- page = receipt['next_page']
- pages[system] = rows
- workflow = exploration._load(identifier, user, session)
- plan = checked_plan(workflow, user, session, plan_from_rows(scope.mine, pages['safety'], pages['equipment']))
- exploration._persist(workflow, user, session, '固定代码发现本矿测点', {'mode': 'fixed_code', 'plan': plan.model_dump(mode='json')})
- return plan
|