Files
2026-08-08 18:28:49 +08:00

919 lines
33 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\controllers;
use core\base\Controller;
use app\models\AsdTestData;
use app\models\AsdDevice;
/**
* 昂盛达上位机 MES 接口 API
* 接口文档版本:1.5
*
* 工作流:UserInfoVerification → GetWorkInfo → CheckFlow → UpDataInfo
*
* 接口地址:http://host/Api/{method}
* 协议:HTTP POST
* 数据格式:JSON
*/
class ApiController extends Controller
{
/**
* 构造函数:所有 API 响应统一输出 JSON。
* 注意:框架 Core::route() 不会自动调用 init(),因此此处设置 Content-Type。
*/
public function __construct($controller, $action)
{
parent::__construct($controller, $action);
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
// 注册 API 专用异常处理器:PDO 处于 ERRMODE_EXCEPTION 模式,
// 任何未捕获异常(如缺列、连接异常)都会冒泡成 500 显示「系统内部错误」。
// 这里覆盖为 JSON 响应,既避免 500,又把真实错误暴露给调用方便于排查。
set_exception_handler(function ($e) {
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
http_response_code(500);
$msg = '服务器处理异常';
if (APP_DEBUG && $e instanceof \Throwable) {
$msg .= ': ' . $e->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());
}
}
}