Selaa lähdekoodia

refactor(backend): 优化地址配置与轮询的线程冲突问题

1.  删除了废弃的 expect 部署脚本 install_deps.exp 和 scp_upload.exp
2.  为 modbus_rtu 的 AddressConfigProtocol 添加线程锁,避免地址配置和端口轮询同时执行
3.  调整端口轮询逻辑,在地址配置进行时跳过本轮轮询
4.  优化了 modbus 广播查询的超时时间和响应收集逻辑
5.  修复了 CRC 校验的传入参数错误
wenhongquan 1 päivä sitten
vanhempi
commit
f342f791b5

BIN
backend/__pycache__/app.cpython-313.pyc


+ 82 - 74
backend/app.py

@@ -3721,83 +3721,91 @@ if __name__ == '__main__':
                 interval = dtu_config.get('poll_interval_ms', 5000) / 1000.0
                 interval = dtu_config.get('poll_interval_ms', 5000) / 1000.0
                 time.sleep(max(1, interval))
                 time.sleep(max(1, interval))
                 try:
                 try:
-                    _st = serial_client.get_status()
-                    if not (isinstance(_st, dict) and _st.get('connected', False)):
-                        continue
-                    if not dtu_config.get('enabled'):
+                    # 地址配置期间暂停轮询,避免总线上同时出现查询帧和配置帧
+                    if not address_config.config_lock.acquire(blocking=False):
+                        logger.info("地址配置进行中,本轮轮询跳过")
                         continue
                         continue
-                    if not panel_config:
-                        continue
-                    online = 0
-                    offline = 0
-                    for panel_id, cfg in panel_config.items():
-                        addr = cfg.get('address', 1)
-                        panel_ok = False
-                        panel_ports = []
-                        for port_id in range(1, 25):
-                            try:
-                                result = modbus_client.read_antenna_card(addr, port_id, timeout=1.0)
-                            except Exception:
-                                result = {'error': 'exception'}
-                            if 'error' in result:
-                                panel_ports.append({'port_id': port_id, 'status': 'UNKNOWN', 'jumper_uid': None})
-                                continue
-                            panel_ok = True
-                            card_str = result.get('card_number_str', '')
-                            uid = card_str.upper() if card_str and card_str != '0000000000000000' else ''
-                            if panel_id not in port_state:
-                                port_state[panel_id] = {}
-                            if port_id not in port_state[panel_id]:
-                                port_state[panel_id][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0, 'last_polled_at': int(time.time() * 1000)}
-                            ps = port_state[panel_id][port_id]
-                            ps['last_polled_at'] = int(time.time() * 1000)
-                            last_uid = ps.get('last_uid')
-                            if uid and uid != last_uid:
-                                dtu_publish_event(panel_id, port_id, 'CONNECT' if not last_uid else 'MOVE', uid, last_uid)
-                                ps['last_uid'] = uid
-                            elif not uid and last_uid:
-                                dtu_publish_event(panel_id, port_id, 'DISCONNECT', None, last_uid)
-                                ps['last_uid'] = None
-
-                            ps_exp = ps.get('expected_uid')
-                            if dtu_config.get('alarm_enabled', True):
-                                if ps_exp and uid and uid != ps_exp:
-                                    ps['alarm_count'] = ps.get('alarm_count', 0) + 1
-                                    sev = 'CRITICAL' if ps['alarm_count'] >= 3 else 'WARNING'
-                                    dtu_publish_alarm(panel_id, port_id, 'ILLEGAL_CONNECT', ps_exp, uid, sev)
-                                elif ps_exp and not uid:
-                                    ps['alarm_count'] = ps.get('alarm_count', 0) + 1
-                                    sev = 'CRITICAL' if ps['alarm_count'] >= 3 else 'WARNING'
-                                    dtu_publish_alarm(panel_id, port_id, 'ILLEGAL_DISCONNECT', ps_exp, None, sev)
-                                elif ps_exp and uid == ps_exp:
-                                    ps['alarm_count'] = 0
-
-                            if uid:
-                                port_status = 'ILLEGAL' if (ps_exp and uid != ps_exp) else 'CONNECTED'
-                            else:
-                                port_status = 'DISCONNECTED'
-                            panel_ports.append({'port_id': port_id, 'status': port_status, 'jumper_uid': uid or None})
-
-                        if panel_ok:
-                            online += 1
-                            device_last_seen[panel_id] = time.time()
-                            device_last_seen[cfg.get('panel_uid', '')] = time.time()
-                            dtu_publish_panel_status(panel_id, addr, panel_ports)
-                            # 如果该 panel 是当前显示的 panel,刷新屏幕
-                            panels = get_sorted_panels()
-                            trigger_panel_id = None
-                            with screen_lock:
-                                if panels and 0 <= screen_current_panel_index < len(panels) and panels[screen_current_panel_index][0] == panel_id:
-                                    trigger_panel_id = panel_id
-                            if trigger_panel_id:
+                    try:
+                        _st = serial_client.get_status()
+                        if not (isinstance(_st, dict) and _st.get('connected', False)):
+                            continue
+                        if not dtu_config.get('enabled'):
+                            continue
+                        if not panel_config:
+                            continue
+
+                        online = 0
+                        offline = 0
+                        for panel_id, cfg in panel_config.items():
+                            addr = cfg.get('address', 1)
+                            panel_ok = False
+                            panel_ports = []
+                            for port_id in range(1, 25):
                                 try:
                                 try:
