1270 lines
54 KiB
PHP
1270 lines
54 KiB
PHP
<?php
|
||
namespace app\controllers;
|
||
|
||
use core\base\Controller;
|
||
use app\models\StationDefinition;
|
||
use app\models\ProductModel;
|
||
use app\models\ProductTypeConfig;
|
||
use app\models\Customer;
|
||
use app\models\Workstation;
|
||
use app\models\EmployeePermission;
|
||
use app\models\SysConfig;
|
||
use app\models\StationRecord;
|
||
use app\models\Document;
|
||
|
||
require_once APP_PATH . 'app/helpers/field_helper.php';
|
||
require_once APP_PATH . 'app/helpers/station_business_helper.php';
|
||
|
||
class FrontController extends Controller
|
||
{
|
||
/**
|
||
* 根据工位类型获取字段配置
|
||
*/
|
||
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'] === \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 '<!DOCTYPE html><html><head><meta charset="utf-8"><title>入库单</title>';
|
||
echo '<style>body{font-family:Arial,sans-serif;padding:20px;}';
|
||
echo 'table{border-collapse:collapse;width:100%;}';
|
||
echo 'th,td{border:1px solid #333;padding:8px;text-align:left;}';
|
||
echo 'th{background:#3c8dbc;color:white;}';
|
||
echo 'h1{text-align:center;color:#333;}</style></head><body>';
|
||
echo '<h1>入库单</h1>';
|
||
echo '<p>操作人:' . htmlspecialchars($user['emp_name']) . '</p>';
|
||
echo '<p>生成时间:' . date('Y-m-d H:i:s') . '</p>';
|
||
echo '<table><tr><th>序号</th><th>成品序列号</th><th>箱序列号</th><th>入库时间</th><th>产品型号</th><th>操作人</th></tr>';
|
||
|
||
foreach ($records as $i => $record) {
|
||
echo '<tr><td>' . ($i + 1) . '</td>';
|
||
echo '<td>' . htmlspecialchars($record['finished_serial']) . '</td>';
|
||
echo '<td>' . htmlspecialchars($record['box_serial'] ?? '') . '</td>';
|
||
echo '<td>' . htmlspecialchars($record['in_time']) . '</td>';
|
||
echo '<td>' . htmlspecialchars($record['product_model']) . '</td>';
|
||
echo '<td>' . htmlspecialchars($record['operator']) . '</td></tr>';
|
||
}
|
||
echo '</table></body></html>';
|
||
}
|
||
|
||
private function generateDeliveryPDF($records, $user)
|
||
{
|
||
$summary = [];
|
||
foreach ($records as $record) {
|
||
$pm = $record['product_model'] ?: '未知型号';
|
||
if (!isset($summary[$pm])) {
|
||
$summary[$pm] = [
|
||
'product_model' => $pm, 'count' => 0,
|
||
'customer_name' => $record['customer_name'] ?? '',
|
||
];
|
||
}
|
||
$summary[$pm]['count']++;
|
||
}
|
||
|
||
header('Content-Type: text/html; charset=utf-8');
|
||
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>出库单</title>';
|
||
echo '<style>body{font-family:Arial,sans-serif;padding:20px;}';
|
||
echo 'table{border-collapse:collapse;width:100%;}';
|
||
echo 'th,td{border:1px solid #333;padding:8px;text-align:left;}';
|
||
echo 'th{background:#3c8dbc;color:white;}';
|
||
echo 'h1{text-align:center;color:#333;}</style></head><body>';
|
||
echo '<h1>出库单</h1>';
|
||
echo '<p>操作人:' . htmlspecialchars($user['emp_name']) . '</p>';
|
||
echo '<p>生成时间:' . date('Y-m-d H:i:s') . '</p>';
|
||
echo '<table><tr><th>序号</th><th>产品型号</th><th>数量</th><th>客户名称</th></tr>';
|
||
|
||
$idx = 1; $totalQty = 0;
|
||
foreach ($summary as $item) {
|
||
echo '<tr><td>' . $idx . '</td>';
|
||
echo '<td>' . htmlspecialchars($item['product_model']) . '</td>';
|
||
echo '<td>' . $item['count'] . '</td>';
|
||
echo '<td>' . htmlspecialchars($item['customer_name']) . '</td></tr>';
|
||
$idx++; $totalQty += $item['count'];
|
||
}
|
||
echo '<tr style="font-weight:bold;background:#f0f0f0;"><td colspan="2" style="text-align:right;">合计</td><td>' . $totalQty . '</td><td></td></tr>';
|
||
echo '</table></body></html>';
|
||
}
|
||
|
||
// ========== 文件管理工位 ==========
|
||
|
||
/**
|
||
* 渲染文件管理工位视图
|
||
*/
|
||
private function renderDocumentManageStation($user)
|
||
{
|
||
$document = new Document();
|
||
$documents = $document->getAll();
|
||
$categories = $document->getCategories();
|
||
|
||
$config = new SysConfig();
|
||
$sysConfig = $config->getConfig();
|
||
|
||
$workstation = new Workstation();
|
||
$stations = $workstation->getAll();
|
||
|
||
$view = new \core\base\View('Front', 'station_document_manage');
|
||
$view->assign('title', '文件管理');
|
||
$view->assign('user', $user);
|
||
$view->assign('sysConfig', $sysConfig);
|
||
$view->assign('stations', $stations);
|
||
$view->assign('documents', $documents);
|
||
$view->assign('categories', $categories);
|
||
$view->assign('csrfToken', $this->csrfToken());
|
||
$view->render();
|
||
}
|
||
|
||
/**
|
||
* 文件上传(管理员专用)
|
||
*/
|
||
public function documentManageUpload()
|
||
{
|
||
$this->checkLogin();
|
||
$user = $this->getCurrentUser();
|
||
|
||
// 权限检查:只有超级管理员和管理员可以上传
|
||
if ($user['role'] !== self::ROLE_SUPER_ADMIN && $user['role'] !== self::ROLE_ADMIN) {
|
||
$this->jsonExit(['success' => false, 'error' => '无权限:仅管理员可以上传文件']);
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||
$this->jsonExit(['success' => false, 'error' => '无效请求']);
|
||
}
|
||
|
||
// 大文件超过 post_max_size 时 PHP 会丢弃整个请求体($_POST/$_FILES 均为空),
|
||
// 进而使 CSRF 校验失败并返回非 JSON,前端误报“网络错误”。先给出明确提示。
|
||
if (empty($_POST) && empty($_FILES)) {
|
||
$this->jsonExit(['success' => false, 'error' => '上传失败:提交内容为空,通常是文件体积超过服务器 post_max_size 限制。请压缩文件后重试,或联系管理员调大上传上限。']);
|
||
}
|
||
|
||
$this->csrfVerify();
|
||
|
||
$data = [
|
||
'title' => trim($_POST['title'] ?? ''),
|
||
'doc_no' => trim($_POST['doc_no'] ?? ''),
|
||
'category' => trim($_POST['category'] ?? ''),
|
||
'version' => trim($_POST['version'] ?? '1.0'),
|
||
'author' => $user['emp_name'],
|
||
'department' => '',
|
||
'status' => 'active',
|
||
'keywords' => '',
|
||
'description' => trim($_POST['description'] ?? ''),
|
||
'operator' => $user['emp_name'],
|
||
];
|
||
|
||
if (empty($data['title'])) {
|
||
$this->jsonExit(['success' => false, 'error' => '文档标题不能为空']);
|
||
}
|
||
|
||
// 文件上传验证
|
||
if (!isset($_FILES['doc_file']) || $_FILES['doc_file']['error'] !== UPLOAD_ERR_OK) {
|
||
$this->jsonExit(['success' => false, 'error' => '请选择要上传的文件']);
|
||
}
|
||
|
||
$allowedExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'zip', 'rar', 'jpg', 'jpeg', 'png', 'gif'];
|
||
$allowedMimeTypes = [
|
||
'application/pdf',
|
||
'application/msword',
|
||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'application/vnd.ms-excel',
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'application/vnd.ms-powerpoint',
|
||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||
'text/plain',
|
||
'text/csv',
|
||
'application/zip',
|
||
'application/x-zip-compressed',
|
||
'application/x-rar-compressed',
|
||
'image/jpeg',
|
||
'image/png',
|
||
'image/gif',
|
||
];
|
||
|
||
$originalName = $_FILES['doc_file']['name'];
|
||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||
$tmpPath = $_FILES['doc_file']['tmp_name'];
|
||
|
||
$mime = '';
|
||
if (function_exists('finfo_open')) {
|
||
$finfo = @finfo_open(FILEINFO_MIME_TYPE);
|
||
if ($finfo !== false) {
|
||
$mime = @finfo_file($finfo, $tmpPath);
|
||
finfo_close($finfo);
|
||
}
|
||
}
|
||
// Fileinfo 不可用时仅按扩展名校验,避免误杀
|
||
$mimeOk = $mime === '' ? true : in_array($mime, $allowedMimeTypes, true);
|
||
|
||
if (!in_array($ext, $allowedExtensions, true) || !$mimeOk) {
|
||
error_log('[documentManageUpload] Upload rejected: ext=' . $ext . ' mime=' . $mime . ' file=' . $originalName);
|
||
$this->jsonExit(['success' => false, 'error' => '不支持的文件类型,仅允许常见文档、图片和压缩包格式']);
|
||
}
|
||
|
||
$uploadDir = APP_PATH . 'static/uploads/documents/';
|
||
if (!is_dir($uploadDir)) {
|
||
mkdir($uploadDir, 0755, true);
|
||
}
|
||
$saveName = date('YmdHis') . '_' . uniqid() . '.' . $ext;
|
||
$savePath = $uploadDir . $saveName;
|
||
|
||
if (!move_uploaded_file($tmpPath, $savePath)) {
|
||
$this->jsonExit(['success' => false, 'error' => '文件保存失败']);
|
||
}
|
||
|
||
$data['file_name'] = $originalName;
|
||
$data['file_path'] = '/static/uploads/documents/' . $saveName;
|
||
$data['file_size'] = $_FILES['doc_file']['size'];
|
||
$data['file_type'] = $ext;
|
||
|
||
try {
|
||
$document = new Document();
|
||
$document->add($data);
|
||
$this->jsonExit(['success' => true]);
|
||
} catch (\Throwable $e) {
|
||
error_log('[documentManageUpload] DB error: ' . $e->getMessage());
|
||
// 清理已上传的文件
|
||
@unlink($savePath);
|
||
$this->jsonExit(['success' => false, 'error' => '保存失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 文件删除(管理员专用)
|
||
*/
|
||
public function documentManageDelete()
|
||
{
|
||
$this->checkLogin();
|
||
$user = $this->getCurrentUser();
|
||
|
||
// 权限检查
|
||
if ($user['role'] !== self::ROLE_SUPER_ADMIN && $user['role'] !== self::ROLE_ADMIN) {
|
||
$this->jsonExit(['success' => false, 'error' => '无权限']);
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||
$this->jsonExit(['success' => false, 'error' => '无效请求']);
|
||
}
|
||
|
||
$this->csrfVerify();
|
||
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
$this->jsonExit(['success' => false, 'error' => '无效的文档ID']);
|
||
}
|
||
|
||
try {
|
||
$document = new Document();
|
||
$record = $document->getById($id);
|
||
if (!$record) {
|
||
$this->jsonExit(['success' => false, 'error' => '文档不存在']);
|
||
}
|
||
|
||
// 删除物理文件
|
||
if (!empty($record['file_path'])) {
|
||
$filePath = APP_PATH . ltrim($record['file_path'], '/');
|
||
if (file_exists($filePath)) {
|
||
@unlink($filePath);
|
||
}
|
||
}
|
||
|
||
$document->delete($id);
|
||
$this->jsonExit(['success' => true]);
|
||
} catch (\Throwable $e) {
|
||
error_log('[documentManageDelete] Error: ' . $e->getMessage());
|
||
$this->jsonExit(['success' => false, 'error' => '删除失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
}
|