Files
MES/app/models/StationDefinition.php
T
2026-08-08 18:28:49 +08:00

604 lines
24 KiB
PHP
Raw 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\models;
use core\base\Model;
/**
* 工位定义模型 - 统一管理所有内置工位的元数据
*
* 替代之前分散在 7 个独立 Model 文件中的硬编码配置。
* 所有内置工位的数据表名、时间列名、筛选开关等统一在此管理。
*
* 数据库表:DGZXY_station_definitions
*/
class StationDefinition extends Model
{
protected $table = 'station_definitions';
/** @var array 缓存已加载的定义 */
private static $cache = [];
/**
* 获取工位定义(表不存在时自动创建并初始化)
* @param string $stationType
* @return array|null
*/
public function getByType($stationType)
{
if (isset(self::$cache[$stationType])) {
return self::$cache[$stationType];
}
try {
$row = $this->where(['station_type = :type'], [':type' => $stationType])->fetch();
} catch (\Throwable $e) {
error_log('[StationDefinition] getByType(' . $stationType . ') failed: ' . $e->getMessage());
// 表不存在,自动创建并初始化
if ($this->autoCreateTable()) {
try {
$row = $this->where(['station_type = :type'], [':type' => $stationType])->fetch();
} catch (\Throwable $e2) {
error_log('[StationDefinition] getByType retry failed: ' . $e2->getMessage());
$row = null;
}
} else {
$row = null;
}
}
if ($row && !empty($row['extra_config'])) {
$row['extra'] = json_decode($row['extra_config'], true) ?: [];
} elseif ($row) {
$row['extra'] = [];
}
self::$cache[$stationType] = $row ?: null;
return self::$cache[$stationType];
}
/**
* 获取完整表名(带前缀)
*/
public function getFullTableName($stationType)
{
$def = $this->getByType($stationType);
if (!$def) {
// 尝试自动重建
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return null;
}
return self::TABLE_PREFIX . $def['db_table'];
}
/**
* 获取时间列名
*/
public function getTimeColumn($stationType)
{
$def = $this->getByType($stationType);
if (!$def) {
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
}
return $def ? $def['time_column'] : 'created_at';
}
/**
* 获取记录列表(支持按类型和型号筛选)
*
* 如果 station_definitions 记录缺失,自动尝试重建。
*/
public function getRecords($stationType, $filterType = '', $filterModel = '', $limit = 100)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] getRecords: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return [];
}
$table = self::TABLE_PREFIX . $def['db_table'];
$conditions = [];
$params = [];
if ($def['has_product_type'] && !empty($filterType)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $filterType;
}
if ($def['has_product_model'] && !empty($filterModel)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $filterModel;
}
$timeCol = $def['time_column'];
$sql = "SELECT * FROM `{$table}`";
if (!empty($conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$sql .= " ORDER BY `{$timeCol}` DESC LIMIT " . intval($limit);
try {
return $this->query($sql, $params);
} catch (\Throwable $e) {
error_log("[StationDefinition] getRecords query failed for {$stationType}: " . $e->getMessage());
// 表可能不存在,尝试创建
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
if ($this->ensureTable($stationType, $fieldConfigs)) {
try {
return $this->query($sql, $params);
} catch (\Throwable $e2) {
error_log("[StationDefinition] getRecords retry failed for {$stationType}: " . $e2->getMessage());
}
}
return [];
}
}
/**
* 获取所有记录
*/
public function getAllRecords($stationType, $limit = 100)
{
return $this->getRecords($stationType, '', '', $limit);
}
/**
* 统计今日某操作员的录入数量
*/
public function countTodayByOperator($stationType, $operator)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] countTodayByOperator: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return 0;
}
$table = self::TABLE_PREFIX . $def['db_table'];
$timeCol = $def['time_column'];
try {
$sql = "SELECT COUNT(*) as cnt FROM `{$table}` WHERE operator = :op AND DATE(`{$timeCol}`) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
} catch (\Throwable $e) {
error_log("[StationDefinition] countTodayByOperator failed for {$stationType}: " . $e->getMessage());
return 0;
}
}
/**
* 统计总记录数(可按型号筛选)
*/
public function countTotal($stationType, $filterType = '', $filterModel = '')
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] countTotal: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return 0;
}
$table = self::TABLE_PREFIX . $def['db_table'];
$conditions = [];
$params = [];
if ($def['has_product_type'] && !empty($filterType)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $filterType;
}
if ($def['has_product_model'] && !empty($filterModel)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $filterModel;
}
try {
$sql = "SELECT COUNT(*) as cnt FROM `{$table}`";
if (!empty($conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$result = $this->query($sql, $params);
return $result[0]['cnt'] ?? 0;
} catch (\Throwable $e) {
error_log("[StationDefinition] countTotal failed for {$stationType}: " . $e->getMessage());
return 0;
}
}
/**
* 添加记录到工位专用表
*
* 如果 station_definitions 中没有该工位记录,自动尝试重建。
* 如果数据表不存在,自动创建后再插入。
*/
public function addRecord($stationType, $data)
{
$def = $this->getByType($stationType);
if (!$def) {
// 尝试自动重建 station_definitions 数据
error_log("[StationDefinition] addRecord: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] addRecord: still no definition for '{$stationType}' after re-init");
return false;
}
}
$table = self::TABLE_PREFIX . $def['db_table'];
// 确保数据表存在(不存在则创建)
try {
$fields = implode('`, `', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO `{$table}` (`{$fields}`) VALUES ({$placeholders})";
return $this->execute($sql, $data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log("[StationDefinition] addRecord failed for {$stationType}: {$msg}");
// 表不存在时自动创建
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
// 需要 fieldConfigs 来创建表,从全局获取
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
if ($this->ensureTable($stationType, $fieldConfigs)) {
// 重试插入
try {
return $this->execute($sql, $data);
} catch (\Throwable $e2) {
error_log("[StationDefinition] addRecord retry failed for {$stationType}: " . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 检查记录是否已存在(防重复)
*/
public function findByField($stationType, $fieldName, $value)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] findByField: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return null;
}
$table = self::TABLE_PREFIX . $def['db_table'];
try {
$sql = "SELECT * FROM `{$table}` WHERE `{$fieldName}` = :val LIMIT 1";
$result = $this->query($sql, [':val' => $value]);
return $result[0] ?? null;
} catch (\Throwable $e) {
error_log("[StationDefinition] findByField failed for {$stationType}: " . $e->getMessage());
return null;
}
}
/**
* 检查工位是否启用筛选
*/
public function isFilterEnabled($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['filter_enabled'] : false;
}
/**
* 检查是否显示今日计数
*/
public function showTodayCount($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['show_today_count'] : false;
}
/**
* 检查是否显示总计数
*/
public function showTotalCount($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['show_total_count'] : false;
}
/**
* 确保工位数据表存在,不存在则自动创建
*
* 通用工位表结构(参考 station_inbound):
* id, product_type, product_model, operator, created_at + 动态字段
*
* @param string $stationType 工位类型标识
* @param array $fieldConfigs 字段配置(从 fields_config 解析)
* @return bool 表是否已存在/创建成功
*/
public function ensureTable($stationType, $fieldConfigs = [])
{
// 先尝试获取工位定义,如果失败则尝试重建 station_definitions 表数据
$def = $this->getByType($stationType);
if (!$def) {
// station_definitions 表中没有该工位的记录,尝试重新插入
error_log("[StationDefinition] ensureTable: no definition for '{$stationType}', trying to re-initialize built-in data");
$this->autoCreateTable();
// 清除缓存后重试
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] ensureTable: still no definition for '{$stationType}' after re-init");
return false;
}
}
$fullTable = self::TABLE_PREFIX . $def['db_table'];
// 检查表是否已存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (!empty($exists)) return true;
// 基础列:所有通用工位表都有的字段
$columns = [
"`id` INT AUTO_INCREMENT PRIMARY KEY",
"`product_type` VARCHAR(100) DEFAULT NULL COMMENT '产品类型'",
"`product_model` VARCHAR(100) DEFAULT NULL COMMENT '产品型号'",
"`operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人'",
"`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间'",
];
// 索引
$indexes = [
"KEY `idx_product_type` (`product_type`)",
"KEY `idx_product_model` (`product_model`)",
"KEY `idx_created_at` (`created_at`)",
];
// 从 fields_config 派生额外字段(排除已存在的基础字段和隐藏字段)
$baseFields = ['product_type', 'product_model', 'operator', 'created_at', 'id'];
$addedFields = [];
foreach ($fieldConfigs as $field) {
$name = $field['name'] ?? '';
$type = $field['type'] ?? 'text';
if (in_array($name, $baseFields) || empty($name)) continue;
// 跳过纯虚拟字段(没有实际存储意义的)
if (in_array($name, ['_meta'])) continue;
// 根据字段类型确定 MySQL 列类型
$colType = 'VARCHAR(200)';
if ($type === 'number') {
$colType = 'VARCHAR(50)';
} elseif ($type === 'textarea') {
$colType = 'TEXT';
}
$label = $field['label'] ?? $name;
$comment = addslashes($label);
$columns[] = "`{$name}` {$colType} DEFAULT NULL COMMENT '{$comment}'";
// 为序列号类字段添加索引
if (!empty($field['is_scan']) || stripos($name, 'serial') !== false) {
$indexes[] = "KEY `idx_{$name}` (`{$name}`)";
}
$addedFields[] = $name;
}
// 从 station_business_helper 的 dataMapping 补充可能缺失的列
// 确保 helper 文件已加载
if (!function_exists('getStationBusinessConfig')) {
@include_once APP_PATH . 'app/helpers/station_business_helper.php';
}
if (function_exists('getStationBusinessConfig')) {
$bizConfig = getStationBusinessConfig($stationType);
if ($bizConfig && !empty($bizConfig['dataMapping'])) {
foreach ($bizConfig['dataMapping'] as $dbField => $postKey) {
if ($postKey === '__operator__') continue;
if (in_array($dbField, $baseFields) || in_array($dbField, $addedFields)) continue;
$columns[] = "`{$dbField}` VARCHAR(200) DEFAULT NULL COMMENT '{$dbField}'";
$addedFields[] = $dbField;
}
}
}
$allColumns = implode(",\n ", $columns);
$allIndexes = !empty($indexes) ? ",\n " . implode(",\n ", $indexes) : '';
$sql = "CREATE TABLE `{$fullTable}` (\n {$allColumns}{$allIndexes}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='{$stationType} 工位数据表'";
try {
$this->execute($sql);
error_log("[StationDefinition] Auto-created table: {$fullTable} for station_type={$stationType}, extra fields: " . implode(', ', $addedFields));
return true;
} catch (\Throwable $e) {
error_log("[StationDefinition] Failed to create table {$fullTable}: " . $e->getMessage());
return false;
}
}
/**
* 确保工位列存在(表已存在时补充缺失的列)
* 当 fields_config 新增字段后,自动 ALTER TABLE 添加
*/
public function ensureColumns($stationType, $fieldConfigs = [])
{
$def = $this->getByType($stationType);
if (!$def) return;
$fullTable = self::TABLE_PREFIX . $def['db_table'];
// 检查表是否存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (empty($exists)) {
// 表不存在,走建表流程
return $this->ensureTable($stationType, $fieldConfigs);
}
// 获取现有列
$existingCols = $this->query("SHOW COLUMNS FROM `{$fullTable}`");
$existingNames = [];
foreach ($existingCols as $col) {
$existingNames[] = strtolower($col['Field']);
}
// 基础字段名
$baseFields = ['product_type', 'product_model', 'operator', 'created_at', 'id'];
foreach ($fieldConfigs as $field) {
$name = $field['name'] ?? '';
$type = $field['type'] ?? 'text';
if (in_array($name, $baseFields) || empty($name)) continue;
if (in_array($name, ['_meta'])) continue;
if (in_array(strtolower($name), $existingNames)) continue;
$colType = 'VARCHAR(200)';
if ($type === 'number') {
$colType = 'VARCHAR(50)';
} elseif ($type === 'textarea') {
$colType = 'TEXT';
}
$label = $field['label'] ?? $name;
$comment = addslashes($label);
try {
$this->execute("ALTER TABLE `{$fullTable}` ADD COLUMN `{$name}` {$colType} DEFAULT NULL COMMENT '{$comment}'");
error_log("[StationDefinition] Added column `{$name}` to {$fullTable}");
} catch (\Throwable $e) {
error_log("[StationDefinition] Failed to add column `{$name}` to {$fullTable}: " . $e->getMessage());
}
}
}
/**
* 自动创建 station_definitions 表并初始化内置工位数据
* @return bool 是否成功
*/
/**
* 获取所有内置工位的默认定义数据
*/
private function getBuiltinDefaults()
{
return [
['station_type' => 'inbound', 'db_table' => 'station_inbound', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => '{"alt_table":"warehouse_in","alt_time_column":"in_time","alt_condition_field":"product_type","alt_condition_value":"成品"}'],
['station_type' => 'pcb_test', 'db_table' => 'pcb_test', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'battery', 'db_table' => 'battery_status', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'assembly', 'db_table' => 'product_assembly', 'time_column' => 'assembly_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'finished_test','db_table' => 'finished_test', 'time_column' => 'test_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'delivery', 'db_table' => 'delivery', 'time_column' => 'delivery_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'warehouse', 'db_table' => 'warehouse_in', 'time_column' => 'in_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 0, 'show_today_count' => 0, 'show_total_count' => 0, 'extra_config' => ''],
];
}
/**
* 插入/更新内置工位默认数据(ON DUPLICATE KEY UPDATE
*
* 使用 ON DUPLICATE KEY UPDATE 而非 INSERT IGNORE,确保即使表中有旧数据,
* 也能更新为正确的默认值(修复数据损坏/不完整的情况)。
*
* @return bool
*/
private function insertBuiltinDefaults()
{
$fullTable = $this->getTable();
$defaults = $this->getBuiltinDefaults();
$insertSql = "INSERT INTO `{$fullTable}`
(`station_type`, `db_table`, `time_column`, `has_product_type`, `has_product_model`,
`filter_enabled`, `show_today_count`, `show_total_count`, `extra_config`)
VALUES (:st, :dt, :tc, :hpt, :hpm, :fe, :stc, :sttc, :ec)
ON DUPLICATE KEY UPDATE
`db_table` = VALUES(`db_table`),
`time_column` = VALUES(`time_column`),
`has_product_type` = VALUES(`has_product_type`),
`has_product_model` = VALUES(`has_product_model`),
`filter_enabled` = VALUES(`filter_enabled`),
`show_today_count` = VALUES(`show_today_count`),
`show_total_count` = VALUES(`show_total_count`),
`extra_config` = VALUES(`extra_config`)";
try {
// 使用 $this->execute() 而非直接访问 $this->pdo
// 确保在 Model 构造链完整的情况下执行
$affected = 0;
foreach ($defaults as $d) {
$params = [
':st' => $d['station_type'],
':dt' => $d['db_table'],
':tc' => $d['time_column'],
':hpt' => $d['has_product_type'],
':hpm' => $d['has_product_model'],
':fe' => $d['filter_enabled'],
':stc' => $d['show_today_count'],
':sttc' => $d['show_total_count'],
':ec' => $d['extra_config'],
];
// execute() 返回 boolrowCount() 需要通过 pdo 获取
if ($this->execute($insertSql, $params)) {
$affected++;
}
}
if ($affected > 0) {
error_log('[StationDefinition] Upserted ' . $affected . ' built-in station definitions');
}
return true;
} catch (\Throwable $e) {
error_log('[StationDefinition] insertBuiltinDefaults failed: ' . $e->getMessage());
return false;
}
}
public function autoCreateTable()
{
$fullTable = $this->getTable();
try {
// 检查表是否存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (empty($exists)) {
// 创建表
$sql = "CREATE TABLE IF NOT EXISTS `{$fullTable}` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`station_type` VARCHAR(50) NOT NULL UNIQUE COMMENT '工位类型标识',
`db_table` VARCHAR(100) NOT NULL COMMENT '数据表名(不含前缀)',
`time_column` VARCHAR(50) NOT NULL DEFAULT 'created_at' COMMENT '时间列名',
`has_product_type` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_type 列',
`has_product_model` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_model 列',
`filter_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用筛选',
`show_today_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示今日计数',
`show_total_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示总计数',
`extra_config` TEXT COMMENT '额外配置(JSON',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_station_type` (`station_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工位定义统一配置表'";
$this->execute($sql);
error_log('[StationDefinition] Auto-created table: ' . $fullTable);
}
// 无论表是否刚创建,确保内置工位数据存在
// (表可能已存在但数据不完整,例如之前创建失败只建了表没插入数据)
$this->insertBuiltinDefaults();
return true;
} catch (\Throwable $e) {
error_log('[StationDefinition] autoCreateTable failed: ' . $e->getMessage());
return false;
}
}
/**
* 清除缓存(数据更新后调用)
*/
public static function clearCache()
{
self::$cache = [];
}
}