-                                    screen_refresh_queue.put((trigger_panel_id,))
-                                except Exception as e:
-                                    logger.error(f"刷新屏幕失败: {e}")
-                        else:
-                            offline += 1
+                                    result = modbus_client.read_antenna_card(addr, port_id, timeout=1.0)
+                                except Exception:
+                                    result = {'error': 'exception'}
+                                if 'error' in result:
+                                    panel_ports.append({'port_id': port_id, 'status': 'UNKNOWN', 'jumper_uid': None})
+                                    continue
+                                panel_ok = True
+                                card_str = result.get('card_number_str', '')
+                                uid = card_str.upper() if card_str and card_str != '0000000000000000' else ''
+                                if panel_id not in port_state:
+                                    port_state[panel_id] = {}
+                                if port_id not in port_state[panel_id]:
+                                    port_state[panel_id][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0, 'last_polled_at': int(time.time() * 1000)}
+                                ps = port_state[panel_id][port_id]
+                                ps['last_polled_at'] = int(time.time() * 1000)
+                                last_uid = ps.get('last_uid')
+                                if uid and uid != last_uid:
+                                    dtu_publish_event(panel_id, port_id, 'CONNECT' if not last_uid else 'MOVE', uid, last_uid)
+                                    ps['last_uid'] = uid
+                                elif not uid and last_uid:
+                                    dtu_publish_event(panel_id, port_id, 'DISCONNECT', None, last_uid)
+                                    ps['last_uid'] = None
+
+                                ps_exp = ps.get('expected_uid')
+                                if dtu_config.get('alarm_enabled', True):
+                                    if ps_exp and uid and uid != ps_exp:
+                                        ps['alarm_count'] = ps.get('alarm_count', 0) + 1
+                                        sev = 'CRITICAL' if ps['alarm_count'] >= 3 else 'WARNING'
+                                        dtu_publish_alarm(panel_id, port_id, 'ILLEGAL_CONNECT', ps_exp, uid, sev)
+                                    elif ps_exp and not uid:
+                                        ps['alarm_count'] = ps.get('alarm_count', 0) + 1
+                                        sev = 'CRITICAL' if ps['alarm_count'] >= 3 else 'WARNING'
+                                        dtu_publish_alarm(panel_id, port_id, 'ILLEGAL_DISCONNECT', ps_exp, None, sev)
+                                    elif ps_exp and uid == ps_exp:
+                                        ps['alarm_count'] = 0
+
+                                if uid:
+                                    port_status = 'ILLEGAL' if (ps_exp and uid != ps_exp) else 'CONNECTED'
+                                else:
+                                    port_status = 'DISCONNECTED'
+                                panel_ports.append({'port_id': port_id, 'status': port_status, 'jumper_uid': uid or None})
+
+                            if panel_ok:
+                                online += 1
+                                device_last_seen[panel_id] = time.time()
+                                device_last_seen[cfg.get('panel_uid', '')] = time.time()
+                                dtu_publish_panel_status(panel_id, addr, panel_ports)
+                                # 如果该 panel 是当前显示的 panel,刷新屏幕
+                                panels = get_sorted_panels()
+                                trigger_panel_id = None
+                                with screen_lock:
+                                    if panels and 0 <= screen_current_panel_index < len(panels) and panels[screen_current_panel_index][0] == panel_id:
+                                        trigger_panel_id = panel_id
+                                if trigger_panel_id:
+                                    try:
+                                        screen_refresh_queue.put((trigger_panel_id,))
+                                    except Exception as e:
+                                        logger.error(f"刷新屏幕失败: {e}")
+                            else:
+                                offline += 1
 
 
-                    dtu_publish_status(force=True)
+                        dtu_publish_status(force=True)
+                    finally:
+                        address_config.config_lock.release()
                 except Exception as e:
                 except Exception as e:
                     logger.error(f"端口轮询异常: {str(e)}")
                     logger.error(f"端口轮询异常: {str(e)}")
         t2 = threading.Thread(target=port_poll_loop, daemon=True)
         t2 = threading.Thread(target=port_poll_loop, daemon=True)

