初始化

This commit is contained in:
2026-08-08 18:28:49 +08:00
parent 9bef4420e0
commit f082037a6e
854 changed files with 217171 additions and 11 deletions
+918
View File
@@ -0,0 +1,918 @@
<?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());
}
}
}