Files

267 lines
9.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\model;
use think\facade\Config;
use think\facade\Db;
use think\Model;
/**
* BaseModel 类
*
* 内聚了 SchemaGuard 数据库结构自愈引擎(纯通用能力,不含任何业务表清单)。
* 各模型在自己的 ensureSchema() 里调用本类静态方法确保「自己这张表」,
* 建表责任与触发点绑定到模型本身,核心库不再按业务域聚合自愈。
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BaseModel extends Model
{
protected $autoWriteTimestamp = 'int';
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
protected function getBaseOptions(): array
{
return [
'createTime' => 'create_at',
'updateTime' => 'update_at',
'dateFormat' => 'Y-m-d H:i:s',
];
}
/**
* 将扁平的父子结构数组转换为带层级缩进的树形下拉数据
* @param array $cate 数据集(含 id/pid 字段)
* @param string $name 用于展示的字段名
* @param string $lefthtml 层级缩进符号
* @param int $pid 父级 ID
* @param int $level 当前层级
* @return array
*/
public static function cateTree($cate, $name = 'title', $lefthtml = '|— ', $pid = 0, $level = 0)
{
$arr = [];
foreach ($cate as $v) {
if (($v['pid'] ?? 0) == $pid) {
$v['level'] = $level;
$v[$name] = str_repeat($lefthtml, $level) . ($v[$name] ?? '');
$arr[] = $v;
$arr = array_merge($arr, self::cateTree($cate, $name, $lefthtml, $v['id'], $level + 1));
}
}
return $arr;
}
public function __construct(array $data = [])
{
parent::__construct($data);
$this->applyOptions();
}
/**
* 将子类 getOptions() 返回的模型配置落实到 think\Model 属性,
* 使 name/strict/schema/autoWriteTimestamp/readonly 等真正生效(之前是死代码)。
*/
protected function applyOptions(): void
{
if (!method_exists($this, 'getOptions')) {
return;
}
foreach ($this->getOptions() as $key => $value) {
if ($value === null) {
continue;
}
switch ($key) {
case 'name':
// think-orm 4.0 语义:$name 是不带前缀的表名,
// 框架会根据 database.prefix 自动拼接完整表名。
// 之前误写成 $this->table(含前缀语义),导致不拼前缀、
// 模型查询裸名表而自愈建的是带前缀表,引发 1146。
$this->name = $value;
$this->table = null;
break;
case 'strict':
$this->strict = (bool)$value;
break;
case 'schema':
$this->schema = $value;
break;
case 'autoWriteTimestamp':
$this->autoWriteTimestamp = $value;
break;
case 'createTime':
$this->createTime = $value;
break;
case 'updateTime':
$this->updateTime = $value;
break;
case 'readonly':
$this->readonly = $value;
break;
case 'hidden':
$this->hidden = $value;
break;
case 'append':
$this->append = $value;
break;
case 'dateFormat':
$this->dateFormat = $value;
break;
case 'deleteTime':
$this->deleteTime = $value;
break;
}
}
}
protected $tenantField = 'tenant_id';
// 自动添加 tenant_id 到查询和保存
public static function onAfterRead($model)
{
$user = request()->auth ?? null;
if ($user && $model->hasField('tenant_id')) {
if ($model->tenant_id != $user['tenant_id']) {
abort(403, '无权访问此数据');
}
}
}
public static function onBeforeWrite($model)
{
$user = request()->auth ?? null;
if ($user && $model->hasField('tenant_id') && ! $model->tenant_id) {
$model->tenant_id = $user['tenant_id'];
}
}
/* ---------------------------------------------------------------------
* 数据库结构自愈引擎(原 SchemaGuard,已内聚到 BaseModel
* 唯一事实源是 public/install/install.sql;模型通过 ensureTableFromInstall()
* 从中提取 DDL 建表,避免 DDL 漂移。
* ------------------------------------------------------------------- */
/**
* 表是否存在(已带前缀的完整表名)
*/
public static function tableExists(string $table): bool
{
try {
return !empty(Db::query("SHOW TABLES LIKE '{$table}'"));
} catch (\Throwable $e) {
return false;
}
}
/**
* 表不存在则创建(已带前缀的完整表名 + 完整 CREATE SQL
*/
public static function ensureTable(string $table, string $sql): void
{
try {
if (!self::tableExists($table)) {
Db::execute($sql);
}
} catch (\Throwable $e) {
// 忽略(如权限不足),由后续业务报错暴露
}
}
/**
* 确保 id 列为自增主键(老库修复:id 定义成 NOT NULL 但无 PRIMARY KEY/AUTO_INCREMENT
* 时,模型 create() 不带 id 会报 1364 Field 'id' doesn't have a default value)。
* @param string $table 已带前缀的完整表名
* @param string $column 主键列名,默认 id
*/
public static function ensureAutoIncrementPk(string $table, string $column = 'id'): void
{
try {
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'");
if (empty($cols)) {
return;
}
$col = $cols[0];
$extra = strtolower((string)($col['Extra'] ?? ''));
$key = strtoupper((string)($col['Key'] ?? ''));
if (strpos($extra, 'auto_increment') !== false) {
return; // 已是自增
}
$type = (string)($col['Type'] ?? 'int unsigned');
if ($key !== 'PRI') {
// 无主键:一并加主键 + 自增
Db::execute("ALTER TABLE `{$table}` MODIFY `{$column}` {$type} NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`{$column}`)");
} else {
Db::execute("ALTER TABLE `{$table}` MODIFY `{$column}` {$type} NOT NULL AUTO_INCREMENT");
}
} catch (\Throwable $e) {
// 忽略(如权限不足),由后续业务报错暴露
}
}
/**
* 列不存在则追加(MySQL 不支持 ADD COLUMN IF NOT EXISTS,故先探测)
* @param string $table 已带前缀的完整表名
*/
public static function ensureColumn(string $table, string $column, string $def): void
{
try {
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$column}'");
if (empty($cols)) {
Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$def}");
}
} catch (\Throwable $e) {
// 自愈失败不应静默吞掉,记录日志便于排查(如 ALTER 权限不足)
try {
\think\facade\Log::error("[BaseModel] ensureColumn failed: {$table}.{$column} - " . $e->getMessage());
} catch (\Throwable $e2) {
// 日志也失败则彻底忽略
}
}
}
/**
* 取得运行时表前缀(CLI / 多应用路由下 Db::getConfig 可能为空,需兜底到配置)。
* 模型壳调用 ensureTableFromInstall() 时统一使用本方法取前缀,避免修错无前缀表。
*/
public static function currentPrefix(): string
{
$prefix = Config::get('database.connections.mysql.prefix', '');
if ($prefix === '') {
$prefix = Db::getConfig('prefix') ?: '';
}
return $prefix;
}
/**
* 从 install.sql 提取指定表的 CREATE TABLE 语句并执行建表(单一事实源)。
* @param string $prefix 运行时表前缀
* @param string $table 不含前缀的表名(如 backend / member_profile
*/
public static function ensureTableFromInstall(string $prefix, string $table): void
{
$p = $prefix;
$sqlFile = root_path() . 'public/install/install.sql';
if (!is_file($sqlFile)) {
return;
}
$content = file_get_contents($sqlFile);
$pattern = '/CREATE TABLE IF NOT EXISTS `__PREFIX__' . preg_quote($table, '/') . '`\s*\(.*?\)\s*ENGINE=[^;]*;/s';
if (!preg_match($pattern, $content, $m)) {
return;
}
$ddl = str_replace('__PREFIX__', $p, $m[0]);
self::ensureTable($p . $table, $ddl);
}
}