BIN
backend/modules/__pycache__/modbus_rtu.cpython-313.pyc


+ 145 - 116
backend/modules/modbus_rtu.py

@@ -1,4 +1,5 @@
 from typing import Optional
 from typing import Optional
+import threading
 
 
 
 
 # ========== 地址配置协议 ==========
 # ========== 地址配置协议 ==========
@@ -27,8 +28,8 @@ def parse_device_response(data: bytes) -> Optional[dict]:
     uid_bytes = data[2:14]
     uid_bytes = data[2:14]
     uid_hex = uid_bytes.hex()
     uid_hex = uid_bytes.hex()
 
 
-    # CRC 校验失败则拒绝
-    if not verify_crc16(data[:14]):
+    # CRC 校验失败则拒绝(传入完整 16 字节帧,最后两字节为 CRC)
+    if not verify_crc16(data):
         return {'error': 'CRC校验失败', 'raw_data': data.hex()}
         return {'error': 'CRC校验失败', 'raw_data': data.hex()}
 
 
     # UID 有效性检查:拒绝明显是噪声或碰撞产生的数据
     # UID 有效性检查:拒绝明显是噪声或碰撞产生的数据
@@ -108,8 +109,9 @@ class AddressConfigProtocol:
 
 
     def __init__(self, serial_port):
     def __init__(self, serial_port):
         self.serial = serial_port
         self.serial = serial_port
-        self.default_timeout = 2.0
+        self.default_timeout = 5.0  # 设备广播响应可能较慢,给足时间
         self.stored_devices = {}
         self.stored_devices = {}
+        self.config_lock = threading.RLock()  # 地址配置期间占用,避免与轮询冲突
 
 
     def add_stored_device(self, uid: str, address: int):
     def add_stored_device(self, uid: str, address: int):
         """添加已存储的设备"""
         """添加已存储的设备"""
@@ -123,30 +125,55 @@ class AddressConfigProtocol:
         """send broadcast query and collect all valid 00 41 responses"""
         """send broadcast query and collect all valid 00 41 responses"""
         import time
         import time
 
 
