初始化
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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_columns(keyed 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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() 返回 bool,rowCount() 需要通过 pdo 获取
|
||||
if ($this->execute($insertSql, $params)) {
|
||||
$affected++;
|
||||
}
|
||||
}
|
||||
if ($affected > 0) {
|
||||
error_log('[StationDefinition] Upserted ' . $affected . ' built-in station definitions');
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[StationDefinition] insertBuiltinDefaults failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function autoCreateTable()
|
||||
{
|
||||
$fullTable = $this->getTable();
|
||||
try {
|
||||
// 检查表是否存在
|
||||
$exists = $this->query("SHOW TABLES LIKE '{$fullTable}'");
|
||||
if (empty($exists)) {
|
||||
// 创建表
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `{$fullTable}` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`station_type` VARCHAR(50) NOT NULL UNIQUE COMMENT '工位类型标识',
|
||||
`db_table` VARCHAR(100) NOT NULL COMMENT '数据表名(不含前缀)',
|
||||
`time_column` VARCHAR(50) NOT NULL DEFAULT 'created_at' COMMENT '时间列名',
|
||||
`has_product_type` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_type 列',
|
||||
`has_product_model` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否有 product_model 列',
|
||||
`filter_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用筛选',
|
||||
`show_today_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示今日计数',
|
||||
`show_total_count` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '显示总计数',
|
||||
`extra_config` TEXT COMMENT '额外配置(JSON)',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_station_type` (`station_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工位定义统一配置表'";
|
||||
$this->execute($sql);
|
||||
error_log('[StationDefinition] Auto-created table: ' . $fullTable);
|
||||
}
|
||||
|
||||
// 无论表是否刚创建,确保内置工位数据存在
|
||||
// (表可能已存在但数据不完整,例如之前创建失败只建了表没插入数据)
|
||||
$this->insertBuiltinDefaults();
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[StationDefinition] autoCreateTable failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存(数据更新后调用)
|
||||
*/
|
||||
public static function clearCache()
|
||||
{
|
||||
self::$cache = [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user