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() 返回 bool,rowCount() 需要通过 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 = []; } }