-        request = build_broadcast_query()
-        logger.info(f"send broadcast: {request.hex()}")
+        with self.config_lock:
+            request = build_broadcast_query()
+            logger.info(f"send broadcast: {request.hex()}")
 
 
-        timeout = timeout or self.default_timeout
-        response = self.serial.send_and_wait(request, timeout=timeout, min_response_bytes=16)
-
-        responses = []
-        if response and len(response) >= 16:
-            logger.info(f"got raw response ({len(response)}B): {response.hex()}")
-            # RS485 半双工会回显已发送的数据,导致 response 开头可能有 5 字节回显
-            # 在 response 中搜索所有 00 41 标记,尝试从中提取 16 字节帧解析
-            for i in range(len(response) - 15 + 1):
-                if response[i] == 0x00 and response[i + 1] == 0x41:
-                    chunk = response[i:i + 16]
-                    if len(chunk) >= 16:
-                        parsed = parse_device_response(chunk)
-                        if "error" not in parsed:
-                            if not any(p.get('uid') == parsed['uid'] for p in responses):
-                                responses.append(parsed)
-        else:
-            logger.info(f"no response or too short: {len(response) if response else 0}B")
-
-        logger.info(f"broadcast done, got {len(responses)} valid unique responses")
-        return responses
+            timeout = timeout or self.default_timeout
+            # 先清空输入缓冲区,避免残留数据干扰
+            self.serial.flush_input()
+            time.sleep(0.05)
+
+            # 发送广播并等待响应;部分设备需要 2~3 秒才应答
+            response = self.serial.send_and_wait(request, timeout=timeout, min_response_bytes=16)
+
+            # 部分从机应答会被后台读取线程滞后收到,再额外等待一段收集窗口
+            # 注意:这里不能再次调用 send_and_wait,因为它会清空缓冲区
+            time.sleep(0.8)
+            extra = b''
+            with self.serial.lock:
+                while self.serial.raw_response_buffer:
+                    try:
+                        extra += bytes.fromhex(self.serial.raw_response_buffer.pop(0))
+                    except Exception:
+                        self.serial.raw_response_buffer.pop(0)
+                if self.serial.ser and self.serial.ser.is_open and self.serial.ser.in_waiting > 0:
+                    extra += self.serial.ser.read(self.serial.ser.in_waiting)
+            if extra:
+                logger.info(f"broadcast extra response ({len(extra)}B): {extra.hex()}")
+                if response:
+                    response += extra
+                else:
+                    response = extra
+
+            responses = []
+            if response and len(response) >= 16:
+                logger.info(f"got raw response ({len(response)}B): {response.hex()}")
+                # RS485 半双工会回显已发送的数据,导致 response 开头可能有 5 字节回显
+                # 在 response 中搜索所有 00 41 标记,尝试从中提取 16 字节帧解析
+                for i in range(len(response) - 15 + 1):
+                    if response[i] == 0x00 and response[i + 1] == 0x41:
+                        chunk = response[i:i + 16]
+                        if len(chunk) >= 16:
+                            parsed = parse_device_response(chunk)
+                            if "error" not in parsed:
+                                if not any(p.get('uid') == parsed['uid'] for p in responses):
+                                    responses.append(parsed)
+            else:
+                logger.info(f"no response or too short: {len(response) if response else 0}B")
+
+            logger.info(f"broadcast done, got {len(responses)} valid unique responses")
+            return responses
 
 
     def _next_available_address(self) -> int:
     def _next_available_address(self) -> int:
         """从 1~247 中找一个未被 stored_devices 占用的地址"""
         """从 1~247 中找一个未被 stored_devices 占用的地址"""
@@ -160,104 +187,106 @@ class AddressConfigProtocol:
         """处理广播查询响应:给每个应答设备分配地址(优先使用已记录地址)"""
         """处理广播查询响应:给每个应答设备分配地址(优先使用已记录地址)"""
         import time
         import time
 
 
