初始化
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user