177 lines
6.4 KiB
PHP
177 lines
6.4 KiB
PHP
<?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();
|
||
}
|
||
}
|