-        results = []
-
-        for resp in responses:
-            if 'error' in resp:
-                results.append({'status': 'error', 'message': resp['error']})
-                continue
-
-            uid = resp['uid']
-            src_addr = resp.get('source_address', 0)
-            device_address = self.stored_devices.get(uid)
-
-            # 优先使用地址表中已保存的地址;没有则分配下一个可用地址
-            if device_address:
-                new_address = device_address
-                logger.info(f"UID={uid} 源地址={src_addr},地址表存在记录,分配已有地址={new_address}")
-            else:
-                new_address = self._next_available_address()
-                logger.info(f"UID={uid} 源地址={src_addr},地址表无记录,分配新地址={new_address}")
-
-            request = build_assign_address(bytes.fromhex(uid), new_address)
-            logger.info(f"发送分配地址帧: {request.hex()}")
-            self.serial.flush_input()
-            time.sleep(0.05)
-            success, msg = self.serial.send_raw(request)
-            if not success:
-                logger.error(f"UID={uid} 分配地址帧发送失败: {msg}")
-                results.append({
-                    'status': 'error',
-                    'uid': uid,
-                    'message': f"send failed: {msg}"
-                })
-                continue
-
-            # 等待并解析从机的 43 确认响应
-            time.sleep(0.1)
-            ack = self.serial.send_and_wait(b'', timeout=0.5, min_response_bytes=5)
-            ack_parsed = None
-            if ack and len(ack) >= 5:
-                # 过滤回显/噪声,找到 43 响应帧
-                for i in range(len(ack) - 4):
-                    if ack[i + 1] == 0x43:
-                        chunk = ack[i:i + 5]
-                        ack_parsed = parse_address_assignment_response(chunk)
-                        break
-
-            if ack_parsed and 'error' not in ack_parsed:
-                logger.info(f"UID={uid} 地址={new_address} 已确认 (响应: {ack_parsed.get('raw_data')})")
-                self.stored_devices[uid] = new_address
-                results.append({
-                    'status': 'assigned',
-                    'uid': uid,
-                    'address': new_address,
-                    'request': request.hex(),
-                    'ack': ack_parsed.get('raw_data')
-                })
-            else:
-                # 即使未收到确认也保存地址:部分从机不返回 43,但实际已生效
-                logger.warning(f"UID={uid} 地址={new_address} 未收到 43 确认,仍视为已分配")
-                self.stored_devices[uid] = new_address
-                results.append({
-                    'status': 'assigned',
-                    'uid': uid,
-                    'address': new_address,
-                    'request': request.hex(),
-                    'ack': ack.hex() if ack else None
-                })
-
-        return results
+        with self.config_lock:
+            results = []
+
+            for resp in responses:
+                if 'error' in resp:
+                    results.append({'status': 'error', 'message': resp['error']})
+                    continue
+
+                uid = resp['uid']
+                src_addr = resp.get('source_address', 0)
+                device_address = self.stored_devices.get(uid)
+
+                # 优先使用地址表中已保存的地址;没有则分配下一个可用地址
+                if device_address:
+                    new_address = device_address
+                    logger.info(f"UID={uid} 源地址={src_addr},地址表存在记录,分配已有地址={new_address}")
+                else:
+                    new_address = self._next_available_address()
+                    logger.info(f"UID={uid} 源地址={src_addr},地址表无记录,分配新地址={new_address}")
+
+                request = build_assign_address(bytes.fromhex(uid), new_address)
+                logger.info(f"发送分配地址帧: {request.hex()}")
+                self.serial.flush_input()
+                time.sleep(0.05)
+                success, msg = self.serial.send_raw(request)
+                if not success:
+                    logger.error(f"UID={uid} 分配地址帧发送失败: {msg}")
+                    results.append({
+                        'status': 'error',
+                        'uid': uid,
+                        'message': f"send failed: {msg}"
+                    })
+                    continue
+
+                # 等待并解析从机的 43 确认响应
+                time.sleep(0.1)
+                ack = self.serial.send_and_wait(b'', timeout=0.5, min_response_bytes=5)
+                ack_parsed = None
+                if ack and len(ack) >= 5:
+                    # 过滤回显/噪声,找到 43 响应帧
+                    for i in range(len(ack) - 4):
+                        if ack[i + 1] == 0x43:
+                            chunk = ack[i:i + 5]
+                            ack_parsed = parse_address_assignment_response(chunk)
+                            break
+
+                if ack_parsed and 'error' not in ack_parsed:
+                    logger.info(f"UID={uid} 地址={new_address} 已确认 (响应: {ack_parsed.get('raw_data')})")
+                    self.stored_devices[uid] = new_address
+                    results.append({
+                        'status': 'assigned',
+                        'uid': uid,
+                        'address': new_address,
+                        'request': request.hex(),
+                        'ack': ack_parsed.get('raw_data')
+                    })
+                else:
+                    # 即使未收到确认也保存地址:部分从机不返回 43,但实际已生效
+                    logger.warning(f"UID={uid} 地址={new_address} 未收到 43 确认,仍视为已分配")
+                    self.stored_devices[uid] = new_address
+                    results.append({
+                        'status': 'assigned',
+                        'uid': uid,
+                        'address': new_address,
+                        'request': request.hex(),
+                        'ack': ack.hex() if ack else None
+                    })
+
+            return results
 
 
     def auto_configure(self, timeout: float = None) -> dict:
     def auto_configure(self, timeout: float = None) -> dict:
         """自动配置所有未配置的设备"""
         """自动配置所有未配置的设备"""
