getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine(); } echo json_encode(['Result' => 0, 'Message' => $msg], JSON_UNESCAPED_UNICODE); exit; }); } /** * 验证 API Token * * 与 generateToken() 一致,统一使用 DGZXY_asd_api_token 表。 * 开放模式(符合接口文档 v1.5「鉴权为可选项」): * - 未携带 Token:放行(返回 null),便于设备直接上传; * - 携带了 Token 但无效/过期:拒绝。 * * @return array|null 成功/开放模式返回用户信息或 null,失败直接 exit */ protected function apiAuth() { // 从 Authorization 头获取 Token $token = ''; $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) { $token = trim($matches[1]); } // 也支持 POST/GET 参数传递 token(向后兼容) if (empty($token)) { $input = $this->getJsonInput(); $token = $input['token'] ?? $_GET['token'] ?? ''; } // 开放模式:未携带 Token 直接放行 if (empty($token)) { return null; } // 携带了 Token,则必须有效 try { $pdo = \core\db\Db::pdo(); $stmt = $pdo->prepare("SELECT * FROM DGZXY_asd_api_token WHERE token = :token"); $stmt->execute([':token' => $token]); $tokenInfo = $stmt->fetch(\PDO::FETCH_ASSOC); } catch (\Throwable $e) { if (APP_DEBUG) { error_log('API auth error: ' . $e->getMessage()); } $this->jsonResponse(0, '认证服务异常'); exit; } if (!$tokenInfo) { $this->jsonResponse(0, 'Token无效或已过期'); exit; } return [ 'id' => 0, 'emp_no' => $tokenInfo['emp_no'] ?? '', 'emp_name' => $tokenInfo['tester'] ?? '', ]; } // ==================== 工具方法 ==================== /** * 频率限制(基于文件滑动窗口,按 IP + key 计数) * @param string $key 业务标识 * @param int $max 时间窗口内最大允许次数 * @param int $window 时间窗口(秒) * @return bool true=已超限需拒绝 */ private function rateLimitExceeded($key, $max, $window) { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $dir = sys_get_temp_dir() . '/mes2_ratelimit'; if (!is_dir($dir)) { @mkdir($dir, 0700, true); } if (!is_dir($dir)) { return false; // 无法记录时放行,不影响业务 } $file = $dir . '/' . md5($key . '|' . $ip); $now = time(); $hits = []; if (file_exists($file)) { $raw = @file_get_contents($file); $hits = $raw ? json_decode($raw, true) ?: [] : []; } // 清除窗口外的记录 $hits = array_filter($hits, function ($t) use ($now, $window) { return ($now - $t) < $window; }); if (count($hits) >= $max) { return true; } $hits[] = $now; @file_put_contents($file, json_encode($hits)); return false; } /** * API 默认入口:不暴露任何方法列表,返回干净的 404 */ public function index() { if (!headers_sent()) { header('HTTP/1.1 404 Not Found'); } $this->jsonResponse(0, 'API endpoint not found'); } /** * 通用 JSON 响应 */ private function jsonResponse($result, $message, $extra = []) { if (!headers_sent()) { header('Content-Type: application/json; charset=utf-8'); } $data = array_merge(['Result' => (int)$result, 'Message' => $message], $extra); echo json_encode($data, JSON_UNESCAPED_UNICODE); } /** * 获取原始 JSON 请求体 */ private function getJsonInput() { $input = file_get_contents('php://input'); $data = json_decode($input, true); return is_array($data) ? $data : []; } /** * 生成持久化 Token(存入数据库以便后续校验) */ private function generateToken($empNo, $tester) { $token = bin2hex(random_bytes(32)); // 使用加密安全的随机数替代 md5 try { $pdo = \core\db\Db::pdo(); $stmt = $pdo->prepare( "INSERT INTO DGZXY_asd_api_token (token, emp_no, tester, created_at) VALUES (:token, :empNo, :tester, NOW()) ON DUPLICATE KEY UPDATE tester = :tester2, created_at = NOW()" ); $stmt->execute([ ':token' => $token, ':empNo' => $empNo, ':tester' => $tester, ':tester2' => $tester, ]); } catch (\Throwable $e) { // Token 表不存在时记录日志,但仍返回 token // 注意:verifyToken 也会因表不存在而拒绝,保证一致性 if (APP_DEBUG) { error_log('API Token table error: ' . $e->getMessage()); } } return $token; } /** * 校验 Token 是否有效 * 安全修复:表不存在时拒绝通过(而非降级放行) */ private function verifyToken($token, $tester = '') { if (empty($token)) return false; try { $pdo = \core\db\Db::pdo(); $sql = "SELECT * FROM DGZXY_asd_api_token WHERE token = :token"; $params = [':token' => $token]; if (!empty($tester)) { $sql .= " AND tester = :tester"; $params[':tester'] = $tester; } $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->fetch(\PDO::FETCH_ASSOC) ?: false; } catch (\Throwable $e) { // 安全修复:表不存在时拒绝通过,而非降级放行 if (APP_DEBUG) { error_log('API Token verification error: ' . $e->getMessage()); } return false; } } // ==================== 接口 1: 用户登录身份信息校验 ==================== /** * 1. AsdUserInfoVerification - 用户登录系统身份信息校验 * POST /Api/asdUserInfoVerification * * 请求参数 (JSON): * Parameter1 - 参数1(员工编号/工号,扫码或手动输入) * Parameter2 - 参数2(密码或其它验证信息,可选) * Parameter3 - 参数3(可选) * Parameter4 - 参数4(可选) * * 成功响应: * Result=1, Message="验证成功", Tester="张三", Token="xxx" * 失败响应: * Result=0, Message="工号不存在" 或 "密码错误" */ public function asdUserInfoVerification() { // 频率限制:同一 IP 60 秒内最多 10 次,防止枚举/暴力破解 if ($this->rateLimitExceeded('asdUserInfoVerification', 10, 60)) { $this->jsonResponse(0, '尝试过于频繁,请稍后重试'); return; } $data = $this->getJsonInput(); $empNo = $data['Parameter1'] ?? ''; $password = $data['Parameter2'] ?? ''; $param3 = $data['Parameter3'] ?? ''; $param4 = $data['Parameter4'] ?? ''; // 安全修复(P0):API 认证必须同时提供工号与密码,禁止无密码签发 Token if (empty($empNo)) { $this->jsonResponse(0, '工号(Parameter1)不能为空'); return; } if (empty($password)) { $this->jsonResponse(0, '缺少密码(Parameter2),API 认证必须提供密码'); return; } $employee = new \app\models\Employee(); $emp = $employee->findByEmpNo($empNo); if (!$emp) { $this->jsonResponse(0, '工号不存在: ' . $empNo); return; } // 严格校验密码(bcrypt 验证) $valid = $employee->validateLogin($empNo, $password); if (!$valid) { $this->jsonResponse(0, '密码错误'); return; } $token = $this->generateToken($empNo, $emp['emp_name']); $this->jsonResponse(1, '验证成功', [ 'Tester' => $emp['emp_name'], 'Token' => $token, ]); } // ==================== 接口 2: 获取站点/工位/关联数据 ==================== /** * 2. AsdGetWorkInfo - 获取站点、工位或其它关联数据 * POST /Api/asdGetWorkInfo * * 请求参数 (JSON): * Parameter1 - 参数1(条码/二维码,用于查找关联工单) * Parameter2 - 参数2(工位编码,可选) * Parameter3 - 参数3(设备编码,可选) * Parameter4 - 参数4(可选) * * 成功响应: * Result=1, Message="获取成功", Mo="xxx", Site="xxx", Station="xxx", WorkOrder="xxx" */ public function asdGetWorkInfo() { $this->apiAuth(); $data = $this->getJsonInput(); $param1 = $data['Parameter1'] ?? ''; // 条码/二维码 $param2 = $data['Parameter2'] ?? ''; // 工位编码 $param3 = $data['Parameter3'] ?? ''; // 设备编码 $param4 = $data['Parameter4'] ?? ''; // 备用 $mo = ''; $site = ''; $station = ''; $workOrder = ''; try { $pdo = \core\db\Db::pdo(); // 1. 如果有条码,查询该条码已关联的制令单/工单 if (!empty($param1)) { $stmt = $pdo->prepare( "SELECT mo, site, station, work_order FROM DGZXY_asd_test_record WHERE qr_code = :qc ORDER BY id DESC LIMIT 1" ); $stmt->execute([':qc' => $param1]); $record = $stmt->fetch(\PDO::FETCH_ASSOC); if ($record && !empty($record['mo'])) { $mo = $record['mo']; $site = $record['site']; $station = $record['station']; $workOrder = $record['work_order']; } } // 2. 根据设备编码查找默认工位 if (!empty($param3)) { $deviceModel = new AsdDevice(); $device = $deviceModel->findByCode($param3); if ($device) { if (empty($station) && !empty($device['station_code'])) { $station = $device['station_code']; } if (empty($site) && !empty($device['line_name'])) { $site = $device['line_name']; } } } // 3. 根据传入工位编码查工位 if (empty($station) && !empty($param2)) { $station = $param2; } // 4. 从工位表获取详细信息 $workstationModel = new \app\models\Workstation(); $allStations = $workstationModel->getAll(); // 自动补全 site 和 station if ((empty($site) || empty($station)) && !empty($allStations)) { $first = $allStations[0]; if (empty($site)) $site = $first['station_name'] ?? ''; if (empty($station)) $station = $first['station_code'] ?? ''; } // 5. 生成制令单号(如果未找到已关联的) if (empty($mo)) { $stmt = $pdo->prepare( "SELECT MAX(mo) AS last_mo FROM DGZXY_asd_test_record WHERE mo LIKE :prefix" ); $todayPrefix = 'MO' . date('Ymd') . '%'; $stmt->execute([':prefix' => $todayPrefix]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!empty($row['last_mo'])) { $lastSeq = intval(substr($row['last_mo'], 10)); $mo = 'MO' . date('Ymd') . str_pad($lastSeq + 1, 3, '0', STR_PAD_LEFT); } else { $mo = 'MO' . date('Ymd') . '001'; } } // 6. 生成工单号 if (empty($workOrder)) { $stmt = $pdo->prepare( "SELECT MAX(work_order) AS last_wo FROM DGZXY_asd_test_record WHERE work_order LIKE :prefix" ); $woPrefix = 'WO-' . date('Ymd') . '%'; $stmt->execute([':prefix' => $woPrefix]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!empty($row['last_wo'])) { $lastSeq = intval(substr($row['last_wo'], 12)); $workOrder = 'WO-' . date('Ymd') . '-' . str_pad($lastSeq + 1, 2, '0', STR_PAD_LEFT); } else { $workOrder = 'WO-' . date('Ymd') . '-01'; } } } catch (\Throwable $e) { // 容错:即使数据库异常,也返回可用数据 if (empty($mo)) $mo = 'MO' . date('Ymd') . '001'; if (empty($workOrder)) $workOrder = 'WO-' . date('Ymd') . '-01'; } $this->jsonResponse(1, '获取成功', [ 'Mo' => $mo, 'Site' => $site, 'Station' => $station, 'WorkOrder' => $workOrder, ]); } // ==================== 接口 3: 数据检验/工艺检查 ==================== /** * 3. AsdCheckFlow - 数据检验、工艺检查 * POST /Api/asdCheckFlow * * 请求参数 (JSON): * QrCode - 条码/二维码(有则必传) * Mo - GetWorkInfo 返回的 Mo * Site - GetWorkInfo 返回的 Site * Station - GetWorkInfo 返回的 Station * WorkOrder - GetWorkInfo 返回的 WorkOrder * Group - 组别(必填) * Device - 设备序列号(必填) * * 成功响应: * Result=1, QrCode="xxx", Message="检验通过,可以开始测试" * 失败响应: * Result=0, QrCode="xxx", Message="该条码已完成测试" / "设备未注册" / ... */ public function asdCheckFlow() { $this->apiAuth(); $data = $this->getJsonInput(); $qrCode = $data['QrCode'] ?? ''; $mo = $data['Mo'] ?? ''; $site = $data['Site'] ?? ''; $station = $data['Station'] ?? ''; $workOrder = $data['WorkOrder'] ?? ''; $group = $data['Group'] ?? ''; $device = $data['Device'] ?? ''; // 1. 必填字段校验:Group & Device if (empty($group)) { $this->jsonResponse(0, '组别(Group)不能为空', ['QrCode' => $qrCode]); return; } if (empty($device)) { $this->jsonResponse(0, '设备序列号(Device)不能为空', ['QrCode' => $qrCode]); return; } // 2. 设备存在性校验(开放模式:设备未注册不阻断,仅做组别一致性校验) $deviceModel = new AsdDevice(); try { $deviceRecord = $deviceModel->isActive($device); if ($deviceRecord && !empty($group) && !empty($deviceRecord['group_name']) && $deviceRecord['group_name'] !== $group) { // 设备已注册但组别与提交不一致:拒绝(数据完整性保护) $this->jsonResponse(0, "设备 {$device} 属于组别 [{$deviceRecord['group_name']}],与提交组别 [{$group}] 不匹配", [ 'QrCode' => $qrCode, ]); return; } // 设备未注册时放行(开放接口,便于先上线采集再补录设备) } catch (\Throwable $e) { // 设备表异常时不阻止流程 } // 3. 条码防重校验(如果有条码) if (!empty($qrCode)) { try { $testData = new AsdTestData(); $existing = $testData->query( "SELECT id, status FROM DGZXY_asd_test_record WHERE qr_code = :qc ORDER BY id DESC LIMIT 1", [':qc' => $qrCode] ); if (!empty($existing)) { $this->jsonResponse(0, '该条码已完成测试,请勿重复测试', [ 'QrCode' => $qrCode, ]); return; } } catch (\Throwable $e) { // 容错 } } $this->jsonResponse(1, '检验通过,可以开始测试', [ 'QrCode' => $qrCode, ]); } // ==================== 接口 4: 提交测试数据 ==================== /** * 4. AsdUpDataInfo - 提交测试数据(核心接口) * POST /Api/asdUpDataInfo * * 请求参数 (JSON): * Tester - 测试员 * QrCode - 条码/二维码 * Mo - 制令单 * Site - 工位 * Station - 站点 * WorkOrder - 工单 * ProjectName - 项目名称 * Status - 测试状态 PASS/NG(必填) * Group - 组别(必填) * Device - 设备号(必填) * RunningTime - 运行时间(必填) * TestTime - 测试时间(必填) * DataList - 测试工步数据(必填) * * 成功响应: * Result=1, Message="测试数据上传成功", RecordId="xxx", DetailCount=N * 失败响应: * Result=0, Message="错误描述" */ public function asdUpDataInfo() { $this->apiAuth(); $data = $this->getJsonInput(); // ===== 必填字段校验 ===== // Status 必填 if (empty($data['Status'])) { $this->jsonResponse(0, '测试状态(Status)是必填项,值必须为 PASS 或 NG'); return; } $status = strtoupper($data['Status']); if (!in_array($status, ['PASS', 'NG'])) { $this->jsonResponse(0, 'Status 值无效,必须为 PASS 或 NG,当前值: ' . $status); return; } // Group 必填 if (empty($data['Group'])) { $this->jsonResponse(0, '组别(Group)是必填项'); return; } // Device 必填 if (empty($data['Device'])) { $this->jsonResponse(0, '设备号(Device)是必填项'); return; } // RunningTime 必填 if (empty($data['RunningTime'])) { $this->jsonResponse(0, '运行时间(RunningTime)是必填项'); return; } // TestTime 必填 if (empty($data['TestTime'])) { $this->jsonResponse(0, '测试时间(TestTime)是必填项'); return; } // DataList 必填且为数组 if (empty($data['DataList']) || !is_array($data['DataList'])) { $this->jsonResponse(0, '测试数据(DataList)是必填项,且必须为数组'); return; } // ===== 设备存在性校验(可选但推荐) ===== $deviceCode = $data['Device']; try { $deviceModel = new AsdDevice(); $deviceRecord = $deviceModel->isActive($deviceCode); if (!$deviceRecord) { // 不阻止上传,但记录警告信息 // 需要在 asd_device 表中预先注册设备 } } catch (\Throwable $e) { // 设备表异常时不影响主流程 } // ===== 写入数据库 ===== try { $testData = new AsdTestData(); // 插入主记录 $recordId = $testData->addRecord([ 'tester' => $data['Tester'] ?? '', 'qr_code' => $data['QrCode'] ?? '', 'mo' => $data['Mo'] ?? '', 'site' => $data['Site'] ?? '', 'station' => $data['Station'] ?? '', 'work_order' => $data['WorkOrder'] ?? '', 'project_name' => $data['ProjectName'] ?? '', 'status' => $status, 'group_name' => $data['Group'], 'device' => $deviceCode, 'running_time' => $data['RunningTime'], 'test_time' => $data['TestTime'], ]); if (!$recordId) { $err = $testData->getError(); $this->jsonResponse(0, '保存测试记录失败: ' . ($err ?: '数据库写入错误')); return; } // 插入工步详情 $detailCount = 0; foreach ($data['DataList'] as $item) { $detailResult = $testData->addDetail([ 'record_id' => $recordId, 'seq' => $item['Seq'] ?? ++$detailCount, 'test_name' => $item['TestName'] ?? '', 'test_item' => $item['TestItem'] ?? '', 'test_units' => $item['TestUnits'] ?? '', 'data_value' => $item['DataValue'] ?? null, 'lower_limit' => $item['LowerLimit'] ?? null, 'upper_limit' => $item['UpperLimit'] ?? null, 'test_value' => $item['TestValue'] ?? '', 'test_limit' => $item['TestLimit'] ?? '', 'test_result' => $item['TestResult'] ?? 'PASS', ]); if ($detailResult) { $detailCount++; } } $this->jsonResponse(1, '测试数据上传成功', [ 'RecordId' => $recordId, 'DetailCount' => $detailCount, ]); } catch (\Throwable $e) { // PDO 异常模式:缺列/约束等会抛异常,这里转成 JSON 错误而非 500 $this->jsonResponse(0, '保存测试记录异常: ' . $e->getMessage()); } } // ==================== 接口 5: 获取设备列表 ==================== /** * 5. AsdGetDevices - 获取已注册设备列表(供扫码选择使用) * POST /Api/asdGetDevices * * 请求参数 (JSON): * Keyword - 搜索关键词(可选,模糊匹配设备编码/名称/IP/组别) * * 返回: * Result=1, Devices=[{id, device_code, device_name, device_ip, group_name, status}, ...] */ public function asdGetDevices() { $this->apiAuth(); $data = $this->getJsonInput(); $keyword = $data['Keyword'] ?? ''; try { $pdo = \core\db\Db::pdo(); if (!empty($keyword)) { $like = '%' . $keyword . '%'; $stmt = $pdo->prepare( "SELECT id, device_code, device_name, device_ip, device_type, group_name, line_name, station_code, status, remark FROM DGZXY_asd_device WHERE device_code LIKE :kw1 OR device_name LIKE :kw2 OR device_ip LIKE :kw3 OR group_name LIKE :kw4 ORDER BY group_name ASC, device_code ASC" ); $stmt->execute([':kw1' => $like, ':kw2' => $like, ':kw3' => $like, ':kw4' => $like]); } else { $stmt = $pdo->prepare("SELECT id,device_code,device_name,device_ip,device_type,group_name,line_name,station_code,status,remark FROM DGZXY_asd_device ORDER BY group_name ASC, device_code ASC"); $stmt->execute(); } $devices = $stmt->fetchAll(\PDO::FETCH_ASSOC); } catch (\Throwable $e) { $devices = []; } $this->jsonResponse(1, '获取成功', ['Devices' => $devices]); } // ==================== 接口 6: 设备连接测试 ==================== /** * 6. AsdDeviceConnect - 连接设备并获取设备信息 * POST /Api/asdDeviceConnect * * 请求参数 (JSON): * DeviceCode - 设备编码(必填) * DeviceIp - 设备IP(可选,传入后自动更新设备IP) * * 成功响应: * Result=1, Message="设备连接成功", * Device={device_code, device_name, device_ip, group_name, status}, * ConnectionInfo={ip, port, reachable, latency_ms} */ public function asdDeviceConnect() { $this->apiAuth(); $data = $this->getJsonInput(); $deviceCode = $data['DeviceCode'] ?? ''; $deviceIp = $data['DeviceIp'] ?? ''; if (empty($deviceCode)) { $this->jsonResponse(0, '设备编码(DeviceCode)不能为空'); return; } try { $deviceModel = new AsdDevice(); $device = $deviceModel->findByCode($deviceCode); if (!$device) { $this->jsonResponse(0, '设备不存在: ' . $deviceCode); return; } if ($device['status'] != 1) { $this->jsonResponse(0, '设备已禁用: ' . $deviceCode); return; } // 如果传入了新 IP,更新到数据库 if (!empty($deviceIp) && $deviceIp !== ($device['device_ip'] ?? '')) { $deviceModel->updateDevice($device['id'], ['device_ip' => $deviceIp]); $device['device_ip'] = $deviceIp; } // 检查 IP 是否存在 if (empty($device['device_ip'])) { $this->jsonResponse(0, '设备 IP 未设置,请先填写设备 IP 地址', [ 'Device' => $device, ]); return; } // 尝试 TCP 连接(如果有 socket 扩展) $reachable = false; $latency = 0; $port = 502; // 默认 Modbus TCP 端口 if (function_exists('fsockopen')) { $start = microtime(true); $errno = 0; $errstr = ''; $fp = @fsockopen($device['device_ip'], $port, $errno, $errstr, 3); if ($fp) { $reachable = true; $latency = round((microtime(true) - $start) * 1000, 1); fclose($fp); } } else { // 无 socket 时,仅做数据库校验即视为"逻辑连接成功" $reachable = true; $latency = 0; $port = 0; } $this->jsonResponse(1, $reachable ? '设备连接成功' : '设备网络不可达,请检查 IP 和网络', [ 'Device' => $device, 'ConnectionInfo' => [ 'ip' => $device['device_ip'], 'port' => $reachable ? $port : 502, 'reachable' => $reachable, 'latency_ms' => $latency, ], ]); } catch (\Throwable $e) { $this->jsonResponse(0, '连接异常: ' . $e->getMessage()); } } // ==================== 接口 7: 批量读取设备数据 ==================== /** * 7. AsdBatchRead - 批量读取指定设备的测试数据 * POST /Api/asdBatchRead * * 请求参数 (JSON): * DeviceCodes - 设备编码列表(数组,如 ["ASD-989A-001","ASD-989A-002"]) * Limit - 每台设备最多返回条数(默认 20) * * 返回: * Result=1, DevicesData={ * "ASD-989A-001": {device_info:{...}, records:[{id,qr_code,status,step_count,test_time,...}], total:N} * } */ public function asdBatchRead() { $this->apiAuth(); $data = $this->getJsonInput(); $deviceCodes = $data['DeviceCodes'] ?? []; $limit = intval($data['Limit'] ?? 20); if (empty($deviceCodes) || !is_array($deviceCodes)) { $this->jsonResponse(0, '设备编码列表(DeviceCodes)不能为空,且必须为数组'); return; } $devicesData = []; try { $pdo = \core\db\Db::pdo(); foreach ($deviceCodes as $code) { // 获取设备信息 $deviceModel = new AsdDevice(); $device = $deviceModel->findByCode($code); // 获取该设备的测试记录(含统计) $stmt = $pdo->prepare( "SELECT r.id, r.qr_code, r.product_model AS project_name, r.tester, r.site, r.device_code AS device, r.test_result AS status, r.test_date AS test_time, r.running_time, COUNT(d.id) as step_count, SUM(CASE WHEN d.test_result = 'FAIL' THEN 1 ELSE 0 END) as fail_count FROM DGZXY_asd_test_record r LEFT JOIN DGZXY_asd_test_detail d ON r.id = d.record_id WHERE r.device_code = :code GROUP BY r.id ORDER BY r.id DESC LIMIT " . intval($limit) ); $stmt->execute([':code' => $code]); $records = $stmt->fetchAll(\PDO::FETCH_ASSOC); // 总数 $countStmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM DGZXY_asd_test_record WHERE device = :code"); $countStmt->execute([':code' => $code]); $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0; $devicesData[$code] = [ 'device_info' => $device, 'records' => $records, 'total' => (int)$total, ]; } } catch (\Throwable $e) { $this->jsonResponse(0, '批量读取异常: ' . $e->getMessage()); return; } $this->jsonResponse(1, '批量读取成功', [ 'DevicesData' => $devicesData, 'DeviceCount' => count($deviceCodes), ]); } // ==================== 接口 8: 删除设备数据 ==================== /** * 8. AsdDeviceDataDelete - 按设备删除测试数据 * POST /Api/asdDeviceDataDelete * * 请求参数 (JSON): * DeviceCode - 设备编码(必填) * Confirm - 确认删除(必须为 "YES") * * 返回: * Result=1, DeletedCount=N, Message="已删除 N 条测试记录" */ public function asdDeviceDataDelete() { $this->apiAuth(); $data = $this->getJsonInput(); $deviceCode = $data['DeviceCode'] ?? ''; $confirm = $data['Confirm'] ?? ''; if (empty($deviceCode)) { $this->jsonResponse(0, '设备编码(DeviceCode)不能为空'); return; } if ($confirm !== 'YES') { $this->jsonResponse(0, '请确认删除操作(Confirm=YES)'); return; } try { $pdo = \core\db\Db::pdo(); // 先查该设备有多少条记录 $countStmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM DGZXY_asd_test_record WHERE device_code = :code"); $countStmt->execute([':code' => $deviceCode]); $recordCount = $countStmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0; if ($recordCount == 0) { $this->jsonResponse(1, '该设备无数据可删除', ['DeletedCount' => 0]); return; } // 获取所有记录 ID $idsStmt = $pdo->prepare("SELECT id FROM DGZXY_asd_test_record WHERE device_code = :code"); $idsStmt->execute([':code' => $deviceCode]); $ids = $idsStmt->fetchAll(\PDO::FETCH_COLUMN); // 删除详情(级联) $placeholders = implode(',', array_fill(0, count($ids), '?')); $pdo->prepare("DELETE FROM DGZXY_asd_test_detail WHERE record_id IN ({$placeholders})")->execute($ids); // 删除主记录 $pdo->prepare("DELETE FROM DGZXY_asd_test_record WHERE device_code = :code")->execute([':code' => $deviceCode]); $this->jsonResponse(1, "已删除 {$recordCount} 条测试记录", [ 'DeletedCount' => (int)$recordCount, ]); } catch (\Throwable $e) { $this->jsonResponse(0, '删除失败: ' . $e->getMessage()); } } }