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
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace ywxapp\model;
use think\facade\Db;
use ywxapp\model\BaseModel;
class Ad extends BaseModel
{
protected $name = 'ad';
// 广告类型
const TYPE_IMAGE = 1; // 图片
const TYPE_TEXT = 2; // 文字
const TYPE_CODE = 3; // 代码
// 广告位标识(预埋位:已挂模板的 4 个 + 预留候选,随时可在前台 include 使用)
const POS_HOME_TOP = 'home_top'; // 首页顶部横幅(已挂)
const POS_HOME_SIDE = 'home_side'; // 首页侧边(已挂)
const POS_POPUP = 'popup'; // 全站弹窗(已挂)
const POS_LIST_BOTTOM = 'list_bottom'; // 列表底部(已挂)
// —— 以下为预埋预留位,前台模板尚未挂载,需要时在对应页面加:
// {assign name="slot" value="home_bottom" /}{include file="common/adslot" /}
const POS_HOME_BOTTOM = 'home_bottom'; // 首页底部
const POS_CONTENT_TOP = 'content_top'; // 内容详情页顶部
const POS_CONTENT_BOTTOM = 'content_bottom'; // 内容详情页底部
const POS_SIDEBAR = 'sidebar'; // 通用侧边栏
const POS_FLOAT = 'float'; // 全站悬浮角标
public function getOptions(): array
{
return [
'name' => 'ad',
'strict' => true,
'autoWriteTimestamp' => true,
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
];
}
protected $type = [
'id' => 'integer',
'type' => 'integer',
'sort' => 'integer',
'status' => 'integer',
'create_at' => 'integer',
'update_at' => 'integer',
'delete_at' => 'integer',
];
protected $readonly = [];
public static function typeList(): array
{
return [
self::TYPE_IMAGE => '图片',
self::TYPE_TEXT => '文字',
self::TYPE_CODE => '代码',
];
}
public static function positionList(): array
{
return [
self::POS_HOME_TOP => '首页顶部横幅',
self::POS_HOME_SIDE => '首页侧边',
self::POS_POPUP => '全站弹窗',
self::POS_LIST_BOTTOM => '列表底部',
self::POS_HOME_BOTTOM => '首页底部',
self::POS_CONTENT_TOP => '内容页顶部',
self::POS_CONTENT_BOTTOM => '内容页底部',
self::POS_SIDEBAR => '通用侧边栏',
self::POS_FLOAT => '全站悬浮角标',
];
}
/**
* 运行时自愈:确保 ad 表存在 position 列,避免老库缺列导致列表/取数报错
*/
public static function ensureSchema(): void
{
try {
$p = self::currentPrefix();
$prefix = $p;
$table = $prefix . 'ad';
// 缺表建表(install.sql L638 同款 DDL,运行时前缀兜底)
BaseModel::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$p}ad` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '广告标题',
`type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型 1图片2文字3代码',
`position` varchar(50) NOT NULL DEFAULT '' COMMENT '广告位标识 home_top首页顶部 home_side首页侧边 popup全站弹窗 list_bottom列表底部',
`content` text COMMENT '广告内容/代码',
`url` varchar(255) NOT NULL DEFAULT '' COMMENT '跳转链接',
`image` varchar(255) NOT NULL DEFAULT '' COMMENT '图片地址',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态',
`create_at` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
`update_at` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间',
`delete_at` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_position` (`position`),
KEY `idx_status_sort` (`status`,`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站点广告'");
// 老库扩展列兜底(幂等)
BaseModel::ensureColumn($table, 'position', "varchar(50) NOT NULL DEFAULT '' COMMENT '广告位标识'");
} catch (\Throwable $e) {
// 自愈失败不影响主流程
}
}
/**
* 按广告位取启用广告(按 sort 升序)
* @param string $position 广告位标识
* @return array
*/
public static function getByPosition(string $position): array
{
self::ensureSchema();
$rows = Db::name('ad')
->where('status', 1)
->where('position', $position)
->where('delete_at', 0)
->order('sort', 'asc')
->order('id', 'desc')
->select()
->toArray();
$typeList = self::typeList();
foreach ($rows as &$row) {
$row['type_text'] = $typeList[$row['type']] ?? '图片';
}
return $rows;
}
/**
* 取全部启用广告,按 position 分组,便于前台模板随处调用
* @return array [position => [ad, ...]]
*/
public static function getSlots(): array
{
self::ensureSchema();
$positions = array_keys(self::positionList());
$slots = array_fill_keys($positions, []);
$rows = Db::name('ad')
->where('status', 1)
->where('position', 'in', $positions)
->where('delete_at', 0)
->order('sort', 'asc')
->order('id', 'desc')
->select()
->toArray();
$typeList = self::typeList();
foreach ($rows as $row) {
$row['type_text'] = $typeList[$row['type']] ?? '图片';
$slots[$row['position']][] = $row;
}
return $slots;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
class AddonModel extends BaseModel
{
// -- 插件主表
// CREATE TABLE `addon` (
// `id` int unsigned AUTO_INCREMENT PRIMARY KEY,
// `name` varchar(50) NOT NULL COMMENT '标识符',
// `title` varchar(100) NOT NULL COMMENT '名称',
// `version` varchar(20) NOT NULL COMMENT '版本号',
// `price` decimal(10,2) NOT NULL COMMENT '价格',
// `file_path` varchar(255) NOT NULL COMMENT '存储路径(非公开)',
// `file_hash` varchar(64) NOT NULL COMMENT 'SHA256校验值',
// `status` tinyint DEFAULT 1 COMMENT '1上架 0下架'
// );
protected function getOptions(): array
{
return [
'strict' => true,
'name' => 'addon',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'schema' => [
'id' => 'int',
'name' => 'string',
'title' => 'string',
'version' => 'string',
'price' => 'float',
'description' => 'string',
'author' => 'string',
'status' => 'tinyint',
'config' => 'text',
'file_path' => 'string',
'file_hash' => 'string',
'sort' => 'int',
'create_at' => 'int',
'update_at' => 'int',
]
];
}
// // 获取已安装插件列表
// public function getInstalledaddon(): array
// {
// return $this->where('status', '<>', self::STATUS_DISABLE)->order('create_at', 'desc')->select()->toArray();
// }
// // 检查插件是否已安装
// public function isInstalled(string $name): bool
// {
// return $this->where('name', $name)->count() > 0;
// }
}
+142
View File
@@ -0,0 +1,142 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace ywxapp\model;
use think\Model;
class Attachment extends BaseModel
{
// 设置字段信息
protected $schema = [
//文件ID
'id' => 'int',
//所属用户ID
'uid' => 'int',
//所属模块 (admin, index, api)
'module' => 'string',
//文件相对路径 (如: 2026/01/21/filename.jpg)
'path' => 'string',
//文件访问URL (全路径)
'url' => 'string',
//原始文件名
'original' => 'string',
//存储文件名 (不含路径)
'name' => 'string',
//文件大小 (字节)
'size' => 'int',
//文件后缀
'ext' => 'string',
//MIME类型
'mime' => 'string',
//存储引擎 (local, alioss, qcos, qiniu)
'storage' => 'string',
//驱动特定信息 (如OSS的ETag, Bucket等)
'driver_info' => 'json',
//是否为图片
'is_image' => 'bool',
//图片宽度
'width' => 'int',
//图片高度
'height' => 'int',
//上传者IP
'upload_ip' => 'string',
//创建时间
'create_at' => 'int',
//更新时间
'update_at' => 'int',
];
// 自动写入时间戳
protected $autoWriteTimestamp = 'int';
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
// 隐藏字段
protected $hidden = ['upload_ip', 'driver_info'];
/**
* 关联用户模型(如果存在)
* @return \think\model\relation\BelongsTo
*/
public function user()
{
return $this->belongsTo(MemberUser::class, 'uid', 'uid');
}
// /**
// * 获取完整URL(如果数据库里的URL是相对路径,自动补全域名)
// * @param string $value
// * @return string
// */
// public function getUrlAttr($value)
// {
// if ($value && ! str_starts_with($value, 'http')) {
// return Request::domain() . '/' . ltrim($value, '/');
// }
// return $value;
// }
/**
* 运行时自愈:确保 attachment 主表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'attachment');
}
/**
* 初始化:自愈建表,避免远程库缺失 wxapp_attachment 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
/**
* 格式化文件大小
* @return string
*/
public function getFormattedSizeAttr(): string
{
$bytes = $this->getData('file_size');
if ($bytes >= 1024 * 1024 * 1024) {
return number_format($bytes / (1024 * 1024 * 1024), 2) . ' GB';
} elseif ($bytes >= 1024 * 1024) {
return number_format($bytes / (1024 * 1024), 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' B';
}
}
/**
* 设置驱动信息(自动JSON编码)
* @param mixed $value
* @return void
*/
public function setDriverInfoAttr($value)
{
$this->setAttr('driver_info', json_encode($value, JSON_UNESCAPED_UNICODE));
}
/**
* 获取驱动信息(自动JSON解码)
* @param string $value
* @return array
*/
public function getDriverInfoAttr($value)
{
return $value ? json_decode($value, true) : [];
}
}
+309
View File
@@ -0,0 +1,309 @@
<?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\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* Backend 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendAdmin extends BaseModel
{
use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'backend_admin',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['status_text', 'last_login'],
'hidden' => ['password', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 管理员角色关联
*/
public function roles()
{
return $this->belongsToMany(BackendRole::class, BackendRoleAccess::class, 'role_id', 'admin_id');
}
// 密码字段自动加密
public function setPasswordAttr($value)
{
// 使用PHP内置的password_hash,无需salt字段
if (! password_get_info($value)['algo']) {
// 如果不是已哈希的密码,则进行哈希处理
return password_hash($value, PASSWORD_DEFAULT);
}
return $value;
}
// 验证密码
public function checkPassword($password)
{
return password_verify($password, $this->password);
}
// 获取状态文本
public function getStatusTextAttr()
{
$statusMap = [0 => '禁用', 1 => '正常', 2 => '锁定'];
return $statusMap[$this->status] ?? '未知';
}
// 获取最后登录描述
public function getLastLoginAttr()
{
if (empty($this->login_time)) {
return '从未登录';
}
$diff = time() - strtotime($this->login_time);
if ($diff < 60) {
return '刚刚';
} elseif ($diff < 3600) {
return floor($diff / 60) . '分钟前';
} elseif ($diff < 86400) {
return floor($diff / 3600) . '小时前';
} else {
return date('Y-m-d', strtotime($this->login_time));
}
}
// 检查账户是否被锁定
public function isLocked()
{
if ($this->status == 0) {
return true; // 已禁用
}
if ($this->fail_count >= 5 && ! empty($this->lock_time)) {
$lockUntil = strtotime($this->lock_time) + 1800; // 锁定30分钟
return time() < $lockUntil;
}
return false;
}
// 重置密码
public function resetPassword($newPassword)
{
$this->password = $newPassword; // 会触发setPasswordAttr自动加密
$this->need_reset = 0; // 标记无需重置
return $this->save();
}
// 记录登录失败(累计失败次数,超过阈值锁定)
public function recordLoginFail($ip)
{
$this->fail_count += 1;
if ($this->fail_count >= 5) {
$this->lock_time = date('Y-m-d H:i:s');
}
$this->save();
}
// 记录登录成功(重置失败计数与锁定)
public function recordLoginSuccess()
{
$this->login_time = time();
$this->fail_count = 0;
$this->lock_time = null;
$this->save();
}
// public function getAvatarAttr($value, $data)
// {
// return '/static/common/images/avatar.jpg';
// }
/**
* 获取用户有权访问的菜单树
*/
public function getAccessibleMenus()
{
$roleIds = $this->roles->column('id');
$roleNames = $this->roles->column('name');
// 超级管理员:直接可见全部后台菜单(与 getPermissionNames 的 * 语义保持一致)
$superRoleId = config('ywxapp.superAdmin', 1);
if (in_array('superadmin', $roleNames, true) || in_array($superRoleId, $roleIds, true)) {
$flatMenus = BackendPower::field('id,title,name,pid,sort,route,icon,type,addon')
->where('type', '<', '3')
->order('sort', 'asc')
->select()
->toArray();
foreach ($flatMenus as &$menu) {
// 依据权限表 addon 字段判定:空=核心后台菜单(拼 /admin/ 前缀),非空=插件菜单(完整路径直出)。
$menu['route'] = $this->buildMenuUrl($menu['route'], $menu['addon'] ?? null);
}
return BackendPower::buildTree($flatMenus);
}
if (empty($roleIds)) {
return [];
}
$permissions = BackendRolePower::where('role_id', 'in', $roleIds)
->distinct(true)
->column('power_id');
if (empty($permissions)) {
return [];
}
$flatMenus = BackendPower::field('id,title,name,pid,sort,route,icon,type,addon')
->whereIn('id', array_values($permissions))
->where('type', '<', '3') // 只获取菜单权限
->order('sort', 'asc')
->select()
->toArray();
foreach ($flatMenus as &$menu) {
// 依据权限表 addon 字段判定:空=核心后台菜单(直接拼 URL),非空=插件菜单(走 url 反转)。
$menu['route'] = $this->buildMenuUrl($menu['route'], $menu['addon'] ?? null);
}
return BackendPower::buildTree($flatMenus);
}
/**
* 获取用户所有权限标识(字符串数组)
*/
public function getAllPermissions(): array
{
$roleIds = $this->roles->column('id');
if (empty($roleIds)) {
return [];
}
return BackendRolePower::where('role_id', 'in', $roleIds)
->distinct(true)
->column('power_id');
}
/**
* 获取用户的所有权限(通过角色)
*/
public function permissions()
{
return $this->belongsToMany(BackendPower::class, BackendRolePower::class, 'power_id', 'role_id')->via('roles'); // 通过 roles 关联自动关联权限
}
/**
* 获取权限标识数组(name)
* 通过 角色→权限 关联可靠地取出当前管理员拥有的权限 key
*/
public function getPermissionNames()
{
$roleIds = $this->roles->column('id');
if (empty($roleIds)) {
return [];
}
// 拥有超级管理员角色:权限恒为 *(全部权限),校验/展示均以 * 表示
$superRoleId = config('ywxapp.superAdmin', 1);
$roleNames = $this->roles->column('name');
if (in_array('superadmin', $roleNames, true) || in_array($superRoleId, $roleIds, true)) {
return ['*'];
}
$powerIds = BackendRolePower::where('role_id', 'in', $roleIds)
->distinct(true)
->column('power_id');
if (empty($powerIds)) {
return [];
}
return BackendPower::whereIn('id', $powerIds)->column('name');
}
/**
* 检查是否有某权限
*/
public function can($permissionName)
{
$names = $this->getPermissionNames();
// 权限集合含 * 表示拥有全部权限
return in_array('*', $names, true) || in_array($permissionName, $names);
}
/**
* 菜单 URL 生成
*
* 依据权限表 addon 字段判定归属:
* - addon 为空:核心后台应用内菜单,直接按 app_map 反查 backend 应用前缀拼接
* `/{prefix}/{route}.html`,不调用 url() 反转,彻底规避插件路由抢注与
* IP/域名访问差异导致的生成异常。
* - addon 非空:插件后台菜单,走 url() 反转(全限定 `addon/backend/...`
* 不会被插件路由同名规则抢注)。
* - 外部链接(http://、https://、//)原样返回;空 route 返回 #。
*
* @param string|null $route 菜单路由(如 links/index、framework/index、haonav/backend/links
* @param string|null $addon 权限表 addon 字段(核心菜单为 null/空,插件菜单为插件名)
*/
protected function buildMenuUrl(?string $route, ?string $addon = null): string
{
if (! $route) {
return '#';
}
if (preg_match('#^(https?://|//)#i', $route)) {
return $route;
}
$route = ltrim($route, '/');
// 插件菜单:route 字段已是完整可访问路径(如 /appmall/backend/developer/index
// 与插件 menu.json 约定一致),直接拼后缀输出即可。
// 切勿用 url() 反转:url() 会将该字符串当成 MVC 控制器地址解析,导致被误映射到
// 核心后台 /admin/ 前缀(如 /admin/developer/index.html),而非插件真实路径。
if (! empty($addon)) {
$suffix = config('route.url_html_suffix');
$u = '/' . $route;
if ($suffix && $suffix !== '' && ! preg_match('/\.' . preg_quote(ltrim($suffix, '.'), '/') . '$/i', $u)) {
$u .= '.' . ltrim($suffix, '.');
}
return $u;
}
// 核心后台菜单:按 app_map 反查 backend 应用前缀,直接拼接,避免 url() 反转歧义
$appMap = (array) config('app.app_map');
$appPrefix = array_search('backend', $appMap, true);
if (! $appPrefix) {
$appPrefix = 'admin';
}
return '/' . $appPrefix . '/' . $route . '.html';
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
foreach (['backend_admin', 'backend_log', 'backend_power', 'backend_role', 'backend_role_access', 'backend_role_power'] as $t) {
BaseModel::ensureTableFromInstall($prefix, $t);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
<?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 ywxapp\model\BaseModel;
/**
* BackendLog 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendLog extends BaseModel{
protected $name = 'backend_log';
protected $updateTime = false;
/**
* 获取用户的角色
*
* 此方法定义了用户与角色之间的多对多关系它解释了用户可以拥有多个角色,
* 同时一个角色也可以被多个用户共享这种关系通过中间表'user_role'来维护,
* 其中'user_id'关联用户的ID'role_id'关联角色的ID
*
* @return \think\model\relation\BelongsToMany
* 返回一个BelongsToMany实例,用于表示多对多的Eloquent关系
*/
public function roles()
{
return $this->belongsToMany(Role::class, 'access', 'rid','aid');
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'backend_log');
}
}
+165
View File
@@ -0,0 +1,165 @@
<?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\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* BackendPower 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendPower extends BaseModel
{
use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'backend_power',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
public static function onAfterRead($data)
{
// 这里可以直接使用$this访问当前模型
// if ($data->type == 2) {
// $data->append(['href', 'openType']);
// $data->openType = '_iframe';
// $data->href = $data->route;
// }
}
public static function onAfterDelete($data)
{
BackendRolePower::where('power_id', $data->id)->delete();
}
/**
* 获取器:获取状态文本
*/
public function getTargetAttr($value, $data)
{
return '_self';
}
// 自关联子菜单
public function children()
{
return $this->hasMany(BackendPower::class, 'pid', 'id')->order('sort', 'asc');
}
// 拥有该权限的角色
public function roles()
{
return $this->belongsToMany(BackendRole::class, BackendRolePower::class, 'role_id', 'power_id');
}
// 拥有该权限的用户(通过角色)
public function admins()
{
return $this->belongsToMany(BackendAdmin::class, BackendRoleAccess::class, 'admin_id', 'role_id')->via('roles');
}
/**
* 将扁平菜单数组转为树形结构
* @param array $menus 扁平菜单列表(每个元素是数组)
* @param int $parentId 父ID(默认0表示根)
* @return array 树形结构
*/
public static function buildTree(array $menus, int $parentId = 0): array
{
$branch = [];
foreach ($menus as $menu) {
if ($menu['pid'] == $parentId) {
$children = self::buildTree($menus, $menu['id']);
if (! empty($children)) {
$menu['child'] = $children;
}
$branch[] = $menu;
}
}
return $branch;
}
/**
* 【可选】从 Collection 转为树(如果你用模型查询)
*/
public static function buildTreeFromCollection(Collection $collection, int $parentId = 0): array
{
$menus = $collection->toArray();
return self::buildTree($menus, $parentId);
}
/**
* 获取权限树形结构
* @param int $parentId 父ID(默认0表示根)
* @return array 树形结构
*/
public static function getTree($parentId = 0)
{
$list = self::where('pid', $parentId)
->order('sort', 'asc')
->select()
->toArray();
foreach ($list as &$item) {
$item['children'] = self::getTree($item['id']);
}
return $list;
}
/**
* 无限分类-权限
* @param array $cate 栏目
* @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'] == $pid) {
$v['level'] = $level + 1;
$v['lefthtml'] = str_repeat($lefthtml, $level);
$v['l' . $name] = $v['lefthtml'] . lang($v[$name]);
$arr[] = $v;
$arr = array_merge($arr, self::cateTree($cate, $name, $lefthtml, $v['id'], $level + 1));
}
}
return $arr;
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'backend_power');
}
}
+69
View File
@@ -0,0 +1,69 @@
<?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\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* BackendRole 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendRole extends BaseModel
{
use SoftDelete;
// 设置表名(think-orm 4.0name 不带前缀,自动拼 wxapp_
protected $name = 'backend_role';
// 设置主键
protected $pk = 'id';
// 自动时间戳
protected $autoWriteTimestamp = 'int';
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
protected $deleteTime = 'delete_at';
// 隐藏字段
protected $hidden = ['password', 'delete_at'];
// 只读字段
protected $readonly = ['id'];
// 角色拥有的用户
public function admins()
{
return $this->belongsToMany(BackendAdmin::class, BackendRoleAccess::class, 'admin_id', 'role_id');
}
/**
* 角色 → 权限字符串(通过 role_permissions 表)
*/
public function powers()
{
// 返回的是 permission 字符串列表(不是 Menu 模型)
return $this->belongsToMany(BackendPower::class, BackendRolePower::class, 'power_id', 'role_id')->field('power_id');
}
// 【推荐】角色 → 菜单模型(通过中间表 role_permissions 关联 menus
public function menus()
{
return $this->belongsToMany(BackendPower::class, BackendRolePower::class, 'power_id', 'role_id', 'permission');
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'backend_role');
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-06 22:05:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-10 09:44:50
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\model\BackendRoleAccess.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
// +----------------------------------------------------------------------
// | 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\model\Pivot;
use ywxapp\model\BaseModel;
/**
* BackendRoleAccess 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendRoleAccess extends Pivot{
protected $name = 'backend_role_access';
// 自动时间戳 (create_at 已统一为 int 时间戳)
protected $autoWriteTimestamp = 'int';
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'backend_role_access');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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\Model;
use ywxapp\model\BaseModel;
use think\model\Pivot;
/**
* BackendRolePower 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendRolePower extends Pivot
{
// 设置表名
protected $name = 'backend_role_power';
protected $pk = ['role_id', 'power_id'];
// 自动时间戳
protected $autoWriteTimestamp = 'int';
protected $createTime = 'create_at';
protected $updateTime = false;
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'backend_role_power');
}
}
+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);
}
}
+165
View File
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\facade\Db;
use think\model\concern\SoftDelete;
/**
* 充值卡密
*/
class Card extends BaseModel
{
use SoftDelete;
protected $name = 'card';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
// 卡密状态
const STATUS_UNSOLD = 0; // 未售
const STATUS_SOLD = 1; // 已售
const STATUS_USED = 2; // 已用(已兑换)
/**
* 运行时自愈:确保 card 主表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'card');
}
/**
* 初始化:自愈建表,避免远程库缺失 wxapp_card 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
/**
* 兑换卡密(会员用卡号+密码充值余额)
*
* @param int $uid 会员ID
* @param string $cardno 卡号
* @param string $password 密码
* @return array ['success'=>bool,'msg'=>string,'data'=>array]
*/
public static function redeem(int $uid, string $cardno, string $password): array
{
if ($uid <= 0 || $cardno === '' || $password === '') {
return ['success' => false, 'msg' => '参数不完整'];
}
$card = self::where('cardno', $cardno)->find();
if (empty($card)) {
return ['success' => false, 'msg' => '卡密不存在'];
}
if ($card->delete_at > 0) {
return ['success' => false, 'msg' => '卡密已失效'];
}
if ((int)$card->status === self::STATUS_USED) {
return ['success' => false, 'msg' => '该卡密已被使用'];
}
// 密码校验(存储若为明文,按需改为 password_verify
if ((string)$card->password !== (string)$password) {
return ['success' => false, 'msg' => '卡号或密码错误'];
}
$amount = (float)$card->amount;
if ($amount <= 0) {
return ['success' => false, 'msg' => '卡密面值异常'];
}
Db::startTrans();
try {
// 1) 卡密标记已用,绑定会员
$card->status = self::STATUS_USED;
$card->use_time = time();
$card->uid = $uid;
$card->save();
// 2) 会员钱包加余额 + 累计充值(无钱包则自动创建)
$wallet = Db::name('member_wallets')->where('uid', $uid)->find();
if (empty($wallet)) {
Db::name('member_wallets')->insert([
'uid' => $uid,
'balance' => $amount,
'total_recharge' => $amount,
'create_at' => time(),
'update_at' => time(),
]);
} else {
Db::name('member_wallets')
->where('uid', $uid)
->inc('balance', $amount)
->inc('total_recharge', $amount)
->update(['update_at' => time()]);
}
// 3) 充值流水
Db::name('member_bill')->insert([
'uid' => $uid,
'type' => 1, // 充值
'amount' => $amount,
'currency' => 1, // 人民币
'channel' => 'card',
'order_no' => 'CARD' . date('YmdHis') . $uid . mt_rand(100, 999),
'status' => 1, // 成功
'description' => '卡密充值:' . $cardno,
'create_at' => time(),
'update_at' => time(),
]);
Db::commit();
return [
'success' => true,
'msg' => '兑换成功,已充值 ¥' . number_format($amount, 2),
'data' => ['amount' => $amount],
];
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
}
/**
* 批量生成卡密
*
* @param int $count 生成数量
* @param float $amount 面值
* @param string $prefix 卡号前缀(如 YX
* @return array 生成的卡号列表
*/
public static function generateBatch(int $count, float $amount, string $prefix = ''): array
{
$count = max(1, min(200, $count)); // 单次上限保护
$batchNo = date('YmdHis') . mt_rand(1000, 9999);
$list = [];
$rows = [];
for ($i = 0; $i < $count; $i++) {
$cardno = ($prefix ?: 'YX') . strtoupper(substr(md5(uniqid((string)mt_rand(), true)), 0, 16));
$password = strtoupper(substr(md5(uniqid((string)mt_rand(), true)), 0, 8));
$list[] = ['cardno' => $cardno, 'password' => $password];
$rows[] = [
'cardno' => $cardno,
'password' => $password,
'amount' => $amount,
'status' => self::STATUS_UNSOLD,
'batch_no' => $batchNo,
'create_at' => time(),
'update_at' => time(),
];
}
self::insertAll($rows);
return $list;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?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;
/**
* 聊天
*/
class Chat extends BaseModel
{
// 设置表名(think-orm 4.0name 不带前缀,自动拼 wxapp_
protected $name = 'chat';
protected $schema = [
'id' => 'int',
'from_uid' => 'int',
'to_uid' => 'int',
'type' => 'int',
'content' => 'string',
'create_at' => 'int',
'update_at' => 'int',
];
}
// CREATE TABLE `chat_users` (
// `id` int(11) NOT NULL AUTO_INCREMENT,
// `username` varchar(50) NOT NULL,
// `password` varchar(255) NOT NULL,
// `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
// PRIMARY KEY (`id`)
// );
// CREATE TABLE `chat_messages` (
// `id` int(11) NOT NULL AUTO_INCREMENT,
// `from_user_id` int(11) NOT NULL,
// `to_user_id` int(11) NOT NULL,
// `message` text NOT NULL,
// `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
// PRIMARY KEY (`id`)
// );
+18
View File
@@ -0,0 +1,18 @@
<?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;
/**
* 聊天消息
*/
class ChatMessage extends \ywxapp\BaseController
{
}
+47
View File
@@ -0,0 +1,47 @@
<?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;
/**
* Configure 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Configure extends BaseModel
{
public function getTitleAttr($value)
{
return lang($value);
}
public function getGroupAttr($value)
{
return lang($value);
}
/**
* 运行时自愈:确保 configure 主表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'configure');
}
/**
* 初始化:自愈建表,避免远程库缺失 wxapp_configure 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
}
+21
View File
@@ -0,0 +1,21 @@
<?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;
/**
* Email 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Email extends BaseModel{
// 设置表名(think-orm 4.0name 不带前缀,自动拼 wxapp_
protected $name = 'email';
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\model\concern\SoftDelete;
/**
* 站点帮助
*/
class Help extends BaseModel
{
use SoftDelete;
protected $name = 'help';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
/**
* 运行时自愈:确保 help 主表存在(install.sql 已含 category/view_count 等列,为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'help');
}
/**
* 模型初始化时自愈 help 表结构(category / view_count 等扩展列由 install.sql 统一提供)。
* 集中在此处,前后台读写共用同一模型,避免各自漏调自愈导致 1054 缺列。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace ywxapp\model;
use think\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
class Links extends BaseModel
{
use SoftDelete;
// 软删除时间字段(与 autoWriteTimestamp=int 保持一致,类型为 int
protected $deleteTime = 'delete_at';
/**
* 模型配置
* @return array
*/
protected function getOptions(): array
{
return [
'strict' => true,
'name' => 'links',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
];
}
/**
* 运行时自愈:确保 links 主表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'links');
}
/**
* 初始化:自愈建表,避免远程库缺失 wxapp_links 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
/**
* 获取器:状态文本
*/
public function getStatusTextAttr($value, $data)
{
$status = [0 => '禁用', 1 => '启用'];
return $status[$data['status']] ?? '未知';
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\facade\Db;
use think\model\concern\SoftDelete;
/**
* 勋章中心
*/
class Medal extends BaseModel
{
use SoftDelete;
protected $name = 'medal';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
/**
* 控制器初始化(模型实例方法,非静态)
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
/**
* 授予勋章(幂等:同一用户同一勋章仅记录一次)
* @return array [bool $ok, string $msg]
*/
public static function grant(int $uid, int $medalId): array
{
self::ensureSchema(); // 确保 user_medal 关联表已就绪(静态入口也可能在未实例化模型时被调用)
if ($uid <= 0) {
return [false, '用户未登录'];
}
$medal = self::where('id', $medalId)->where('status', 1)->find();
if (!$medal) {
return [false, '勋章不存在或未启用'];
}
$exists = Db::name('member_medal')
->where('uid', $uid)
->where('medal_id', $medalId)
->find();
if ($exists) {
return [true, '已拥有该勋章'];
}
Db::name('member_medal')->insert([
'uid' => $uid,
'medal_id' => $medalId,
'create_at' => time(),
]);
return [true, '恭喜获得勋章:' . $medal->title];
}
/**
* 取某用户拥有的勋章列表(含勋章信息)
*/
public static function getUserMedals(int $uid): array
{
self::ensureSchema(); // 确保 user_medal 关联表已就绪(静态入口也可能在未实例化模型时被调用)
if ($uid <= 0) {
return [];
}
return Db::name('member_medal')
->alias('um')
->join('medal m', 'm.id = um.medal_id')
->where('um.uid', $uid)
->where('m.delete_at', 0)
->field('m.id,m.title,m.image,m.description,um.create_at')
->order('um.create_at', 'desc')
->select()
->toArray();
}
/**
* 表自愈:确保勋章相关表(medal / member_medal)已就绪。
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'medal');
BaseModel::ensureTableFromInstall($prefix, 'member_medal');
}
}
+62
View File
@@ -0,0 +1,62 @@
<?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\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* MemberGroup 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class MemberGroup extends BaseModel
{
use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_group',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => [],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
// 角色拥有的用户
public function users()
{
return $this->belongsToMany(MemberUser::class, MemberGroupAccess::class, 'uid', 'gid');
}
// 角色拥有的权限
public function rules()
{
return $this->belongsToMany(MemberRule::class, GroupRule::class, 'rid', 'gid');
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_group');
}
}
+47
View File
@@ -0,0 +1,47 @@
<?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\model\Pivot;
use ywxapp\model\BaseModel;
/**
* MemberGroupAccess 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class MemberGroupAccess extends Pivot
{
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_group_access',
'autoWriteTimestamp' => 'int', // create_at 已统一为 int 时间戳
'createTime' => 'create_at',
'updateTime' => false,
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_group_access');
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace ywxapp\model;
use think\Model;
use ywxapp\model\BaseModel;
class MemberGroupRule extends Model
{
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_group_rule',
'autoWriteTimestamp' => 'int', // create_at 已统一为 int 时间戳
'createTime' => 'create_at',
'updateTime' => false,
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_group_rule');
}
}
+44
View File
@@ -0,0 +1,44 @@
<?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 ywxapp\model\BaseModel;
/**
* 用户日志
*/
class MemberLog extends BaseModel
{
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_log',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => false,
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_log');
}
}
+121
View File
@@ -0,0 +1,121 @@
<?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 ywxapp\model\BaseModel;
/**
* 用户资料模型
*/
class MemberProfile extends BaseModel
{
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_profile',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['is_parent', 'age', 'is_online'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['uid'],
];
}
/**
* 计算年龄
*/
public function getAgeAttr()
{
if (empty($this->birthday)) {
return 0;
}
$b = strtotime($this->birthday);
if ($b === false) {
return 0;
}
return (int) floor((time() - $b) / 31557600);
}
/**
* 是否在线(5 分钟内活跃)
*/
public function getIsOnlineAttr()
{
return $this->online_status == 1
|| (! empty($this->last_active_at) && time() - (int) $this->last_active_at < 300);
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_profile');
}
}
// realname varchar(255) NO 实名
// gender tinyint(1) 0 NO 性别 (0:保密 1:男 2:女)
// birthyear smallint(6) unsigned 0 NO
// birthmonth tinyint(3) unsigned 0 NO
// birthday tinyint(3) unsigned 0 NO
// constellation varchar(255) NO 星座(根据生日自动计算)
// zodiac varchar(255) NO 生肖(根据生日自动计算)
// telephone varchar(255) NO 固定电话
// mobile varchar(255) NO 手机
// idcardtype varchar(255) NO 证件类型:身份证 护照 军官证等
// idcard varchar(255) NO 证件号码
// address varchar(255) NO 邮寄地址
// zipcode varchar(255) NO 邮编
// nationality varchar(255) NO 国籍
// birthprovince varchar(255) NO 出生省份
// birthcity varchar(255) NO 出生城市
// birthdist varchar(20) NO 出生行政区/县
// birthcommunity varchar(255) NO 出生小区
// resideprovince varchar(255) NO 居住省份
// residecity varchar(255) NO 居住城市
// residedist varchar(20) NO 居住行政区/县
// residecommunity varchar(255) NO 居住小区
// residesuite varchar(255) NO 小区、写字楼门牌号
// graduateschool varchar(255) NO 毕业学校
// company varchar(255) NO 公司
// education varchar(255) NO 学历
// occupation varchar(255) NO 职业
// position varchar(255) NO 职位
// revenue varchar(255) NO 年收入
// affectivestatus varchar(255) NO 情感状态
// lookingfor varchar(255) NO 交友目的(交友类型)
// bloodtype varchar(255) NO 血型
// height varchar(255) NO 身高
// weight varchar(255) NO 体重
// alipay varchar(255) NO 支付宝帐号
// icq varchar(255) NO ICQ
// qq varchar(255) NO QQ
// yahoo varchar(255) NO YAHOO
// msn varchar(255) NO MSN
// taobao varchar(255) NO 阿里旺旺
// site varchar(255) NO 主页
// bio text NO 自我介绍 来自论坛bio字段
// interest text NO 兴趣爱好
// field1 text NO 自定义字段1
// field2 text NO 自定义字段2
// field3 text NO 自定义字段3
// field4 text NO 自定义字段4
// field5 text NO 自定义字段5
// field6 text NO 自定义字段6
// field7 text NO 自定义字段7
// field8 text NO 自定义字段8
+159
View File
@@ -0,0 +1,159 @@
<?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\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* MemberRule 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class MemberRule extends BaseModel
{
use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_rule',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['is_parent' ],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
// 拥有该权限的角色
public function groups()
{
return $this->belongsToMany(MemberGroup::class, MemberGroupRule::class, 'gid', 'rid');
}
// 拥有该权限的用户(通过角色)
public function users()
{
return $this->belongsToMany(MemberUser::class, MemberGroupRule::class, 'uid', 'gid')->via('roles');
}
public static function onAfterRead($data)
{
// 这里可以直接使用$this访问当前模型
// if ($data->type == 2) {
// $data->append(['href', 'openType']);
// $data->openType = '_iframe';
// $data->href = $data->route;
// }
}
public static function onAfterDelete($data)
{
MemberGroupRule::where('rid', $data->id)->delete();
}
/**
* 获取器:获取状态文本
*/
public function getIsParentAttr($value, $data)
{
return $this->where('pid', $data['id'])->count() > 0;
}
/**
* 将扁平菜单数组转为树形结构
* @param array $menus 扁平菜单列表(每个元素是数组)
* @param int $parentId 父ID(默认0表示根)
* @return array 树形结构
*/
public static function buildTree(array $menus, int $parentId = 0): array
{
$branch = [];
foreach ($menus as $menu) {
if ($menu['pid'] == $parentId) {
$children = self::buildTree($menus, $menu['id']);
if (! empty($children)) {
$menu['children'] = $children;
}
$branch[] = $menu;
}
}
return $branch;
}
/**
* 递归生成树形结构
* @param array $list 原始数据
* @param int $pid 父级ID
* @return array
*/
public static function toTree($list, $pid = 0)
{
$tree = [];
foreach ($list as $item) {
if ($item['pid'] == $pid) {
$children = self::toTree($list, $item['id']);
if ($children) {
$item['children'] = $children;
}
$tree[] = $item;
}
}
return $tree;
}
public static function toTreeS($list, $pid = 0, $level = 0)
{
$tree = [];
foreach ($list as $item) {
if ($item['pid'] == $pid) {
// ✅ 创建新变量,不修改原始数据
// $item['title'] = str_repeat(' ', $level * 2) . ($level > 0 ? '├─ ' : '') . $item['title'];
// 或者用 |-- 但控制层级
$item['title'] = str_repeat('|-- ', $level + 1) . $item['title'];
$children = self::toTreeS($list, $item['id'], $level + 1);
if ($children) {
$item['children'] = $children;
}
$tree[] = $item;
}
}
return $tree;
}
/**
* 获取所有权限并转为树形
* @return array
*/
public static function getTreeList()
{
$list = self::order('sort', 'asc')->select()->toArray();
return self::toTree($list);
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_rule');
}
}
+201
View File
@@ -0,0 +1,201 @@
<?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\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* @author ywx <1790337789@qq.com>
* @date 2021-07-06
* 用户模型
* @return \think\Model
*/
class MemberUser extends BaseModel
{
use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'name' => 'member_user',
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'hidden' => ['password', 'delete_at'],
'append' => ['status_text', 'last_login'],
];
}
// 关联用户资料
public function profile()
{
return $this->hasOne(MemberProfile::class, 'uid', 'uid');
}
// 关联用户组(会员组)
public function groups()
{
return $this->belongsToMany(MemberGroup::class, MemberGroupAccess::class, 'gid', 'uid');
}
/**
* 获取用户有权访问的菜单树
*/
public function getAccessibleMenus()
{
$permissions = $this->getAllPermissions();
if (empty($permissions)) {
return [];
}
$flatMenus = MemberRule::field('id,title,name,pid,sort,route,icon,type')
->whereIn('id', $permissions)
->order('sort', 'asc')
->select()
->toArray();
return MemberRule::buildTree($flatMenus);
}
/**
* 获取用户所有权限标识(字符串数组)
*/
public function getAllPermissions(): array
{
$roleIds = $this->groups->column('id');
if (empty($roleIds)) {
return [];
}
return MemberGroupRule::where('gid', 'in', $roleIds)
->distinct(true)
->column('rid');
}
// 密码加密
public function setPasswordAttr($value)
{
$this->salt = $this->generateSalt();
return password_hash($value . $this->salt, PASSWORD_DEFAULT);
}
// 生成随机盐
protected function generateSalt($length = 6)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_[]{}<>`+=,.;:/?|';
$salt = '';
for ($i = 0; $i < $length; $i++) {
$salt .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $salt;
}
// 验证密码
public function checkPassword($password)
{
return password_verify($password . $this->salt, $this->password);
}
// 获取状态文本
public function getStatusTextAttr()
{
$statusMap = [0 => '禁用', 1 => '正常', 2 => '锁定'];
return $statusMap[$this->status] ?? '未知';
}
// 获取最后登录描述
public function getLastLoginAttr()
{
if (empty($this->login_time)) {
return '从未登录';
}
$diff = time() - strtotime($this->login_time);
if ($diff < 60) {
return '刚刚';
} elseif ($diff < 3600) {
return floor($diff / 60) . '分钟前';
} elseif ($diff < 86400) {
return floor($diff / 3600) . '小时前';
} else {
return date('Y-m-d', strtotime($this->login_time));
}
}
// 检查账户是否被锁定(统一使用 user 表已有的 loginfailure / loginfailuretime 字段)
// 复用该字段:失败次数 >=5 且最后一次失败距今不足 30 分钟则锁定。
public function isLocked()
{
if ($this->status == 0) {
return true; // 已禁用
}
if ($this->loginfailure >= 5 && $this->loginfailuretime > 0) {
$lockUntil = $this->loginfailuretime + 1800; // 锁定 30 分钟
return time() < $lockUntil;
}
return false;
}
// 记录登录成功
public function recordLogin($ip)
{
$this->login_ip = $ip;
$this->login_time = date('Y-m-d H:i:s');
$this->login_count += 1;
$this->loginfailure = 0; // 重置失败计数
$this->loginfailuretime = 0; // 清除锁定时间
$this->save();
}
// 记录登录失败(统一使用 loginfailure / loginfailuretime 字段)
public function recordLoginFail($ip)
{
$this->loginfailure += 1;
$this->loginfailuretime = time(); // 记录最后一次失败时间,满 5 次即锁定 30 分钟
$this->save();
}
// 重置密码
public function resetPassword($newPassword)
{
$this->password = $newPassword; // 会触发setPasswordAttr自动加密
$this->need_reset = 0; // 标记无需重置
return $this->save();
}
// public function getAvatarAttr($value, $data)
// {
// return '/static/common/images/avatar.jpg';
// }
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
foreach (['member', 'member_bill', 'member_group', 'member_group_access', 'member_group_rule', 'member_log', 'member_profile', 'member_rule', 'member_wallets'] as $t) {
BaseModel::ensureTableFromInstall($prefix, $t);
}
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\model\concern\SoftDelete;
/**
* 站点公告
*/
class Notice extends BaseModel
{
use SoftDelete;
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 类型:1系统维护 2活动公告 3版本更新 4其他
public const TYPE_MAINTAIN = 1;
public const TYPE_ACTIVITY = 2;
public const TYPE_UPDATE = 3;
public const TYPE_OTHER = 4;
// 字段类型转换
protected $type = [
'type' => 'integer',
'is_top' => 'integer',
'start_time' => 'integer',
'end_time' => 'integer',
'view_count' => 'integer',
'sort' => 'integer',
'status' => 'integer',
'create_at' => 'integer',
'update_at' => 'integer',
'delete_at' => 'integer',
];
// 只读字段
protected $readonly = ['view_count'];
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
protected function getOptions(): array
{
return [
'strict' => true,
'name' => 'notice',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
];
}
/**
* 公告类型映射
*/
public static function typeList(): array
{
return [
self::TYPE_MAINTAIN => '系统维护',
self::TYPE_ACTIVITY => '活动公告',
self::TYPE_UPDATE => '版本更新',
self::TYPE_OTHER => '其他',
];
}
/**
* 取全站生效的置顶公告(用于前台公告条)。
* 过滤:status=1 + 在有效期内(start_time<=now<=end_time 或 0=长期) + is_top=1
*/
public static function getActiveTopNotices(): array
{
self::ensureSchema();
$now = time();
return (new self())
->where('status', 1)
->where('is_top', 1)
->where(function ($q) use ($now) {
$q->where('start_time', 0)->whereOr('start_time', '<=', $now);
})
->where(function ($q) use ($now) {
$q->where('end_time', 0)->whereOr('end_time', '>=', $now);
})
->order('sort', 'desc')
->order('id', 'desc')
->limit(5)
->select()
->toArray();
}
/**
* 取前台公告列表(分页),按置顶优先 + 排序 + 时间倒序
*/
public static function getList(int $page = 1, int $limit = 10, int $type = 0): \think\Paginator
{
self::ensureSchema();
$now = time();
$query = (new self())
->where('status', 1)
->where(function ($q) use ($now) {
$q->where('start_time', 0)->whereOr('start_time', '<=', $now);
})
->where(function ($q) use ($now) {
$q->where('end_time', 0)->whereOr('end_time', '>=', $now);
});
if ($type > 0) {
$query->where('type', $type);
}
return $query->order('is_top', 'desc')
->order('sort', 'desc')
->order('id', 'desc')
->paginate(['page' => $page, 'list_rows' => $limit]);
}
/**
* 运行时结构自愈:老库缺新列时自动补齐(install.sql 为唯一事实源,此处兜底)
*/
public static function ensureSchema(): void
{
try {
$p = self::currentPrefix();
$prefix = $p;
$table = $prefix . 'notice';
// 缺表建表(install.sql L618 同款 DDL,运行时前缀兜底)
self::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$p}notice` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '公告标题',
`content` text COMMENT '公告内容',
`author` varchar(50) NOT NULL DEFAULT '' COMMENT '发布人',
`type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型 1系统维护 2活动公告 3版本更新 4其他',
`is_top` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否置顶 0否 1是',
`start_time` int(11) NOT NULL DEFAULT 0 COMMENT '展示开始时间 0=长期',
`end_time` int(11) NOT NULL DEFAULT 0 COMMENT '展示结束时间 0=长期',
`view_count` int(11) NOT NULL DEFAULT 0 COMMENT '浏览量',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态 0禁用 1启用',
`create_at` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
`update_at` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间',
`delete_at` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_type` (`type`),
KEY `idx_is_top` (`is_top`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站点公告'");
// 老库扩展列兜底(幂等)
self::ensureColumn($table, 'type', "tinyint(1) NOT NULL DEFAULT 1 COMMENT '公告类型 1系统维护 2活动公告 3版本更新 4其他'");
self::ensureColumn($table, 'is_top', "tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否置顶 0否 1是'");
self::ensureColumn($table, 'start_time', "int(11) NOT NULL DEFAULT 0 COMMENT '展示开始时间 0=长期'");
self::ensureColumn($table, 'end_time', "int(11) NOT NULL DEFAULT 0 COMMENT '展示结束时间 0=长期'");
self::ensureColumn($table, 'view_count', "int(11) NOT NULL DEFAULT 0 COMMENT '浏览量'");
} catch (\Throwable $e) {
// 忽略自愈失败,由业务暴露
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\model\concern\SoftDelete;
/**
* 道具中心
*/
class Prop extends BaseModel
{
use SoftDelete;
protected $name = 'prop';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
/**
* 运行时自愈:确保 prop 主表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'prop');
}
/**
* 初始化:自愈建表,避免远程库缺失 wxapp_prop 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\facade\Db;
use think\model\concern\SoftDelete;
/**
* 积分规则 & 积分流水
*/
class Score extends BaseModel
{
use SoftDelete;
protected $name = 'score_rule';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
// 规则类型
const TYPE_GAIN = 1; // 获取
const TYPE_SPEND = 2; // 消费
public static function typeList(): array
{
return [
self::TYPE_GAIN => '获取',
self::TYPE_SPEND => '消费',
];
}
/**
* 确保 score_rule / score_log 关联表已就绪(静态入口可能未实例化模型)
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'score_rule');
BaseModel::ensureTableFromInstall($prefix, 'score_log');
BaseModel::ensureColumn("{$prefix}member", 'score', "int unsigned NOT NULL DEFAULT 0 COMMENT '积分(做任务/互动获得)'");
}
/**
* 积分变动(带流水记录,事务安全)
* @param int $uid 用户ID
* @param int $type 1=收入 2=支出
* @param int $value 变动值(正数)
* @param string $remark 备注
* @param int $ruleId 关联规则ID(默认0
* @return array [bool, string]
*/
public static function change(int $uid, int $type, int $value, string $remark = '', int $ruleId = 0): array
{
self::ensureSchema();
$uid = (int) $uid;
$value = (int) $value;
if ($uid <= 0 || $value <= 0) {
return [false, '参数错误'];
}
$type = $type === self::TYPE_SPEND ? self::TYPE_SPEND : self::TYPE_GAIN;
Db::startTrans();
try {
$user = Db::name('member')->where('uid', $uid)->lock(true)->find();
if (!$user) {
throw new \Exception('用户不存在');
}
if ($type === self::TYPE_SPEND && $user['score'] < $value) {
throw new \Exception('积分余额不足');
}
$balance = $type === self::TYPE_SPEND
? $user['score'] - $value
: $user['score'] + $value;
Db::name('member')->where('uid', $uid)->update(['score' => $balance]);
Db::name('score_log')->insert([
'uid' => $uid,
'rule_id' => $ruleId,
'type' => $type,
'value' => $value,
'balance' => $balance,
'remark' => $remark,
'create_at' => time(),
]);
Db::commit();
return [true, 'ok'];
} catch (\Throwable $e) {
Db::rollback();
return [false, $e->getMessage()];
}
}
/**
* 积分流水列表(后台查看)
*/
public static function logList(int $uid = 0, int $page = 1, int $limit = 15): array
{
self::ensureSchema();
$query = Db::name('score_log');
if ($uid > 0) {
$query->where('uid', $uid);
}
$count = $query->count();
$list = $query->order('id', 'desc')
->page($page, $limit)
->select()
->toArray();
return ['count' => $count, 'list' => $list];
}
}
+54
View File
@@ -0,0 +1,54 @@
<?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 ywxapp\model\BaseModel;
/*
* 会员积分日志
*/
/**
* ScoreLog 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class ScoreLog extends BaseModel
{
protected function initialize()
{
parent::initialize();
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'score_log');
}
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'score_log',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
* 查询/写入前调用,避免老库缺表导致 1146。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'score_log');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\model\concern\SoftDelete;
/**
* 电子商务
*/
class Shop extends BaseModel
{
use SoftDelete;
protected $name = 'shop';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
/**
* 运行时自愈:确保 shop 主表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'shop');
}
/**
* 初始化:自愈建表,避免远程库缺失 wxapp_shop 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\model\concern\SoftDelete;
/**
* 短信服务
*/
class Sms extends BaseModel
{
use SoftDelete;
protected $name = 'sms';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
/**
* 运行时自愈:确保 sms 主表存在(install.sql 为事实源),并幂等补 delete_at 软删除列。
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'sms');
self::ensureColumn($prefix . 'sms', 'delete_at', "int NOT NULL DEFAULT 0 COMMENT '删除时间(软删除)'");
}
/**
* 初始化:软删除依赖 delete_at 列、且需保证主表存在。
* install.sql 的短信日志表(sms)可能缺失,先自愈建表,再幂等补 delete_at 列。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use ywxapp\model\Medal;
use think\facade\Db;
use think\model\concern\SoftDelete;
/**
* 站点任务
*/
class Task extends BaseModel
{
use SoftDelete;
protected $name = 'task';
protected $deleteTime = 'delete_at';
protected $defaultSoftDelete = 0;
// 时间戳自动写入
protected $autoWriteTimestamp = true;
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
// 奖励类型
const REWARD_SCORE = 1; // 积分
const REWARD_COIN = 2; // 金币
const REWARD_PROP = 3; // 道具
const REWARD_MEDAL = 4; // 勋章
/**
* 奖励类型列表(后台下拉 / 前台展示)
*/
public static function rewardTypeList(): array
{
return [
self::REWARD_SCORE => '积分',
self::REWARD_COIN => '金币',
self::REWARD_PROP => '道具',
self::REWARD_MEDAL => '勋章',
];
}
public static function getRewardTypeText(int $type): string
{
return self::rewardTypeList()[$type] ?? '未知';
}
/**
* 启用中的任务(前台展示)
*/
public static function getEnabledList()
{
return self::where('status', 1)->order('sort', 'asc')->select();
}
/**
* 取某用户已领取的任务ID集合
*/
public static function getClaimedTaskIds(int $uid): array
{
self::ensureSchema(); // 确保 member_task / member_prop 关联表已就绪(静态入口可能未实例化模型)
if ($uid <= 0) {
return [];
}
return Db::name('member_task')->where('uid', $uid)->column('task_id') ?: [];
}
/**
* 领取任务并发放奖励(幂等:同一任务仅可领取一次)
* @return array [bool $ok, string $msg]
*/
public static function claim(int $uid, int $taskId): array
{
self::ensureSchema(); // 确保 user_task / user_prop 等关联表已就绪(静态入口可能未实例化模型)
if ($uid <= 0) {
return [false, '请先登录'];
}
$task = self::where('id', $taskId)->where('status', 1)->find();
if (!$task) {
return [false, '任务不存在或未启用'];
}
$task = $task->toArray();
// 是否已领取
$exists = Db::name('member_task')->where('uid', $uid)->where('task_id', $taskId)->find();
if ($exists) {
return [false, '该任务已领取'];
}
$num = (int) $task['reward_num'];
if ($num <= 0) {
return [false, '奖励数量无效'];
}
Db::startTrans();
try {
// 发放奖励
switch ((int) $task['reward_type']) {
case self::REWARD_SCORE:
[$ok, $msg] = \ywxapp\model\Score::change($uid, \ywxapp\model\Score::TYPE_GAIN, $num, '完成任务:' . $task['title']);
if (! $ok) {
throw new \Exception($msg);
}
break;
case self::REWARD_COIN:
Db::name('member_wallets')->where('uid', $uid)->inc('coins', $num)->update();
Db::name('member_bill')->insert([
'uid' => $uid,
'currency' => 2, // 2=金币
'type' => 1, // 1=收入
'amount' => $num,
'balance' => Db::name('member_wallets')->where('uid', $uid)->value('coins') ?: $num,
'remark' => '完成任务:' . $task['title'],
'create_at' => time(),
]);
break;
case self::REWARD_PROP:
Db::name('member_prop')->insert([
'uid' => $uid,
'prop_id' => $num, // reward_num 存道具ID
'num' => (int) ($task['prop_num'] ?: 1),
'create_at' => time(),
]);
break;
case self::REWARD_MEDAL:
// reward_num 存勋章ID
$grant = Medal::grant($uid, $num);
if (!$grant[0]) {
throw new \Exception($grant[1]);
}
break;
default:
throw new \Exception('未知奖励类型');
}
// 领取记录
Db::name('member_task')->insert([
'uid' => $uid,
'task_id' => $taskId,
'reward_type' => $task['reward_type'],
'reward_num' => $num,
'create_at' => time(),
]);
Db::commit();
return [true, '领取成功'];
} catch (\Throwable $e) {
Db::rollback();
return [false, '领取失败:' . $e->getMessage()];
}
}
/**
* 表自愈:确保任务相关表(member_prop / member_task)与扩展列已就绪。
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'task');
BaseModel::ensureTableFromInstall($prefix, 'member_prop');
BaseModel::ensureTableFromInstall($prefix, 'member_task');
BaseModel::ensureColumn("{$prefix}member", 'score', "int unsigned NOT NULL DEFAULT 0 COMMENT '积分(做任务/互动获得)'");
BaseModel::ensureColumn("{$prefix}task", 'prop_num', "int NOT NULL DEFAULT 1 COMMENT '道具数量(奖励类型为道具时生效)'");
}
}