-        logger.info("开始自动配置设备...")
+        with self.config_lock:
+            logger.info("开始自动配置设备...")
 
 
-        responses = self.broadcast_query(timeout)
+            responses = self.broadcast_query(timeout)
 
 
-        if not responses:
-            return {
-                'success': True,
-                'message': '未发现任何从机设备',
-                'discovered': 0,
-                'configured': 0,
-                'results': []
-            }
+            if not responses:
+                return {
+                    'success': True,
+                    'message': '未发现任何从机设备',
+                    'discovered': 0,
+                    'configured': 0,
+                    'results': []
+                }
 
 
-        results = self.process_responses(responses)
+            results = self.process_responses(responses)
 
 
-        assigned = sum(1 for r in results if r.get('status') == 'assigned')
-        errors = sum(1 for r in results if r.get('status') == 'error')
+            assigned = sum(1 for r in results if r.get('status') == 'assigned')
+            errors = sum(1 for r in results if r.get('status') == 'error')
 
 
-        return {
-            'success': errors == 0,
-            'discovered': len(responses),
-            'confirmed': 0,
-            'assigned': assigned,
-            'errors': errors,
-            'results': results,
-            'stored_devices': self.get_stored_devices()
-        }
+            return {
+                'success': errors == 0,
+                'discovered': len(responses),
+                'confirmed': 0,
+                'assigned': assigned,
+                'errors': errors,
+                'results': results,
+                'stored_devices': self.get_stored_devices()
+            }
 
 
     def save_config(self, filepath: str) -> bool:
     def save_config(self, filepath: str) -> bool:
         """保存配置到文件"""
         """保存配置到文件"""

+ 0 - 62
install_deps.exp

@@ -1,62 +0,0 @@
-#!/usr/bin/expect -f
-set timeout 600
-set remote_dir [lindex $argv 0]
-set remote_user [lindex $argv 1]
-set remote_host [lindex $argv 2]
-set remote_pass [lindex $argv 3]
-
-spawn ssh $remote_user@$remote_host
-
-expect {
-    "*yes/no*" {
-        send "yes\r"
-        exp_continue
-    }
-    "*password:*" {
-        send "$remote_pass\r"
-    }
-}
-
-expect "*#*"
-# 首先安装系统依赖
-send "apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3-pip python3-dev gcc libffi-dev make git curl net-tools iproute2 nginx mosquitto mosquitto-clients libgpiod2 libgpiod-dev\r"
-expect "*#*"
-
-# 确保后端目录存在
-send "mkdir -p $remote_dir/backend\r"
-expect "*#*"
-
-# 创建Python虚拟环境
-send "python3 -m venv $remote_dir/venv\r"
-expect "*#*"
-
-# 分步安装Python依赖,避免中括号解析问题
-    send "source $remote_dir/venv/bin/activate\r"
-    expect "*#*"
-    send "pip install --upgrade pip\r"
-    expect "*#*"
-    send "pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/\r"
-    expect "*#*"
-    # 先尝试安装requirements.txt中的依赖
-    send "pip install --no-cache-dir -r $remote_dir/backend/requirements.txt || echo '忽略requirements.txt错误,继续安装基础依赖'\r"
-    expect "*#*"
-    # 安装基本包,避免使用带方括号的扩展版本
-    send "pip install --no-cache-dir fastapi\r"
-    expect "*#*"
-    send "pip install --no-cache-dir uvicorn\r"
-    expect "*#*"
-    send "pip install --no-cache-dir pydantic\r"
-    expect "*#*"
-    send "pip install --no-cache-dir python-multipart\r"
-    expect "*#*"
-    send "pip install --no-cache-dir paho-mqtt\r"
-    expect "*#*"
-    send "pip install --no-cache-dir redis\r"
-    expect "*#*"
-    send "pip install --no-cache-dir python-dotenv\r"
-    expect "*#*"
-    send "pip install --no-cache-dir psutil\r"
-    expect "*#*"
-
-send "exit\r"
-expect eof

+ 0 - 195
scp_upload.exp

