chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+266
View File
@@ -0,0 +1,266 @@
<?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);
}
}