初始化

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
+603
View File
@@ -0,0 +1,603 @@
<?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 = [];
}
}