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'] === \core\base\Controller::ROLE_SUPER_ADMIN || $user['role'] === \core\base\Controller::ROLE_ADMIN) { return $allStations; } // 操作员:过滤出有权限的工位 $permModel = new EmployeePermission(); $employeeId = $_SESSION['user_id'] ?? 0; $allowedTypes = $permModel->getAllowedStations($employeeId); // 如果没有配置任何权限,默认允许所有工位 if (empty($allowedTypes)) { return $allStations; } // 过滤 $filtered = []; foreach ($allStations as $station) { if (in_array($station['station_type'], $allowedTypes)) { $filtered[] = $station; } } return $filtered; } /** * 检查当前用户是否有访问指定工位的权限 */ private function checkStationPermission($stationType) { $user = $this->getCurrentUser(); // 超级管理员和管理员始终允许 if ($user['role'] === \core\base\Controller::ROLE_SUPER_ADMIN || $user['role'] === \core\base\Controller::ROLE_ADMIN) { return true; } $permModel = new EmployeePermission(); $employeeId = $_SESSION['user_id'] ?? 0; if (!$permModel->canAccess($employeeId, $user['role'], $stationType)) { // 渲染无权限视图 $config = new SysConfig(); $sysConfig = $config->getConfig(); $noPermView = new \core\base\View('Front', 'noPermission'); $noPermView->assign('title', '无权限访问'); $noPermView->assign('user', $user); $noPermView->assign('sysConfig', $sysConfig); $noPermView->assign('stationType', $stationType); $noPermView->render(); exit; } return true; } // AJAX 接口:根据产品类型获取对应型号列表 public function getModelsByType() { $this->checkLogin(); $product_type = isset($_GET['product_type']) ? trim($_GET['product_type']) : ''; $productModel = new ProductModel(); if ($product_type === '') { // 未选类型,返回全部型号 $models = $productModel->getAll(); } else { $models = $productModel->getByProductType($product_type); } echo json_encode(['success' => true, 'models' => $models]); exit; } public function index() { $this->checkLogin(); $user = $this->getCurrentUser(); $stations = $this->getAllowedStations($user); $productModel = new ProductModel(); $models = $productModel->getAll(); $types = $productModel->getProductTypes(); $config = new SysConfig(); $sysConfig = $config->getConfig(); $this->assign('title', '前台工位'); $this->assign('user', $user); $this->assign('sysConfig', $sysConfig); $this->assign('stations', $stations); $this->assign('models', $models); $this->assign('types', $types); $this->assign('csrfToken', $this->csrfToken()); $this->render(); } // ========== 统一工位处理器 ========== /** * 统一工位处理方法 - 通过 __call 动态路由所有工位 * * 所有工位(内置和动态添加)都通过此方法统一处理。 * 内置工位在 station_business_helper.php 中定义业务配置。 * 动态工位使用 station_records 表的通用存储。 */ public function __call($method, $args) { return $this->handleStation($method); } /** * 核心工位处理器 * @param string $stationType 工位类型标识 */ private function handleStation($stationType) { try { return $this->_handleStation($stationType); } catch (\Throwable $e) { error_log('[' . $stationType . '] 工位加载失败: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); // AJAX 提交:返回 JSON 错误,避免渲染 HTML 导致前端解析失败 if ($this->isAjax()) { if (ob_get_level() > 0) { @ob_clean(); } header('Content-Type: application/json; charset=utf-8'); $msg = $e->getMessage(); $decoded = json_decode($msg, true); // 结构化校验错误(code_prefix / duplicate)直接透传 if (is_array($decoded) && (isset($decoded['type']) || isset($decoded['codePrefixErrors']) || isset($decoded['duplicateErrors']) || isset($decoded['errors']))) { echo $msg; } else { echo json_encode(['success' => false, 'error' => $msg]); } exit; } // 如果已经有输出内容,清除之前的输出缓冲再渲染错误页 if (ob_get_level() > 0) { @ob_clean(); } // 重置 header 防御标志(如果之前部分渲染已设置了标志) unset($GLOBALS['_station_header_rendered'], $GLOBALS['_station_footer_rendered']); // 尝试渲染友好错误页面 $user = $this->getCurrentUser(); $config = new SysConfig(); $sysConfig = $config->getConfig(); $workstation = new Workstation(); $stations = $workstation->getAll(); $errorView = new \core\base\View('Front', 'station_generic'); $errorView->assign('title', '工位加载失败'); $errorView->assign('user', $user); $errorView->assign('sysConfig', $sysConfig); $errorView->assign('stations', $stations); $errorView->assign('stationType', $stationType); $errorView->assign('currentStation', null); $errorView->assign('bizConfig', null); $errorView->assign('fieldConfigs', []); $errorView->assign('models', []); $errorView->assign('types', []); $errorView->assign('customers', []); $errorView->assign('records', []); $errorView->assign('filterType', ''); $errorView->assign('filterModel', ''); $errorView->assign('todayCount', 0); $errorView->assign('csrfToken', $this->csrfToken()); $errorView->assign('stationError', '工位「' . htmlspecialchars($stationType) . '」尚未配置,请联系管理员在后台添加该工位。错误详情:' . $e->getMessage()); $errorView->render(); exit; } } private function _handleStation($stationType) { $this->checkLogin(); $this->checkStationPermission($stationType); $user = $this->getCurrentUser(); // ===== document_manage 工位:直接渲染专用视图 ===== if ($stationType === 'document_manage') { $this->renderDocumentManageStation($user); exit; } // 获取业务配置(内置工位有,动态工位为 null) $bizConfig = getStationBusinessConfig($stationType); // 如果 station_business_helper 中没有配置,但有 station_definitions 记录, // 自动生成通用业务配置,统一走内置工位路径(专用物理表) if (!$bizConfig) { $stationDef = new StationDefinition(); $def = $stationDef->getByType($stationType); if ($def) { $bizConfig = $this->buildGenericBizConfig($stationType, $def); } } // 处理重定向工位 if ($bizConfig && isset($bizConfig['redirect'])) { header('Location: ' . BASE_URL . '/Front/' . $bizConfig['redirect']); exit; } // 获取字段配置 $rawFieldConfigs = $this->getStationFieldConfig($stationType); // 分离元信息(_meta)和字段列表,确保后续遍历不受影响 $fieldMeta = $rawFieldConfigs['_meta'] ?? []; unset($rawFieldConfigs['_meta']); $fieldConfigs = array_values($rawFieldConfigs); // ========== 准备数据源 ========== $productModel = new ProductModel(); $models = $productModel->getAll(); $types = $productModel->getProductTypes(); $customer = new Customer(); $customers = $customer->getAll(); $workstation = new Workstation(); $stations = $workstation->getAll(); $config = new SysConfig(); $sysConfig = $config->getConfig(); // 当前工位信息 $currentStation = $workstation->getByType($stationType); // 型号筛选:优先从 fields_config._meta.model_type_filter 读取 $modelTypeFilter = $fieldMeta['model_type_filter'] ?? null; if ($modelTypeFilter) { $models = $productModel->getByProductType($modelTypeFilter); } elseif ($bizConfig) { // 向后兼容:bizConfig 中的 fixedType/defaultType if (!empty($bizConfig['fixedType'])) { $models = $productModel->getByProductType($bizConfig['fixedType']); } elseif (!empty($bizConfig['defaultType'])) { $models = $productModel->getByProductType($bizConfig['defaultType']); } } // 类型筛选:优先从 fields_config._meta.type_filter 读取 $typeFilter = $fieldMeta['type_filter'] ?? null; if ($typeFilter) { $typeConfigModel = new ProductTypeConfig(); if ($typeFilter === 'inbound') { $types = $typeConfigModel->getInboundVisible(); if (empty($types)) { // fallback:使用默认配置,但过滤掉非产品类型(外壳颜色等) $allDefaults = ProductTypeConfig::getDefaultConfigs(); $types = array_filter($allDefaults, function($c) { return ($c['enabled'] ?? 1) && ($c['show_in_inbound'] ?? 1) && !in_array($c['type_name'], ['外壳颜色']); // 外壳颜色不是产品类型 }); $types = array_values($types); } } else { $types = $typeConfigModel->getEnabled(); if (empty($types)) { $allDefaults = ProductTypeConfig::getDefaultConfigs(); $types = array_filter($allDefaults, function($c) { return ($c['enabled'] ?? 1) && !in_array($c['type_name'], ['外壳颜色']); }); $types = array_values($types); } } } elseif ($bizConfig) { // 向后兼容:bizConfig 中的 typeFilter if (!empty($bizConfig['typeFilter'])) { $typeConfigModel = new ProductTypeConfig(); if ($bizConfig['typeFilter'] === 'inbound') { $types = $typeConfigModel->getInboundVisible(); if (empty($types)) { $allDefaults = ProductTypeConfig::getDefaultConfigs(); $types = array_filter($allDefaults, function($c) { return ($c['enabled'] ?? 1) && ($c['show_in_inbound'] ?? 1) && !in_array($c['type_name'], ['外壳颜色']); }); $types = array_values($types); } } else { $types = $typeConfigModel->getEnabled(); if (empty($types)) { $allDefaults = ProductTypeConfig::getDefaultConfigs(); $types = array_filter($allDefaults, function($c) { return ($c['enabled'] ?? 1) && !in_array($c['type_name'], ['外壳颜色']); }); $types = array_values($types); } } } } // 外壳颜色:优先从 fields_config._meta 读取 $shellColors = []; $needShellColors = $fieldMeta['extra_data_sources']['shellColors'] ?? false; if ($needShellColors) { $shellColors = $productModel->getByType('外壳颜色'); } elseif ($bizConfig && !empty($bizConfig['extraDataSources']['shellColors'])) { // 向后兼容 $shellColors = $productModel->getByType('外壳颜色'); } // ========== 获取已有记录 ========== $records = []; $filterType = ''; $filterModel = ''; $todayCount = 0; $totalCount = 0; if ($bizConfig && !empty($bizConfig['model'])) { // 内置工位:使用 StationDefinition 统一模型 $stationDef = new StationDefinition(); // ★ 关键修复:在查询数据之前,主动确保 station_definitions 数据完整 // 之前的设计依赖 getRecords() 抛异常来触发 ensureTable(), // 但 getByType() 返回 null 时直接返回空数组,不抛异常, // 导致数据不完整时永远无法自动恢复。 $defCheck = $stationDef->getByType($stationType); if (!$defCheck) { error_log('[' . $stationType . '] station_definitions record missing, auto-initializing...'); $stationDef->autoCreateTable(); StationDefinition::clearCache(); } // 筛选参数 $filterType = isset($_GET['product_type']) ? trim($_GET['product_type']) : ''; $filterModel = isset($_GET['product_model']) ? trim($_GET['product_model']) : ''; if (!empty($bizConfig['view']['enableFilter'])) { if ($filterType || $filterModel) { try { $records = $stationDef->getRecords($stationType, $filterType, $filterModel); } catch (\Throwable $e) { error_log('[' . $stationType . '] getRecords failed: ' . $e->getMessage()); $stationDef->ensureTable($stationType, $fieldConfigs); try { $records = $stationDef->getRecords($stationType, $filterType, $filterModel); } catch (\Throwable $e2) { error_log('[' . $stationType . '] getRecords retry failed: ' . $e2->getMessage()); $records = []; } } } // enableFilter 模式:初始不加载记录 } else { try { $records = $stationDef->getAllRecords($stationType); } catch (\Throwable $e) { error_log('[' . $stationType . '] getAllRecords failed: ' . $e->getMessage()); $stationDef->ensureTable($stationType, $fieldConfigs); try { $records = $stationDef->getAllRecords($stationType); } catch (\Throwable $e2) { error_log('[' . $stationType . '] getAllRecords retry failed: ' . $e2->getMessage()); $records = []; } } } // 今日计数 if (!empty($bizConfig['showTodayCount'])) { try { $todayCount = $stationDef->countTodayByOperator($stationType, $user['emp_name']); } catch (\Throwable $e) { error_log('[' . $stationType . '] countTodayByOperator failed: ' . $e->getMessage()); // 尝试自动创建表 $stationDef->ensureTable($stationType, $fieldConfigs); try { $todayCount = $stationDef->countTodayByOperator($stationType, $user['emp_name']); } catch (\Throwable $e2) { error_log('[' . $stationType . '] countTodayByOperator retry failed: ' . $e2->getMessage()); $todayCount = 0; } } } // 总计数(与当前筛选对齐) if (!empty($bizConfig['showTotalCount'])) { try { $totalCount = $stationDef->countTotal($stationType, $filterType, $filterModel); } catch (\Throwable $e) { error_log('[' . $stationType . '] countTotal failed: ' . $e->getMessage()); // 尝试自动创建表 $stationDef->ensureTable($stationType, $fieldConfigs); try { $totalCount = $stationDef->countTotal($stationType, $filterType, $filterModel); } catch (\Throwable $e2) { error_log('[' . $stationType . '] countTotal retry failed: ' . $e2->getMessage()); $totalCount = count($records); } } } else { $totalCount = count($records); } } else { // 动态工位:使用 station_records $stationRecord = new StationRecord(); $records = $stationRecord->getByStationType($stationType); $totalCount = count($records); } // ========== POST 请求处理 ========== if ($_SERVER['REQUEST_METHOD'] === 'POST') { $this->csrfVerify(); $action = isset($_POST['action']) ? $_POST['action'] : 'add'; // 处理特殊 actions(导出、PDF、lookup 等) if ($action !== 'add' && $bizConfig) { $this->handleSpecialAction($action, $bizConfig, $stationType, $records, $user); exit; } // ===== add 操作:提交数据 ===== if ($action === 'add') { try { if ($bizConfig && !empty($bizConfig['model'])) { // 内置工位:写入专用表 $this->handleBuiltinSubmit($bizConfig, $stationType, $user); } else { // 动态工位:写入 station_records $recordData = []; foreach ($fieldConfigs as $field) { if (!empty($field['enabled'])) { $name = $field['name']; $recordData[$name] = $_POST[$name] ?? ''; } } $sr = new StationRecord(); $sr->addRecord($stationType, $recordData, $user['emp_name']); } if ($this->isAjax()) { echo json_encode(['success' => true]); exit; } // 非 AJAX 提交:保留筛选参数 $redirectUrl = BASE_URL . '/Front/' . $stationType; $queryParams = []; if (!empty($_GET['product_type'])) $queryParams[] = 'product_type=' . urlencode($_GET['product_type']); if (!empty($_GET['product_model'])) $queryParams[] = 'product_model=' . urlencode($_GET['product_model']); if (!empty($queryParams)) $redirectUrl .= '?' . implode('&', $queryParams); header('Location: ' . $redirectUrl); exit; } catch (\Throwable $e) { error_log('[' . $stationType . '] ' . $e->getMessage()); if ($this->isAjax()) { @ob_clean(); header('Content-Type: application/json; charset=utf-8'); // 尝试解析结构化错误(code_prefix / duplicate) $errMsg = $e->getMessage(); $codePrefixErrors = null; $duplicateErrors = null; $isUserError = false; $decoded = json_decode($errMsg, true); if ($decoded && isset($decoded['type'])) { if ($decoded['type'] === 'code_prefix') { $codePrefixErrors = $decoded['errors']; $errMsg = implode(';', array_values($codePrefixErrors)); $isUserError = true; } elseif ($decoded['type'] === 'duplicate') { $duplicateErrors = $decoded['errors']; $errMsg = implode(';', array_values($duplicateErrors)); $isUserError = true; } } // 非用户错误(如 SQL 异常)不暴露原始信息给前端 if (!$isUserError && ($e instanceof \PDOException)) { $errMsg = '系统错误,请联系管理员'; } echo json_encode([ 'success' => false, 'error' => '提交失败:' . $errMsg, 'codePrefixErrors' => $codePrefixErrors, 'duplicateErrors' => $duplicateErrors, ]); exit; } $GLOBALS['stationError'] = '提交失败:' . $e->getMessage(); } } } // ========== 渲染视图 ========== // 构建视图变量 $viewVars = [ 'title' => ($currentStation['station_name'] ?? $stationType), 'user' => $user, 'sysConfig' => $sysConfig, 'stations' => $stations, 'stationType' => $stationType, 'currentStation' => $currentStation, 'models' => $models, 'types' => $types, 'customers' => $customers, 'fieldConfigs' => $fieldConfigs, 'records' => $records, 'bizConfig' => $bizConfig, 'filterType' => $filterType, 'filterModel' => $filterModel, 'todayCount' => $todayCount, 'csrfToken' => $this->csrfToken(), ]; // 额外数据源 if (isset($shellColors)) { $viewVars['shellColors'] = $shellColors; } // 始终使用通用模板视图 $this->_view = new \core\base\View('Front', 'station_generic'); foreach ($viewVars as $k => $v) { $this->_view->assign($k, $v); } $this->_view->render(); exit; } /** * 为没有 station_business_helper 配置的工位自动生成通用业务配置 * * 只要 station_definitions 中有记录,就自动生成配置, * 使该工位统一走内置工位路径(专用物理表),无需在 helper 中手动添加。 */ private function buildGenericBizConfig($stationType, $def) { $workstation = new Workstation(); $station = $workstation->getByType($stationType); $stationName = $station['station_name'] ?? $stationType; // 从 fields_config 中检测是否有 hidden 类型的 product_type 字段 // 如果有,提取 fixed_value 作为 fixedType,确保页面渲染时输出 hidden input $fixedType = null; $rawFieldConfigs = $this->getStationFieldConfig($stationType); $fieldMeta = $rawFieldConfigs['_meta'] ?? []; unset($rawFieldConfigs['_meta']); $fieldList = array_values($rawFieldConfigs); foreach ($fieldList as $field) { if (($field['name'] ?? '') === 'product_type' && ($field['type'] ?? '') === 'hidden') { $fixedType = $field['fixed_value'] ?? '半成品'; break; } } // ===== 自动生成 codePrefix 配置(序列号前缀防错) ===== // 优先从 _meta.prefix_field_map 读取显式映射 // 其次:对 is_scan=true 的字段,尝试从 hidden product_type 字段推断产品类型 // 最后:使用 *dynamic* 标记,由提交时的 product_type 动态匹配前缀 $prefixFieldMap = $fieldMeta['prefix_field_map'] ?? []; // 装配页(assembly)没有 product_type 下拉,三栏序列号各自固定对应产品类型, // 不能直接用 *dynamic*(提交时 product_type 为空会失控)。在此显式绑定字段→类型, // 落实「所有工位序列号都受前缀管控」原则。 if ($stationType === 'assembly') { $prefixFieldMap = array_merge($prefixFieldMap, [ 'finished_serial' => '成品', 'battery_serial' => '电池', 'pcb_serial' => 'PCBA', ]); } $codePrefix = []; if (!empty($prefixFieldMap)) { foreach ($prefixFieldMap as $fieldName => $productType) { $codePrefix[$fieldName] = [ $productType === '*dynamic*' ? '*dynamic*' : $productType, '', // message 由 buildCodePrefixRules 或后端动态生成 ]; } } else { // 自动推断:对 is_scan=true 的字段,若工位有固定 product_type,则映射到该类型 // 否则使用 *dynamic* 让后端根据提交时的 product_type 动态判断 foreach ($fieldList as $field) { if (!empty($field['enabled']) && !empty($field['name']) && !empty($field['is_scan']) && ($field['type'] ?? 'text') === 'text') { // 如果工位有固定的 product_type (hidden),使用它 if ($fixedType && $fixedType !== '半成品') { $codePrefix[$field['name']] = [$fixedType, "{$field['label']}必须以 {$fixedType} 对应前缀开头"]; } else { // 没有固定类型时使用动态匹配 $codePrefix[$field['name']] = ['*dynamic*', '序列号前缀与产品类型不匹配']; } } } } // 通过 buildCodePrefixRules 将产品类型转换为实际前缀 if (!empty($codePrefix)) { $prefixTypeMap = []; try { $sysConfigModel = new SysConfig(); $prefixTypeMap = $sysConfigModel->getCodePrefixRules(); } catch (\Throwable $e) { error_log('[' . $stationType . '] getCodePrefixRules failed: ' . $e->getMessage()); $prefixTypeMap = []; } $codePrefix = buildCodePrefixRules( array_combine( array_keys($codePrefix), array_map(function($r) { return $r[0]; }, array_values($codePrefix)) ), $prefixTypeMap ); } // ===== 自动生成 duplicateCheck 配置(防重复) ===== // 对每个 is_scan=true 或 unique=true 的文本字段生成重复检查 // 使用数组格式支持多字段检查 $duplicateChecks = []; foreach ($fieldList as $field) { if (!empty($field['enabled']) && !empty($field['name'])) { $isUnique = !empty($field['unique']); $isScanText = !empty($field['is_scan']) && ($field['type'] ?? 'text') === 'text'; if ($isUnique || $isScanText) { $label = $field['label'] ?? $field['name']; $duplicateChecks[] = [ 'field' => $field['name'], 'message' => "{$label}「{value}」已存在,不允许重复录入!", ]; } } } $config = [ 'model' => 'StationDefinition', // 使用 StationDefinition 统一模型 'fixedType' => $fixedType, 'view' => [ 'stationKey' => $stationType, 'pageTitle' => $stationName, 'pageIcon' => 'cube', 'cardTitle' => '快速录入', 'tableTitle' => $stationName . '记录', 'submitMode' => 'auto', 'codeFields' => [], 'labelFields' => [], 'enableFilter' => !empty($def['filter_enabled']), 'filterUrl' => '/Front/' . $stationType, 'reloadAfterSubmit' => true, 'emptyText' => '暂无记录,请在下方录入第一条数据', ], 'showTodayCount' => !empty($def['show_today_count']), 'showTotalCount' => !empty($def['show_total_count']), 'dataMapping' => '__auto__', // 标记:自动从 fields_config 收集字段 ]; if (!empty($codePrefix)) { $config['codePrefix'] = $codePrefix; } if (!empty($duplicateChecks)) { // 多字段 duplicateCheck:数组格式,后端逐个检查 $config['duplicateChecks'] = $duplicateChecks; } return $config; } /** * 处理内置工位的提交 */ private function handleBuiltinSubmit($bizConfig, $stationType, $user) { $stationDef = new StationDefinition(); // ===== delivery 特殊处理:序列号→箱序列号批量 ===== if ($stationType === 'delivery') { $serial = trim($_POST['finished_serial'] ?? ''); $customerName = trim($_POST['customer_name'] ?? ''); // 检查是否是箱序列号(从 warehouse_in 表查找) $boxItems = $stationDef->findByField('warehouse', 'box_serial', $serial); // 箱序列号批量处理需要从 warehouse_in 表查该 box_serial 下所有记录 $boxRecords = $this->getBoxItemsBySerial($serial); if (!empty($boxRecords)) { $result = $this->handleDeliveryBoxSubmit($serial, $customerName, $user, $stationDef); if ($result && $this->isAjax()) { echo json_encode([ 'success' => true, 'type' => 'box', 'imported' => $result['imported'], 'total' => $result['total'], 'skipped' => $result['skipped'], 'message' => "箱序列号 {$serial}:共导入 {$result['imported']} 件(总 {$result['total']} 件,跳过 {$result['skipped']} 件已出货)", ]); exit; } return; // 非 AJAX 重定向在调用方处理 } // 单件成品序列号:从 warehouse_in 查找 product_model $item = $stationDef->findByField('warehouse', 'finished_serial', $serial); $productModel = $item ? $item['product_model'] : ($_POST['product_model'] ?? ''); $data = [ 'finished_serial' => $serial, 'customer_name' => $customerName, 'product_model' => $productModel, 'operator' => $user['emp_name'], ]; $stationDef->addRecord('delivery', $data); return; } // ===== 通用提交逻辑 ===== // 确定写入目标:检查是否需要切换到备用表 $targetStationType = $stationType; $dataMapping = $bizConfig['dataMapping'] ?? []; if (!empty($bizConfig['alt_condition'])) { $cond = $bizConfig['alt_condition']; $condValue = $_POST[$cond['field']] ?? ''; if ($condValue === $cond['value']) { // inbound 选"成品"时,写入 warehouse_in 表 $targetStationType = 'warehouse'; if (!empty($bizConfig['alt_dataMapping'])) { $dataMapping = $bizConfig['alt_dataMapping']; } } } // 验证 if (!empty($bizConfig['validators'])) { $errors = []; foreach ($bizConfig['validators'] as $fieldName => $rules) { $val = trim($_POST[$fieldName] ?? ''); if (!empty($rules['required']) && $val === '') { $errors[] = $rules['required']; } } if (!empty($errors)) { throw new \Exception(implode(';', $errors)); } } // 代码前缀验证 if (!empty($bizConfig['codePrefix'])) { $prefixErrors = []; foreach ($bizConfig['codePrefix'] as $fieldName => $rule) { $prefix = $rule[0]; $message = $rule[1] ?? "{$fieldName} 前缀错误"; $val = trim($_POST[$fieldName] ?? ''); if ($val === '') continue; // 空值跳过 // 动态前缀:根据提交时的 product_type 动态获取前缀 if ($prefix === '*dynamic*') { $productType = trim($_POST['product_type'] ?? ''); // 防御:前端可能因缺失 product_type 下拉而取到 undefined,按“未关联类型”跳过而非报错卡死 if ($productType === '' || $productType === 'undefined') continue; $expectedPrefix = ''; try { $sysConfig = new SysConfig(); $expectedPrefix = $sysConfig->getPrefixByType($productType); } catch (\Throwable $e) { error_log('[' . $stationType . '] getPrefixByType failed: ' . $e->getMessage()); $expectedPrefix = ''; } // 原则上所有工位序列号都受前缀管控:未配置前缀视为配置缺失,明确报错而非静默放行 if ($expectedPrefix === '') { $prefixErrors[$fieldName] = "产品类型「{$productType}」未配置序列号前缀,请联系管理员在后台设置「序列号前缀规则」后再操作"; continue; } if (stripos($val, $expectedPrefix) !== 0) { $prefixErrors[$fieldName] = "{$productType}序列号必须以 {$expectedPrefix} 开头"; } } else { if (stripos($val, $prefix) !== 0) { $prefixErrors[$fieldName] = $message; } } } if (!empty($prefixErrors)) { throw new \Exception(json_encode([ 'type' => 'code_prefix', 'errors' => $prefixErrors, ], JSON_UNESCAPED_UNICODE)); } } // 防重复检查(支持两种格式) // 新格式 duplicateChecks: [{field, message}, ...] 多字段 // 旧格式 duplicateCheck: {field, message} 单字段(兼容) $dupChecks = []; if (!empty($bizConfig['duplicateChecks'])) { $dupChecks = $bizConfig['duplicateChecks']; } elseif (!empty($bizConfig['duplicateCheck'])) { $dupChecks = [$bizConfig['duplicateCheck']]; } if (!empty($dupChecks)) { $dupErrors = []; foreach ($dupChecks as $dc) { $checkValue = trim($_POST[$dc['field']] ?? ''); if ($checkValue === '') continue; $existing = $stationDef->findByField($targetStationType, $dc['field'], $checkValue); if ($existing) { $msg = str_replace('{value}', $checkValue, $dc['message']); $dupErrors[$dc['field']] = $msg; } } if (!empty($dupErrors)) { throw new \Exception(json_encode([ 'type' => 'duplicate', 'errors' => $dupErrors, ], JSON_UNESCAPED_UNICODE)); } } // 构建数据 $data = []; if ($dataMapping === '__auto__') { // 通用工位:自动从 fields_config 收集所有启用字段 $rawFieldConfigs = $this->getStationFieldConfig($stationType); unset($rawFieldConfigs['_meta']); $fieldConfigs = array_values($rawFieldConfigs); foreach ($fieldConfigs as $field) { if (!empty($field['enabled']) && !empty($field['name'])) { $name = $field['name']; $val = $_POST[$name] ?? ''; $data[$name] = $val !== '' ? $val : null; } } $data['operator'] = $user['emp_name']; } else { foreach ($dataMapping as $dbField => $postKey) { if ($postKey === '__operator__') { $data[$dbField] = $user['emp_name']; } else { $val = $_POST[$postKey] ?? ''; $data[$dbField] = $val !== '' ? $val : null; } } } // 特殊处理:inbound 走 station_inbound 时,移除 box_serial if ($stationType === 'inbound' && $targetStationType === 'inbound') { unset($data['box_serial']); // test_status 默认值 if (!isset($data['test_status'])) { $data['test_status'] = 'pending'; } } $stationDef->addRecord($targetStationType, $data); } /** * 从 warehouse_in 表查找箱序列号下的所有成品 */ private function getBoxItemsBySerial($boxSerial) { $stationDef = new StationDefinition(); $def = $stationDef->getByType('warehouse'); if (!$def) return []; $table = \core\base\Model::TABLE_PREFIX . $def['db_table']; $sql = "SELECT * FROM `{$table}` WHERE `box_serial` = :bs"; return $stationDef->query($sql, [':bs' => $boxSerial]); } /** * 处理特殊 action(导出CSV、PDF、序列号查找等) */ private function handleSpecialAction($action, $bizConfig, $stationType, $records, $user) { // CSV 导出 if ($action === 'export' && !empty($bizConfig['export'])) { $exp = $bizConfig['export']; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=' . $exp['filename']); $output = fopen('php://output', 'w'); fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($output, $exp['headers']); foreach ($records as $record) { $row = []; foreach ($exp['columns'] as $col) { $row[] = $record[$col] ?? ''; } fputcsv($output, $row); } fclose($output); exit; } // PDF 生成 if ($action === 'pdf' && !empty($bizConfig['pdfGenerator'])) { $method = $bizConfig['pdfGenerator']; if (method_exists($this, $method)) { $this->$method($records, $user); } exit; } // 序列号查找(delivery 工位) if ($action === 'lookup' && !empty($bizConfig['serialLookup'])) { $serial = trim($_POST['finished_serial'] ?? ''); if ($serial === '') { echo json_encode(['success' => false, 'error' => '请输入序列号']); exit; } $stationDef = new StationDefinition(); // 单件查找:从 warehouse_in 表 $item = $stationDef->findByField('warehouse', 'finished_serial', $serial); if ($item) { echo json_encode([ 'success' => true, 'type' => 'single', 'product_model' => $item['product_model'], 'finished_serial' => $item['finished_serial'], ]); exit; } // 箱序列号查找 if (!empty($bizConfig['boxBatch'])) { $boxItems = $this->getBoxItemsBySerial($serial); if (!empty($boxItems)) { $summary = []; foreach ($boxItems as $bi) { $pm = $bi['product_model']; if (!isset($summary[$pm])) { $summary[$pm] = ['count' => 0, 'models' => []]; } $summary[$pm]['count']++; $summary[$pm]['models'][] = $bi['finished_serial']; } echo json_encode([ 'success' => true, 'type' => 'box', 'box_serial' => $serial, 'total' => count($boxItems), 'summary' => $summary, ]); exit; } } echo json_encode(['success' => false, 'error' => '未找到该序列号']); exit; } // 未知 action echo json_encode(['success' => false, 'error' => '未知操作: ' . $action]); exit; } /** * 处理 delivery 的箱序列号批量提交 */ private function handleDeliveryBoxSubmit($serial, $customerName, $user, $stationDef) { $boxItems = $this->getBoxItemsBySerial($serial); if (empty($boxItems)) return false; $batchRecords = []; foreach ($boxItems as $bi) { $existing = $stationDef->findByField('delivery', 'finished_serial', $bi['finished_serial']); if ($existing) continue; $batchRecords[] = [ 'finished_serial' => $bi['finished_serial'], 'customer_name' => $customerName, 'product_model' => $bi['product_model'], 'operator' => $user['emp_name'], ]; } $imported = 0; foreach ($batchRecords as $rec) { $stationDef->addRecord('delivery', $rec); $imported++; } return [ 'imported' => $imported, 'total' => count($boxItems), 'skipped' => count($boxItems) - $imported, ]; } // ========== PDF 生成方法(从原硬编码方法中提取) ========== private function generateInboundPDF($records, $user) { header('Content-Type: text/html; charset=utf-8'); echo '
操作人:' . htmlspecialchars($user['emp_name']) . '
'; echo '生成时间:' . date('Y-m-d H:i:s') . '
'; echo '| 序号 | 成品序列号 | 箱序列号 | 入库时间 | 产品型号 | 操作人 |
|---|---|---|---|---|---|
| ' . ($i + 1) . ' | '; echo '' . htmlspecialchars($record['finished_serial']) . ' | '; echo '' . htmlspecialchars($record['box_serial'] ?? '') . ' | '; echo '' . htmlspecialchars($record['in_time']) . ' | '; echo '' . htmlspecialchars($record['product_model']) . ' | '; echo '' . htmlspecialchars($record['operator']) . ' |
操作人:' . htmlspecialchars($user['emp_name']) . '
'; echo '生成时间:' . date('Y-m-d H:i:s') . '
'; echo '| 序号 | 产品型号 | 数量 | 客户名称 |
|---|---|---|---|
| ' . $idx . ' | '; echo '' . htmlspecialchars($item['product_model']) . ' | '; echo '' . $item['count'] . ' | '; echo '' . htmlspecialchars($item['customer_name']) . ' |
| 合计 | ' . $totalQty . ' | ||