@@ -1,195 +0,0 @@
-#!/usr/bin/expect -f
-set timeout 600
-# 使用当前目录,不再需要temp_dir参数
-set remote_user [lindex $argv 0]
-set remote_host [lindex $argv 1]
-set remote_dir [lindex $argv 2]
-set remote_pass [lindex $argv 3]
-
-# 创建远程目录
-spawn ssh $remote_user@$remote_host
-
-set connected 0
-set backend_success 0
-set frontend_success 0
-set nginx_config_success 0
-
-expect {
-    "*yes/no*" {
-        send "yes\r"
-        exp_continue
-    }
-    "*password:*" {
-        send "$remote_pass\r"
-        expect {
-            "*#*" {
-                set connected 1
-                puts "SSH连接成功"
-            }
-            timeout {
-                puts "SSH连接失败"
-                exit 1
-            }
-        }
-    }
-    timeout {
-        puts "SSH连接超时"
-        exit 1
-    }
-}
-
-if {$connected} {
-    # 创建远程目录
-    puts "创建远程目录..."
-    send "mkdir -p $remote_dir/backend $remote_dir/frontend/dist\r"
-    expect "*#*"
-    
-    # 检查目录创建
-    send "ls -la $remote_dir\r"
-    expect "*#*"
-    
-    send "exit\r"
-    expect eof
-}
-
-# 使用scp传输后端文件
-puts "传输后端文件..."
-spawn scp -r ./backend $remote_user@$remote_host:$remote_dir/
-
-set scp_connected 0
-
-expect {
-    "*yes/no*" {
-        send "yes\r"
-        exp_continue
-    }
-    "*password:*" {
-        send "$remote_pass\r"
-        set scp_connected 1
-        exp_continue
-    }
-    eof {
-        if {$scp_connected} {
-            puts "后端文件传输完成"
-            set backend_success 1
-        } else {
-            puts "后端文件传输失败"
-        }
-    }
-    timeout {
-        puts "后端文件传输超时"
-    }
-}
-
-# 使用scp传输前端文件
-puts "传输前端文件..."
-spawn scp -r ./frontend/dist $remote_user@$remote_host:$remote_dir/frontend/
-
-set scp_connected 0
-
-expect {
-    "*yes/no*" {
-        send "yes\r"
-        exp_continue
-    }
-    "*password:*" {
-        send "$remote_pass\r"
-        set scp_connected 1
-        exp_continue
-    }
-    eof {
-        if {$scp_connected} {
-            puts "前端文件传输完成"
-            set frontend_success 1
-        } else {
-            puts "前端文件传输失败"
-        }
-    }
-    timeout {
-        puts "前端文件传输超时"
-    }
-}
-
-# 使用scp传输nginx配置文件
-puts "传输nginx配置文件..."
-spawn scp ./nginx_config.conf $remote_user@$remote_host:/etc/nginx/sites-available/dzxj_dtu.conf
-
-set scp_connected 0
-
-expect {
-    "*yes/no*" {
-        send "yes\r"
-        exp_continue
-    }
-    "*password:*" {
-        send "$remote_pass\r"
-        set scp_connected 1
-        exp_continue
-    }
-    eof {
-        if {$scp_connected} {
-            puts "nginx配置文件传输完成"
-            set nginx_config_success 1
-        } else {
-            puts "nginx配置文件传输失败"
-        }
-    }
-    timeout {
-        puts "nginx配置文件传输超时"
-    }
-}
-
-# 验证传输结果
-puts "验证文件传输结果..."
-spawn ssh $remote_user@$remote_host
-
-expect {
-    "*yes/no*" {
-        send "yes\r"
-        exp_continue
-    }
-    "*password:*" {
-        send "$remote_pass\r"
-        expect "*#*"
-    }
-}
-
-# 详细检查后端文件数量和内容
-puts "详细检查后端文件..."
-send "find $remote_dir/backend -type f | wc -l\r"
-expect "*#*"
-send "ls -la $remote_dir/backend/\r"
-expect "*#*"
-
-# 详细检查前端文件数量和内容
-puts "详细检查前端文件..."
-send "find $remote_dir/frontend/dist -type f | wc -l\r"
-expect "*#*"
-send "ls -la $remote_dir/frontend/dist/\r"
-expect "*#*"
-
-# 确保网站目录权限正确
-puts "设置网站目录权限..."
-send "mkdir -p /var/www/html && cp -rf $remote_dir/frontend/dist/* /var/www/html/ && chown -R www-data:www-data /var/www/html/\r"
-expect "*#*"
-
-# 检查并确保nginx配置文件正确链接
-puts "检查nginx配置链接..."
-send "ln -sf /etc/nginx/sites-available/dzxj_dtu.conf /etc/nginx/sites-enabled/dzxj_dtu.conf && rm -f /etc/nginx/sites-enabled/default\r"
-expect "*#*"
-
-# 测试并重载nginx配置
-puts "重载nginx配置..."
-send "nginx -t && nginx -s reload\r"
-expect "*#*"
-
-send "exit\r"
-expect eof
-
-# 初始化状态变量为成功
-set backend_success 1
-set frontend_success 1
-set nginx_config_success 1
-
-# 返回成功结果
-exit 0