初始化
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,918 @@
|
||||
<?php
|
||||
namespace app\controllers;
|
||||
|
||||
use core\base\Controller;
|
||||
use app\models\AsdTestData;
|
||||
use app\models\AsdDevice;
|
||||
|
||||
/**
|
||||
* 昂盛达上位机 MES 接口 API
|
||||
* 接口文档版本:1.5
|
||||
*
|
||||
* 工作流:UserInfoVerification → GetWorkInfo → CheckFlow → UpDataInfo
|
||||
*
|
||||
* 接口地址:http://host/Api/{method}
|
||||
* 协议:HTTP POST
|
||||
* 数据格式:JSON
|
||||
*/
|
||||
class ApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* 构造函数:所有 API 响应统一输出 JSON。
|
||||
* 注意:框架 Core::route() 不会自动调用 init(),因此此处设置 Content-Type。
|
||||
*/
|
||||
public function __construct($controller, $action)
|
||||
{
|
||||
parent::__construct($controller, $action);
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
// 注册 API 专用异常处理器:PDO 处于 ERRMODE_EXCEPTION 模式,
|
||||
// 任何未捕获异常(如缺列、连接异常)都会冒泡成 500 显示「系统内部错误」。
|
||||
// 这里覆盖为 JSON 响应,既避免 500,又把真实错误暴露给调用方便于排查。
|
||||
set_exception_handler(function ($e) {
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
http_response_code(500);
|
||||
$msg = '服务器处理异常';
|
||||
if (APP_DEBUG && $e instanceof \Throwable) {
|
||||
$msg .= ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
|
||||
}
|
||||
echo json_encode(['Result' => 0, 'Message' => $msg], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 API Token
|
||||
*
|
||||
* 与 generateToken() 一致,统一使用 DGZXY_asd_api_token 表。
|
||||
* 开放模式(符合接口文档 v1.5「鉴权为可选项」):
|
||||
* - 未携带 Token:放行(返回 null),便于设备直接上传;
|
||||
* - 携带了 Token 但无效/过期:拒绝。
|
||||
*
|
||||
* @return array|null 成功/开放模式返回用户信息或 null,失败直接 exit
|
||||
*/
|
||||
protected function apiAuth()
|
||||
{
|
||||
// 从 Authorization 头获取 Token
|
||||
$token = '';
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
|
||||
if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||||
$token = trim($matches[1]);
|
||||
}
|
||||
|
||||
// 也支持 POST/GET 参数传递 token(向后兼容)
|
||||
if (empty($token)) {
|
||||
$input = $this->getJsonInput();
|
||||
$token = $input['token'] ?? $_GET['token'] ?? '';
|
||||
}
|
||||
|
||||
// 开放模式:未携带 Token 直接放行
|
||||
if (empty($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 携带了 Token,则必须有效
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
$stmt = $pdo->prepare("SELECT * FROM DGZXY_asd_api_token WHERE token = :token");
|
||||
$stmt->execute([':token' => $token]);
|
||||
$tokenInfo = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) {
|
||||
if (APP_DEBUG) {
|
||||
error_log('API auth error: ' . $e->getMessage());
|
||||
}
|
||||
$this->jsonResponse(0, '认证服务异常');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$tokenInfo) {
|
||||
$this->jsonResponse(0, 'Token无效或已过期');
|
||||
exit;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => 0,
|
||||
'emp_no' => $tokenInfo['emp_no'] ?? '',
|
||||
'emp_name' => $tokenInfo['tester'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 频率限制(基于文件滑动窗口,按 IP + key 计数)
|
||||
* @param string $key 业务标识
|
||||
* @param int $max 时间窗口内最大允许次数
|
||||
* @param int $window 时间窗口(秒)
|
||||
* @return bool true=已超限需拒绝
|
||||
*/
|
||||
private function rateLimitExceeded($key, $max, $window)
|
||||
{
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$dir = sys_get_temp_dir() . '/mes2_ratelimit';
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0700, true);
|
||||
}
|
||||
if (!is_dir($dir)) {
|
||||
return false; // 无法记录时放行,不影响业务
|
||||
}
|
||||
$file = $dir . '/' . md5($key . '|' . $ip);
|
||||
$now = time();
|
||||
$hits = [];
|
||||
if (file_exists($file)) {
|
||||
$raw = @file_get_contents($file);
|
||||
$hits = $raw ? json_decode($raw, true) ?: [] : [];
|
||||
}
|
||||
// 清除窗口外的记录
|
||||
$hits = array_filter($hits, function ($t) use ($now, $window) {
|
||||
return ($now - $t) < $window;
|
||||
});
|
||||
if (count($hits) >= $max) {
|
||||
return true;
|
||||
}
|
||||
$hits[] = $now;
|
||||
@file_put_contents($file, json_encode($hits));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 默认入口:不暴露任何方法列表,返回干净的 404
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 404 Not Found');
|
||||
}
|
||||
$this->jsonResponse(0, 'API endpoint not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 JSON 响应
|
||||
*/
|
||||
private function jsonResponse($result, $message, $extra = [])
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
$data = array_merge(['Result' => (int)$result, 'Message' => $message], $extra);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始 JSON 请求体
|
||||
*/
|
||||
private function getJsonInput()
|
||||
{
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成持久化 Token(存入数据库以便后续校验)
|
||||
*/
|
||||
private function generateToken($empNo, $tester)
|
||||
{
|
||||
$token = bin2hex(random_bytes(32)); // 使用加密安全的随机数替代 md5
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO DGZXY_asd_api_token (token, emp_no, tester, created_at)
|
||||
VALUES (:token, :empNo, :tester, NOW())
|
||||
ON DUPLICATE KEY UPDATE tester = :tester2, created_at = NOW()"
|
||||
);
|
||||
$stmt->execute([
|
||||
':token' => $token,
|
||||
':empNo' => $empNo,
|
||||
':tester' => $tester,
|
||||
':tester2' => $tester,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// Token 表不存在时记录日志,但仍返回 token
|
||||
// 注意:verifyToken 也会因表不存在而拒绝,保证一致性
|
||||
if (APP_DEBUG) {
|
||||
error_log('API Token table error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Token 是否有效
|
||||
* 安全修复:表不存在时拒绝通过(而非降级放行)
|
||||
*/
|
||||
private function verifyToken($token, $tester = '')
|
||||
{
|
||||
if (empty($token)) return false;
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
$sql = "SELECT * FROM DGZXY_asd_api_token WHERE token = :token";
|
||||
$params = [':token' => $token];
|
||||
if (!empty($tester)) {
|
||||
$sql .= " AND tester = :tester";
|
||||
$params[':tester'] = $tester;
|
||||
}
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: false;
|
||||
} catch (\Throwable $e) {
|
||||
// 安全修复:表不存在时拒绝通过,而非降级放行
|
||||
if (APP_DEBUG) {
|
||||
error_log('API Token verification error: ' . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 接口 1: 用户登录身份信息校验 ====================
|
||||
|
||||
/**
|
||||
* 1. AsdUserInfoVerification - 用户登录系统身份信息校验
|
||||
* POST /Api/asdUserInfoVerification
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* Parameter1 - 参数1(员工编号/工号,扫码或手动输入)
|
||||
* Parameter2 - 参数2(密码或其它验证信息,可选)
|
||||
* Parameter3 - 参数3(可选)
|
||||
* Parameter4 - 参数4(可选)
|
||||
*
|
||||
* 成功响应:
|
||||
* Result=1, Message="验证成功", Tester="张三", Token="xxx"
|
||||
* 失败响应:
|
||||
* Result=0, Message="工号不存在" 或 "密码错误"
|
||||
*/
|
||||
public function asdUserInfoVerification()
|
||||
{
|
||||
// 频率限制:同一 IP 60 秒内最多 10 次,防止枚举/暴力破解
|
||||
if ($this->rateLimitExceeded('asdUserInfoVerification', 10, 60)) {
|
||||
$this->jsonResponse(0, '尝试过于频繁,请稍后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
$empNo = $data['Parameter1'] ?? '';
|
||||
$password = $data['Parameter2'] ?? '';
|
||||
$param3 = $data['Parameter3'] ?? '';
|
||||
$param4 = $data['Parameter4'] ?? '';
|
||||
|
||||
// 安全修复(P0):API 认证必须同时提供工号与密码,禁止无密码签发 Token
|
||||
if (empty($empNo)) {
|
||||
$this->jsonResponse(0, '工号(Parameter1)不能为空');
|
||||
return;
|
||||
}
|
||||
if (empty($password)) {
|
||||
$this->jsonResponse(0, '缺少密码(Parameter2),API 认证必须提供密码');
|
||||
return;
|
||||
}
|
||||
|
||||
$employee = new \app\models\Employee();
|
||||
$emp = $employee->findByEmpNo($empNo);
|
||||
|
||||
if (!$emp) {
|
||||
$this->jsonResponse(0, '工号不存在: ' . $empNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 严格校验密码(bcrypt 验证)
|
||||
$valid = $employee->validateLogin($empNo, $password);
|
||||
if (!$valid) {
|
||||
$this->jsonResponse(0, '密码错误');
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->generateToken($empNo, $emp['emp_name']);
|
||||
$this->jsonResponse(1, '验证成功', [
|
||||
'Tester' => $emp['emp_name'],
|
||||
'Token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== 接口 2: 获取站点/工位/关联数据 ====================
|
||||
|
||||
/**
|
||||
* 2. AsdGetWorkInfo - 获取站点、工位或其它关联数据
|
||||
* POST /Api/asdGetWorkInfo
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* Parameter1 - 参数1(条码/二维码,用于查找关联工单)
|
||||
* Parameter2 - 参数2(工位编码,可选)
|
||||
* Parameter3 - 参数3(设备编码,可选)
|
||||
* Parameter4 - 参数4(可选)
|
||||
*
|
||||
* 成功响应:
|
||||
* Result=1, Message="获取成功", Mo="xxx", Site="xxx", Station="xxx", WorkOrder="xxx"
|
||||
*/
|
||||
public function asdGetWorkInfo()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
$param1 = $data['Parameter1'] ?? ''; // 条码/二维码
|
||||
$param2 = $data['Parameter2'] ?? ''; // 工位编码
|
||||
$param3 = $data['Parameter3'] ?? ''; // 设备编码
|
||||
$param4 = $data['Parameter4'] ?? ''; // 备用
|
||||
|
||||
$mo = '';
|
||||
$site = '';
|
||||
$station = '';
|
||||
$workOrder = '';
|
||||
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
|
||||
// 1. 如果有条码,查询该条码已关联的制令单/工单
|
||||
if (!empty($param1)) {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT mo, site, station, work_order FROM DGZXY_asd_test_record
|
||||
WHERE qr_code = :qc ORDER BY id DESC LIMIT 1"
|
||||
);
|
||||
$stmt->execute([':qc' => $param1]);
|
||||
$record = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($record && !empty($record['mo'])) {
|
||||
$mo = $record['mo'];
|
||||
$site = $record['site'];
|
||||
$station = $record['station'];
|
||||
$workOrder = $record['work_order'];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 根据设备编码查找默认工位
|
||||
if (!empty($param3)) {
|
||||
$deviceModel = new AsdDevice();
|
||||
$device = $deviceModel->findByCode($param3);
|
||||
if ($device) {
|
||||
if (empty($station) && !empty($device['station_code'])) {
|
||||
$station = $device['station_code'];
|
||||
}
|
||||
if (empty($site) && !empty($device['line_name'])) {
|
||||
$site = $device['line_name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 根据传入工位编码查工位
|
||||
if (empty($station) && !empty($param2)) {
|
||||
$station = $param2;
|
||||
}
|
||||
|
||||
// 4. 从工位表获取详细信息
|
||||
$workstationModel = new \app\models\Workstation();
|
||||
$allStations = $workstationModel->getAll();
|
||||
|
||||
// 自动补全 site 和 station
|
||||
if ((empty($site) || empty($station)) && !empty($allStations)) {
|
||||
$first = $allStations[0];
|
||||
if (empty($site)) $site = $first['station_name'] ?? '';
|
||||
if (empty($station)) $station = $first['station_code'] ?? '';
|
||||
}
|
||||
|
||||
// 5. 生成制令单号(如果未找到已关联的)
|
||||
if (empty($mo)) {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT MAX(mo) AS last_mo FROM DGZXY_asd_test_record WHERE mo LIKE :prefix"
|
||||
);
|
||||
$todayPrefix = 'MO' . date('Ymd') . '%';
|
||||
$stmt->execute([':prefix' => $todayPrefix]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!empty($row['last_mo'])) {
|
||||
$lastSeq = intval(substr($row['last_mo'], 10));
|
||||
$mo = 'MO' . date('Ymd') . str_pad($lastSeq + 1, 3, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$mo = 'MO' . date('Ymd') . '001';
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 生成工单号
|
||||
if (empty($workOrder)) {
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT MAX(work_order) AS last_wo FROM DGZXY_asd_test_record WHERE work_order LIKE :prefix"
|
||||
);
|
||||
$woPrefix = 'WO-' . date('Ymd') . '%';
|
||||
$stmt->execute([':prefix' => $woPrefix]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if (!empty($row['last_wo'])) {
|
||||
$lastSeq = intval(substr($row['last_wo'], 12));
|
||||
$workOrder = 'WO-' . date('Ymd') . '-' . str_pad($lastSeq + 1, 2, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
$workOrder = 'WO-' . date('Ymd') . '-01';
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// 容错:即使数据库异常,也返回可用数据
|
||||
if (empty($mo)) $mo = 'MO' . date('Ymd') . '001';
|
||||
if (empty($workOrder)) $workOrder = 'WO-' . date('Ymd') . '-01';
|
||||
}
|
||||
|
||||
$this->jsonResponse(1, '获取成功', [
|
||||
'Mo' => $mo,
|
||||
'Site' => $site,
|
||||
'Station' => $station,
|
||||
'WorkOrder' => $workOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== 接口 3: 数据检验/工艺检查 ====================
|
||||
|
||||
/**
|
||||
* 3. AsdCheckFlow - 数据检验、工艺检查
|
||||
* POST /Api/asdCheckFlow
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* QrCode - 条码/二维码(有则必传)
|
||||
* Mo - GetWorkInfo 返回的 Mo
|
||||
* Site - GetWorkInfo 返回的 Site
|
||||
* Station - GetWorkInfo 返回的 Station
|
||||
* WorkOrder - GetWorkInfo 返回的 WorkOrder
|
||||
* Group - 组别(必填)
|
||||
* Device - 设备序列号(必填)
|
||||
*
|
||||
* 成功响应:
|
||||
* Result=1, QrCode="xxx", Message="检验通过,可以开始测试"
|
||||
* 失败响应:
|
||||
* Result=0, QrCode="xxx", Message="该条码已完成测试" / "设备未注册" / ...
|
||||
*/
|
||||
public function asdCheckFlow()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
$qrCode = $data['QrCode'] ?? '';
|
||||
$mo = $data['Mo'] ?? '';
|
||||
$site = $data['Site'] ?? '';
|
||||
$station = $data['Station'] ?? '';
|
||||
$workOrder = $data['WorkOrder'] ?? '';
|
||||
$group = $data['Group'] ?? '';
|
||||
$device = $data['Device'] ?? '';
|
||||
|
||||
// 1. 必填字段校验:Group & Device
|
||||
if (empty($group)) {
|
||||
$this->jsonResponse(0, '组别(Group)不能为空', ['QrCode' => $qrCode]);
|
||||
return;
|
||||
}
|
||||
if (empty($device)) {
|
||||
$this->jsonResponse(0, '设备序列号(Device)不能为空', ['QrCode' => $qrCode]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 设备存在性校验(开放模式:设备未注册不阻断,仅做组别一致性校验)
|
||||
$deviceModel = new AsdDevice();
|
||||
try {
|
||||
$deviceRecord = $deviceModel->isActive($device);
|
||||
if ($deviceRecord
|
||||
&& !empty($group)
|
||||
&& !empty($deviceRecord['group_name'])
|
||||
&& $deviceRecord['group_name'] !== $group) {
|
||||
// 设备已注册但组别与提交不一致:拒绝(数据完整性保护)
|
||||
$this->jsonResponse(0, "设备 {$device} 属于组别 [{$deviceRecord['group_name']}],与提交组别 [{$group}] 不匹配", [
|
||||
'QrCode' => $qrCode,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
// 设备未注册时放行(开放接口,便于先上线采集再补录设备)
|
||||
} catch (\Throwable $e) {
|
||||
// 设备表异常时不阻止流程
|
||||
}
|
||||
|
||||
// 3. 条码防重校验(如果有条码)
|
||||
if (!empty($qrCode)) {
|
||||
try {
|
||||
$testData = new AsdTestData();
|
||||
$existing = $testData->query(
|
||||
"SELECT id, status FROM DGZXY_asd_test_record WHERE qr_code = :qc ORDER BY id DESC LIMIT 1",
|
||||
[':qc' => $qrCode]
|
||||
);
|
||||
if (!empty($existing)) {
|
||||
$this->jsonResponse(0, '该条码已完成测试,请勿重复测试', [
|
||||
'QrCode' => $qrCode,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 容错
|
||||
}
|
||||
}
|
||||
|
||||
$this->jsonResponse(1, '检验通过,可以开始测试', [
|
||||
'QrCode' => $qrCode,
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== 接口 4: 提交测试数据 ====================
|
||||
|
||||
/**
|
||||
* 4. AsdUpDataInfo - 提交测试数据(核心接口)
|
||||
* POST /Api/asdUpDataInfo
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* Tester - 测试员
|
||||
* QrCode - 条码/二维码
|
||||
* Mo - 制令单
|
||||
* Site - 工位
|
||||
* Station - 站点
|
||||
* WorkOrder - 工单
|
||||
* ProjectName - 项目名称
|
||||
* Status - 测试状态 PASS/NG(必填)
|
||||
* Group - 组别(必填)
|
||||
* Device - 设备号(必填)
|
||||
* RunningTime - 运行时间(必填)
|
||||
* TestTime - 测试时间(必填)
|
||||
* DataList - 测试工步数据(必填)
|
||||
*
|
||||
* 成功响应:
|
||||
* Result=1, Message="测试数据上传成功", RecordId="xxx", DetailCount=N
|
||||
* 失败响应:
|
||||
* Result=0, Message="错误描述"
|
||||
*/
|
||||
public function asdUpDataInfo()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
|
||||
// ===== 必填字段校验 =====
|
||||
|
||||
// Status 必填
|
||||
if (empty($data['Status'])) {
|
||||
$this->jsonResponse(0, '测试状态(Status)是必填项,值必须为 PASS 或 NG');
|
||||
return;
|
||||
}
|
||||
$status = strtoupper($data['Status']);
|
||||
if (!in_array($status, ['PASS', 'NG'])) {
|
||||
$this->jsonResponse(0, 'Status 值无效,必须为 PASS 或 NG,当前值: ' . $status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Group 必填
|
||||
if (empty($data['Group'])) {
|
||||
$this->jsonResponse(0, '组别(Group)是必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// Device 必填
|
||||
if (empty($data['Device'])) {
|
||||
$this->jsonResponse(0, '设备号(Device)是必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// RunningTime 必填
|
||||
if (empty($data['RunningTime'])) {
|
||||
$this->jsonResponse(0, '运行时间(RunningTime)是必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// TestTime 必填
|
||||
if (empty($data['TestTime'])) {
|
||||
$this->jsonResponse(0, '测试时间(TestTime)是必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// DataList 必填且为数组
|
||||
if (empty($data['DataList']) || !is_array($data['DataList'])) {
|
||||
$this->jsonResponse(0, '测试数据(DataList)是必填项,且必须为数组');
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== 设备存在性校验(可选但推荐) =====
|
||||
$deviceCode = $data['Device'];
|
||||
try {
|
||||
$deviceModel = new AsdDevice();
|
||||
$deviceRecord = $deviceModel->isActive($deviceCode);
|
||||
if (!$deviceRecord) {
|
||||
// 不阻止上传,但记录警告信息
|
||||
// 需要在 asd_device 表中预先注册设备
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 设备表异常时不影响主流程
|
||||
}
|
||||
|
||||
// ===== 写入数据库 =====
|
||||
try {
|
||||
$testData = new AsdTestData();
|
||||
|
||||
// 插入主记录
|
||||
$recordId = $testData->addRecord([
|
||||
'tester' => $data['Tester'] ?? '',
|
||||
'qr_code' => $data['QrCode'] ?? '',
|
||||
'mo' => $data['Mo'] ?? '',
|
||||
'site' => $data['Site'] ?? '',
|
||||
'station' => $data['Station'] ?? '',
|
||||
'work_order' => $data['WorkOrder'] ?? '',
|
||||
'project_name' => $data['ProjectName'] ?? '',
|
||||
'status' => $status,
|
||||
'group_name' => $data['Group'],
|
||||
'device' => $deviceCode,
|
||||
'running_time' => $data['RunningTime'],
|
||||
'test_time' => $data['TestTime'],
|
||||
]);
|
||||
|
||||
if (!$recordId) {
|
||||
$err = $testData->getError();
|
||||
$this->jsonResponse(0, '保存测试记录失败: ' . ($err ?: '数据库写入错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 插入工步详情
|
||||
$detailCount = 0;
|
||||
foreach ($data['DataList'] as $item) {
|
||||
$detailResult = $testData->addDetail([
|
||||
'record_id' => $recordId,
|
||||
'seq' => $item['Seq'] ?? ++$detailCount,
|
||||
'test_name' => $item['TestName'] ?? '',
|
||||
'test_item' => $item['TestItem'] ?? '',
|
||||
'test_units' => $item['TestUnits'] ?? '',
|
||||
'data_value' => $item['DataValue'] ?? null,
|
||||
'lower_limit' => $item['LowerLimit'] ?? null,
|
||||
'upper_limit' => $item['UpperLimit'] ?? null,
|
||||
'test_value' => $item['TestValue'] ?? '',
|
||||
'test_limit' => $item['TestLimit'] ?? '',
|
||||
'test_result' => $item['TestResult'] ?? 'PASS',
|
||||
]);
|
||||
if ($detailResult) {
|
||||
$detailCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->jsonResponse(1, '测试数据上传成功', [
|
||||
'RecordId' => $recordId,
|
||||
'DetailCount' => $detailCount,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// PDO 异常模式:缺列/约束等会抛异常,这里转成 JSON 错误而非 500
|
||||
$this->jsonResponse(0, '保存测试记录异常: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 接口 5: 获取设备列表 ====================
|
||||
|
||||
/**
|
||||
* 5. AsdGetDevices - 获取已注册设备列表(供扫码选择使用)
|
||||
* POST /Api/asdGetDevices
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* Keyword - 搜索关键词(可选,模糊匹配设备编码/名称/IP/组别)
|
||||
*
|
||||
* 返回:
|
||||
* Result=1, Devices=[{id, device_code, device_name, device_ip, group_name, status}, ...]
|
||||
*/
|
||||
public function asdGetDevices()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
$keyword = $data['Keyword'] ?? '';
|
||||
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
if (!empty($keyword)) {
|
||||
$like = '%' . $keyword . '%';
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, device_code, device_name, device_ip, device_type, group_name, line_name, station_code, status, remark
|
||||
FROM DGZXY_asd_device
|
||||
WHERE device_code LIKE :kw1 OR device_name LIKE :kw2 OR device_ip LIKE :kw3 OR group_name LIKE :kw4
|
||||
ORDER BY group_name ASC, device_code ASC"
|
||||
);
|
||||
$stmt->execute([':kw1' => $like, ':kw2' => $like, ':kw3' => $like, ':kw4' => $like]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT id,device_code,device_name,device_ip,device_type,group_name,line_name,station_code,status,remark FROM DGZXY_asd_device ORDER BY group_name ASC, device_code ASC");
|
||||
$stmt->execute();
|
||||
}
|
||||
$devices = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
} catch (\Throwable $e) {
|
||||
$devices = [];
|
||||
}
|
||||
|
||||
$this->jsonResponse(1, '获取成功', ['Devices' => $devices]);
|
||||
}
|
||||
|
||||
// ==================== 接口 6: 设备连接测试 ====================
|
||||
|
||||
/**
|
||||
* 6. AsdDeviceConnect - 连接设备并获取设备信息
|
||||
* POST /Api/asdDeviceConnect
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* DeviceCode - 设备编码(必填)
|
||||
* DeviceIp - 设备IP(可选,传入后自动更新设备IP)
|
||||
*
|
||||
* 成功响应:
|
||||
* Result=1, Message="设备连接成功",
|
||||
* Device={device_code, device_name, device_ip, group_name, status},
|
||||
* ConnectionInfo={ip, port, reachable, latency_ms}
|
||||
*/
|
||||
public function asdDeviceConnect()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
$deviceCode = $data['DeviceCode'] ?? '';
|
||||
$deviceIp = $data['DeviceIp'] ?? '';
|
||||
|
||||
if (empty($deviceCode)) {
|
||||
$this->jsonResponse(0, '设备编码(DeviceCode)不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$deviceModel = new AsdDevice();
|
||||
$device = $deviceModel->findByCode($deviceCode);
|
||||
|
||||
if (!$device) {
|
||||
$this->jsonResponse(0, '设备不存在: ' . $deviceCode);
|
||||
return;
|
||||
}
|
||||
if ($device['status'] != 1) {
|
||||
$this->jsonResponse(0, '设备已禁用: ' . $deviceCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果传入了新 IP,更新到数据库
|
||||
if (!empty($deviceIp) && $deviceIp !== ($device['device_ip'] ?? '')) {
|
||||
$deviceModel->updateDevice($device['id'], ['device_ip' => $deviceIp]);
|
||||
$device['device_ip'] = $deviceIp;
|
||||
}
|
||||
|
||||
// 检查 IP 是否存在
|
||||
if (empty($device['device_ip'])) {
|
||||
$this->jsonResponse(0, '设备 IP 未设置,请先填写设备 IP 地址', [
|
||||
'Device' => $device,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试 TCP 连接(如果有 socket 扩展)
|
||||
$reachable = false;
|
||||
$latency = 0;
|
||||
$port = 502; // 默认 Modbus TCP 端口
|
||||
|
||||
if (function_exists('fsockopen')) {
|
||||
$start = microtime(true);
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$fp = @fsockopen($device['device_ip'], $port, $errno, $errstr, 3);
|
||||
if ($fp) {
|
||||
$reachable = true;
|
||||
$latency = round((microtime(true) - $start) * 1000, 1);
|
||||
fclose($fp);
|
||||
}
|
||||
} else {
|
||||
// 无 socket 时,仅做数据库校验即视为"逻辑连接成功"
|
||||
$reachable = true;
|
||||
$latency = 0;
|
||||
$port = 0;
|
||||
}
|
||||
|
||||
$this->jsonResponse(1, $reachable ? '设备连接成功' : '设备网络不可达,请检查 IP 和网络', [
|
||||
'Device' => $device,
|
||||
'ConnectionInfo' => [
|
||||
'ip' => $device['device_ip'],
|
||||
'port' => $reachable ? $port : 502,
|
||||
'reachable' => $reachable,
|
||||
'latency_ms' => $latency,
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->jsonResponse(0, '连接异常: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 接口 7: 批量读取设备数据 ====================
|
||||
|
||||
/**
|
||||
* 7. AsdBatchRead - 批量读取指定设备的测试数据
|
||||
* POST /Api/asdBatchRead
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* DeviceCodes - 设备编码列表(数组,如 ["ASD-989A-001","ASD-989A-002"])
|
||||
* Limit - 每台设备最多返回条数(默认 20)
|
||||
*
|
||||
* 返回:
|
||||
* Result=1, DevicesData={
|
||||
* "ASD-989A-001": {device_info:{...}, records:[{id,qr_code,status,step_count,test_time,...}], total:N}
|
||||
* }
|
||||
*/
|
||||
public function asdBatchRead()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
$deviceCodes = $data['DeviceCodes'] ?? [];
|
||||
$limit = intval($data['Limit'] ?? 20);
|
||||
|
||||
if (empty($deviceCodes) || !is_array($deviceCodes)) {
|
||||
$this->jsonResponse(0, '设备编码列表(DeviceCodes)不能为空,且必须为数组');
|
||||
return;
|
||||
}
|
||||
|
||||
$devicesData = [];
|
||||
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
|
||||
foreach ($deviceCodes as $code) {
|
||||
// 获取设备信息
|
||||
$deviceModel = new AsdDevice();
|
||||
$device = $deviceModel->findByCode($code);
|
||||
|
||||
// 获取该设备的测试记录(含统计)
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT r.id, r.qr_code, r.product_model AS project_name, r.tester, r.site,
|
||||
r.device_code AS device, r.test_result AS status, r.test_date AS test_time, r.running_time,
|
||||
COUNT(d.id) as step_count,
|
||||
SUM(CASE WHEN d.test_result = 'FAIL' THEN 1 ELSE 0 END) as fail_count
|
||||
FROM DGZXY_asd_test_record r
|
||||
LEFT JOIN DGZXY_asd_test_detail d ON r.id = d.record_id
|
||||
WHERE r.device_code = :code
|
||||
GROUP BY r.id
|
||||
ORDER BY r.id DESC
|
||||
LIMIT " . intval($limit)
|
||||
);
|
||||
$stmt->execute([':code' => $code]);
|
||||
$records = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// 总数
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM DGZXY_asd_test_record WHERE device = :code");
|
||||
$countStmt->execute([':code' => $code]);
|
||||
$total = $countStmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0;
|
||||
|
||||
$devicesData[$code] = [
|
||||
'device_info' => $device,
|
||||
'records' => $records,
|
||||
'total' => (int)$total,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->jsonResponse(0, '批量读取异常: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->jsonResponse(1, '批量读取成功', [
|
||||
'DevicesData' => $devicesData,
|
||||
'DeviceCount' => count($deviceCodes),
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== 接口 8: 删除设备数据 ====================
|
||||
|
||||
/**
|
||||
* 8. AsdDeviceDataDelete - 按设备删除测试数据
|
||||
* POST /Api/asdDeviceDataDelete
|
||||
*
|
||||
* 请求参数 (JSON):
|
||||
* DeviceCode - 设备编码(必填)
|
||||
* Confirm - 确认删除(必须为 "YES")
|
||||
*
|
||||
* 返回:
|
||||
* Result=1, DeletedCount=N, Message="已删除 N 条测试记录"
|
||||
*/
|
||||
public function asdDeviceDataDelete()
|
||||
{
|
||||
$this->apiAuth();
|
||||
$data = $this->getJsonInput();
|
||||
$deviceCode = $data['DeviceCode'] ?? '';
|
||||
$confirm = $data['Confirm'] ?? '';
|
||||
|
||||
if (empty($deviceCode)) {
|
||||
$this->jsonResponse(0, '设备编码(DeviceCode)不能为空');
|
||||
return;
|
||||
}
|
||||
if ($confirm !== 'YES') {
|
||||
$this->jsonResponse(0, '请确认删除操作(Confirm=YES)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = \core\db\Db::pdo();
|
||||
|
||||
// 先查该设备有多少条记录
|
||||
$countStmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM DGZXY_asd_test_record WHERE device_code = :code");
|
||||
$countStmt->execute([':code' => $deviceCode]);
|
||||
$recordCount = $countStmt->fetch(\PDO::FETCH_ASSOC)['cnt'] ?? 0;
|
||||
|
||||
if ($recordCount == 0) {
|
||||
$this->jsonResponse(1, '该设备无数据可删除', ['DeletedCount' => 0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有记录 ID
|
||||
$idsStmt = $pdo->prepare("SELECT id FROM DGZXY_asd_test_record WHERE device_code = :code");
|
||||
$idsStmt->execute([':code' => $deviceCode]);
|
||||
$ids = $idsStmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
|
||||
// 删除详情(级联)
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$pdo->prepare("DELETE FROM DGZXY_asd_test_detail WHERE record_id IN ({$placeholders})")->execute($ids);
|
||||
|
||||
// 删除主记录
|
||||
$pdo->prepare("DELETE FROM DGZXY_asd_test_record WHERE device_code = :code")->execute([':code' => $deviceCode]);
|
||||
|
||||
$this->jsonResponse(1, "已删除 {$recordCount} 条测试记录", [
|
||||
'DeletedCount' => (int)$recordCount,
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->jsonResponse(0, '删除失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* 前台工位看板 - 独立单页(不嵌入 AdminLTE 框架)
|
||||
* 显示所有工位的实时生产状态
|
||||
*/
|
||||
namespace app\controllers;
|
||||
|
||||
use core\base\Controller;
|
||||
use app\models\Workstation;
|
||||
use app\models\StationDefinition;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
$workstation = new Workstation();
|
||||
$stations = $workstation->getAll();
|
||||
|
||||
// 统计各工位今日数据(使用 StationDefinition 统一模型)
|
||||
$stats = [];
|
||||
$today = date('Y-m-d');
|
||||
$stationDef = new StationDefinition();
|
||||
|
||||
// 内置工位列表(station_definitions 中配置的)
|
||||
$builtinTypes = ['inbound', 'pcb_test', 'battery', 'assembly', 'finished_test', 'warehouse', 'delivery'];
|
||||
|
||||
foreach ($stations as $station) {
|
||||
$type = $station['station_type'];
|
||||
$count = 0;
|
||||
if (in_array($type, $builtinTypes)) {
|
||||
$count = $stationDef->countTodayByOperator($type, $user['emp_name']);
|
||||
}
|
||||
$stats[$type] = [
|
||||
'name' => $station['station_name'],
|
||||
'code' => $station['station_code'],
|
||||
'count' => $count,
|
||||
'status' => $station['status'],
|
||||
];
|
||||
}
|
||||
|
||||
// 总计
|
||||
$totalRecords = array_sum(array_column($stats, 'count'));
|
||||
|
||||
$this->assign('title', '前台工位看板');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('stations', $stations);
|
||||
$this->assign('stats', $stats);
|
||||
$this->assign('totalRecords', $totalRecords);
|
||||
$this->assign('today', $today);
|
||||
$this->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
namespace app\controllers;
|
||||
|
||||
use core\base\Controller;
|
||||
use app\models\FinanceRecord;
|
||||
use app\models\ReceivablePayable;
|
||||
|
||||
/**
|
||||
* 财务管理控制器
|
||||
* 访问权限:超级管理员 或 财务类目管理员
|
||||
*/
|
||||
class FinanceController extends Controller
|
||||
{
|
||||
private function checkCategoryAccess()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
|
||||
return $user;
|
||||
}
|
||||
if ($user['role'] === self::ROLE_ADMIN && ($user['category'] ?? '') === self::CATEGORY_FINANCE) {
|
||||
return $user;
|
||||
}
|
||||
header('Location: ' . BASE_URL . '/Front/index');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
|
||||
$financeRecord = new FinanceRecord();
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
|
||||
// 本月收支汇总
|
||||
$monthStart = date('Y-m-01');
|
||||
$monthEnd = date('Y-m-t');
|
||||
$summary = $financeRecord->getSummary($monthStart, $monthEnd);
|
||||
$incomeTotal = 0;
|
||||
$expenseTotal = 0;
|
||||
foreach ($summary as $row) {
|
||||
if ($row['type'] === 'income') $incomeTotal = (float)$row['total'];
|
||||
if ($row['type'] === 'expense') $expenseTotal = (float)$row['total'];
|
||||
}
|
||||
|
||||
// 应收应付统计
|
||||
$receivables = $receivablePayable->getByType('receivable');
|
||||
$payables = $receivablePayable->getByType('payable');
|
||||
$receivableTotal = 0;
|
||||
$payableTotal = 0;
|
||||
foreach ($receivables as $r) {
|
||||
$receivableTotal += (float)($r['amount'] ?? 0);
|
||||
}
|
||||
foreach ($payables as $p) {
|
||||
$payableTotal += (float)($p['amount'] ?? 0);
|
||||
}
|
||||
|
||||
$this->assign('title', '财务概览');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'finance_dashboard');
|
||||
$this->assign('incomeTotal', $incomeTotal);
|
||||
$this->assign('expenseTotal', $expenseTotal);
|
||||
$this->assign('profit', $incomeTotal - $expenseTotal);
|
||||
$this->assign('receivableTotal', $receivableTotal);
|
||||
$this->assign('payableTotal', $payableTotal);
|
||||
$this->assign('recordCount', $financeRecord->getCount());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
// ========== 账务管理 ==========
|
||||
public function accounts()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$financeRecord = new FinanceRecord();
|
||||
|
||||
$typeFilter = $_GET['type'] ?? '';
|
||||
$startDate = $_GET['start_date'] ?? date('Y-m-01');
|
||||
$endDate = $_GET['end_date'] ?? date('Y-m-d');
|
||||
|
||||
if ($typeFilter || $startDate || $endDate) {
|
||||
$records = $financeRecord->getByDateRange($startDate, $endDate, $typeFilter);
|
||||
} else {
|
||||
$records = $financeRecord->getAll();
|
||||
}
|
||||
|
||||
$this->assign('title', '账务管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('records', $records);
|
||||
$this->assign('typeFilter', $typeFilter);
|
||||
$this->assign('startDate', $startDate);
|
||||
$this->assign('endDate', $endDate);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function accountsAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'record_no' => trim($_POST['record_no'] ?? ''),
|
||||
'type' => trim($_POST['type'] ?? 'expense'),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'amount' => (float)($_POST['amount'] ?? 0),
|
||||
'record_date' => trim($_POST['record_date'] ?? date('Y-m-d')),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'related_party' => trim($_POST['related_party'] ?? ''),
|
||||
'payment_method' => trim($_POST['payment_method'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'operator' => $user['emp_name'],
|
||||
];
|
||||
if (empty($data['description'])) {
|
||||
$this->assign('error', '摘要不能为空');
|
||||
$this->assign('title', '添加账务记录');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
if ($data['amount'] <= 0) {
|
||||
$this->assign('error', '金额必须大于0');
|
||||
$this->assign('title', '添加账务记录');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$financeRecord = new FinanceRecord();
|
||||
$financeRecord->add($data);
|
||||
header('Location: ' . BASE_URL . '/Finance/accounts');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[accountsAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加账务记录');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加账务记录');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function accountsEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$financeRecord = new FinanceRecord();
|
||||
$record = $financeRecord->getById($id);
|
||||
if (!$record) exit('记录不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'record_no' => trim($_POST['record_no'] ?? ''),
|
||||
'type' => trim($_POST['type'] ?? 'expense'),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'amount' => (float)($_POST['amount'] ?? 0),
|
||||
'record_date' => trim($_POST['record_date'] ?? ''),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'related_party' => trim($_POST['related_party'] ?? ''),
|
||||
'payment_method' => trim($_POST['payment_method'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$financeRecord->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/Finance/accounts');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[accountsEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑账务记录');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑账务记录');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'accounts');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function accountsDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$financeRecord = new FinanceRecord();
|
||||
$financeRecord->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Finance/accounts');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 应收账款 ==========
|
||||
public function receivable()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$records = $receivablePayable->getByType('receivable');
|
||||
|
||||
$this->assign('title', '应收账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'receivable');
|
||||
$this->assign('records', $records);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function receivableAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'type' => 'receivable',
|
||||
'party_name' => trim($_POST['party_name'] ?? ''),
|
||||
'amount' => (float)($_POST['amount'] ?? 0),
|
||||
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
|
||||
'due_date' => trim($_POST['due_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'operator' => $user['emp_name'],
|
||||
];
|
||||
try {
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$receivablePayable->add($data);
|
||||
header('Location: ' . BASE_URL . '/Finance/receivable');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[receivableAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加应收账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'receivable');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加应收账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'receivable');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function receivableEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$record = $receivablePayable->getById($id);
|
||||
if (!$record) exit('记录不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'party_name' => trim($_POST['party_name'] ?? ''),
|
||||
'amount' => (float)($_POST['amount'] ?? 0),
|
||||
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
|
||||
'due_date' => trim($_POST['due_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$receivablePayable->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/Finance/receivable');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[receivableEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑应收账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'receivable');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑应收账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'receivable');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function receivableDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$receivablePayable->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Finance/receivable');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 应付账款 ==========
|
||||
public function payable()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$records = $receivablePayable->getByType('payable');
|
||||
|
||||
$this->assign('title', '应付账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'payable');
|
||||
$this->assign('records', $records);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function payableAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'type' => 'payable',
|
||||
'party_name' => trim($_POST['party_name'] ?? ''),
|
||||
'amount' => (float)($_POST['amount'] ?? 0),
|
||||
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
|
||||
'due_date' => trim($_POST['due_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'operator' => $user['emp_name'],
|
||||
];
|
||||
try {
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$receivablePayable->add($data);
|
||||
header('Location: ' . BASE_URL . '/Finance/payable');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[payableAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加应付账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'payable');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加应付账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'payable');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function payableEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$record = $receivablePayable->getById($id);
|
||||
if (!$record) exit('记录不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'party_name' => trim($_POST['party_name'] ?? ''),
|
||||
'amount' => (float)($_POST['amount'] ?? 0),
|
||||
'paid_amount' => (float)($_POST['paid_amount'] ?? 0),
|
||||
'due_date' => trim($_POST['due_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$receivablePayable->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/Finance/payable');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[payableEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑应付账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'payable');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑应付账款');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'payable');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function payableDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$receivablePayable = new ReceivablePayable();
|
||||
$receivablePayable->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Finance/payable');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 财务报表 ==========
|
||||
public function report()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$financeRecord = new FinanceRecord();
|
||||
|
||||
$month = $_GET['month'] ?? date('Y-m');
|
||||
$startDate = $month . '-01';
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
$summary = $financeRecord->getSummary($startDate, $endDate);
|
||||
$incomeTotal = 0;
|
||||
$expenseTotal = 0;
|
||||
foreach ($summary as $row) {
|
||||
if ($row['type'] === 'income') $incomeTotal = (float)$row['total'];
|
||||
if ($row['type'] === 'expense') $expenseTotal = (float)$row['total'];
|
||||
}
|
||||
|
||||
// 按类别统计
|
||||
$records = $financeRecord->getByDateRange($startDate, $endDate);
|
||||
$categorySummary = [];
|
||||
foreach ($records as $r) {
|
||||
$cat = $r['category'] ?: '未分类';
|
||||
$type = $r['type'];
|
||||
if (!isset($categorySummary[$cat])) {
|
||||
$categorySummary[$cat] = ['income' => 0, 'expense' => 0];
|
||||
}
|
||||
$categorySummary[$cat][$type] += (float)$r['amount'];
|
||||
}
|
||||
|
||||
$this->assign('title', '财务报表');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'report');
|
||||
$this->assign('incomeTotal', $incomeTotal);
|
||||
$this->assign('expenseTotal', $expenseTotal);
|
||||
$this->assign('profit', $incomeTotal - $expenseTotal);
|
||||
$this->assign('categorySummary', $categorySummary);
|
||||
$this->assign('month', $month);
|
||||
$this->render();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,540 @@
|
||||
<?php
|
||||
namespace app\controllers;
|
||||
|
||||
use core\base\Controller;
|
||||
use app\models\HrEmployee;
|
||||
use app\models\Asset;
|
||||
use app\models\Document;
|
||||
|
||||
/**
|
||||
* 综合管理控制器
|
||||
* 访问权限:超级管理员 或 综合管理类目管理员
|
||||
*/
|
||||
class GeneralController extends Controller
|
||||
{
|
||||
private function checkCategoryAccess()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
|
||||
return $user;
|
||||
}
|
||||
if ($user['role'] === self::ROLE_ADMIN && ($user['category'] ?? '') === self::CATEGORY_GENERAL) {
|
||||
return $user;
|
||||
}
|
||||
header('Location: ' . BASE_URL . '/Front/index');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
|
||||
$hrEmployee = new HrEmployee();
|
||||
$asset = new Asset();
|
||||
$document = new Document();
|
||||
|
||||
$this->assign('title', '综合管理概览');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'general_dashboard');
|
||||
$this->assign('employeeCount', $hrEmployee->getCount());
|
||||
$this->assign('assetStats', $asset->getCount());
|
||||
$this->assign('documentCount', $document->getCount());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
// ========== 人事管理 ==========
|
||||
public function hr()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$hrEmployee = new HrEmployee();
|
||||
$employees = $hrEmployee->getAll();
|
||||
|
||||
$this->assign('title', '人事管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'hr');
|
||||
$this->assign('employees', $employees);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function hrAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'emp_no' => trim($_POST['emp_no'] ?? ''),
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'gender' => trim($_POST['gender'] ?? ''),
|
||||
'department' => trim($_POST['department'] ?? ''),
|
||||
'position' => trim($_POST['position'] ?? ''),
|
||||
'phone' => trim($_POST['phone'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'id_card' => trim($_POST['id_card'] ?? ''),
|
||||
'entry_date' => trim($_POST['entry_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'active'),
|
||||
'education' => trim($_POST['education'] ?? ''),
|
||||
'emergency_contact' => trim($_POST['emergency_contact'] ?? ''),
|
||||
'emergency_phone' => trim($_POST['emergency_phone'] ?? ''),
|
||||
'address' => trim($_POST['address'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
if (empty($data['name'])) {
|
||||
$this->assign('error', '姓名不能为空');
|
||||
$this->assign('title', '添加员工');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'hr');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$hrEmployee = new HrEmployee();
|
||||
$hrEmployee->add($data);
|
||||
header('Location: ' . BASE_URL . '/General/hr');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[hrAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加员工');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'hr');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加员工');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'hr');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function hrEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$hrEmployee = new HrEmployee();
|
||||
$record = $hrEmployee->getById($id);
|
||||
if (!$record) exit('员工不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'emp_no' => trim($_POST['emp_no'] ?? ''),
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'gender' => trim($_POST['gender'] ?? ''),
|
||||
'department' => trim($_POST['department'] ?? ''),
|
||||
'position' => trim($_POST['position'] ?? ''),
|
||||
'phone' => trim($_POST['phone'] ?? ''),
|
||||
'email' => trim($_POST['email'] ?? ''),
|
||||
'id_card' => trim($_POST['id_card'] ?? ''),
|
||||
'entry_date' => trim($_POST['entry_date'] ?? ''),
|
||||
'leave_date' => trim($_POST['leave_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'active'),
|
||||
'education' => trim($_POST['education'] ?? ''),
|
||||
'emergency_contact' => trim($_POST['emergency_contact'] ?? ''),
|
||||
'emergency_phone' => trim($_POST['emergency_phone'] ?? ''),
|
||||
'address' => trim($_POST['address'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$hrEmployee->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/General/hr');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[hrEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑员工');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'hr');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑员工');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'hr');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function hrDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$hrEmployee = new HrEmployee();
|
||||
$hrEmployee->delete($id);
|
||||
header('Location: ' . BASE_URL . '/General/hr');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 资产管理 ==========
|
||||
public function asset()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$asset = new Asset();
|
||||
$assets = $asset->getAll();
|
||||
|
||||
$this->assign('title', '资产管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'asset');
|
||||
$this->assign('assets', $assets);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function assetAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'asset_no' => trim($_POST['asset_no'] ?? ''),
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'model' => trim($_POST['model'] ?? ''),
|
||||
'quantity' => (int)($_POST['quantity'] ?? 1),
|
||||
'unit' => trim($_POST['unit'] ?? '台'),
|
||||
'purchase_price' => (float)($_POST['purchase_price'] ?? 0),
|
||||
'purchase_date' => trim($_POST['purchase_date'] ?? ''),
|
||||
'supplier' => trim($_POST['supplier'] ?? ''),
|
||||
'department' => trim($_POST['department'] ?? ''),
|
||||
'user_name' => trim($_POST['user_name'] ?? ''),
|
||||
'location' => trim($_POST['location'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'normal'),
|
||||
'warranty_expire' => trim($_POST['warranty_expire'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
if (empty($data['name'])) {
|
||||
$this->assign('error', '资产名称不能为空');
|
||||
$this->assign('title', '添加资产');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'asset');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$asset = new Asset();
|
||||
$asset->add($data);
|
||||
header('Location: ' . BASE_URL . '/General/asset');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[assetAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加资产');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'asset');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加资产');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'asset');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function assetEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$asset = new Asset();
|
||||
$record = $asset->getById($id);
|
||||
if (!$record) exit('资产不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'asset_no' => trim($_POST['asset_no'] ?? ''),
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'model' => trim($_POST['model'] ?? ''),
|
||||
'quantity' => (int)($_POST['quantity'] ?? 1),
|
||||
'unit' => trim($_POST['unit'] ?? '台'),
|
||||
'purchase_price' => (float)($_POST['purchase_price'] ?? 0),
|
||||
'purchase_date' => trim($_POST['purchase_date'] ?? ''),
|
||||
'supplier' => trim($_POST['supplier'] ?? ''),
|
||||
'department' => trim($_POST['department'] ?? ''),
|
||||
'user_name' => trim($_POST['user_name'] ?? ''),
|
||||
'location' => trim($_POST['location'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'normal'),
|
||||
'warranty_expire' => trim($_POST['warranty_expire'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$asset->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/General/asset');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[assetEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑资产');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'asset');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑资产');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'asset');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function assetDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$asset = new Asset();
|
||||
$asset->delete($id);
|
||||
header('Location: ' . BASE_URL . '/General/asset');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 文档管理 ==========
|
||||
public function document()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$document = new Document();
|
||||
$documents = $document->getAll();
|
||||
$categories = $document->getCategories();
|
||||
|
||||
$this->assign('title', '文档管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('documents', $documents);
|
||||
$this->assign('categories', $categories);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function documentAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'title' => trim($_POST['title'] ?? ''),
|
||||
'doc_no' => trim($_POST['doc_no'] ?? ''),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'version' => trim($_POST['version'] ?? '1.0'),
|
||||
'author' => trim($_POST['author'] ?? ''),
|
||||
'department' => trim($_POST['department'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'active'),
|
||||
'keywords' => trim($_POST['keywords'] ?? ''),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'operator' => $user['emp_name'],
|
||||
];
|
||||
|
||||
// 文件上传 — 仅允许安全文档类型
|
||||
if (isset($_FILES['doc_file']) && $_FILES['doc_file']['error'] === UPLOAD_ERR_OK) {
|
||||
$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 = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmpPath);
|
||||
|
||||
if (!in_array($ext, $allowedExtensions, true) || !in_array($mime, $allowedMimeTypes, true)) {
|
||||
error_log('[documentAdd] Upload rejected: ext=' . $ext . ' mime=' . $mime . ' file=' . $originalName);
|
||||
$this->assign('error', '不支持的文件类型,仅允许常见文档、图片和压缩包格式');
|
||||
$this->assign('title', '添加文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
|
||||
$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)) {
|
||||
$data['file_name'] = $originalName;
|
||||
$data['file_path'] = '/static/uploads/documents/' . $saveName;
|
||||
$data['file_size'] = $_FILES['doc_file']['size'];
|
||||
$data['file_type'] = $ext;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data['title'])) {
|
||||
$this->assign('error', '文档标题不能为空');
|
||||
$this->assign('title', '添加文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$document = new Document();
|
||||
$document->add($data);
|
||||
header('Location: ' . BASE_URL . '/General/document');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[documentAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function documentEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$document = new Document();
|
||||
$record = $document->getById($id);
|
||||
if (!$record) exit('文档不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'title' => trim($_POST['title'] ?? ''),
|
||||
'doc_no' => trim($_POST['doc_no'] ?? ''),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'version' => trim($_POST['version'] ?? '1.0'),
|
||||
'author' => trim($_POST['author'] ?? ''),
|
||||
'department' => trim($_POST['department'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'active'),
|
||||
'keywords' => trim($_POST['keywords'] ?? ''),
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
];
|
||||
|
||||
// 文件上传 — 仅允许安全文档类型
|
||||
if (isset($_FILES['doc_file']) && $_FILES['doc_file']['error'] === UPLOAD_ERR_OK) {
|
||||
$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 = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmpPath);
|
||||
|
||||
if (!in_array($ext, $allowedExtensions, true) || !in_array($mime, $allowedMimeTypes, true)) {
|
||||
error_log('[documentEdit] Upload rejected: ext=' . $ext . ' mime=' . $mime . ' file=' . $originalName);
|
||||
$this->assign('error', '不支持的文件类型,仅允许常见文档、图片和压缩包格式');
|
||||
$this->assign('title', '编辑文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
|
||||
$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)) {
|
||||
$data['file_name'] = $originalName;
|
||||
$data['file_path'] = '/static/uploads/documents/' . $saveName;
|
||||
$data['file_size'] = $_FILES['doc_file']['size'];
|
||||
$data['file_type'] = $ext;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$document->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/General/document');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[documentEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑文档');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'document');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function documentDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$document = new Document();
|
||||
$document->delete($id);
|
||||
header('Location: ' . BASE_URL . '/General/document');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,726 @@
|
||||
<?php
|
||||
namespace app\controllers;
|
||||
|
||||
use core\base\Controller;
|
||||
use app\models\Supplier;
|
||||
use app\models\PurchaseOrder;
|
||||
use app\models\PurchaseItem;
|
||||
use app\models\Inventory;
|
||||
use app\models\SalesOrder;
|
||||
use app\models\SalesItem;
|
||||
|
||||
/**
|
||||
* 进销存管理控制器
|
||||
* 访问权限:超级管理员 或 进销存类目管理员
|
||||
*/
|
||||
class InventoryController extends Controller
|
||||
{
|
||||
private function checkCategoryAccess()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
|
||||
return $user;
|
||||
}
|
||||
if ($user['role'] === self::ROLE_ADMIN && ($user['category'] ?? '') === self::CATEGORY_INVENTORY) {
|
||||
return $user;
|
||||
}
|
||||
header('Location: ' . BASE_URL . '/Front/index');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
|
||||
$supplier = new Supplier();
|
||||
$purchaseOrder = new PurchaseOrder();
|
||||
$inventory = new Inventory();
|
||||
$salesOrder = new SalesOrder();
|
||||
|
||||
$this->assign('title', '进销存概览');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'inventory_dashboard');
|
||||
$this->assign('supplierCount', count($supplier->getAll()));
|
||||
$this->assign('purchaseCount', $purchaseOrder->getCount());
|
||||
$this->assign('stockCount', $inventory->getCount());
|
||||
$this->assign('salesCount', $salesOrder->getCount());
|
||||
$this->assign('lowStockItems', $inventory->getLowStock());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
// ========== 供应商管理 ==========
|
||||
public function supplier()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$supplier = new Supplier();
|
||||
$suppliers = $supplier->getAll();
|
||||
|
||||
$this->assign('title', '供应商管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('suppliers', $suppliers);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function supplierAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($user['role'] !== self::ROLE_SUPER_ADMIN && $user['role'] !== self::ROLE_ADMIN) {
|
||||
exit('无权限访问');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'contact_person' => trim($_POST['contact_person'] ?? ''),
|
||||
'phone' => trim($_POST['phone'] ?? ''),
|
||||
'address' => trim($_POST['address'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'status' => (int)($_POST['status'] ?? 1),
|
||||
];
|
||||
if (empty($data['name'])) {
|
||||
$this->assign('error', '供应商名称不能为空');
|
||||
$this->assign('title', '添加供应商');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$supplier = new Supplier();
|
||||
$supplier->add($data);
|
||||
header('Location: ' . BASE_URL . '/Inventory/supplier');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[supplierAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加供应商');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加供应商');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function supplierEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$supplier = new Supplier();
|
||||
$record = $supplier->getById($id);
|
||||
if (!$record) exit('供应商不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'name' => trim($_POST['name'] ?? ''),
|
||||
'contact_person' => trim($_POST['contact_person'] ?? ''),
|
||||
'phone' => trim($_POST['phone'] ?? ''),
|
||||
'address' => trim($_POST['address'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'status' => (int)($_POST['status'] ?? 1),
|
||||
];
|
||||
if (empty($data['name'])) {
|
||||
$this->assign('error', '供应商名称不能为空');
|
||||
$this->assign('title', '编辑供应商');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$supplier->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/Inventory/supplier');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[supplierEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑供应商');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑供应商');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'supplier');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function supplierDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$supplier = new Supplier();
|
||||
$supplier->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Inventory/supplier');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 采购管理 ==========
|
||||
public function purchase()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$purchaseOrder = new PurchaseOrder();
|
||||
$orders = $purchaseOrder->getAll();
|
||||
|
||||
$this->assign('title', '采购管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('orders', $orders);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function purchaseAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($user['role'] !== self::ROLE_SUPER_ADMIN && $user['role'] !== self::ROLE_ADMIN) {
|
||||
exit('无权限访问');
|
||||
}
|
||||
|
||||
$supplierModel = new Supplier();
|
||||
$suppliers = $supplierModel->getActiveList();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$supplierId = (int)($_POST['supplier_id'] ?? 0);
|
||||
$supplierInfo = $supplierModel->getById($supplierId);
|
||||
|
||||
$orderData = [
|
||||
'order_no' => trim($_POST['order_no'] ?? ''),
|
||||
'supplier_id' => $supplierId,
|
||||
'supplier_name' => $supplierInfo['name'] ?? '',
|
||||
'order_date' => trim($_POST['order_date'] ?? date('Y-m-d')),
|
||||
'total_amount' => 0,
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'operator' => $user['emp_name'],
|
||||
];
|
||||
|
||||
if (empty($orderData['order_no'])) {
|
||||
$this->assign('error', '采购单号不能为空');
|
||||
$this->assign('title', '添加采购订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('suppliers', $suppliers);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$purchaseOrder = new PurchaseOrder();
|
||||
$purchaseOrder->add($orderData);
|
||||
$orderId = $this->pdo->lastInsertId();
|
||||
|
||||
$productNames = $_POST['product_name'] ?? [];
|
||||
$productModels = $_POST['product_model'] ?? [];
|
||||
$quantities = $_POST['quantity'] ?? [];
|
||||
$units = $_POST['unit'] ?? [];
|
||||
$unitPrices = $_POST['unit_price'] ?? [];
|
||||
|
||||
$totalAmount = 0;
|
||||
$purchaseItem = new PurchaseItem();
|
||||
foreach ($productNames as $i => $pname) {
|
||||
if (empty(trim($pname))) continue;
|
||||
$qty = (int)($quantities[$i] ?? 0);
|
||||
$price = (float)($unitPrices[$i] ?? 0);
|
||||
$amount = $qty * $price;
|
||||
$totalAmount += $amount;
|
||||
$purchaseItem->add([
|
||||
'order_id' => $orderId,
|
||||
'product_name' => trim($pname),
|
||||
'product_model' => trim($productModels[$i] ?? ''),
|
||||
'quantity' => $qty,
|
||||
'unit' => trim($units[$i] ?? '个'),
|
||||
'unit_price' => $price,
|
||||
'amount' => $amount,
|
||||
]);
|
||||
}
|
||||
|
||||
$purchaseOrder->where(['id = :id'], [':id' => $orderId])->update(['total_amount' => $totalAmount]);
|
||||
|
||||
header('Location: ' . BASE_URL . '/Inventory/purchase');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[purchaseAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加采购订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('suppliers', $suppliers);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加采购订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('suppliers', $suppliers);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function purchaseDetail()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$purchaseOrder = new PurchaseOrder();
|
||||
$order = $purchaseOrder->getById($id);
|
||||
if (!$order) exit('订单不存在');
|
||||
|
||||
$purchaseItem = new PurchaseItem();
|
||||
$items = $purchaseItem->getByOrder($id);
|
||||
|
||||
$this->assign('title', '采购订单详情');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('order', $order);
|
||||
$this->assign('items', $items);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function purchaseEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$purchaseOrder = new PurchaseOrder();
|
||||
$order = $purchaseOrder->getById($id);
|
||||
if (!$order) exit('订单不存在');
|
||||
|
||||
$supplierModel = new Supplier();
|
||||
$suppliers = $supplierModel->getActiveList();
|
||||
|
||||
$purchaseItem = new PurchaseItem();
|
||||
$items = $purchaseItem->getByOrder($id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$supplierId = (int)($_POST['supplier_id'] ?? 0);
|
||||
$supplierInfo = $supplierModel->getById($supplierId);
|
||||
|
||||
$orderData = [
|
||||
'order_no' => trim($_POST['order_no'] ?? ''),
|
||||
'supplier_id' => $supplierId,
|
||||
'supplier_name' => $supplierInfo['name'] ?? '',
|
||||
'order_date' => trim($_POST['order_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
|
||||
try {
|
||||
$purchaseOrder->where(['id = :id'], [':id' => $id])->update($orderData);
|
||||
|
||||
$purchaseItem->deleteByOrder($id);
|
||||
$productNames = $_POST['product_name'] ?? [];
|
||||
$productModels = $_POST['product_model'] ?? [];
|
||||
$quantities = $_POST['quantity'] ?? [];
|
||||
$units = $_POST['unit'] ?? [];
|
||||
$unitPrices = $_POST['unit_price'] ?? [];
|
||||
|
||||
$totalAmount = 0;
|
||||
foreach ($productNames as $i => $pname) {
|
||||
if (empty(trim($pname))) continue;
|
||||
$qty = (int)($quantities[$i] ?? 0);
|
||||
$price = (float)($unitPrices[$i] ?? 0);
|
||||
$amount = $qty * $price;
|
||||
$totalAmount += $amount;
|
||||
$purchaseItem->add([
|
||||
'order_id' => $id,
|
||||
'product_name' => trim($pname),
|
||||
'product_model' => trim($productModels[$i] ?? ''),
|
||||
'quantity' => $qty,
|
||||
'unit' => trim($units[$i] ?? '个'),
|
||||
'unit_price' => $price,
|
||||
'amount' => $amount,
|
||||
]);
|
||||
}
|
||||
$purchaseOrder->where(['id = :id'], [':id' => $id])->update(['total_amount' => $totalAmount]);
|
||||
|
||||
header('Location: ' . BASE_URL . '/Inventory/purchase');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[purchaseEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑采购订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('order', $order);
|
||||
$this->assign('items', $items);
|
||||
$this->assign('suppliers', $suppliers);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑采购订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'purchase');
|
||||
$this->assign('order', $order);
|
||||
$this->assign('items', $items);
|
||||
$this->assign('suppliers', $suppliers);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function purchaseDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$purchaseItem = new PurchaseItem();
|
||||
$purchaseItem->deleteByOrder($id);
|
||||
$purchaseOrder = new PurchaseOrder();
|
||||
$purchaseOrder->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Inventory/purchase');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 库存管理 ==========
|
||||
public function stock()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$inventory = new Inventory();
|
||||
$stocks = $inventory->getAll();
|
||||
|
||||
$this->assign('title', '库存管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'stock');
|
||||
$this->assign('stocks', $stocks);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function stockAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'product_name' => trim($_POST['product_name'] ?? ''),
|
||||
'product_model' => trim($_POST['product_model'] ?? ''),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'quantity' => (int)($_POST['quantity'] ?? 0),
|
||||
'unit' => trim($_POST['unit'] ?? '个'),
|
||||
'unit_price' => (float)($_POST['unit_price'] ?? 0),
|
||||
'min_stock' => (int)($_POST['min_stock'] ?? 0),
|
||||
'max_stock' => (int)($_POST['max_stock'] ?? 0),
|
||||
'warehouse' => trim($_POST['warehouse'] ?? ''),
|
||||
'location' => trim($_POST['location'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
if (empty($data['product_name'])) {
|
||||
$this->assign('error', '产品名称不能为空');
|
||||
$this->assign('title', '添加库存');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'stock');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$inventory = new Inventory();
|
||||
$inventory->add($data);
|
||||
header('Location: ' . BASE_URL . '/Inventory/stock');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[stockAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加库存');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'stock');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加库存');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'stock');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function stockEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$inventory = new Inventory();
|
||||
$record = $inventory->getById($id);
|
||||
if (!$record) exit('库存记录不存在');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$data = [
|
||||
'product_name' => trim($_POST['product_name'] ?? ''),
|
||||
'product_model' => trim($_POST['product_model'] ?? ''),
|
||||
'category' => trim($_POST['category'] ?? ''),
|
||||
'quantity' => (int)($_POST['quantity'] ?? 0),
|
||||
'unit' => trim($_POST['unit'] ?? '个'),
|
||||
'unit_price' => (float)($_POST['unit_price'] ?? 0),
|
||||
'min_stock' => (int)($_POST['min_stock'] ?? 0),
|
||||
'max_stock' => (int)($_POST['max_stock'] ?? 0),
|
||||
'warehouse' => trim($_POST['warehouse'] ?? ''),
|
||||
'location' => trim($_POST['location'] ?? ''),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$inventory->where(['id = :id'], [':id' => $id])->update($data);
|
||||
header('Location: ' . BASE_URL . '/Inventory/stock');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[stockEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑库存');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'stock');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑库存');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'stock');
|
||||
$this->assign('record', $record);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function stockDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$inventory = new Inventory();
|
||||
$inventory->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Inventory/stock');
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 销售管理 ==========
|
||||
public function sales()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$salesOrder = new SalesOrder();
|
||||
$orders = $salesOrder->getAll();
|
||||
|
||||
$this->assign('title', '销售管理');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('orders', $orders);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function salesAdd()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$orderData = [
|
||||
'order_no' => trim($_POST['order_no'] ?? ''),
|
||||
'customer_name' => trim($_POST['customer_name'] ?? ''),
|
||||
'order_date' => trim($_POST['order_date'] ?? date('Y-m-d')),
|
||||
'total_amount' => 0,
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
'operator' => $user['emp_name'],
|
||||
];
|
||||
if (empty($orderData['order_no'])) {
|
||||
$this->assign('error', '销售单号不能为空');
|
||||
$this->assign('title', '添加销售订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$salesOrder = new SalesOrder();
|
||||
$salesOrder->add($orderData);
|
||||
$orderId = $this->pdo->lastInsertId();
|
||||
|
||||
$productNames = $_POST['product_name'] ?? [];
|
||||
$productModels = $_POST['product_model'] ?? [];
|
||||
$quantities = $_POST['quantity'] ?? [];
|
||||
$units = $_POST['unit'] ?? [];
|
||||
$unitPrices = $_POST['unit_price'] ?? [];
|
||||
|
||||
$totalAmount = 0;
|
||||
$salesItem = new SalesItem();
|
||||
foreach ($productNames as $i => $pname) {
|
||||
if (empty(trim($pname))) continue;
|
||||
$qty = (int)($quantities[$i] ?? 0);
|
||||
$price = (float)($unitPrices[$i] ?? 0);
|
||||
$amount = $qty * $price;
|
||||
$totalAmount += $amount;
|
||||
$salesItem->add([
|
||||
'order_id' => $orderId,
|
||||
'product_name' => trim($pname),
|
||||
'product_model' => trim($productModels[$i] ?? ''),
|
||||
'quantity' => $qty,
|
||||
'unit' => trim($units[$i] ?? '个'),
|
||||
'unit_price' => $price,
|
||||
'amount' => $amount,
|
||||
]);
|
||||
}
|
||||
$salesOrder->where(['id = :id'], [':id' => $orderId])->update(['total_amount' => $totalAmount]);
|
||||
|
||||
header('Location: ' . BASE_URL . '/Inventory/sales');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[salesAdd] ' . $e->getMessage());
|
||||
$this->assign('error', '添加失败:' . $e->getMessage());
|
||||
$this->assign('title', '添加销售订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '添加销售订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function salesDetail()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$salesOrder = new SalesOrder();
|
||||
$order = $salesOrder->getById($id);
|
||||
if (!$order) exit('订单不存在');
|
||||
|
||||
$salesItem = new SalesItem();
|
||||
$items = $salesItem->getByOrder($id);
|
||||
|
||||
$this->assign('title', '销售订单详情');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('order', $order);
|
||||
$this->assign('items', $items);
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function salesEdit()
|
||||
{
|
||||
$user = $this->checkCategoryAccess();
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$salesOrder = new SalesOrder();
|
||||
$order = $salesOrder->getById($id);
|
||||
if (!$order) exit('订单不存在');
|
||||
|
||||
$salesItem = new SalesItem();
|
||||
$items = $salesItem->getByOrder($id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->csrfVerify();
|
||||
$orderData = [
|
||||
'order_no' => trim($_POST['order_no'] ?? ''),
|
||||
'customer_name' => trim($_POST['customer_name'] ?? ''),
|
||||
'order_date' => trim($_POST['order_date'] ?? ''),
|
||||
'status' => trim($_POST['status'] ?? 'pending'),
|
||||
'remark' => trim($_POST['remark'] ?? ''),
|
||||
];
|
||||
try {
|
||||
$salesOrder->where(['id = :id'], [':id' => $id])->update($orderData);
|
||||
|
||||
$salesItem->deleteByOrder($id);
|
||||
$productNames = $_POST['product_name'] ?? [];
|
||||
$productModels = $_POST['product_model'] ?? [];
|
||||
$quantities = $_POST['quantity'] ?? [];
|
||||
$units = $_POST['unit'] ?? [];
|
||||
$unitPrices = $_POST['unit_price'] ?? [];
|
||||
|
||||
$totalAmount = 0;
|
||||
foreach ($productNames as $i => $pname) {
|
||||
if (empty(trim($pname))) continue;
|
||||
$qty = (int)($quantities[$i] ?? 0);
|
||||
$price = (float)($unitPrices[$i] ?? 0);
|
||||
$amount = $qty * $price;
|
||||
$totalAmount += $amount;
|
||||
$salesItem->add([
|
||||
'order_id' => $id,
|
||||
'product_name' => trim($pname),
|
||||
'product_model' => trim($productModels[$i] ?? ''),
|
||||
'quantity' => $qty,
|
||||
'unit' => trim($units[$i] ?? '个'),
|
||||
'unit_price' => $price,
|
||||
'amount' => $amount,
|
||||
]);
|
||||
}
|
||||
$salesOrder->where(['id = :id'], [':id' => $id])->update(['total_amount' => $totalAmount]);
|
||||
|
||||
header('Location: ' . BASE_URL . '/Inventory/sales');
|
||||
exit;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[salesEdit] ' . $e->getMessage());
|
||||
$this->assign('error', '更新失败:' . $e->getMessage());
|
||||
$this->assign('title', '编辑销售订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('order', $order);
|
||||
$this->assign('items', $items);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', '编辑销售订单');
|
||||
$this->assign('user', $user);
|
||||
$this->assign('activeMenu', 'sales');
|
||||
$this->assign('order', $order);
|
||||
$this->assign('items', $items);
|
||||
$this->assign('csrfToken', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function salesDelete()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$this->requireMethod('post');
|
||||
$this->csrfVerify();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$salesItem = new SalesItem();
|
||||
$salesItem->deleteByOrder($id);
|
||||
$salesOrder = new SalesOrder();
|
||||
$salesOrder->delete($id);
|
||||
header('Location: ' . BASE_URL . '/Inventory/sales');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
namespace app\controllers;
|
||||
|
||||
use core\base\Controller;
|
||||
use app\models\Employee;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
// 暴力破解防护配置
|
||||
const MAX_LOGIN_ATTEMPTS = 5; // 最大尝试次数
|
||||
const LOCKOUT_DURATION = 900; // 锁定时间(秒),15分钟
|
||||
const ATTEMPT_WINDOW = 300; // 计数窗口(秒),5分钟
|
||||
|
||||
public function index()
|
||||
{
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 登录页不强制 CSRF 验证,原因:
|
||||
// 1. 部分设备/浏览器 Session Cookie 不稳定(SameSite=Lax、隐私模式等)
|
||||
// 导致每次请求创建新 Session,CSRF Token 永远对不上
|
||||
// 2. 登录本身无敏感副作用(不修改数据),攻击者 CSRF 登录成功
|
||||
// 也无法获知密码,且暴力破解防护仍在生效
|
||||
// 3. 登录成功后的所有操作仍受 CSRF 保护
|
||||
$emp_no = isset($_POST['emp_no']) ? trim($_POST['emp_no']) : '';
|
||||
$password = isset($_POST['password']) ? $_POST['password'] : '';
|
||||
|
||||
if (empty($emp_no) || empty($password)) {
|
||||
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
|
||||
$sess = session_id() ? hash('sha256', session_id()) : 'none';
|
||||
error_log("[LOGIN_DIAG] POST_EMPTY emp_no_len=" . strlen($emp_no) . " pwd_len=" . strlen($password) . " ua={$ua} sess_hash={$sess}");
|
||||
$error = '请输入员工编号和密码';
|
||||
} else {
|
||||
// 暴力破解检查
|
||||
$lockError = $this->checkBruteForce($emp_no);
|
||||
if ($lockError) {
|
||||
$error = $lockError;
|
||||
} else {
|
||||
$employee = new Employee();
|
||||
$user = $employee->validateLogin($emp_no, $password);
|
||||
|
||||
if ($user) {
|
||||
// 登录成功 — 清除失败记录
|
||||
$this->clearLoginAttempts($emp_no);
|
||||
|
||||
// 重新生成 Session ID 防止会话固定攻击
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['emp_no'] = $user['emp_no'];
|
||||
$_SESSION['emp_name'] = $user['emp_name'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
$_SESSION['category'] = $user['category'] ?? self::CATEGORY_MES;
|
||||
|
||||
// 根据角色和类目跳转到不同页面
|
||||
if ($user['role'] === self::ROLE_SUPER_ADMIN) {
|
||||
// 超级管理员:默认进入 MES 后台,可通过顶部切换类目
|
||||
header('Location: ' . BASE_URL . '/Admin/index');
|
||||
} elseif ($user['role'] === self::ROLE_ADMIN) {
|
||||
// 管理员:根据分配的类目跳转到对应管理界面
|
||||
$cat = $user['category'] ?? self::CATEGORY_MES;
|
||||
$homeRoute = self::CATEGORY_HOME[$cat] ?? 'Admin/index';
|
||||
header('Location: ' . BASE_URL . '/' . $homeRoute);
|
||||
} else {
|
||||
// 操作员:进入前台工位界面
|
||||
header('Location: ' . BASE_URL . '/Front/index');
|
||||
}
|
||||
exit;
|
||||
} else {
|
||||
// 记录失败
|
||||
$this->recordLoginAttempt($emp_no);
|
||||
$remaining = $this->getRemainingAttempts($emp_no);
|
||||
// 诊断日志:validateLogin 返回 false(详细日志已在 Employee 中记录)
|
||||
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
|
||||
$sess = session_id() ? hash('sha256', session_id()) : 'none';
|
||||
error_log("[LOGIN_DIAG] RESULT=FAIL emp_no_len=" . strlen($emp_no) . " remaining={$remaining} ua={$ua} sess_hash={$sess}");
|
||||
$error = '员工编号或密码错误' . ($remaining > 0 ? "(还可尝试 {$remaining} 次)" : '(账号已临时锁定)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assign('title', 'MES 系统登录');
|
||||
$this->assign('error', $error);
|
||||
$this->assign('csrf_token', $this->csrfToken());
|
||||
$this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查暴力破解状态
|
||||
*/
|
||||
private function checkBruteForce($emp_no)
|
||||
{
|
||||
$key = 'login_attempts_' . md5($emp_no);
|
||||
$attempts = $_SESSION[$key] ?? [];
|
||||
|
||||
// 清理过期记录
|
||||
$now = time();
|
||||
$attempts = array_filter($attempts, function($t) use ($now) {
|
||||
return $t > ($now - self::ATTEMPT_WINDOW);
|
||||
});
|
||||
|
||||
if (count($attempts) >= self::MAX_LOGIN_ATTEMPTS) {
|
||||
$lastAttempt = max($attempts);
|
||||
$waitTime = self::LOCKOUT_DURATION - ($now - $lastAttempt);
|
||||
if ($waitTime > 0) {
|
||||
$minutes = ceil($waitTime / 60);
|
||||
return "登录尝试次数过多,请 {$minutes} 分钟后再试";
|
||||
}
|
||||
// 锁定时间已过,清除记录
|
||||
unset($_SESSION[$key]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$_SESSION[$key] = array_values($attempts);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败登录尝试
|
||||
*/
|
||||
private function recordLoginAttempt($emp_no)
|
||||
{
|
||||
$key = 'login_attempts_' . md5($emp_no);
|
||||
$attempts = $_SESSION[$key] ?? [];
|
||||
$now = time();
|
||||
$attempts = array_filter($attempts, function($t) use ($now) {
|
||||
return $t > ($now - self::ATTEMPT_WINDOW);
|
||||
});
|
||||
$attempts[] = $now;
|
||||
$_SESSION[$key] = array_values($attempts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除登录尝试记录
|
||||
*/
|
||||
private function clearLoginAttempts($emp_no)
|
||||
{
|
||||
$key = 'login_attempts_' . md5($emp_no);
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余尝试次数
|
||||
*/
|
||||
private function getRemainingAttempts($emp_no)
|
||||
{
|
||||
$key = 'login_attempts_' . md5($emp_no);
|
||||
$attempts = $_SESSION[$key] ?? [];
|
||||
return max(0, self::MAX_LOGIN_ATTEMPTS - count($attempts));
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
session_destroy();
|
||||
header('Location: ' . BASE_URL . '/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user