1034 lines
37 KiB
PHP
1034 lines
37 KiB
PHP
<?php
|
||
namespace app\controllers;
|
||
|
||
use core\base\Controller;
|
||
use app\models\Employee;
|
||
use app\models\Workstation;
|
||
use app\models\StationDefinition;
|
||
use app\models\ProductModel;
|
||
use app\models\Customer;
|
||
use app\models\EmployeePermission;
|
||
|
||
/**
|
||
* 微信小程序 API 控制器
|
||
*
|
||
* 为 MES2 系统提供小程序端 JSON API,复用现有 Model 层
|
||
*
|
||
* 认证方式:Token (Bearer) — 登录后返回 token,后续请求通过 Authorization 头携带
|
||
* Token 存储在 DGZXY_miniapp_token 表中
|
||
*
|
||
* 响应格式:{ code: 0|1, message: "...", data: {...} }
|
||
* code=1 成功,code=0 失败
|
||
*/
|
||
class MiniappController extends Controller
|
||
{
|
||
// ==================== 构造函数 & CORS ====================
|
||
|
||
/**
|
||
* 覆盖基类构造函数:设置 CORS 和 JSON 响应头
|
||
*/
|
||
public function __construct($controller, $action)
|
||
{
|
||
parent::__construct($controller, $action);
|
||
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Access-Control-Allow-Origin: *');
|
||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||
|
||
// 处理 OPTIONS 预检请求
|
||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||
http_response_code(204);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// ==================== 工具方法 ====================
|
||
|
||
/**
|
||
* 成功响应
|
||
*/
|
||
private function success($data = [], $message = 'ok')
|
||
{
|
||
echo json_encode([
|
||
'code' => 1,
|
||
'message' => $message,
|
||
'data' => $data,
|
||
], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* 失败响应
|
||
*/
|
||
private function fail($message, $code = 0, $data = [])
|
||
{
|
||
echo json_encode([
|
||
'code' => $code,
|
||
'message' => $message,
|
||
'data' => $data,
|
||
], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* 获取 JSON 请求体
|
||
*/
|
||
private function getJsonInput()
|
||
{
|
||
$input = file_get_contents('php://input');
|
||
$data = json_decode($input, true);
|
||
return is_array($data) ? $data : [];
|
||
}
|
||
|
||
/**
|
||
* Token 认证 — 验证请求中的 Bearer Token
|
||
* @return array 用户信息 {id, emp_no, emp_name, role}
|
||
*/
|
||
protected function tokenAuth()
|
||
{
|
||
$token = '';
|
||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
||
if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||
$token = $matches[1];
|
||
}
|
||
|
||
if (empty($token)) {
|
||
$this->fail('未登录,请先登录');
|
||
}
|
||
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
$stmt = $pdo->prepare(
|
||
"SELECT t.*, e.emp_name, e.role
|
||
FROM DGZXY_miniapp_token t
|
||
JOIN DGZXY_employee e ON t.employee_id = e.id
|
||
WHERE t.token = :token AND t.expires_at > NOW() AND t.status = 1"
|
||
);
|
||
$stmt->execute([':token' => $token]);
|
||
$tokenInfo = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||
|
||
if (!$tokenInfo) {
|
||
$this->fail('Token无效或已过期,请重新登录', 401);
|
||
}
|
||
|
||
return [
|
||
'id' => (int)$tokenInfo['employee_id'],
|
||
'emp_no' => $tokenInfo['emp_no'],
|
||
'emp_name' => $tokenInfo['emp_name'],
|
||
'role' => $tokenInfo['role'],
|
||
];
|
||
} catch (\Throwable $e) {
|
||
$this->fail('认证服务异常');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成 Token(登录成功后调用)
|
||
*/
|
||
private function generateToken($employeeId, $empNo)
|
||
{
|
||
$token = bin2hex(random_bytes(32));
|
||
$expiresAt = date('Y-m-d H:i:s', time() + 7 * 24 * 3600); // 7天有效期
|
||
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
// 使旧 token 失效
|
||
$pdo->prepare("UPDATE DGZXY_miniapp_token SET status = 0 WHERE employee_id = :eid")
|
||
->execute([':eid' => $employeeId]);
|
||
// 插入新 token
|
||
$pdo->prepare(
|
||
"INSERT INTO DGZXY_miniapp_token (token, employee_id, emp_no, expires_at, status, created_at)
|
||
VALUES (:token, :eid, :emp_no, :expires, 1, NOW())"
|
||
)->execute([
|
||
':token' => $token,
|
||
':eid' => $employeeId,
|
||
':emp_no' => $empNo,
|
||
':expires' => $expiresAt,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
// Token 表不存在时创建
|
||
if (strpos($e->getMessage(), 'exist') !== false || strpos($e->getMessage(), 'not found') !== false) {
|
||
$this->createTokenTable();
|
||
return $this->generateToken($employeeId, $empNo);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'token' => $token,
|
||
'expires_at' => $expiresAt,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 创建 Token 表(首次运行时)
|
||
*/
|
||
private function createTokenTable()
|
||
{
|
||
$pdo = \core\db\Db::pdo();
|
||
$pdo->exec("
|
||
CREATE TABLE IF NOT EXISTS DGZXY_miniapp_token (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
token VARCHAR(128) NOT NULL,
|
||
employee_id INT NOT NULL,
|
||
emp_no VARCHAR(50) NOT NULL,
|
||
expires_at DATETIME NOT NULL,
|
||
status TINYINT DEFAULT 1,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_token (token),
|
||
INDEX idx_employee (employee_id),
|
||
INDEX idx_expires (expires_at)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||
");
|
||
}
|
||
|
||
/**
|
||
* 检查工位权限
|
||
*/
|
||
private function checkStationPermission($user, $stationType)
|
||
{
|
||
if ($user['role'] === Controller::ROLE_SUPER_ADMIN || $user['role'] === Controller::ROLE_ADMIN) {
|
||
return true;
|
||
}
|
||
$permModel = new EmployeePermission();
|
||
return $permModel->canAccess($user['id'], $user['role'], $stationType);
|
||
}
|
||
|
||
/**
|
||
* 获取工位字段配置
|
||
*/
|
||
private function getStationFieldConfig($stationType)
|
||
{
|
||
$ws = new Workstation();
|
||
$station = $ws->getByType($stationType);
|
||
if ($station && !empty($station['fields_config'])) {
|
||
return parseFieldsConfig($stationType, $station['fields_config']);
|
||
}
|
||
return getDefaultFieldsConfig($stationType);
|
||
}
|
||
|
||
/**
|
||
* 获取用户可访问的工位列表
|
||
*/
|
||
private function getAllowedStations($user)
|
||
{
|
||
$workstation = new Workstation();
|
||
$allStations = $workstation->getAll();
|
||
|
||
if ($user['role'] === Controller::ROLE_SUPER_ADMIN || $user['role'] === Controller::ROLE_ADMIN) {
|
||
return $allStations;
|
||
}
|
||
|
||
$permModel = new EmployeePermission();
|
||
$allowedTypes = $permModel->getAllowedStations($user['id']);
|
||
|
||
if (empty($allowedTypes)) {
|
||
return $allStations;
|
||
}
|
||
|
||
return array_values(array_filter($allStations, function($s) use ($allowedTypes) {
|
||
return in_array($s['station_type'], $allowedTypes);
|
||
}));
|
||
}
|
||
|
||
// ==================== 1. 登录 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/login
|
||
* 请求: { emp_no: "员工编号", password: "密码" }
|
||
* 响应: { code:1, data: { token, expires_at, user: { id, emp_no, emp_name, role } } }
|
||
*/
|
||
public function login()
|
||
{
|
||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||
$this->fail('请使用 POST 请求');
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
$empNo = trim($data['emp_no'] ?? '');
|
||
$password = $data['password'] ?? '';
|
||
|
||
if (empty($empNo) || empty($password)) {
|
||
$this->fail('员工编号和密码不能为空');
|
||
}
|
||
|
||
// 暴力破解防护(基于数据库记录)
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
// 检查最近 15 分钟内的失败次数
|
||
$stmt = $pdo->prepare(
|
||
"SELECT COUNT(*) as cnt FROM DGZXY_miniapp_login_log
|
||
WHERE emp_no = :eno AND success = 0 AND created_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)"
|
||
);
|
||
$stmt->execute([':eno' => $empNo]);
|
||
$failCount = $stmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0;
|
||
|
||
if ($failCount >= 5) {
|
||
$this->fail('登录尝试次数过多,请15分钟后再试');
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 日志表不存在时忽略
|
||
}
|
||
|
||
$employee = new Employee();
|
||
$user = $employee->validateLogin($empNo, $password);
|
||
|
||
if (!$user) {
|
||
// 记录失败日志
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
$pdo->prepare(
|
||
"INSERT INTO DGZXY_miniapp_login_log (emp_no, success, ip, created_at)
|
||
VALUES (:eno, 0, :ip, NOW())"
|
||
)->execute([':eno' => $empNo, ':ip' => $_SERVER['REMOTE_ADDR'] ?? '']);
|
||
} catch (\Throwable $e) {}
|
||
$this->fail('员工编号或密码错误');
|
||
}
|
||
|
||
// 生成 Token
|
||
$tokenInfo = $this->generateToken($user['id'], $user['emp_no']);
|
||
|
||
// 记录成功日志
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
$pdo->prepare(
|
||
"INSERT INTO DGZXY_miniapp_login_log (emp_no, success, ip, created_at)
|
||
VALUES (:eno, 1, :ip, NOW())"
|
||
)->execute([':eno' => $empNo, ':ip' => $_SERVER['REMOTE_ADDR'] ?? '']);
|
||
} catch (\Throwable $e) {}
|
||
|
||
$this->success([
|
||
'token' => $tokenInfo['token'],
|
||
'expires_at' => $tokenInfo['expires_at'],
|
||
'user' => [
|
||
'id' => (int)$user['id'],
|
||
'emp_no' => $user['emp_no'],
|
||
'emp_name' => $user['emp_name'],
|
||
'role' => $user['role'],
|
||
],
|
||
], '登录成功');
|
||
}
|
||
|
||
// ==================== 2. 退出登录 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/logout
|
||
* Headers: Authorization: Bearer <token>
|
||
*/
|
||
public function logout()
|
||
{
|
||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
||
$token = '';
|
||
if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||
$token = $matches[1];
|
||
}
|
||
|
||
if (!empty($token)) {
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
$pdo->prepare("UPDATE DGZXY_miniapp_token SET status = 0 WHERE token = :t")
|
||
->execute([':t' => $token]);
|
||
} catch (\Throwable $e) {}
|
||
}
|
||
|
||
$this->success([], '已退出登录');
|
||
}
|
||
|
||
// ==================== 3. 获取用户信息 ====================
|
||
|
||
/**
|
||
* GET /MiniApp/userInfo
|
||
* Headers: Authorization: Bearer <token>
|
||
*/
|
||
public function userInfo()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
$this->success(['user' => $user]);
|
||
}
|
||
|
||
// ==================== 4. 获取工位列表 ====================
|
||
|
||
/**
|
||
* GET /MiniApp/stations
|
||
* Headers: Authorization: Bearer <token>
|
||
* 响应: { stations: [{id, station_name, station_type, station_code, icon, sort_order}] }
|
||
*/
|
||
public function stations()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
$stations = $this->getAllowedStations($user);
|
||
|
||
// 只返回小程序需要的字段
|
||
$result = array_map(function($s) {
|
||
return [
|
||
'id' => (int)$s['id'],
|
||
'station_name' => $s['station_name'] ?? '',
|
||
'station_type' => $s['station_type'] ?? '',
|
||
'station_code' => $s['station_code'] ?? '',
|
||
'icon' => $s['icon'] ?? 'cube',
|
||
'sort_order' => (int)($s['sort_order'] ?? 0),
|
||
];
|
||
}, $stations);
|
||
|
||
$this->success(['stations' => $result]);
|
||
}
|
||
|
||
// ==================== 5. 获取工位字段配置 ====================
|
||
|
||
/**
|
||
* GET /MiniApp/stationConfig?type=inbound
|
||
* Headers: Authorization: Bearer <token>
|
||
* 响应: { station_type, fields: [...], data_sources: { types, models, customers } }
|
||
*/
|
||
public function stationConfig()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
$stationType = trim($_GET['type'] ?? '');
|
||
|
||
if (empty($stationType)) {
|
||
$this->fail('缺少工位类型参数 type');
|
||
}
|
||
|
||
// 权限检查
|
||
if (!$this->checkStationPermission($user, $stationType)) {
|
||
$this->fail('无权限访问该工位', 403);
|
||
}
|
||
|
||
// 获取字段配置
|
||
$rawFieldConfigs = $this->getStationFieldConfig($stationType);
|
||
$fieldMeta = $rawFieldConfigs['_meta'] ?? [];
|
||
unset($rawFieldConfigs['_meta']);
|
||
$fieldConfigs = array_values($rawFieldConfigs);
|
||
|
||
// 获取数据源
|
||
$productModel = new ProductModel();
|
||
$customer = new Customer();
|
||
|
||
// 从 fields_config._meta.model_type_filter 读取型号筛选类型
|
||
$defaultProductType = $fieldMeta['model_type_filter'] ?? '';
|
||
// 向后兼容:bizConfig 硬编码兜底
|
||
if (empty($defaultProductType)) {
|
||
if ($stationType === 'inbound') $defaultProductType = '半成品';
|
||
if ($stationType === 'warehouse') $defaultProductType = '成品';
|
||
}
|
||
|
||
$models = !empty($defaultProductType)
|
||
? $productModel->getByProductType($defaultProductType)
|
||
: $productModel->getAll();
|
||
|
||
$dataSources = [
|
||
'types' => $productModel->getProductTypes(),
|
||
'models' => $models,
|
||
'customers' => ($stationType === 'delivery') ? $customer->getAll() : [],
|
||
];
|
||
|
||
$this->success([
|
||
'station_type' => $stationType,
|
||
'fields' => $fieldConfigs,
|
||
'data_sources' => $dataSources,
|
||
]);
|
||
}
|
||
|
||
// ==================== 6. 获取型号列表(按产品类型筛选) ====================
|
||
|
||
/**
|
||
* GET /MiniApp/models?product_type=半成品
|
||
* Headers: Authorization: Bearer <token>
|
||
*/
|
||
public function models()
|
||
{
|
||
$this->tokenAuth();
|
||
$productType = trim($_GET['product_type'] ?? '');
|
||
|
||
$productModel = new ProductModel();
|
||
if ($productType === '') {
|
||
$models = $productModel->getAll();
|
||
} else {
|
||
$models = $productModel->getByProductType($productType);
|
||
}
|
||
|
||
$this->success(['models' => $models]);
|
||
}
|
||
|
||
// ==================== 7. 半成品入库 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/inbound
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { product_type, product_model, serial_no }
|
||
*/
|
||
public function inbound()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'inbound')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
// 必填校验
|
||
$errors = [];
|
||
if (empty($data['product_type'])) $errors[] = '产品类型不能为空';
|
||
if (empty($data['product_model'])) $errors[] = '产品型号不能为空';
|
||
if (empty($data['serial_no'])) $errors[] = '产品序列号不能为空';
|
||
|
||
if (!empty($errors)) {
|
||
$this->fail(implode(';', $errors));
|
||
}
|
||
|
||
// 检查序列号是否已存在
|
||
$stationDef = new StationDefinition();
|
||
$existing = $stationDef->findByField('inbound', 'serial_no', $data['serial_no']);
|
||
if ($existing) {
|
||
$this->fail('该序列号已入库');
|
||
}
|
||
|
||
$record = [
|
||
'product_type' => $data['product_type'],
|
||
'product_model' => $data['product_model'],
|
||
'serial_no' => $data['serial_no'],
|
||
'test_status' => 'pending',
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('inbound', $record);
|
||
if (!$result) {
|
||
$this->fail('入库失败');
|
||
}
|
||
|
||
$this->success(['id' => $id], '入库成功');
|
||
}
|
||
|
||
// ==================== 8. PCB 测试 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/pcbTest
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { product_type, product_model, serial_no, static_power?, input_output?, test_status? }
|
||
*/
|
||
public function pcbTest()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'pcb_test')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
if (empty($data['serial_no'])) {
|
||
$this->fail('PCB序列号不能为空');
|
||
}
|
||
|
||
$stationDef = new StationDefinition();
|
||
$record = [
|
||
'product_type' => $data['product_type'] ?? '',
|
||
'product_model' => $data['product_model'] ?? '',
|
||
'serial_no' => $data['serial_no'],
|
||
'static_power' => isset($data['static_power']) && $data['static_power'] !== '' ? $data['static_power'] : null,
|
||
'input_output' => $data['input_output'] ?? null,
|
||
'test_status' => $data['test_status'] ?? null,
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('pcb_test', $record);
|
||
if (!$result) {
|
||
$this->fail('提交失败');
|
||
}
|
||
|
||
$this->success([], 'PCB测试数据提交成功');
|
||
}
|
||
|
||
// ==================== 9. 电池状态 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/battery
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { serial_no, voltage_platform?, no_load_voltage?, internal_resistance? }
|
||
*/
|
||
public function battery()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'battery')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
if (empty($data['serial_no'])) {
|
||
$this->fail('产品序列号不能为空');
|
||
}
|
||
|
||
$stationDef = new StationDefinition();
|
||
$record = [
|
||
'serial_no' => $data['serial_no'],
|
||
'voltage_platform' => isset($data['voltage_platform']) && $data['voltage_platform'] !== '' ? $data['voltage_platform'] : null,
|
||
'no_load_voltage' => isset($data['no_load_voltage']) && $data['no_load_voltage'] !== '' ? $data['no_load_voltage'] : null,
|
||
'internal_resistance' => isset($data['internal_resistance']) && $data['internal_resistance'] !== '' ? $data['internal_resistance'] : null,
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('battery', $record);
|
||
if (!$result) {
|
||
$this->fail('提交失败');
|
||
}
|
||
|
||
$this->success([], '电池数据提交成功');
|
||
}
|
||
|
||
// ==================== 10. 产品组装(三码关联) ====================
|
||
|
||
/**
|
||
* POST /MiniApp/assembly
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { product_model, finished_serial, battery_serial, pcb_serial }
|
||
*/
|
||
public function assembly()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'assembly')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
$errors = [];
|
||
if (empty($data['finished_serial'])) $errors[] = '成品序列号不能为空';
|
||
if (empty($data['battery_serial'])) $errors[] = '电池序列号不能为空';
|
||
if (empty($data['pcb_serial'])) $errors[] = 'PCB序列号不能为空';
|
||
if (empty($data['product_model'])) $errors[] = '产品型号不能为空';
|
||
|
||
if (!empty($errors)) {
|
||
$this->fail(implode(';', $errors));
|
||
}
|
||
|
||
$stationDef = new StationDefinition();
|
||
|
||
// 检查成品序列号是否已组装
|
||
$existing = $stationDef->findByField('assembly', 'finished_serial', $data['finished_serial']);
|
||
if ($existing) {
|
||
$this->fail('该成品序列号(' . $data['finished_serial'] . ')已组装,不能重复提交');
|
||
}
|
||
|
||
$record = [
|
||
'finished_serial' => $data['finished_serial'],
|
||
'battery_serial' => $data['battery_serial'],
|
||
'pcb_serial' => $data['pcb_serial'],
|
||
'product_model' => $data['product_model'],
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('assembly', $record);
|
||
if (!$result) {
|
||
$this->fail('组装记录提交失败');
|
||
}
|
||
|
||
$this->success([], '组装记录提交成功');
|
||
}
|
||
|
||
// ==================== 11. 成品测试 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/finishedTest
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { finished_serial, test_item, test_result, test_steps?, product_model? }
|
||
*/
|
||
public function finishedTest()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'finished_test')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
if (empty($data['finished_serial'])) $this->fail('成品序列号不能为空');
|
||
if (empty($data['test_item'])) $this->fail('测试项目不能为空');
|
||
if (empty($data['test_result'])) $this->fail('测试结果不能为空');
|
||
|
||
$stationDef = new StationDefinition();
|
||
$record = [
|
||
'finished_serial' => $data['finished_serial'],
|
||
'test_item' => $data['test_item'],
|
||
'test_result' => $data['test_result'],
|
||
'test_steps' => $data['test_steps'] ?? '',
|
||
'product_model' => $data['product_model'] ?? '',
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('finished_test', $record);
|
||
if (!$result) {
|
||
$this->fail('提交失败');
|
||
}
|
||
|
||
$this->success([], '测试数据提交成功');
|
||
}
|
||
|
||
// ==================== 12. 成品入库 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/warehouse
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { finished_serial, product_model }
|
||
*/
|
||
public function warehouse()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'warehouse')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
if (empty($data['finished_serial'])) $this->fail('成品序列号不能为空');
|
||
|
||
$stationDef = new StationDefinition();
|
||
$record = [
|
||
'finished_serial' => $data['finished_serial'],
|
||
'product_model' => $data['product_model'] ?? '',
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('warehouse', $record);
|
||
if (!$result) {
|
||
$this->fail('入库失败');
|
||
}
|
||
|
||
$this->success([], '入库成功');
|
||
}
|
||
|
||
// ==================== 13. 出货登记 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/delivery
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { finished_serial, customer_name, product_model }
|
||
*/
|
||
public function delivery()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
if (!$this->checkStationPermission($user, 'delivery')) {
|
||
$this->fail('无权限操作该工位', 403);
|
||
}
|
||
|
||
$data = $this->getJsonInput();
|
||
|
||
if (empty($data['finished_serial'])) $this->fail('成品序列号不能为空');
|
||
if (empty($data['customer_name'])) $this->fail('客户名称不能为空');
|
||
|
||
$stationDef = new StationDefinition();
|
||
$record = [
|
||
'finished_serial' => $data['finished_serial'],
|
||
'customer_name' => $data['customer_name'],
|
||
'product_model' => $data['product_model'] ?? '',
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
$result = $stationDef->addRecord('delivery', $record);
|
||
if (!$result) {
|
||
$this->fail('出货登记失败');
|
||
}
|
||
|
||
$this->success([], '出货登记成功');
|
||
}
|
||
|
||
// ==================== 14. 工位记录查询 ====================
|
||
|
||
/**
|
||
* GET /MiniApp/records?type=inbound&page=1&limit=20
|
||
* Headers: Authorization: Bearer <token>
|
||
*/
|
||
public function records()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
$stationType = trim($_GET['type'] ?? '');
|
||
$page = max(1, intval($_GET['page'] ?? 1));
|
||
$limit = min(100, max(1, intval($_GET['limit'] ?? 20)));
|
||
$offset = ($page - 1) * $limit;
|
||
|
||
if (empty($stationType)) {
|
||
$this->fail('缺少工位类型参数 type');
|
||
}
|
||
|
||
if (!$this->checkStationPermission($user, $stationType)) {
|
||
$this->fail('无权限查看该工位记录', 403);
|
||
}
|
||
|
||
// 获取字段配置(用于表头信息)
|
||
$rawFieldConfigs = $this->getStationFieldConfig($stationType);
|
||
unset($rawFieldConfigs['_meta']);
|
||
$fieldConfigs = array_values($rawFieldConfigs);
|
||
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
|
||
$tableMap = [
|
||
'inbound' => 'station_inbound',
|
||
'pcb_test' => 'pcb_test',
|
||
'battery' => 'station_battery',
|
||
'assembly' => 'product_assembly',
|
||
'finished_test' => 'station_finished_test',
|
||
'warehouse' => 'station_warehouse',
|
||
'delivery' => 'station_delivery',
|
||
];
|
||
|
||
$table = $tableMap[$stationType] ?? null;
|
||
if (!$table) {
|
||
$this->fail('无效的工位类型');
|
||
}
|
||
|
||
$fullTable = 'DGZXY_' . $table;
|
||
|
||
// 总数
|
||
$countSql = "SELECT COUNT(*) as total FROM {$fullTable}";
|
||
$countStmt = $pdo->query($countSql);
|
||
$total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total'] ?? 0;
|
||
|
||
// 分页查询
|
||
$stmt = $pdo->prepare("SELECT * FROM {$fullTable} ORDER BY id DESC LIMIT :limit OFFSET :offset");
|
||
$stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
|
||
$stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
$records = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||
|
||
} catch (\Throwable $e) {
|
||
$this->fail('查询失败: ' . $e->getMessage());
|
||
}
|
||
|
||
$this->success([
|
||
'records' => $records,
|
||
'fields' => $fieldConfigs,
|
||
'total' => (int)$total,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'total_pages' => ceil($total / $limit),
|
||
]);
|
||
}
|
||
|
||
// ==================== 15. 今日统计 ====================
|
||
|
||
/**
|
||
* GET /MiniApp/dashboard
|
||
* Headers: Authorization: Bearer <token>
|
||
* 响应: { today: { inbound, pcb_test, battery, assembly, finished_test, warehouse, delivery }, total }
|
||
*/
|
||
public function dashboard()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
|
||
$tables = [
|
||
'inbound' => 'station_inbound',
|
||
'pcb_test' => 'pcb_test',
|
||
'battery' => 'station_battery',
|
||
'assembly' => 'product_assembly',
|
||
'finished_test' => 'station_finished_test',
|
||
'warehouse' => 'station_warehouse',
|
||
'delivery' => 'station_delivery',
|
||
];
|
||
|
||
$today = [];
|
||
foreach ($tables as $key => $table) {
|
||
$stmt = $pdo->prepare(
|
||
"SELECT COUNT(*) as cnt FROM DGZXY_{$table} WHERE operator = :op AND DATE(created_at) = CURDATE()"
|
||
);
|
||
$stmt->execute([':op' => $user['emp_name']]);
|
||
$today[$key] = (int)($stmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0);
|
||
}
|
||
|
||
$total = array_sum($today);
|
||
|
||
} catch (\Throwable $e) {
|
||
$today = [];
|
||
$total = 0;
|
||
}
|
||
|
||
$this->success([
|
||
'today' => $today,
|
||
'total' => $total,
|
||
]);
|
||
}
|
||
|
||
// ==================== 16. 产品追溯查询 ====================
|
||
|
||
/**
|
||
* GET /MiniApp/productSearch?keyword=序列号
|
||
* Headers: Authorization: Bearer <token>
|
||
* 跨工位搜索序列号,返回产品全链路信息
|
||
*/
|
||
public function productSearch()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
$keyword = trim($_GET['keyword'] ?? '');
|
||
|
||
if (empty($keyword)) {
|
||
$this->fail('请输入搜索关键词');
|
||
}
|
||
|
||
$pdo = \core\db\Db::pdo();
|
||
$result = [
|
||
'keyword' => $keyword,
|
||
'traces' => [],
|
||
];
|
||
|
||
// 每个表查询独立 try-catch,某个表不存在不影响其他查询
|
||
$queries = [
|
||
['source' => 'inbound', 'sql' =>
|
||
"SELECT 'inbound' as source, id, serial_no, product_type, product_model, test_status, operator, created_at
|
||
FROM DGZXY_station_inbound WHERE serial_no = :kw LIMIT 1", 'params' => [':kw' => $keyword]],
|
||
['source' => 'pcb_test', 'sql' =>
|
||
"SELECT 'pcb_test' as source, id, serial_no, product_type, product_model, static_power, input_output, test_status, operator, created_at
|
||
FROM DGZXY_pcb_test WHERE serial_no = :kw LIMIT 1", 'params' => [':kw' => $keyword]],
|
||
['source' => 'battery', 'sql' =>
|
||
"SELECT 'battery' as source, id, serial_no, voltage_platform, no_load_voltage, internal_resistance, operator, created_at
|
||
FROM DGZXY_station_battery WHERE serial_no = :kw LIMIT 1", 'params' => [':kw' => $keyword]],
|
||
['source' => 'assembly', 'sql' =>
|
||
"SELECT 'assembly' as source, id, finished_serial, battery_serial, pcb_serial, product_model, operator, created_at
|
||
FROM DGZXY_product_assembly
|
||
WHERE finished_serial = :kw OR battery_serial = :kw2 OR pcb_serial = :kw3
|
||
LIMIT 5", 'params' => [':kw' => $keyword, ':kw2' => $keyword, ':kw3' => $keyword]],
|
||
['source' => 'finished_test', 'sql' =>
|
||
"SELECT 'finished_test' as source, id, finished_serial, test_item, test_result, test_steps, product_model, operator, created_at
|
||
FROM DGZXY_station_finished_test WHERE finished_serial = :kw LIMIT 5", 'params' => [':kw' => $keyword]],
|
||
['source' => 'warehouse', 'sql' =>
|
||
"SELECT 'warehouse' as source, id, finished_serial, product_model, operator, created_at as in_time
|
||
FROM DGZXY_station_warehouse WHERE finished_serial = :kw LIMIT 1", 'params' => [':kw' => $keyword]],
|
||
['source' => 'delivery', 'sql' =>
|
||
"SELECT 'delivery' as source, id, finished_serial, customer_name, product_model, operator, created_at
|
||
FROM DGZXY_station_delivery WHERE finished_serial = :kw LIMIT 1", 'params' => [':kw' => $keyword]],
|
||
];
|
||
|
||
foreach ($queries as $q) {
|
||
try {
|
||
$stmt = $pdo->prepare($q['sql']);
|
||
$stmt->execute($q['params']);
|
||
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||
$result['traces'][] = $row;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 表不存在时跳过,继续查询其他表
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$result['count'] = count($result['traces']);
|
||
|
||
$this->success($result);
|
||
}
|
||
|
||
// ==================== 17. 修改密码 ====================
|
||
|
||
/**
|
||
* POST /MiniApp/changePassword
|
||
* Headers: Authorization: Bearer <token>
|
||
* 请求: { old_password, new_password }
|
||
*/
|
||
public function changePassword()
|
||
{
|
||
$user = $this->tokenAuth();
|
||
$data = $this->getJsonInput();
|
||
|
||
$oldPassword = $data['old_password'] ?? '';
|
||
$newPassword = $data['new_password'] ?? '';
|
||
|
||
if (empty($oldPassword)) $this->fail('请输入旧密码');
|
||
if (empty($newPassword)) $this->fail('请输入新密码');
|
||
if (strlen($newPassword) < 8) $this->fail('新密码长度不能少于8位');
|
||
|
||
// 验证旧密码
|
||
$employee = new Employee();
|
||
$valid = $employee->validateLogin($user['emp_no'], $oldPassword);
|
||
if (!$valid) {
|
||
$this->fail('旧密码错误');
|
||
}
|
||
|
||
// 更新密码
|
||
$employee->resetPassword($user['id'], $newPassword);
|
||
|
||
// 使所有 token 失效(强制重新登录)
|
||
try {
|
||
$pdo = \core\db\Db::pdo();
|
||
$pdo->prepare("UPDATE DGZXY_miniapp_token SET status = 0 WHERE employee_id = :eid")
|
||
->execute([':eid' => $user['id']]);
|
||
} catch (\Throwable $e) {}
|
||
|
||
$this->success([], '密码修改成功,请重新登录');
|
||
}
|
||
}
|
||
|
||
// ==================== 辅助函数 ====================
|
||
|
||
/**
|
||
* 解析工位字段配置(JSON字符串转数组)
|
||
*/
|
||
function parseFieldsConfig($stationType, $fieldsConfigJson)
|
||
{
|
||
$config = is_string($fieldsConfigJson) ? json_decode($fieldsConfigJson, true) : $fieldsConfigJson;
|
||
if (!is_array($config)) {
|
||
return getDefaultFieldsConfig($stationType);
|
||
}
|
||
return $config;
|
||
}
|
||
|
||
/**
|
||
* 获取工位默认字段配置
|
||
* 根据工位类型返回对应字段定义
|
||
*/
|
||
function getDefaultFieldsConfig($stationType)
|
||
{
|
||
$configs = [
|
||
'inbound' => [
|
||
['field' => 'product_type', 'label' => '产品类型', 'type' => 'select', 'required' => true, 'source' => 'types'],
|
||
['field' => 'product_model', 'label' => '产品型号', 'type' => 'select', 'required' => true, 'source' => 'models'],
|
||
['field' => 'serial_no', 'label' => '产品序列号', 'type' => 'text', 'required' => true],
|
||
],
|
||
'pcb_test' => [
|
||
['field' => 'product_type', 'label' => '产品类型', 'type' => 'select', 'required' => false, 'source' => 'types'],
|
||
['field' => 'product_model', 'label' => '产品型号', 'type' => 'select', 'required' => false, 'source' => 'models'],
|
||
['field' => 'serial_no', 'label' => 'PCB序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'static_power', 'label' => '静态功耗', 'type' => 'number', 'required' => false],
|
||
['field' => 'input_output', 'label' => '输入输出', 'type' => 'text', 'required' => false],
|
||
['field' => 'test_status', 'label' => '测试状态', 'type' => 'select', 'required' => false, 'options' => ['pass', 'fail', 'pending']],
|
||
],
|
||
'battery' => [
|
||
['field' => 'serial_no', 'label' => '产品序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'voltage_platform', 'label' => '电压平台(V)', 'type' => 'number', 'required' => false],
|
||
['field' => 'no_load_voltage', 'label' => '空载电压(V)', 'type' => 'number', 'required' => false],
|
||
['field' => 'internal_resistance', 'label' => '内阻(mOhm)', 'type' => 'number', 'required' => false],
|
||
],
|
||
'assembly' => [
|
||
['field' => 'finished_serial', 'label' => '成品序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'battery_serial', 'label' => '电池序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'pcb_serial', 'label' => 'PCB序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'product_model', 'label' => '产品型号', 'type' => 'select', 'required' => true, 'source' => 'models'],
|
||
],
|
||
'finished_test' => [
|
||
['field' => 'finished_serial', 'label' => '成品序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'test_item', 'label' => '测试项目', 'type' => 'text', 'required' => true],
|
||
['field' => 'test_result', 'label' => '测试结果', 'type' => 'select', 'required' => true, 'options' => ['pass', 'fail']],
|
||
['field' => 'test_steps', 'label' => '测试步骤', 'type' => 'textarea', 'required' => false],
|
||
['field' => 'product_model', 'label' => '产品型号', 'type' => 'select', 'required' => false, 'source' => 'models'],
|
||
],
|
||
'warehouse' => [
|
||
['field' => 'finished_serial', 'label' => '成品序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'product_model', 'label' => '产品型号', 'type' => 'select', 'required' => false, 'source' => 'models'],
|
||
],
|
||
'delivery' => [
|
||
['field' => 'finished_serial', 'label' => '成品序列号', 'type' => 'text', 'required' => true],
|
||
['field' => 'customer_name', 'label' => '客户名称', 'type' => 'select', 'required' => true, 'source' => 'customers'],
|
||
['field' => 'product_model', 'label' => '产品型号', 'type' => 'select', 'required' => false, 'source' => 'models'],
|
||
],
|
||
];
|
||
|
||
return $configs[$stationType] ?? [];
|
||
}
|