初始化

This commit is contained in:
2026-08-08 18:28:49 +08:00
parent 9bef4420e0
commit f082037a6e
854 changed files with 217171 additions and 11 deletions
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 昂盛达设备模型
*/
class AsdDevice extends Model
{
protected $table = 'asd_device';
/**
* 获取 PDO 实例
*/
private function db()
{
return \core\db\Db::pdo();
}
/**
* 根据设备编码查找
*/
public function findByCode($device_code)
{
return $this->where(['device_code = :code'], [':code' => $device_code])->fetch();
}
/**
* 检查设备是否存在且启用
*/
public function isActive($device_code)
{
$device = $this->where([
'device_code = :code',
'status = 1'
], [':code' => $device_code])->fetch();
return $device ? $device : false;
}
/**
* 根据设备编码和组别校验
*/
public function validate($device_code, $group_name = '')
{
$conditions = ['device_code = :code', 'status = 1'];
$params = [':code' => $device_code];
if (!empty($group_name)) {
$conditions[] = 'group_name = :group';
$params[':group'] = $group_name;
}
return $this->where($conditions, $params)->fetch();
}
/**
* 获取所有启用设备
*/
public function getAllActive()
{
return $this->where(['status = 1'])->order(['group_name ASC', 'device_code ASC'])->fetchAll();
}
/**
* 添加设备
*/
public function addDevice($data)
{
return $this->add($data);
}
/**
* 更新设备
*/
public function updateDevice($id, $data)
{
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
/**
* 根据 IP 查找设备
*/
public function findByIp($device_ip)
{
return $this->where(['device_ip = :ip'], [':ip' => $device_ip])->fetch();
}
/**
* 根据 ID 查找设备
*/
public function findById($id)
{
return $this->where(['id = :id'], [':id' => intval($id)])->fetch();
}
/**
* 获取全部设备(调试页用,含禁用设备)
*/
public function getAll()
{
return $this->order(['group_name ASC', 'device_code ASC'])->fetchAll();
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 昂盛达上位机测试数据模型
*/
class AsdTestData extends Model
{
protected $table = 'asd_test_record';
/**
* 获取 PDO 实例
*/
private function db()
{
return \core\db\Db::pdo();
}
/**
* 获取所有测试记录(含统计信息)
*/
public function getAllWithStats($keyword = '', $limit = 50)
{
$sql = "SELECT r.*,
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";
$params = [];
if (!empty($keyword)) {
$sql .= " WHERE r.qr_code LIKE :keyword";
$params[':keyword'] = '%' . $keyword . '%';
}
$sql .= " GROUP BY r.id ORDER BY r.id DESC LIMIT " . intval($limit);
$stmt = $this->db()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* 新增测试记录主表,返回插入ID
*
* 兼容接口字段名(v1.5)与数据库实际列名的差异:
* status -> test_result (整体测试结果 PASS/NG
* device -> device_code (设备序列号)
* test_time -> test_date (测试时间)
* project_name -> product_model (产品/项目名称)
* 其余接口字段(tester/mo/site/station/work_order/group_name/running_time
* 已通过迁移脚本 upgrade_asd_api_20260718.sql 直接补齐为独立列。
*/
public function addRecord($data)
{
$map = [
'status' => 'test_result',
'device' => 'device_code',
'test_time' => 'test_date',
'project_name' => 'product_model',
];
foreach ($map as $from => $to) {
if (array_key_exists($from, $data)) {
// 仅当目标列尚未显式传入时才用接口值填充,避免覆盖
if (!array_key_exists($to, $data) || $data[$to] === '' || $data[$to] === null) {
$data[$to] = $data[$from];
}
unset($data[$from]);
}
}
$result = $this->add($data);
if ($result) {
return $this->db()->lastInsertId();
}
return false;
}
/**
* 根据ID获取测试记录
*/
public function findById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
/**
* 新增测试工步详情
*/
public function addDetail($data)
{
$sql = "INSERT INTO DGZXY_asd_test_detail
(record_id, seq, test_name, test_item, test_units, data_value, lower_limit, upper_limit, test_value, test_limit, test_result)
VALUES
(:record_id, :seq, :test_name, :test_item, :test_units, :data_value, :lower_limit, :upper_limit, :test_value, :test_limit, :test_result)";
$stmt = $this->db()->prepare($sql);
return $stmt->execute([
':record_id' => $data['record_id'],
':seq' => $data['seq'] ?? 1,
':test_name' => $data['test_name'] ?? '',
':test_item' => $data['test_item'] ?? '',
':test_units' => $data['test_units'] ?? '',
':data_value' => $data['data_value'] ?? null,
':lower_limit' => $data['lower_limit'] ?? null,
':upper_limit' => $data['upper_limit'] ?? null,
':test_value' => $data['test_value'] ?? '',
':test_limit' => $data['test_limit'] ?? '',
':test_result' => $data['test_result'] ?? 'PASS',
]);
}
/**
* 根据记录ID获取所有工步详情
*/
public function getDetails($recordId)
{
$stmt = $this->db()->prepare("SELECT * FROM DGZXY_asd_test_detail WHERE record_id = :rid ORDER BY seq ASC");
$stmt->execute([':rid' => $recordId]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* 删除测试记录(级联删除详情)
*/
public function deleteRecord($id)
{
// 先删详情(FOREIGN KEY ON DELETE CASCADE 会自动处理,这里显式处理更安全)
$this->db()->prepare("DELETE FROM DGZXY_asd_test_detail WHERE record_id = :rid")->execute([':rid' => $id]);
// 再删主表
return $this->delete($id);
}
/**
* 统计记录数
*/
public function countRecords($keyword = '')
{
if (!empty($keyword)) {
$stmt = $this->db()->prepare("SELECT COUNT(*) AS cnt FROM DGZXY_asd_test_record WHERE qr_code LIKE :kw");
$stmt->execute([':kw' => '%' . $keyword . '%']);
return $stmt->fetch(\PDO::FETCH_ASSOC)['cnt'];
}
$stmt = $this->db()->prepare("SELECT COUNT(*) AS cnt FROM DGZXY_asd_test_record");
$stmt->execute();
return $stmt->fetch(\PDO::FETCH_ASSOC)['cnt'];
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 产品组装模型
* @deprecated
*/
class Assembly extends Model
{
protected $table = 'product_assembly';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findByFinishedSerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetch();
}
// 根据产品型号筛选记录(assembly 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(assembly_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 固定资产模型
*/
class Asset extends Model
{
protected $table = 'asset';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt, SUM(purchase_price * quantity) AS total_value FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $stmt->fetch();
}
public function getByStatus($status)
{
return $this->where(['status = :st'], [':st' => $status])->order(['id DESC'])->fetchAll();
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 电池状态模型
* @deprecated
*/
class Battery extends Model
{
protected $table = 'battery_status';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($serial_no)
{
return $this->where(['serial_no = :serial'], [':serial' => $serial_no])->fetch();
}
// 根据产品类型和型号筛选记录
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_type) && empty($product_model)) {
return [];
}
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
return $this->where($conditions, $params)->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(created_at) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
if (!empty($conditions)) {
$result = $this->where($conditions, $params)->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 客户模型
*/
class Customer extends Model
{
protected $table = 'customer';
// 获取所有客户
public function getAll()
{
return $this->order(['id ASC'])->fetchAll();
}
// 添加客户
public function addCustomer($data)
{
return $this->add($data);
}
// 更新客户
public function updateCustomer($id, $data)
{
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 删除客户
public function deleteCustomer($id)
{
return $this->delete($id);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 出货登记模型
* @deprecated
*/
class Delivery extends Model
{
protected $table = 'delivery';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetch();
}
/**
* 批量添加出货记录
* @param array $records 记录数组
*/
public function batchAdd($records)
{
foreach ($records as $record) {
// 跳过已出货的序列号
$existing = $this->findBySerial($record['finished_serial']);
if ($existing) {
continue;
}
$this->add($record);
}
return true;
}
/**
* 从 warehouse_in 表查询成品序列号对应的产品型号
* @param string $finished_serial 成品序列号
* @return array|null ['product_model' => ..., 'box_serial' => ...] 或 null
*/
public function lookupFinishedSerial($finished_serial)
{
$table = self::TABLE_PREFIX . 'warehouse_in';
$sql = "SELECT finished_serial, product_model, box_serial FROM {$table} WHERE finished_serial = :serial LIMIT 1";
$result = $this->query($sql, [':serial' => $finished_serial]);
return !empty($result) ? $result[0] : null;
}
// 根据产品型号筛选记录(delivery 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(delivery_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
/**
* 根据箱序列号查询该箱下所有成品
* @param string $box_serial 箱序列号
* @return array 该箱下所有成品记录数组
*/
public function getBoxItems($box_serial)
{
$table = self::TABLE_PREFIX . 'warehouse_in';
$sql = "SELECT finished_serial, product_model FROM {$table} WHERE box_serial = :box ORDER BY id ASC";
return $this->query($sql, [':box' => $box_serial]);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 文档管理模型
*/
class Document extends Model
{
protected $table = 'document';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByCategory($category)
{
return $this->where(['category = :cat'], [':cat' => $category])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
public function getCategories()
{
$sql = 'SELECT DISTINCT category FROM ' . $this->getTable() . " WHERE category != '' ORDER BY category";
$stmt = $this->pdo->query($sql);
return $stmt->fetchAll();
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 员工模型
*/
class Employee extends Model
{
protected $table = 'employee';
// 根据员工编号查找
public function findByEmpNo($emp_no)
{
return $this->where(['emp_no = :emp_no'], [':emp_no' => $emp_no])->fetch();
}
/**
* 验证登录(支持 bcrypt 和旧版 MD5 兼容)
*/
public function validateLogin($emp_no, $password)
{
// 先按工号查找用户
$user = $this->where(['emp_no = :emp_no'], [':emp_no' => $emp_no])->fetch();
if (!$user) {
$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] emp_no_len=" . strlen($emp_no) . " emp_no_raw='" . $emp_no . "' found_user=0 ua={$ua} sess_hash={$sess}");
return false;
}
// 判断密码是否为 bcrypt 格式(bcrypt 哈希总是以 $2y$ 开头)
$isBcrypt = (substr($user['password'], 0, 4) === '$2y$');
$pwdFmt = $isBcrypt ? 'bcrypt' : (strlen($user['password']) === 32 ? 'md5' : 'other');
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 150) : 'unknown';
$sess = session_id() ? hash('sha256', session_id()) : 'none';
$pwdLen = strlen($password);
error_log("[LOGIN_DIAG] emp_no_len=" . strlen($emp_no) . " emp_no_raw='" . $emp_no . "' found_user=1 pwd_fmt={$pwdFmt} pwd_len={$pwdLen} ua={$ua} sess_hash={$sess}");
if ($isBcrypt) {
if (password_verify($password, $user['password'])) {
// 如果算法需要升级,自动更新哈希
if (password_needs_rehash($user['password'], PASSWORD_BCRYPT)) {
$this->updatePasswordHash($user['id'], $password);
}
return $user;
}
error_log("[LOGIN_DIAG] PWD_VERIFY_FAIL pwd_len={$pwdLen} pwd_hash_len=" . strlen($user['password']) . " ua={$ua} sess_hash={$sess}");
return false;
}
// 兼容旧版 MD5 密码
if (md5($password) === $user['password']) {
// 自动升级为 bcrypt
$this->updatePasswordHash($user['id'], $password);
return $user;
}
return false;
}
/**
* 更新密码哈希为 bcrypt
*/
private function updatePasswordHash($id, $password)
{
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$this->where(['id = :id'], [':id' => $id])->update(['password' => $hash]);
}
/**
* 生成密码哈希
*/
public function hashPassword($password)
{
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
}
/**
* 密码复杂度校验(P1:强制强密码策略)
* 要求:≥12 位,且同时包含大写字母、小写字母、数字、特殊字符
* @throws \RuntimeException 不满足时抛出异常
*/
public function validatePasswordStrength($password)
{
if (!is_string($password) || $password === '') {
throw new \RuntimeException('密码不能为空');
}
if (mb_strlen($password) < 12) {
throw new \RuntimeException('密码长度至少 12 位');
}
if (!preg_match('/[A-Z]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个大写字母');
}
if (!preg_match('/[a-z]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个小写字母');
}
if (!preg_match('/[0-9]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个数字');
}
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
throw new \RuntimeException('密码必须包含至少一个特殊字符(如 !@#$%^&*');
}
// 拒绝常见弱密码
$weak = ['123456', 'password', 'admin123', 'qwerty', '111111', '12345678', 'abc123'];
if (in_array(strtolower($password), $weak, true)) {
throw new \RuntimeException('密码过于常见,请更换');
}
}
// 重置密码
public function resetPassword($id, $new_password)
{
$this->validatePasswordStrength($new_password);
$hash = $this->hashPassword($new_password);
return $this->where(['id = :id'], [':id' => $id])->update(['password' => $hash]);
}
// 获取所有员工
public function getAll()
{
return $this->order(['id ASC'])->fetchAll();
}
// 添加员工
public function addEmployee($data)
{
if (!isset($data['password']) || empty(trim($data['password']))) {
// 生成随机强密码
$randomPwd = bin2hex(random_bytes(8)); // 16位随机密码
$data['password'] = $this->hashPassword($randomPwd);
// 返回生成的密码供管理员告知用户
$data['_generated_password'] = $randomPwd;
} else {
// P1:密码复杂度校验(≥12位,含大小写+数字+特殊字符)
$this->validatePasswordStrength($data['password']);
$data['password'] = $this->hashPassword($data['password']);
}
// 移除 _generated_password 再写入
$generatedPwd = $data['_generated_password'] ?? null;
unset($data['_generated_password']);
$id = $this->add($data);
if ($id && $generatedPwd) {
// 返回生成的密码
$this->_lastGeneratedPwd = $generatedPwd;
}
return $id;
}
// 获取最后生成的密码
public function getLastGeneratedPwd()
{
return $this->_lastGeneratedPwd ?? null;
}
private $_lastGeneratedPwd = null;
// 更新员工
public function updateEmployee($id, $data)
{
unset($data['password']); // 不允许直接更新密码
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 根据类目获取员工列表
public function getByCategory($category)
{
if ($category === 'all') {
return $this->order(['id ASC'])->fetchAll();
}
return $this->where(['category = :cat OR role = :super'], [
':cat' => $category,
':super' => 'super_admin'
])->order(['id ASC'])->fetchAll();
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 员工工位权限模型
*/
class EmployeePermission extends Model
{
protected $table = 'employee_station_permission';
/**
* 获取某员工所有工位权限(返回 station_type => is_allowed 的键值对)
*/
public function getByEmployee($employeeId)
{
$rows = $this->where(['employee_id = :eid'], [':eid' => $employeeId])->fetchAll();
$permissions = [];
foreach ($rows as $row) {
$permissions[$row['station_type']] = (int)$row['is_allowed'];
}
return $permissions;
}
/**
* 检查员工是否有某个工位的访问权限
* 超级管理员和管理员始终返回 true
*/
public function canAccess($employeeId, $role, $stationType)
{
// 超级管理员和管理员拥有所有工位权限
if ($role === \core\base\Controller::ROLE_SUPER_ADMIN || $role === \core\base\Controller::ROLE_ADMIN) {
return true;
}
$row = $this->where([
'employee_id = :eid AND station_type = :stype'
], [
':eid' => $employeeId,
':stype' => $stationType
])->fetch();
// 如果没配置过权限,默认允许访问
if (!$row) {
return true;
}
return (int)$row['is_allowed'] === 1;
}
/**
* 获取某员工允许访问的工位类型列表
*/
public function getAllowedStations($employeeId)
{
$rows = $this->where([
'employee_id = :eid AND is_allowed = 1'
], [':eid' => $employeeId])->fetchAll();
return array_column($rows, 'station_type');
}
/**
* 批量设置员工工位权限
* @param int $employeeId 员工ID
* @param array $permissions 格式:['station_type' => 1/0, ...]
*/
public function savePermissions($employeeId, $permissions)
{
// 先删除旧权限
$this->where(['employee_id = :eid'], [':eid' => $employeeId])->deleteWhere();
// 批量插入新权限
foreach ($permissions as $stationType => $isAllowed) {
$this->add([
'employee_id' => $employeeId,
'station_type' => $stationType,
'is_allowed' => $isAllowed ? 1 : 0,
]);
}
return true;
}
/**
* 删除某员工所有权限记录
*/
public function deleteByEmployee($employeeId)
{
return $this->where(['employee_id = :eid'], [':eid' => $employeeId])->deleteWhere();
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 财务账务记录模型
*/
class FinanceRecord extends Model
{
protected $table = 'finance_record';
public function getAll()
{
return $this->order(['record_date DESC', 'id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByDateRange($startDate, $endDate, $type = '')
{
$where = 'record_date >= :start AND record_date <= :end';
$params = [':start' => $startDate, ':end' => $endDate];
if ($type) {
$where .= ' AND type = :type';
$params[':type'] = $type;
}
return $this->where([$where], $params)->order(['record_date DESC', 'id DESC'])->fetchAll();
}
public function getSummary($startDate, $endDate)
{
$sql = "SELECT
type,
SUM(amount) AS total,
COUNT(*) AS cnt
FROM " . $this->getTable() . "
WHERE record_date >= :start AND record_date <= :end
GROUP BY type";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':start' => $startDate, ':end' => $endDate]);
return $stmt->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 成品测试模型
* @deprecated
*/
class FinishedTest extends Model
{
protected $table = 'finished_test';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetchAll();
}
// 根据产品型号筛选记录(finished_test 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(test_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 人事档案模型
*/
class HrEmployee extends Model
{
protected $table = 'hr_employee';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByDepartment($department)
{
return $this->where(['department = :dept'], [':dept' => $department])->order(['id DESC'])->fetchAll();
}
public function getByStatus($status)
{
return $this->where(['status = :st'], [':st' => $status])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
* 所有工位数据操作统一通过 StationDefinition 模型处理。
* 此文件保留仅用于向后兼容,后续版本将删除。
*/
namespace app\models;
use core\base\Model;
/**
* 入库工位模型(成品入库)
* @deprecated
*/
class Inbound extends Model
{
protected $table = 'station_inbound';
// 获取所有记录
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
// 根据产品类型和型号筛选记录
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_type) && empty($product_model)) {
return [];
}
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
return $this->where($conditions, $params)->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(created_at) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
if (!empty($conditions)) {
$result = $this->where($conditions, $params)->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
// 添加记录
public function addRecord($data)
{
return $this->add($data);
}
// 根据序列号查找
public function findBySerial($serial_no)
{
return $this->where(['serial_no = :serial'], [':serial' => $serial_no])->fetch();
}
// 批量导入
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 库存模型
*/
class Inventory extends Model
{
protected $table = 'inventory';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getLowStock()
{
return $this->where(['quantity <= min_stock AND min_stock > 0'])->order(['quantity ASC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt, SUM(quantity) AS total_qty FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $stmt->fetch();
}
public function search($keyword)
{
return $this->where(
['product_name LIKE :kw OR product_model LIKE :kw2 OR category LIKE :kw3'],
[':kw' => "%{$keyword}%", ':kw2' => "%{$keyword}%", ':kw3' => "%{$keyword}%"]
)->order(['id DESC'])->fetchAll();
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* PCB 测试模型
* @deprecated
*/
class PcbTest extends Model
{
protected $table = 'pcb_test';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($serial_no)
{
return $this->where(['serial_no = :serial'], [':serial' => $serial_no])->fetch();
}
// 根据产品类型和型号筛选记录
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_type) && empty($product_model)) {
return [];
}
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
return $this->where($conditions, $params)->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(created_at) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
$conditions = [];
$params = [];
if (!empty($product_type)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $product_type;
}
if (!empty($product_model)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $product_model;
}
if (!empty($conditions)) {
$result = $this->where($conditions, $params)->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 产品型号模型
*/
class ProductModel extends Model
{
protected $table = 'product_model';
/** @var bool 迁移是否已检查(进程级缓存,避免重复检查) */
private static $migrated = false;
/** @var bool type 列是否存在 */
private static $typeColumnExists = false;
/**
* 确保 type 列存在(自动迁移)
* @return bool type 列是否存在(true=可用,false=不存在且无法创建)
*/
private function ensureTypeColumn()
{
if (self::$migrated) {
return self::$typeColumnExists;
}
$table = $this->getTable();
try {
$this->query("SELECT type FROM {$table} LIMIT 1");
self::$typeColumnExists = true;
self::$migrated = true;
} catch (\Throwable $e) {
// 列不存在 → 尝试自动迁移
try {
$this->execute("ALTER TABLE {$table}
ADD COLUMN `type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '类型(成品/半成品/配件)'
AFTER `id`");
$this->execute("UPDATE {$table} SET `type` = `product_type` WHERE `type` = ''");
self::$typeColumnExists = true;
} catch (\Throwable $migErr) {
error_log('[ProductModel] Auto-migration failed: ' . $migErr->getMessage());
self::$typeColumnExists = false;
}
self::$migrated = true;
}
return self::$typeColumnExists;
}
// 获取所有产品型号
public function getAll()
{
$this->ensureTypeColumn();
return $this->order(['id ASC'])->fetchAll();
}
// 添加产品型号
public function addModel($data)
{
if (!$this->ensureTypeColumn()) {
unset($data['type']); // type 列不存在,移除该字段
}
return $this->add($data);
}
// 更新产品型号
public function updateModel($id, $data)
{
if (!$this->ensureTypeColumn()) {
unset($data['type']);
}
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 删除产品型号
public function deleteModel($id)
{
return $this->delete($id);
}
// 根据型号代码查找
public function findByCode($model_code)
{
return $this->where(['model_code = :code'], [':code' => $model_code])->fetch();
}
// 根据类型+产品类型和型号代码联合查找(用于检测同类型下是否重复)
public function findByTypeAndCode($type, $product_type, $model_code)
{
if ($this->ensureTypeColumn()) {
return $this->where(
['type = :t', 'product_type = :pt', 'model_code = :mc'],
[':t' => $type, ':pt' => $product_type, ':mc' => $model_code]
)->fetch();
}
// Fallback: type 列不存在,仅在 product_type 和 model_code 上查重
return $this->where(
['product_type = :pt', 'model_code = :mc'],
[':pt' => $product_type, ':mc' => $model_code]
)->fetch();
}
// 根据类型获取该类型下的所有型号(用于前端联动)
public function getByType($type)
{
if (!$this->ensureTypeColumn()) {
return [];
}
return $this->where(['type = :t'], [':t' => $type])
->order(['model_code ASC'])
->fetchAll();
}
// 获取类型列表(去重)
public function getTypes()
{
if (!$this->ensureTypeColumn()) {
return [];
}
$sql = 'SELECT DISTINCT type FROM ' . $this->getTable() . ' WHERE type != \'\' ORDER BY type';
return $this->query($sql);
}
/**
* 获取所有产品类型(product_type 去重列表,用于下拉筛选)
* 注意:这是 product_type 字段(自由文本),不是 type(成品/半成品/配件)
*/
public function getProductTypes()
{
$table = $this->getTable();
return $this->query(
"SELECT DISTINCT product_type FROM {$table} WHERE product_type != '' ORDER BY product_type"
);
}
/**
* 根据类型获取产品型号
* 优先通过 type 字段查询(大分类:成品/半成品/配件),
* 如果 type 查不到则通过 product_type 字段查询(子分类:PCBA/电池/外壳等)
* 若 type 列不可用,回退到 product_type 字段查询
*/
public function getByProductType($productType)
{
if ($this->ensureTypeColumn()) {
// 先尝试 type 字段
$result = $this->getByType($productType);
if (!empty($result)) {
return $result;
}
// type 查不到,回退到 product_type 字段
return $this->where(['product_type = :pt'], [':pt' => $productType])
->order(['model_code ASC'])
->fetchAll();
}
// Fallback: type 列不存在,使用 product_type 字段
return $this->where(['product_type = :pt'], [':pt' => $productType])
->order(['model_code ASC'])
->fetchAll();
}
}
+390
View File
@@ -0,0 +1,390 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 产品类型配置模型
*/
class ProductTypeConfig extends Model
{
protected $table = 'DGZXY_product_type_config';
/**
* 安全执行查询:表不存在时返回空数组
*/
private function safeQuery(callable $query, $default = [])
{
try {
return $query();
} catch (\Throwable $e) {
error_log('[ProductTypeConfig] DB error: ' . $e->getMessage());
return $default;
}
}
/**
* 获取所有启用的类型(按 sort_order 排序)
*/
public function getEnabled()
{
return $this->safeQuery(function () {
return $this->where(['enabled = 1'])
->order(['sort_order ASC'])
->fetchAll();
}, []);
}
/**
* 获取所有类型(含未启用,按 sort_order 排序)
*/
public function getAll()
{
return $this->safeQuery(function () {
return $this->order(['sort_order ASC'])->fetchAll();
}, []);
}
/**
* 根据类型名称获取配置
*/
public function getByTypeName($typeName)
{
return $this->safeQuery(function () use ($typeName) {
return $this->where(['type_name = :name'], [':name' => $typeName])->fetch();
}, null);
}
/**
* 获取类型名称列表(用于下拉选择)
*/
public function getTypeNames()
{
$rows = $this->getEnabled();
return array_column($rows, 'type_name');
}
/**
* 获取入库工位可见的类型(enabled=1 且 show_in_inbound=1
*/
public function getInboundVisible()
{
return $this->safeQuery(function () {
return $this->where(['enabled = 1', 'show_in_inbound = 1'])
->order(['sort_order ASC'])
->fetchAll();
}, []);
}
/**
* 获取类型的 display_columns(解析 JSON
*/
public function getDisplayColumns($typeName)
{
$config = $this->getByTypeName($typeName);
if ($config && !empty($config['display_columns'])) {
$columns = json_decode($config['display_columns'], true);
if (is_array($columns)) {
return $columns;
}
}
// 默认列
return [
['key' => 'type', 'label' => '类型'],
['key' => 'model_code', 'label' => '产品型号'],
['key' => 'model_desc', 'label' => '产品说明'],
];
}
/**
* 获取所有类型的 display_columnskeyed by type_name
*/
public function getAllDisplayColumns()
{
$all = $this->getAll();
$result = [];
foreach ($all as $row) {
$cols = [];
if (!empty($row['display_columns'])) {
$cols = json_decode($row['display_columns'], true);
}
if (!is_array($cols)) {
$cols = [
['key' => 'type', 'label' => '类型'],
['key' => 'model_code', 'label' => '产品型号'],
['key' => 'model_desc', 'label' => '产品说明'],
];
}
$result[$row['type_name']] = $cols;
}
return $result;
}
/**
* 添加类型配置(表不存在时自动创建)
*/
public function addConfig($data)
{
try {
return $this->add($data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log('[ProductTypeConfig] addConfig error: ' . $msg);
// 表不存在时自动创建
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
if ($this->autoCreateTable()) {
// 重试添加
try {
return $this->add($data);
} catch (\Throwable $e2) {
error_log('[ProductTypeConfig] addConfig retry error: ' . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 更新类型配置
*/
public function updateConfig($id, $data)
{
try {
return $this->where(['id = :id'], [':id' => $id])->update($data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log('[ProductTypeConfig] updateConfig error: ' . $msg);
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
if ($this->autoCreateTable()) {
try {
return $this->where(['id = :id'], [':id' => $id])->update($data);
} catch (\Throwable $e2) {
error_log('[ProductTypeConfig] updateConfig retry error: ' . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 删除类型配置
*/
public function deleteConfig($id)
{
try {
return $this->delete($id);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log('[ProductTypeConfig] deleteConfig error: ' . $msg);
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
if ($this->autoCreateTable()) {
try {
return $this->delete($id);
} catch (\Throwable $e2) {
error_log('[ProductTypeConfig] deleteConfig retry error: ' . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 自动创建 product_type_config 表并初始化默认数据
*/
public function autoCreateTable()
{
try {
$tableName = $this->getTable();
$sql = "CREATE TABLE IF NOT EXISTS `{$tableName}` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`type_name` VARCHAR(50) NOT NULL COMMENT '类型名称',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`display_columns` TEXT COMMENT '列表页额外列(JSON)',
`product_type_state` VARCHAR(10) NOT NULL DEFAULT 'editable' COMMENT '产品类型输入框状态:hidden/readonly/editable',
`product_type_label` VARCHAR(50) DEFAULT '产品类型' COMMENT '产品类型输入框标签',
`product_type_field` VARCHAR(50) DEFAULT 'product_type' COMMENT '提交字段名',
`product_type_placeholder` VARCHAR(100) DEFAULT '' COMMENT '输入框占位提示',
`model_code_state` VARCHAR(10) NOT NULL DEFAULT 'editable' COMMENT '产品型号输入框状态:hidden/readonly/editable',
`color_state` VARCHAR(10) NOT NULL DEFAULT 'editable' COMMENT '颜色输入框状态:hidden/readonly/editable',
`color` VARCHAR(30) DEFAULT 'default' COMMENT '列表页box颜色',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
`show_in_inbound` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '入库页是否显示',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品类型配置'";
$this->pdo->exec($sql);
// 检查是否已有数据
$count = $this->pdo->query("SELECT COUNT(*) FROM `{$tableName}`")->fetchColumn();
if ($count == 0) {
$defaults = self::getDefaultConfigs();
$insertSql = "INSERT INTO `{$tableName}`
(`type_name`, `sort_order`, `display_columns`, `product_type_state`,
`product_type_label`, `product_type_field`, `product_type_placeholder`,
`model_code_state`, `color_state`, `color`, `enabled`, `show_in_inbound`)
VALUES (:type_name, :sort_order, :display_columns, :product_type_state,
:product_type_label, :product_type_field, :product_type_placeholder,
:model_code_state, :color_state, :color, :enabled, :show_in_inbound)";
$stmt = $this->pdo->prepare($insertSql);
foreach ($defaults as $row) {
$stmt->execute([
':type_name' => $row['type_name'],
':sort_order' => $row['sort_order'],
':display_columns' => $row['display_columns'],
':product_type_state' => $row['product_type_state'],
':product_type_label' => $row['product_type_label'],
':product_type_field' => $row['product_type_field'],
':product_type_placeholder' => $row['product_type_placeholder'],
':model_code_state' => $row['model_code_state'] ?? 'editable',
':color_state' => $row['color_state'] ?? 'editable',
':color' => $row['color'],
':enabled' => $row['enabled'],
':show_in_inbound' => $row['show_in_inbound'] ?? 1,
]);
}
}
error_log('[ProductTypeConfig] Table auto-created successfully');
return true;
} catch (\Throwable $e) {
error_log('[ProductTypeConfig] autoCreateTable error: ' . $e->getMessage());
return false;
}
}
/**
* 获取硬编码默认配置(数据库表不存在时的 fallback)
*/
/**
* 三态值常量
* hidden: 隐藏输入框
* readonly: 显示但只读
* editable: 可编辑
*/
const STATE_HIDDEN = 'hidden';
const STATE_READONLY = 'readonly';
const STATE_EDITABLE = 'editable';
public static function getDefaultConfigs()
{
return [
[
'type_name' => 'PCBA',
'sort_order' => 0,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_HIDDEN,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'primary',
'enabled' => 1,
'show_in_inbound' => 0, // PCBA 默认不在入库页显示
],
[
'type_name' => '成品',
'sort_order' => 1,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_HIDDEN,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'success',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '电池',
'sort_order' => 2,
'display_columns' => '[{"key":"model_code","label":"产品型号"},{"key":"capacity","label":"容量"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '容量',
'product_type_field' => 'capacity',
'product_type_placeholder' => '如:2000mAh',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'primary',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '外壳',
'sort_order' => 3,
'display_columns' => '[{"key":"model_code","label":"产品型号"},{"key":"color","label":"颜色"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_HIDDEN,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_EDITABLE,
'color' => 'info',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '外壳颜色',
'sort_order' => 4,
'display_columns' => '[{"key":"color","label":"颜色"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '颜色',
'product_type_field' => 'color',
'product_type_placeholder' => '如:黑色',
'model_code_state' => self::STATE_HIDDEN,
'color_state' => self::STATE_EDITABLE,
'color' => 'default',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '包装',
'sort_order' => 5,
'display_columns' => '[{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '如:通用包装',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'warning',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '半成品',
'sort_order' => 6,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '如:电池、外壳',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'warning',
'enabled' => 1,
'show_in_inbound' => 1,
],
[
'type_name' => '配件',
'sort_order' => 7,
'display_columns' => '[{"key":"type","label":"类型"},{"key":"model_code","label":"产品型号"},{"key":"model_desc","label":"产品说明"}]',
'product_type_state' => self::STATE_EDITABLE,
'product_type_label' => '产品类型',
'product_type_field' => 'product_type',
'product_type_placeholder' => '如:电池、外壳',
'model_code_state' => self::STATE_EDITABLE,
'color_state' => self::STATE_HIDDEN,
'color' => 'info',
'enabled' => 1,
'show_in_inbound' => 1,
],
];
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 采购明细模型
*/
class PurchaseItem extends Model
{
protected $table = 'purchase_item';
public function getByOrder($orderId)
{
return $this->where(['order_id = :oid'], [':oid' => $orderId])->fetchAll();
}
public function deleteByOrder($orderId)
{
$sql = 'DELETE FROM ' . $this->getTable() . ' WHERE order_id = :oid';
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([':oid' => $orderId]);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 采购订单模型
*/
class PurchaseOrder extends Model
{
protected $table = 'purchase_order';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getBySupplier($supplierId)
{
return $this->where(['supplier_id = :sid'], [':sid' => $supplierId])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 应收应付账款模型
*/
class ReceivablePayable extends Model
{
protected $table = 'receivable_payable';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getByType($type)
{
return $this->where(['type = :type'], [':type' => $type])->order(['id DESC'])->fetchAll();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 销售明细模型
*/
class SalesItem extends Model
{
protected $table = 'sales_item';
public function getByOrder($orderId)
{
return $this->where(['order_id = :oid'], [':oid' => $orderId])->fetchAll();
}
public function deleteByOrder($orderId)
{
$sql = 'DELETE FROM ' . $this->getTable() . ' WHERE order_id = :oid';
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([':oid' => $orderId]);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 销售订单模型
*/
class SalesOrder extends Model
{
protected $table = 'sales_order';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function getCount()
{
$sql = 'SELECT COUNT(*) AS cnt FROM ' . $this->getTable();
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$row = $stmt->fetch();
return (int)($row['cnt'] ?? 0);
}
}
+603
View File
@@ -0,0 +1,603 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 工位定义模型 - 统一管理所有内置工位的元数据
*
* 替代之前分散在 7 个独立 Model 文件中的硬编码配置。
* 所有内置工位的数据表名、时间列名、筛选开关等统一在此管理。
*
* 数据库表:DGZXY_station_definitions
*/
class StationDefinition extends Model
{
protected $table = 'station_definitions';
/** @var array 缓存已加载的定义 */
private static $cache = [];
/**
* 获取工位定义(表不存在时自动创建并初始化)
* @param string $stationType
* @return array|null
*/
public function getByType($stationType)
{
if (isset(self::$cache[$stationType])) {
return self::$cache[$stationType];
}
try {
$row = $this->where(['station_type = :type'], [':type' => $stationType])->fetch();
} catch (\Throwable $e) {
error_log('[StationDefinition] getByType(' . $stationType . ') failed: ' . $e->getMessage());
// 表不存在,自动创建并初始化
if ($this->autoCreateTable()) {
try {
$row = $this->where(['station_type = :type'], [':type' => $stationType])->fetch();
} catch (\Throwable $e2) {
error_log('[StationDefinition] getByType retry failed: ' . $e2->getMessage());
$row = null;
}
} else {
$row = null;
}
}
if ($row && !empty($row['extra_config'])) {
$row['extra'] = json_decode($row['extra_config'], true) ?: [];
} elseif ($row) {
$row['extra'] = [];
}
self::$cache[$stationType] = $row ?: null;
return self::$cache[$stationType];
}
/**
* 获取完整表名(带前缀)
*/
public function getFullTableName($stationType)
{
$def = $this->getByType($stationType);
if (!$def) {
// 尝试自动重建
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return null;
}
return self::TABLE_PREFIX . $def['db_table'];
}
/**
* 获取时间列名
*/
public function getTimeColumn($stationType)
{
$def = $this->getByType($stationType);
if (!$def) {
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
}
return $def ? $def['time_column'] : 'created_at';
}
/**
* 获取记录列表(支持按类型和型号筛选)
*
* 如果 station_definitions 记录缺失,自动尝试重建。
*/
public function getRecords($stationType, $filterType = '', $filterModel = '', $limit = 100)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] getRecords: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return [];
}
$table = self::TABLE_PREFIX . $def['db_table'];
$conditions = [];
$params = [];
if ($def['has_product_type'] && !empty($filterType)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $filterType;
}
if ($def['has_product_model'] && !empty($filterModel)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $filterModel;
}
$timeCol = $def['time_column'];
$sql = "SELECT * FROM `{$table}`";
if (!empty($conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$sql .= " ORDER BY `{$timeCol}` DESC LIMIT " . intval($limit);
try {
return $this->query($sql, $params);
} catch (\Throwable $e) {
error_log("[StationDefinition] getRecords query failed for {$stationType}: " . $e->getMessage());
// 表可能不存在,尝试创建
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
if ($this->ensureTable($stationType, $fieldConfigs)) {
try {
return $this->query($sql, $params);
} catch (\Throwable $e2) {
error_log("[StationDefinition] getRecords retry failed for {$stationType}: " . $e2->getMessage());
}
}
return [];
}
}
/**
* 获取所有记录
*/
public function getAllRecords($stationType, $limit = 100)
{
return $this->getRecords($stationType, '', '', $limit);
}
/**
* 统计今日某操作员的录入数量
*/
public function countTodayByOperator($stationType, $operator)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] countTodayByOperator: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return 0;
}
$table = self::TABLE_PREFIX . $def['db_table'];
$timeCol = $def['time_column'];
try {
$sql = "SELECT COUNT(*) as cnt FROM `{$table}` WHERE operator = :op AND DATE(`{$timeCol}`) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
} catch (\Throwable $e) {
error_log("[StationDefinition] countTodayByOperator failed for {$stationType}: " . $e->getMessage());
return 0;
}
}
/**
* 统计总记录数(可按型号筛选)
*/
public function countTotal($stationType, $filterType = '', $filterModel = '')
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] countTotal: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return 0;
}
$table = self::TABLE_PREFIX . $def['db_table'];
$conditions = [];
$params = [];
if ($def['has_product_type'] && !empty($filterType)) {
$conditions[] = 'product_type = :pt';
$params[':pt'] = $filterType;
}
if ($def['has_product_model'] && !empty($filterModel)) {
$conditions[] = 'product_model = :pm';
$params[':pm'] = $filterModel;
}
try {
$sql = "SELECT COUNT(*) as cnt FROM `{$table}`";
if (!empty($conditions)) {
$sql .= ' WHERE ' . implode(' AND ', $conditions);
}
$result = $this->query($sql, $params);
return $result[0]['cnt'] ?? 0;
} catch (\Throwable $e) {
error_log("[StationDefinition] countTotal failed for {$stationType}: " . $e->getMessage());
return 0;
}
}
/**
* 添加记录到工位专用表
*
* 如果 station_definitions 中没有该工位记录,自动尝试重建。
* 如果数据表不存在,自动创建后再插入。
*/
public function addRecord($stationType, $data)
{
$def = $this->getByType($stationType);
if (!$def) {
// 尝试自动重建 station_definitions 数据
error_log("[StationDefinition] addRecord: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] addRecord: still no definition for '{$stationType}' after re-init");
return false;
}
}
$table = self::TABLE_PREFIX . $def['db_table'];
// 确保数据表存在(不存在则创建)
try {
$fields = implode('`, `', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO `{$table}` (`{$fields}`) VALUES ({$placeholders})";
return $this->execute($sql, $data);
} catch (\Throwable $e) {
$msg = $e->getMessage();
error_log("[StationDefinition] addRecord failed for {$stationType}: {$msg}");
// 表不存在时自动创建
if (stripos($msg, 'exist') !== false || stripos($msg, 'not found') !== false || stripos($msg, '1146') !== false) {
// 需要 fieldConfigs 来创建表,从全局获取
$fieldConfigs = $GLOBALS['fieldConfigs'] ?? [];
if ($this->ensureTable($stationType, $fieldConfigs)) {
// 重试插入
try {
return $this->execute($sql, $data);
} catch (\Throwable $e2) {
error_log("[StationDefinition] addRecord retry failed for {$stationType}: " . $e2->getMessage());
return false;
}
}
}
return false;
}
}
/**
* 检查记录是否已存在(防重复)
*/
public function findByField($stationType, $fieldName, $value)
{
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] findByField: no definition for '{$stationType}', auto-initializing...");
$this->autoCreateTable();
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) return null;
}
$table = self::TABLE_PREFIX . $def['db_table'];
try {
$sql = "SELECT * FROM `{$table}` WHERE `{$fieldName}` = :val LIMIT 1";
$result = $this->query($sql, [':val' => $value]);
return $result[0] ?? null;
} catch (\Throwable $e) {
error_log("[StationDefinition] findByField failed for {$stationType}: " . $e->getMessage());
return null;
}
}
/**
* 检查工位是否启用筛选
*/
public function isFilterEnabled($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['filter_enabled'] : false;
}
/**
* 检查是否显示今日计数
*/
public function showTodayCount($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['show_today_count'] : false;
}
/**
* 检查是否显示总计数
*/
public function showTotalCount($stationType)
{
$def = $this->getByType($stationType);
return $def ? (bool)$def['show_total_count'] : false;
}
/**
* 确保工位数据表存在,不存在则自动创建
*
* 通用工位表结构(参考 station_inbound):
* id, product_type, product_model, operator, created_at + 动态字段
*
* @param string $stationType 工位类型标识
* @param array $fieldConfigs 字段配置(从 fields_config 解析)
* @return bool 表是否已存在/创建成功
*/
public function ensureTable($stationType, $fieldConfigs = [])
{
// 先尝试获取工位定义,如果失败则尝试重建 station_definitions 表数据
$def = $this->getByType($stationType);
if (!$def) {
// station_definitions 表中没有该工位的记录,尝试重新插入
error_log("[StationDefinition] ensureTable: no definition for '{$stationType}', trying to re-initialize built-in data");
$this->autoCreateTable();
// 清除缓存后重试
self::clearCache();
$def = $this->getByType($stationType);
if (!$def) {
error_log("[StationDefinition] ensureTable: still no definition for '{$stationType}' after re-init");
return false;
}
}
$fullTable = self::TABLE_PREFIX . $def['db_table'];
// 检查表是否已存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (!empty($exists)) return true;
// 基础列:所有通用工位表都有的字段
$columns = [
"`id` INT AUTO_INCREMENT PRIMARY KEY",
"`product_type` VARCHAR(100) DEFAULT NULL COMMENT '产品类型'",
"`product_model` VARCHAR(100) DEFAULT NULL COMMENT '产品型号'",
"`operator` VARCHAR(50) DEFAULT NULL COMMENT '操作人'",
"`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间'",
];
// 索引
$indexes = [
"KEY `idx_product_type` (`product_type`)",
"KEY `idx_product_model` (`product_model`)",
"KEY `idx_created_at` (`created_at`)",
];
// 从 fields_config 派生额外字段(排除已存在的基础字段和隐藏字段)
$baseFields = ['product_type', 'product_model', 'operator', 'created_at', 'id'];
$addedFields = [];
foreach ($fieldConfigs as $field) {
$name = $field['name'] ?? '';
$type = $field['type'] ?? 'text';
if (in_array($name, $baseFields) || empty($name)) continue;
// 跳过纯虚拟字段(没有实际存储意义的)
if (in_array($name, ['_meta'])) continue;
// 根据字段类型确定 MySQL 列类型
$colType = 'VARCHAR(200)';
if ($type === 'number') {
$colType = 'VARCHAR(50)';
} elseif ($type === 'textarea') {
$colType = 'TEXT';
}
$label = $field['label'] ?? $name;
$comment = addslashes($label);
$columns[] = "`{$name}` {$colType} DEFAULT NULL COMMENT '{$comment}'";
// 为序列号类字段添加索引
if (!empty($field['is_scan']) || stripos($name, 'serial') !== false) {
$indexes[] = "KEY `idx_{$name}` (`{$name}`)";
}
$addedFields[] = $name;
}
// 从 station_business_helper 的 dataMapping 补充可能缺失的列
// 确保 helper 文件已加载
if (!function_exists('getStationBusinessConfig')) {
@include_once APP_PATH . 'app/helpers/station_business_helper.php';
}
if (function_exists('getStationBusinessConfig')) {
$bizConfig = getStationBusinessConfig($stationType);
if ($bizConfig && !empty($bizConfig['dataMapping'])) {
foreach ($bizConfig['dataMapping'] as $dbField => $postKey) {
if ($postKey === '__operator__') continue;
if (in_array($dbField, $baseFields) || in_array($dbField, $addedFields)) continue;
$columns[] = "`{$dbField}` VARCHAR(200) DEFAULT NULL COMMENT '{$dbField}'";
$addedFields[] = $dbField;
}
}
}
$allColumns = implode(",\n ", $columns);
$allIndexes = !empty($indexes) ? ",\n " . implode(",\n ", $indexes) : '';
$sql = "CREATE TABLE `{$fullTable}` (\n {$allColumns}{$allIndexes}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='{$stationType} 工位数据表'";
try {
$this->execute($sql);
error_log("[StationDefinition] Auto-created table: {$fullTable} for station_type={$stationType}, extra fields: " . implode(', ', $addedFields));
return true;
} catch (\Throwable $e) {
error_log("[StationDefinition] Failed to create table {$fullTable}: " . $e->getMessage());
return false;
}
}
/**
* 确保工位列存在(表已存在时补充缺失的列)
* 当 fields_config 新增字段后,自动 ALTER TABLE 添加
*/
public function ensureColumns($stationType, $fieldConfigs = [])
{
$def = $this->getByType($stationType);
if (!$def) return;
$fullTable = self::TABLE_PREFIX . $def['db_table'];
// 检查表是否存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (empty($exists)) {
// 表不存在,走建表流程
return $this->ensureTable($stationType, $fieldConfigs);
}
// 获取现有列
$existingCols = $this->query("SHOW COLUMNS FROM `{$fullTable}`");
$existingNames = [];
foreach ($existingCols as $col) {
$existingNames[] = strtolower($col['Field']);
}
// 基础字段名
$baseFields = ['product_type', 'product_model', 'operator', 'created_at', 'id'];
foreach ($fieldConfigs as $field) {
$name = $field['name'] ?? '';
$type = $field['type'] ?? 'text';
if (in_array($name, $baseFields) || empty($name)) continue;
if (in_array($name, ['_meta'])) continue;
if (in_array(strtolower($name), $existingNames)) continue;
$colType = 'VARCHAR(200)';
if ($type === 'number') {
$colType = 'VARCHAR(50)';
} elseif ($type === 'textarea') {
$colType = 'TEXT';
}
$label = $field['label'] ?? $name;
$comment = addslashes($label);
try {
$this->execute("ALTER TABLE `{$fullTable}` ADD COLUMN `{$name}` {$colType} DEFAULT NULL COMMENT '{$comment}'");
error_log("[StationDefinition] Added column `{$name}` to {$fullTable}");
} catch (\Throwable $e) {
error_log("[StationDefinition] Failed to add column `{$name}` to {$fullTable}: " . $e->getMessage());
}
}
}
/**
* 自动创建 station_definitions 表并初始化内置工位数据
* @return bool 是否成功
*/
/**
* 获取所有内置工位的默认定义数据
*/
private function getBuiltinDefaults()
{
return [
['station_type' => 'inbound', 'db_table' => 'station_inbound', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => '{"alt_table":"warehouse_in","alt_time_column":"in_time","alt_condition_field":"product_type","alt_condition_value":"成品"}'],
['station_type' => 'pcb_test', 'db_table' => 'pcb_test', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'battery', 'db_table' => 'battery_status', 'time_column' => 'created_at', 'has_product_type' => 1, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'assembly', 'db_table' => 'product_assembly', 'time_column' => 'assembly_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'finished_test','db_table' => 'finished_test', 'time_column' => 'test_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'delivery', 'db_table' => 'delivery', 'time_column' => 'delivery_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 1, 'show_today_count' => 1, 'show_total_count' => 1, 'extra_config' => ''],
['station_type' => 'warehouse', 'db_table' => 'warehouse_in', 'time_column' => 'in_time', 'has_product_type' => 0, 'has_product_model' => 1, 'filter_enabled' => 0, 'show_today_count' => 0, 'show_total_count' => 0, 'extra_config' => ''],
];
}
/**
* 插入/更新内置工位默认数据(ON DUPLICATE KEY UPDATE
*
* 使用 ON DUPLICATE KEY UPDATE 而非 INSERT IGNORE,确保即使表中有旧数据,
* 也能更新为正确的默认值(修复数据损坏/不完整的情况)。
*
* @return bool
*/
private function insertBuiltinDefaults()
{
$fullTable = $this->getTable();
$defaults = $this->getBuiltinDefaults();
$insertSql = "INSERT INTO `{$fullTable}`
(`station_type`, `db_table`, `time_column`, `has_product_type`, `has_product_model`,
`filter_enabled`, `show_today_count`, `show_total_count`, `extra_config`)
VALUES (:st, :dt, :tc, :hpt, :hpm, :fe, :stc, :sttc, :ec)
ON DUPLICATE KEY UPDATE
`db_table` = VALUES(`db_table`),
`time_column` = VALUES(`time_column`),
`has_product_type` = VALUES(`has_product_type`),
`has_product_model` = VALUES(`has_product_model`),
`filter_enabled` = VALUES(`filter_enabled`),
`show_today_count` = VALUES(`show_today_count`),
`show_total_count` = VALUES(`show_total_count`),
`extra_config` = VALUES(`extra_config`)";
try {
// 使用 $this->execute() 而非直接访问 $this->pdo
// 确保在 Model 构造链完整的情况下执行
$affected = 0;
foreach ($defaults as $d) {
$params = [
':st' => $d['station_type'],
':dt' => $d['db_table'],
':tc' => $d['time_column'],
':hpt' => $d['has_product_type'],
':hpm' => $d['has_product_model'],
':fe' => $d['filter_enabled'],
':stc' => $d['show_today_count'],
':sttc' => $d['show_total_count'],
':ec' => $d['extra_config'],
];
// execute() 返回 boolrowCount() 需要通过 pdo 获取
if ($this->execute($insertSql, $params)) {
$affected++;
}
}
if ($affected > 0) {
error_log('[StationDefinition] Upserted ' . $affected . ' built-in station definitions');
}
return true;
} catch (\Throwable $e) {
error_log('[StationDefinition] insertBuiltinDefaults failed: ' . $e->getMessage());
return false;
}
}
public function autoCreateTable()
{
$fullTable = $this->getTable();
try {
// 检查表是否存在
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
if (empty($exists)) {
// 创建表
$sql = "CREATE TABLE IF NOT EXISTS `{$fullTable}` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`station_type` VARCHAR(50) NOT NULL UNIQUE COMMENT '工位类型标识',
`db_table` VARCHAR(100) NOT NULL COMMENT '数据表名(不含前缀)',
`time_column` VARCHAR(50) NOT NULL DEFAULT 'created_at' COMMENT '时间列名',
`has_product_type` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_type 列',
`has_product_model` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_model 列',
`filter_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用筛选',
`show_today_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示今日计数',
`show_total_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示总计数',
`extra_config` TEXT COMMENT '额外配置(JSON',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_station_type` (`station_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工位定义统一配置表'";
$this->execute($sql);
error_log('[StationDefinition] Auto-created table: ' . $fullTable);
}
// 无论表是否刚创建,确保内置工位数据存在
// (表可能已存在但数据不完整,例如之前创建失败只建了表没插入数据)
$this->insertBuiltinDefaults();
return true;
} catch (\Throwable $e) {
error_log('[StationDefinition] autoCreateTable failed: ' . $e->getMessage());
return false;
}
}
/**
* 清除缓存(数据更新后调用)
*/
public static function clearCache()
{
self::$cache = [];
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 通用工位记录模型
* 用于动态添加的工位类型的数据存储
* 通过 station_type 字段区分不同工位
*/
class StationRecord extends Model
{
protected $table = 'station_records';
/**
* 根据工位类型获取记录
*/
public function getByStationType($stationType, $limit = 100)
{
return $this->where(['station_type = :type'], [':type' => $stationType])
->order(['created_at DESC'])
->limit($limit)
->fetchAll();
}
/**
* 添加记录
* @param string $stationType 工位类型
* @param array $data 字段名→值的键值对
* @param string $operator 操作人
*/
public function addRecord($stationType, $data, $operator = '')
{
return $this->add([
'station_type' => $stationType,
'record_data' => json_encode($data, JSON_UNESCAPED_UNICODE),
'operator' => $operator,
'created_at' => date('Y-m-d H:i:s'),
]);
}
/**
* 统计今日某操作人的记录数
*/
public function countTodayByOperator($operator, $stationType = null)
{
$conditions = [];
$params = [];
$conditions[] = 'operator = :operator';
$params[':operator'] = $operator;
$conditions[] = 'DATE(created_at) = CURDATE()';
if ($stationType) {
$conditions[] = 'station_type = :type';
$params[':type'] = $stationType;
}
$result = $this->where($conditions, $params)
->field('COUNT(*) as cnt')
->fetch();
return $result['cnt'] ?? 0;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 供应商模型
*/
class Supplier extends Model
{
protected $table = 'supplier';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function getActiveList()
{
return $this->where(['status = 1'])->order(['name ASC'])->fetchAll();
}
public function getById($id)
{
return $this->where(['id = :id'], [':id' => $id])->fetch();
}
public function search($keyword)
{
return $this->where(
['name LIKE :kw OR contact_person LIKE :kw2 OR phone LIKE :kw3'],
[':kw' => "%{$keyword}%", ':kw2' => "%{$keyword}%", ':kw3' => "%{$keyword}%"]
)->order(['id DESC'])->fetchAll();
}
}
+192
View File
@@ -0,0 +1,192 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 系统配置模型
*/
class SysConfig extends Model
{
protected $table = 'sys_config';
// 获取配置
public function getConfig()
{
return $this->where(['id = :id'], [':id' => 1])->fetch();
}
// 更新配置
public function updateConfig($data)
{
return $this->where(['id = :id'], [':id' => 1])->update($data);
}
/**
* 获取代码前缀规则
* 格式: ['成品' => 'CP', '电芯' => 'DX', 'PCBA' => 'XLB']
*/
public function getCodePrefixRules()
{
$config = $this->getConfig();
$raw = $config['code_prefix_rules'] ?? '';
if (empty($raw)) {
// 返回默认规则
$rules = $this->getDefaultCodePrefixRules();
} else {
$rules = json_decode($raw, true);
if (!is_array($rules)) {
$rules = $this->getDefaultCodePrefixRules();
}
}
// 产品类型别名对齐:确保同义类型能匹配到同一前缀规则
// 例如配置表中使用「电池」,而前缀规则可能使用旧名「电芯」
$aliases = [
'电池' => '电芯',
'电芯' => '电池',
'底壳' => '外壳',
'外壳' => '底壳',
'半成品' => '半成',
'半成' => '半成品',
];
foreach ($aliases as $from => $to) {
if (!isset($rules[$from]) && isset($rules[$to])) {
$rules[$from] = $rules[$to];
}
}
return $rules;
}
/**
* 获取默认代码前缀规则
*/
public function getDefaultCodePrefixRules()
{
return [
'成品' => 'CP',
'电池' => 'dc',
'PCBA' => 'xlb',
'半成品' => '',
'外壳' => '',
'包装' => '',
'配件' => '',
];
}
/**
* 根据产品类型获取代码前缀
* @param string $productType 产品类型(如:成品、电芯、PCBA)
* @return string 前缀,无规则返回空字符串
*/
public function getPrefixByType($productType)
{
$rules = $this->getCodePrefixRules();
$prefix = $rules[$productType] ?? '';
// 原则:所有工位序列号都受前缀管控。配置了该类型但前缀为空时,
// 自动回退为「产品类型名拼音首字母」,避免无前缀类型被放行或卡死。
if ($prefix === '' && isset($rules[$productType])) {
$prefix = $this->pinyinInitials($productType);
}
return $prefix;
}
/**
* 取得字符串的拼音首字母(中文取首字拼音首字母,英文/数字原样保留)。
* 兼容「外壳」「包装」「半成品」「配件」等常见类型,英文部分(如 PCBA)原样输出。
* @param string $str
* @return string
*/
public function pinyinInitials($str)
{
$str = trim($str);
if ($str === '') return '';
// 常见产品类型精确映射(避免多音字/误判)
$map = [
'外壳' => 'WK',
'底壳' => 'WK',
'包装' => 'BZ',
'半成品' => 'BCP',
'半成' => 'BCP',
'配件' => 'PJ',
'成品' => 'CP',
'电池' => 'DC',
'电芯' => 'DC',
'PCBA' => 'XLB',
];
if (isset($map[$str])) return $map[$str];
$first = mb_substr($str, 0, 1, 'UTF-8');
// 英文字母/数字:直接原样(大写)
if (preg_match('/^[A-Za-z0-9]$/', $first)) {
return strtoupper($first) . (mb_strlen($str, 'UTF-8') > 1 ? strtoupper(mb_substr($str, 1, 1, 'UTF-8')) : '');
}
// 中文:取首字拼音首字母
return $this->chineseInitial($first);
}
/**
* 单汉字 → 拼音首字母(GB2312 区位区间法,覆盖常用汉字)
*/
private function chineseInitial($char)
{
$py = 'A';
try {
$gbk = iconv('UTF-8', 'GB2312//IGNORE', $char);
} catch (\Throwable $e) {
$gbk = '';
}
if ($gbk === '' || strlen($gbk) < 2) {
return $py; // 生僻字/无法转换时回退
}
$hi = ord($gbk[0]);
$lo = ord($gbk[1]);
$code = $hi * 256 + $lo;
// GB2312 拼音首字母分区(常用字)
$ranges = [
[0xB0A1, 0xB0C4, 'A'], [0xB0C5, 0xB2C0, 'B'], [0xB2C1, 0xB4ED, 'C'],
[0xB4EE, 0xB6E9, 'D'], [0xB6EA, 0xB7A1, 'E'], [0xB7A2, 0xB8C0, 'F'],
[0xB8C1, 0xB9FD, 'G'], [0xB9FE, 0xBBF6, 'H'], [0xBBF7, 0xBFA5, 'J'],
[0xBFA6, 0xC0AB, 'K'], [0xC0AC, 0xC2E7, 'L'], [0xC2E8, 0xC4C2, 'M'],
[0xC4C3, 0xC5B5, 'N'], [0xC5B6, 0xC5BD, 'O'], [0xC5BE, 0xC6DA, 'P'],
[0xC6DB, 0xC8BA, 'Q'], [0xC8BB, 0xC8F5, 'R'], [0xC8F6, 0xCBF0, 'S'],
[0xCBF1, 0xCDDA, 'T'], [0xCDDB, 0xCEF3, 'W'], [0xCEF4, 0xD1B8, 'X'],
[0xD1B9, 0xD4D0, 'Y'], [0xD4D1, 0xD7F9, 'Z'],
];
foreach ($ranges as $r) {
if ($code >= $r[0] && $code <= $r[1]) { $py = $r[2]; break; }
}
return $py;
}
/**
* 获取所有已配置的非空前缀规则(用于验证)
* @return array [前缀 => 产品类型名]
*/
public function getActivePrefixMap()
{
$rules = $this->getCodePrefixRules();
$active = [];
foreach ($rules as $type => $prefix) {
if (!empty($prefix)) {
$active[$prefix] = $type;
}
}
return $active;
}
/**
* 更新代码前缀规则(JSON 存储)
* @param array $rules ['成品' => 'CP', '电池' => 'dc', ...]
*/
public function updateCodePrefixRules($rules)
{
$json = json_encode($rules, JSON_UNESCAPED_UNICODE);
// 使用原生 PDO 直写,避免 Model::update 链式调用在二次写入时的潜在问题
$pdo = \core\db\Db::pdo();
$table = $this->getTable();
$stmt = $pdo->prepare("UPDATE {$table} SET code_prefix_rules = :v WHERE id = 1");
$stmt->execute([':v' => $json]);
return true;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
/**
* @deprecated 2026-06-18 已废弃,请使用 StationDefinition 替代。
*/
namespace app\models;
use core\base\Model;
/**
* 入库模型
* @deprecated
*/
class WarehouseIn extends Model
{
protected $table = 'warehouse_in';
public function getAll()
{
return $this->order(['id DESC'])->fetchAll();
}
public function addRecord($data)
{
return $this->add($data);
}
public function findBySerial($finished_serial)
{
return $this->where(['finished_serial = :serial'], [':serial' => $finished_serial])->fetch();
}
// 根据产品型号筛选记录(warehouse_in 表无 product_type 列)
public function getByTypeAndModel($product_type, $product_model)
{
if (empty($product_model)) {
return [];
}
return $this->where(['product_model = :pm'], [':pm' => $product_model])->order(['id DESC'])->fetchAll();
}
// 统计当日某操作员的录入数量
public function countTodayByOperator($operator)
{
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable() . " WHERE operator = :op AND DATE(in_time) = CURDATE()";
$result = $this->query($sql, [':op' => $operator]);
return $result[0]['cnt'] ?? 0;
}
// 统计总记录数(可按型号筛选)
public function countTotal($product_type = '', $product_model = '')
{
if (!empty($product_model)) {
$result = $this->where(['product_model = :pm'], [':pm' => $product_model])->field('COUNT(*) as cnt')->fetch();
} else {
$sql = "SELECT COUNT(*) as cnt FROM " . $this->getTable();
$result = $this->query($sql);
}
return $result[0]['cnt'] ?? 0;
}
public function batchImport($records)
{
foreach ($records as $record) {
$this->add($record);
}
return true;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace app\models;
use core\base\Model;
/**
* 工位模型
*/
class Workstation extends Model
{
protected $table = 'workstation';
// 获取所有工位(按排序)
public function getAll()
{
return $this->order(['sort_order ASC'])->fetchAll();
}
// 添加工位
public function addStation($data)
{
return $this->add($data);
}
// 更新工位
public function updateStation($id, $data)
{
return $this->where(['id = :id'], [':id' => $id])->update($data);
}
// 删除工位
public function deleteStation($id)
{
return $this->delete($id);
}
// 更新排序
public function updateSort($id, $sort_order)
{
return $this->where(['id = :id'], [':id' => $id])->update(['sort_order' => $sort_order]);
}
// 根据类型获取工位
public function getByType($station_type)
{
return $this->where(['station_type = :type'], [':type' => $station_type])->fetch();
}
}