feat: 公共表更名 common/member 前缀 + 插件命令自动加载 + 版本 1.1.1

- 18 张公共表更名(addon/attachment/configure/links/spider_log/spider_stat/sms/notice/ad/task/prop/medal/help/card -> common_*,member_wallets->member_wallet,score_rule/score_log -> member_*,addon_config->common_addonconf),模型全部对齐新表名,Db 直引用清零
- Attachment 模型补  表名绑定,修复富文本上传查 wxapp_attachment 1146 隐患
- install.sql + 迁移 SQL:backend_admin/backend_role delete_at 默认 0,修复软删除(NULL != 0)误过滤导致后台菜单为空
- AppService::boot() 支持插件 info.php 声明 commands 自动注册插件命令(psr-4 自动加载,坏类名自动跳过)
- 各插件(haonav/mqttbroker/wxchat/articles/blog/forum 等)字段与配置同步调整
- 框架版本 1.1.0 -> 1.1.1
This commit is contained in:
ywxapp
2026-08-20 20:19:15 +08:00
parent 303f548664
commit 1d49e6f5ee
395 changed files with 13342 additions and 2012 deletions
+1 -2
View File
@@ -126,8 +126,7 @@ abstract class AddonBase
*/
public static function readConfig(string $name): array
{
$addonPath = (defined('ADDON_PATH') ? ADDON_PATH : app()->getRootPath() . 'addon' . DIRECTORY_SEPARATOR)
. $name . DIRECTORY_SEPARATOR;
$addonPath = (defined('ADDON_PATH') ? ADDON_PATH : app()->getRootPath() . 'addon' . DIRECTORY_SEPARATOR) . $name . DIRECTORY_SEPARATOR;
$config = [];
// ① 静态代码级配置:addon/<name>/config/<name>.php(随插件分发、支持 env() 覆盖)。
// 与表单 config.php 分工:此处放「运行期/部署相关、需在代码中用 env() 取默认」的项
+14 -8
View File
@@ -372,15 +372,21 @@ class AddonHealth extends Command
Cache::delete($testKey);
// 插件缓存命中情况(仅警告,不影响健康)
$addon = Config::get('addon', []);
foreach ($addon as $addon) {
if (!isset($addon['name']) || !($addon['state'] ?? false)) {
continue;
}
if (Cache::get('addon_config_' . $addon['name']) === false) {
$result['warnings'][] = "插件 {$addon['name']} 缓存未命中,可能影响性能";
// 插件缓存命中情况(仅警告,不影响健康)。
// 注意:Config::get('addon') 是框架 addon 组件配置(multi_app/priority 等),非插件列表;
// 直接用 AddonModel 的插件名列表,避免遍历配置时访问 true['name']。
try {
$addons = \ywxapp\model\AddonModel::field('name,state')->select()->toArray();
foreach ($addons as $addon) {
if (!isset($addon['name']) || !($addon['state'] ?? false)) {
continue;
}
if (Cache::get('addon_config_' . $addon['name']) === false) {
$result['warnings'][] = "插件 {$addon['name']} 缓存未命中,可能影响性能";
}
}
} catch (\Throwable $e) {
// 数据表可能尚未创建,忽略
}
} catch (\Exception $e) {
$result['healthy'] = false;
+32 -9
View File
@@ -53,7 +53,7 @@ class AddonMake extends Make
$this->setName('addon:make')
->addArgument('addon', Argument::OPTIONAL, 'addon name, e.g. demo (required for build & generators)')
->addOption('action', 'a', Option::VALUE_REQUIRED, 'build|clear|controller|model|middleware|validate|event|listener|service|subscribe')
->addOption('name', 'n', Option::VALUE_OPTIONAL, 'class name for generator actions, e.g. User')
->addOption('name', null, Option::VALUE_OPTIONAL, 'class name for generator actions, e.g. User')
->addOption('api', null, Option::VALUE_NONE, 'controller: generate an api controller class.')
->addOption('plain', null, Option::VALUE_NONE, 'controller: generate an empty controller class.')
->addOption('cache', 'c', Option::VALUE_NONE, 'clear: clear cache file')
@@ -62,7 +62,7 @@ class AddonMake extends Make
->setDescription('Unified addon scaffold & class generator (build / clear / controller / model / middleware / validate / event / listener / service / subscribe)');
}
protected function execute(Input $input, Output $output): int
{
$addon = trim((string) $input->getArgument('addon'));
@@ -227,8 +227,12 @@ class AddonMake extends Make
mkdir($appPath . 'controller');
mkdir($appPath . 'controller/backend');
mkdir($appPath . 'controller/member');
mkdir($appPath . 'controller/api');
}
$this->buildIndexController($app, $namespace);
$this->buildIndexController($app, $namespace); // 前台 FrontendBase
$this->buildIndexController($app, $namespace, 'backend'); // BackendBase
$this->buildIndexController($app, $namespace, 'member'); // MemberBase
$this->buildIndexController($app, $namespace, 'api'); // ApiBase
if (! is_dir($appPath . 'lang')) {
mkdir($appPath . 'lang');
}
@@ -342,14 +346,25 @@ class AddonMake extends Make
* @param string $namespace 类库命名空间
* @return void
*/
public function buildIndexController(string $app, string $namespace): void
public function buildIndexController(string $app, string $namespace, string $area = ''): void
{
$suffix = $this->app->config->get('route.controller_suffix') ? 'Controller' : '';
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php';
$dir = 'controller' . ($area !== '' ? DIRECTORY_SEPARATOR . $area : '');
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . $dir . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php';
if (! is_file($filename)) {
$content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'controller.stub');
$content = str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], ['Index', $suffix, $namespace . '\\controller', $namespace], $content);
$base_class = $area == 'backend' ? 'BackendBase' : ($area == 'member' ? 'MemberBase' : ($area == 'api' ? 'ApiBase' : 'FrontendBase'));
$content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'controller.stub');
$content = str_replace(
[
'{%className%}',
'{%actionSuffix%}',
'{%namespace%}',
'{%app_namespace%}',
'{%baseClass%}'
],
['Index', $suffix, $namespace . ($area !== '' ? '\\controller\\' . $area : '\\controller'), $namespace, $base_class],
$content
);
$this->checkDirBuild(dirname($filename));
file_put_contents($filename, $content);
}
@@ -366,8 +381,16 @@ class AddonMake extends Make
{
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'route' . DIRECTORY_SEPARATOR . 'app.php';
if (! is_file($filename)) {
$content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'route.stub');
$content = str_replace(
[
'{%app%}'
],
[$app],
$content
);
$this->checkDirBuild(dirname($filename));
file_put_contents($filename, "<?php" . PHP_EOL . "use think\\facade\\Route;" . PHP_EOL . PHP_EOL);
file_put_contents($filename, $content);
}
}
+8 -1
View File
@@ -19,6 +19,7 @@ use think\File;
use ywxapp\exception\AddonException;
use ywxapp\service\AddonService;
use ywxapp\service\AddonHotReload;
use think\facade\Env;
/**
* 插件统一管理命令(合并 develop / install / reload)
@@ -94,8 +95,14 @@ class AddonManage extends Command
} catch (AddonException $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
} catch (\Exception $e) {
} catch (\Throwable $e) {
// \Throwable 兜底:\Error(如类型错误)不是 \Exception 子类,必须单独捕获,
// 否则会漏到框架层导致无友好提示。
$output->writeln('<error>开发模式安装失败:' . $e->getMessage() . '</error>');
if (Env::get('app_debug')) {
$output->writeln('<error>FILE: ' . $e->getFile() . ':' . $e->getLine() . '</error>');
$output->writeln('<error>TRACE: ' . $e->getTraceAsString() . '</error>');
}
return 1;
}
return 0;
+15 -11
View File
@@ -26,7 +26,7 @@ use ywxapp\model\BaseModel;
* 整合以下自愈能力,使一个出问题的插件恢复到「菜单正确 + 表结构完整 + 配置齐全」的状态:
* 1. 重载菜单:refreshMenu()(清旧 admin_power/user_rule + 重建,顺带修复菜单路由前缀)
* 2. 数据库自愈:解析 install.sql 中该插件全部 CREATE TABLE,缺表建表、缺列补列
* 3. 配置行补全:确保 addon_config 表存在该插件配置行(无则按默认 info 写入)
* 3. 配置行补全:确保 common_addonconf 表存在该插件配置行(无则按默认 info 写入)
*
* 用法:
* php think addon:repair <插件名> # 修复指定插件(菜单 + 表 + 配置)
@@ -245,24 +245,25 @@ class AddonRepair extends Command
}
/**
* 配置行补全:确保 addon_config 表有该插件配置行。
* 配置行补全:确保 common_addonconf 表有该插件配置行。
*/
private function repairConfig(AddonService $svc, string $name, Output $output): bool
{
$default = Config::get('database.default');
$prefix = Config::get("database.connections.{$default}.prefix") ?: '';
$configTable = $prefix . 'addon_config';
$configTable = $prefix . 'common_addonconf';
try {
if (!BaseModel::tableExists($configTable)) {
$output->writeln('<comment>[配置] addon_config 表不存在,跳过</comment>');
$output->writeln('<comment>[配置] common_addonconf 表不存在,跳过</comment>');
return true;
}
$row = Db::table($configTable)->where('name', $name)->find();
if (!empty($row)) {
// common_addonconf 设计:每行一个配置项(addon + name 唯一),故按 addon 判断是否存在
$existing = Db::table($configTable)->where('addon', $name)->count();
if ($existing > 0) {
$output->writeln('<comment>[配置] 配置行已存在,跳过</comment>');
return true;
}
// 读取插件默认配置(config.php 返回数组)
// 读取插件默认配置(config.php 返回关联数组 name=>value)
$configFile = $this->addonDir($name) . 'config.php';
$value = [];
if (is_file($configFile)) {
@@ -271,10 +272,13 @@ class AddonRepair extends Command
$value = $cfg;
}
}
$data = [
'name' => $name,
'value' => json_encode($value, JSON_UNESCAPED_UNICODE),
];
if (empty($value)) {
$output->writeln('<comment>[配置] 无默认配置,跳过</comment>');
return true;
}
// 逐配置项写入独立行(与 AddonService::config/$changed 的存储格式一致)
\ywxapp\service\AddonService::config($name, $value);
$data = []; // 已通过 AddonService::config 写入,无需再 insert
// 兼容不同时间戳字段名(createtime/updatetime 或 create_time/update_time)
$cols = Db::query("SHOW COLUMNS FROM `{$configTable}`");
$colNames = array_column($cols, 'Field');
+70 -253
View File
@@ -17,6 +17,8 @@ use think\console\Output;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\command\Make;
/**
* AppCommand 应用命令统一入口
*
@@ -44,25 +46,8 @@ use think\console\input\Option;
* php think app clear --cache # 清理缓存
* php think app build myapp # 构建应用目录
*/
class AppCommand extends Command
{
/**
* Make 型子命令配置
* type : 类型标签(用于成功/存在提示)
* stub : stub 文件名(不含 .stub 扩展)
* ns : 命名空间末尾段(如 controller/model/event ...)
* suffix : 类名后缀(仅 controller 在开启 controller_suffix 时追加 Controller)
*/
private const MAKE_TYPES = [
'controller' => ['type' => 'Controller', 'stub' => 'controller', 'ns' => 'controller', 'suffix' => 'Controller'],
'model' => ['type' => 'Model', 'stub' => 'model', 'ns' => 'model', 'suffix' => ''],
'event' => ['type' => 'Event', 'stub' => 'event', 'ns' => 'event', 'suffix' => ''],
'listener' => ['type' => 'Listener', 'stub' => 'listener', 'ns' => 'listener', 'suffix' => ''],
'middleware' => ['type' => 'Middleware', 'stub' => 'middleware', 'ns' => 'middleware', 'suffix' => ''],
'service' => ['type' => 'Service', 'stub' => 'service', 'ns' => 'service', 'suffix' => ''],
'subscribe' => ['type' => 'Subscribe', 'stub' => 'subscribe', 'ns' => 'subscribe', 'suffix' => ''],
'validate' => ['type' => 'Validate', 'stub' => 'validate', 'ns' => 'validate', 'suffix' => ''],
];
class AppCommand extends Make
{
/**
* 应用基础目录(Build 使用)
@@ -70,12 +55,43 @@ class AppCommand extends Command
*/
private $basePath = '';
/**
* stub 模板目录(已从 ywxapp/command/app/stubs 上移至此)
*/
private function stubDir(): string
private $subCommon = '';
protected function getStub(): string
{
return __DIR__ . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR;
if ($this->input->getOption('api')) {
return __DIR__ . DIRECTORY_SEPARATOR . 'appstubs' . DIRECTORY_SEPARATOR . $this->subCommon . '.api.stub';
}
if ($this->input->getOption('plain')) {
return __DIR__ . DIRECTORY_SEPARATOR . 'appstubs' . DIRECTORY_SEPARATOR . $this->subCommon . '.plain.stub';
}
return __DIR__ . DIRECTORY_SEPARATOR . 'appstubs' . DIRECTORY_SEPARATOR . $this->subCommon . '.stub';
}
protected function getNamespace(string $app): string
{
if ($this->subCommon === 'model') {
return 'ywxapp\\model';
}
return parent::getNamespace($app) . '\\' . $this->subCommon;
}
protected function getClassName(string $name): string
{
if ($this->subCommon == 'controller') {
return parent::getClassName($name) . ($this->app->config->get('route.controller_suffix') ? 'Controller' : '');
}
return parent::getClassName($name);
}
protected function getPathName(string $name): string
{
if ($this->subCommon == 'model') {
$name = substr($name, 6);
return $this->app->getRootPath() . 'ywxapp' . '/' . ltrim(str_replace('\\', '/', $name), '/') . '.php';
}
$name = substr($name, 4);
return $this->app->getBasePath() . ltrim(str_replace('\\', '/', $name), '/') . '.php';
}
/**
@@ -98,171 +114,42 @@ class AppCommand extends Command
/**
* 执行命令
*/
protected function execute(Input $input, Output $output): int
protected function execute(Input $input, Output $output)
{
if ($input->getOption('list') || empty($input->getArgument('subcommand'))) {
$this->showList($output);
return 0;
}
$sub = $input->getArgument('subcommand');
$name = (string) $input->getArgument('name');
if (isset(self::MAKE_TYPES[$sub])) {
return $this->runMake($sub, $name, $input, $output);
}
if ($sub === 'build') {
$this->subCommon = $input->getArgument('subcommand');
$name = trim($input->getArgument('name'));
if ($this->subCommon === 'build') {
return $this->runBuild($name, $output);
}
if ($sub === 'clear') {
if ($this->subCommon === 'clear') {
return $this->runClear($name, $input, $output);
}
$output->writeln('<error>未知子命令: ' . $sub . '</error>');
$this->showList($output);
return 1;
}
/**
* 列出所有可用子命令
*/
private function showList(Output $output): void
{
$output->writeln('<info>YwxApp 应用命令(`php think app <子命令> [参数]`):</info>');
$output->writeln('');
$width = max(array_map('strlen', array_keys(self::MAKE_TYPES + ['build' => 1, 'clear' => 1])));
foreach (array_keys(['build' => 1, 'clear' => 1] + self::MAKE_TYPES) as $name) {
$desc = [
'build' => '构建应用目录',
'clear' => '清理运行时文件',
'controller' => '生成控制器(--api/--plain)',
'model' => '生成模型',
'event' => '生成事件',
'listener' => '生成监听器',
'middleware' => '生成中间件',
'service' => '生成服务',
'subscribe' => '生成订阅器',
'validate' => '生成验证器',
][$name];
$output->writeln(' ' . str_pad($name, $width) . ' ' . $desc);
}
$output->writeln('');
$output->writeln('示例:php think app controller app\\index');
$output->writeln(' php think app clear --cache');
}
/* ===================== Make 型生成器 ===================== */
/**
* 运行 Make 型子命令
*/
private function runMake(string $type, string $name, Input $input, Output $output): int
{
if ($name === '') {
$output->writeln('<error>缺少类名参数,用法:php think app ' . $type . ' &lt;类名&gt;</error>');
return 1;
}
$cfg = self::MAKE_TYPES[$type];
$classname = $this->makeClassName($type, $name);
$pathname = $this->makePathName($classname);
$classname = $this->getClassName($name);
$pathname = $this->getPathName($classname);
if (is_file($pathname)) {
$output->writeln('<error>' . $cfg['type'] . ':' . $classname . ' already exists!</error>');
return 1;
$output->writeln('<error>' . $this->type . ':' . $classname . ' already exists!</error>');
return false;
}
if (!is_dir(dirname($pathname))) {
mkdir(dirname($pathname), 0755, true);
}
file_put_contents($pathname, $this->buildClass($classname, $type, $input));
$output->writeln('<info>' . $cfg['type'] . ':' . $classname . ' created successfully.</info>');
return 0;
file_put_contents($pathname, $this->buildClass($classname));
$output->writeln('<info>' . $this->type . ':' . $classname . ' created successfully.</info>');
}
/**
* 解析 stub 路径(controller 支持 --api/--plain)
*/
private function makeStub(string $type, Input $input): string
protected function buildClass(string $name)
{
if ($type === 'controller') {
if ($input->getOption('api')) {
return $this->stubDir() . 'controller.api.stub';
}
if ($input->getOption('plain')) {
return $this->stubDir() . 'controller.plain.stub';
}
return $this->stubDir() . 'controller.stub';
}
return $this->stubDir() . self::MAKE_TYPES[$type]['stub'] . '.stub';
}
/**
* 解析命名空间
*/
private function makeNamespace(string $type, string $app): string
{
return 'app' . ($app ? '\\' . $app : '') . '\\' . self::MAKE_TYPES[$type]['ns'];
}
/**
* 解析完整类名(等价于 think\console\command\Make::getClassName + Controller 后缀)
*/
private function makeClassName(string $type, string $name): string
{
if (str_contains($name, '\\')) {
$class = $name;
} else {
$app = '';
if (str_contains($name, '@')) {
[$app, $name] = explode('@', $name);
}
if (str_contains($name, '/')) {
$name = str_replace('/', '\\', $name);
}
$class = $this->makeNamespace($type, $app) . '\\' . $name;
}
$suffix = self::MAKE_TYPES[$type]['suffix'];
if ($suffix !== '' && $this->app->config->get('route.controller_suffix')) {
$class .= $suffix;
}
return $class;
}
/**
* 解析文件路径(等价于各 Make 命令重写的 getPathName)
*/
private function makePathName(string $name): string
{
return $this->app->getRootPath() . ltrim(str_replace('\\', '/', $name), '/') . '.php';
}
/**
* 用 stub 渲染类文件(等价于 think\console\command\Make::buildClass)
*/
private function buildClass(string $name, string $type, Input $input): string
{
$stub = file_get_contents($this->makeStub($type, $input));
$stub = file_get_contents($this->getStub());
$namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
$class = str_replace($namespace . '\\', '', $name);
return str_replace(
['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'],
[
$class,
$this->app->config->get('route.action_suffix'),
$namespace,
$this->app->getNamespace(),
],
$stub
);
$class = str_replace($namespace . '\\', '', $name);
return str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], [
$class,
$this->app->config->get('route.action_suffix'),
$namespace,
$this->app->getNamespace(),
], $stub);
}
/* ===================== Build 命令 ===================== */
@@ -273,18 +160,20 @@ class AppCommand extends Command
{
$this->basePath = $this->app->getRootPath() . 'app' . DIRECTORY_SEPARATOR;
$app = $app ?: '';
if (is_file($this->basePath . 'build.php')) {
$list = include $this->basePath . 'build.php';
} else {
$list = [
'__dir__' => ['controller', 'model', 'view'],
'__file__' => ['common.php', 'event.php', 'middleware.php', 'service.php'],
'__dir__' => ['controller', 'model', 'view', 'route'],
'controller' => ['Index'],
'model' => [],
'view' => ['index/index'],
'route' => ['app'],
];
}
$this->buildApp($app, $list);
$output->writeln('<info>Successed</info>');
return 0;
}
@@ -296,15 +185,8 @@ class AppCommand extends Command
if (!is_dir($this->basePath . $app)) {
mkdir($this->basePath . $app);
}
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
$namespace = 'app' . ($app ? '\\' . $app : '');
$this->buildCommon($app);
$this->buildHello($app, $namespace);
$this->buildapp1($app, $namespace);
$this->buildInfo($app);
foreach ($list as $path => $file) {
if ('__dir__' == $path) {
foreach ($file as $dir) {
@@ -330,6 +212,9 @@ class AppCommand extends Command
}
$content = '<?php' . PHP_EOL . 'namespace ' . $space . ';' . PHP_EOL . PHP_EOL . 'class ' . $class . PHP_EOL . '{' . PHP_EOL . PHP_EOL . '}';
break;
case 'route':
$content = "<?php" . PHP_EOL . PHP_EOL . "use think\\facade\Route;";
break;
case 'model':
$content = '<?php' . PHP_EOL . 'namespace ' . $space . ';' . PHP_EOL . PHP_EOL . 'use think\Model;' . PHP_EOL . PHP_EOL . 'class ' . $class . ' extends Model' . PHP_EOL . '{' . PHP_EOL . PHP_EOL . '}';
break;
@@ -350,70 +235,7 @@ class AppCommand extends Command
}
}
/**
* 创建应用的欢迎页面
*/
private function buildHello(string $app, string $namespace): void
{
$suffix = $this->app->config->get('route.controller_suffix') ? 'Controller' : '';
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php';
if (!is_file($filename)) {
$content = file_get_contents($this->stubDir() . 'controller.stub');
$content = str_replace(
['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'],
['Index', $suffix, $namespace . '\\controller', $this->app->getNamespace()],
$content
);
$this->checkDirBuild(dirname($filename));
file_put_contents($filename, $content);
}
}
private function buildapp1(string $app, string $namespace): void
{
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
if (!is_file($appPath . 'app.php')) {
$content = file_get_contents($this->stubDir() . 'app.stub');
$content = str_replace(['{%namespace%}', '{%app%}', '{%className%}'], [$namespace, $app, 'app'], $content);
file_put_contents($appPath . 'app.php', $content);
}
}
private function buildInfo(string $app): void
{
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
if (!is_file($appPath . 'info.php')) {
$array = [
'name' => '',
'title' => '',
'intro' => '',
'author' => '',
'website' => '',
'version' => '1.0.0',
'state' => 1,
'url' => '',
'license' => '',
'licenseto' => 0,
];
$array = array_merge($array, ['name' => $app, 'url' => '/' . $app]);
$output = "<?php\n return " . var_export($array, true) . ";\n?>";
file_put_contents($appPath . 'info.php', $output);
}
}
/**
* 创建应用的公共文件
*/
private function buildCommon(string $app): void
{
$appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '');
foreach (['event', 'middleware', 'common', 'config'] as $nm) {
if (!is_file($appPath . $nm . '.php')) {
file_put_contents($appPath . $nm . '.php', '<?php' . PHP_EOL . '// 这是系统自动生成的' . $nm . '定义文件' . PHP_EOL . 'return [' . PHP_EOL . PHP_EOL . '];' . PHP_EOL);
}
}
}
/**
* 创建目录
@@ -433,7 +255,6 @@ class AppCommand extends Command
private function runClear(string $app, Input $input, Output $output): int
{
$runtimePath = $this->app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
if ($input->getOption('cache')) {
$path = $runtimePath . 'cache';
} elseif ($input->getOption('log')) {
@@ -441,12 +262,9 @@ class AppCommand extends Command
} else {
$path = $runtimePath;
}
$rmdir = $input->getOption('dir') ? true : false;
$this->clearRuntime(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir);
$output->writeln('<info>Clear Successed</info>');
return 0;
}
@@ -456,7 +274,6 @@ class AppCommand extends Command
private function clearRuntime(string $path, bool $rmdir): void
{
$files = is_dir($path) ? scandir($path) : [];
foreach ($files as $file) {
if ('.' != $file && '..' != $file && is_dir($path . $file)) {
array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*'));
+3 -3
View File
@@ -5,7 +5,7 @@ namespace {%namespace%};
use think\Request;
class {%className%} extends \ywxapp\controller\AddonFrontend
class {%className%} extends \ywxapp\controller\{%baseClass%}
{
/**
* Summary of needLogin
@@ -33,7 +33,7 @@ class {%className%} extends \ywxapp\controller\AddonFrontend
*/
public function index{%actionSuffix%}()
{
return "这是一个{%app_namespace%} 插件应用";
return "这是一个{%app_namespace%} 插件应用控制器{%namespace%}";
}
/**
@@ -74,7 +74,7 @@ class {%className%} extends \ywxapp\controller\AddonFrontend
* @param int $id
* @return \think\Response
*/
public function edit{%actionSuffix%}($id)
public function edit{%actionSuffix%}($id = null)
{
//
}
+24
View File
@@ -0,0 +1,24 @@
<?php
use think\facade\Route;
// 公共前缀 /{%app%},同时设置默认命名空间为 addon\{%app%}\controller
// 后台 → addon\{%app%}\controller\backend\Index
Route::group('backend', function () {
Route::get('index/index', 'index/index')->name('{%app%}_backend_index');
})->namespace('addon\{%app%}\controller\backend');
// 用户中心 → addon\{%app%}\controller\member\Index
Route::group('member', function () {
Route::get('index/index', 'index/index')->name('{%app%}_member_index');
})->namespace('addon\{%app%}\controller\member');
// API → apaddonp\{%app%}\controller\api\Index
Route::group('api', function () {
Route::get('index/index', 'index/index')->name('{%app%}_api_index');
})->namespace('addon\{%app%}\controller\api');
// 前台 → addon\{%app%}\controller\Index(不加子分组)
Route::get('index/index', 'index/index')->name('{%app%}_index');
+2 -2
View File
@@ -35,7 +35,7 @@ class Addon
if (!isset($info['name'])) {
continue;
}
$installedInfo = Db::name('addon')->where('name', $name)->find();
$installedInfo = Db::name('common_addon')->where('name', $name)->find();
$list[] = [
'name' => $name,
'title' => $info['title'] ?? $name,
@@ -125,7 +125,7 @@ class Addon
} catch (\Exception $e) {
return json(['code' => 404, 'msg' => '插件不存在']);
}
$installed = Db::name('addon')->where('name', $name)->find();
$installed = Db::name('common_addon')->where('name', $name)->find();
return json([
'code' => 200,
+146
View File
@@ -0,0 +1,146 @@
<?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\controller;
use think\App;
/**
* API 控制器基础类(App / 小程序 / 第三方客户端通用)
*
* 统一提供:JWT 登录态注入、未登录 401 拦截、标准响应封装。
* 业务控制器继承本类即可复用登录态(\ywxapp\model\MemberUser)与响应(\ywxapp\library\Result)。
*
* 鉴权约定:
* - 子类通过 $noNeedLogin 声明“免登录即可访问”的 action,['*'] 表示全部放行。
* - 其余 action 在构造阶段统一校验登录态,未登录直接返回 HTTP 401。
* - 统一响应:成功 $this->result->success($data),失败 $this->result->error($msg, $code)。
*
* @package ywxapp\controller
*/
class ApiBase extends BaseController
{
/**
* 无需登录即可访问的 action 列表,['*'] 表示全部放行。
*
* @var array<int, string>
*/
protected $noNeedLogin = [];
/**
* 无需额外校验的 action 列表(兼容旧写法,语义同 $noNeedLogin)。
*
* @var array<int, string>
*/
protected $noNeedVerify = [];
/**
* 构造方法。
*
* 完成父类初始化后,依次调用 initialize()(子类自定义初始化)与
* checkLogin()(统一登录拦截)。
*
* @param App $app 应用实例
*/
public function __construct(App $app)
{
parent::__construct($app);
$this->initialize();
$this->checkLogin();
}
/**
* 控制器初始化钩子,子类可重写以执行构造后的自定义初始化逻辑。
*
* @return void
*/
protected function initialize() {}
/**
* 统一登录拦截。
*
* 当当前 action 不在 $noNeedLogin / $noNeedVerify 白名单,且会员未登录时,
* 直接返回 HTTP 401,阻止后续业务执行。
*
* @return void
*/
protected function checkLogin(): void
{
$action = $this->request->action();
$white = array_merge($this->noNeedLogin, $this->noNeedVerify);
if (in_array('*', $white, true) || in_array($action, $white, true)) {
return;
}
if (!$this->auth || !$this->auth->isLogin) {
$this->result->apiError('请先登录', 401, null, 401);
}
}
/**
* 获取当前登录会员模型(\ywxapp\model\MemberUser)。
*
* 未登录访问已被 checkLogin() 拦截,此处仅作兜底返回。
*
* @return \ywxapp\model\MemberUser 当前登录会员模型
*/
protected function user()
{
if ($this->auth && $this->auth->isLogin) {
return $this->auth->info;
}
$this->result->apiError('请先登录', 401, null, 401);
}
/**
* 获取当前登录会员的主键 ID(uid)。
*
* @return int 会员 uid,未登录时为 0
*/
protected function uid(): int
{
return (int) ($this->auth->model->uid ?? 0);
}
/**
* API 成功响应(强制 JSON,统一契约).
*
* 透传至 \ywxapp\library\Result::apiSuccess():固定 JSON、字段
* code/msg/data/timestamp,自动携带 token 与分页 count。
*
* @param mixed $data 业务数据
* @param string $message 成功提示语
* @param int $count 分页总数(可选)
*
* @return void
*/
protected function apiSuccess($data = null, string $message = 'success', int $count = 0): void
{
$this->result->apiSuccess($data, $message, $count);
}
/**
* API 失败响应(强制 JSON,业务 code 与 HTTP 状态解耦).
*
* 透传至 \ywxapp\library\Result::apiError():固定 JSON、字段
* code/msg/data/timestamp;业务 code 原样透传,HTTP 状态由 $httpStatus 控制。
*
* @param string $message 失败提示语
* @param int $code 业务状态码(默认 1,鉴权失败建议 401)
* @param mixed $data 附加数据(可选)
* @param int $httpStatus HTTP 状态码(默认 200)
*
* @return void
*/
protected function apiError(string $message = 'Error', int $code = 1, $data = null, int $httpStatus = 200): void
{
$this->result->apiError($message, $code, $data, $httpStatus);
}
}
-47
View File
@@ -1,47 +0,0 @@
<?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\controller;
/**
* 控制器基础类
*/
class ApiController extends BaseController
{
// use \ywxapp\traits\Backend;
/**
* 控制器初始化 _initialize
* @return void
*/
public function _initialize()
{
$this->initialize();
}
/** 控制器初始化 initialize
* @return void
*/
protected function initialize() {}
/**
* 获取当前登录用户信息
* \ywxapp\model\Member
*/
protected function user()
{
if ($this->auth && $this->auth->isLogin) {
return $this->auth->info;
}
$this->result->error('获取用户信息失败!');
}
}
+9 -9
View File
@@ -12,10 +12,10 @@ declare(strict_types=1);
namespace ywxapp\controller;
use think\facade\View;
use ywxapp\model\Configure as ConfModel;
use ywxapp\model\Links;
use ywxapp\model\Notice;
use ywxapp\model\Ad;
use ywxapp\model\CommonConfigure as ConfModel;
use ywxapp\model\CommonLinks;
use ywxapp\model\CommonNotice;
use ywxapp\model\CommonAd;
use ywxapp\library\SkinOverlay;
use ywxapp\library\SkinVariables;
use ywxapp\library\TemplateManager;
@@ -77,21 +77,21 @@ abstract class FrontendBase extends BaseController
}
$siteConf = ConfModel::cache(true)->column('value', 'name');
$this->view->assign('siteConf', $siteConf);
$links = Links::cache(true)->where('status', 1)->order('sort desc')->select();
$links = CommonLinks::cache(true)->where('status', 1)->order('sort desc')->select();
$this->view->assign('links', $links);
// 全站置顶公告条(方案 A):仅取当前生效 + 置顶 + 启用的公告
$typeList = Notice::typeList();
$typeList = CommonNotice::typeList();
$topNotices = array_map(function ($n) use ($typeList) {
$n['type_text'] = $typeList[$n['type']] ?? '公告';
return $n;
}, Notice::getActiveTopNotices());
}, CommonNotice::getActiveTopNotices());
$this->view->assign('topNotices', $topNotices);
// 全站广告位:按 position 分组的启用广告,模板可随时调用 {notempty name="adSlots.home_top"}
$this->view->assign('adSlots', Ad::getSlots());
$this->view->assign('adSlots', CommonAd::getSlots());
// 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用
$this->assignSite();
// 前台主导航菜单(后台可配置,两级下拉),供 header.html 渲染
$this->view->assign('navMenus', \app\backend\model\NavMenu::getNavTree());
$this->view->assign('navMenus', \ywxapp\model\CommonNavMenu::getNavTree());
$this->initialize();
}
+2 -2
View File
@@ -81,9 +81,9 @@ class MemberBase extends BaseController
if ($this->auth && $this->auth->isLogin) {
$this->view->assign('member', $this->auth->info);
}
$siteConf = \ywxapp\model\Configure::cache(true)->column('value', 'name');
$siteConf = \ywxapp\model\CommonConfigure::cache(true)->column('value', 'name');
$this->view->assign('siteConf', $siteConf);
$links = \ywxapp\model\Links::cache(true)->where('status', 1)->order('sort desc')->select();
$links = \ywxapp\model\CommonLinks::cache(true)->where('status', 1)->order('sort desc')->select();
$this->view->assign('links', $links);
// 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用
$this->assignSite();
+3 -3
View File
@@ -8,7 +8,7 @@
// +----------------------------------------------------------------------
namespace ywxapp\controller;
use ywxapp\model\Attachment;
use ywxapp\model\CommonAttachment;
use ywxapp\service\FileStorageService;
/**
@@ -141,7 +141,7 @@ class UeditorPlus
{
try {
$user = app()->auth->info;
$datas = Attachment::where('uid', $user->uid)->where('mime', 'like', 'image%')->field('name,url')->select();
$datas = CommonAttachment::where('uid', $user->uid)->where('mime', 'like', 'image%')->field('name,url')->select();
return json([
"state" => "SUCCESS",
"list" => $datas,
@@ -171,7 +171,7 @@ class UeditorPlus
try {
$user = app()->auth->info;
$data['uid'] = $user->uid; // 根据你的登录逻辑获取用户ID
$info = Attachment::create($data);
$info = CommonAttachment::create($data);
return $info;
} catch (\Exception $e) {
// 记录日志,但不要因为入库失败而中断上传流程(或者根据需求决定)
+46
View File
@@ -121,3 +121,49 @@ if (!function_exists('is_market_client')) {
return true;
}
}
if (!function_exists('cross_url')) {
function cross_url(string $appName, string $url = '', array $vars = [], string $suffix = ''): string
{
$map = config('app.domain_bind', []);
$mainDomain = config('app.url_main_domain');
$domainKey = '';
foreach ($map as $key => $mappedApp) {
if ($mappedApp === $appName) {
$domainKey = $key;
break;
}
}
// 解析 host 和 port
$host = '';
$port = '';
if ($domainKey === '') {
$host = true; // 用当前域名
} elseif (str_contains($domainKey, ':')) {
// 带端口:api.example.com:8080
[$host, $port] = explode(':', $domainKey, 2);
$port = ':' . $port;
} elseif (str_contains($domainKey, '.')) {
// 完整域名
$host = $domainKey;
} else {
// 子域名
$host = "{$domainKey}.{$mainDomain}";
}
return (string) \think\facade\Url::build($url)
->vars($vars)
->suffix($suffix)
->domain($host);
}
}
if (!function_exists('L')) {
function L($value)
{
return \think\facade\Lang::get($value);
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ namespace ywxapp\library;
use think\facade\Event;
use ywxapp\library\Result;
use ywxapp\model\Email as EmailModel;
use ywxapp\model\CommonEmail as EmailModel;
use ywxapp\model\MemberUser as UsersModel;
/**
+82 -1
View File
@@ -134,13 +134,18 @@ class Result
}
/**
* 返回封装后的 API 数据到客户端.
* 返回封装后的 API 数据到客户端(已弃用,字段契约与 success/error 不一致).
*
* 请改用 apiSuccess() / apiError():统一 JSON、统一字段(code/msg/data/timestamp),
* 业务 code 与 HTTP 状态码解耦,不再受后台 HTML/401 跳转逻辑影响。
*
* @param mixed $data 要返回的数据
* @param int $code 返回的 code
* @param mixed $msg 提示信息
* @param string $type 返回数据格式
* @param array $header 发送的 Header 信息
*
* @deprecated 使用 apiSuccess() / apiError() 替代
*/
protected function result($data, $code = 0, $msg = '', $type = '', array $header = [])
{
@@ -156,6 +161,82 @@ class Result
throw new HttpResponseException($response);
}
/**
* API 成功响应(强制 JSON,契约稳定).
*
* 专供 App / 小程序 / 第三方客户端调用,与后台 HTML 响应完全解耦:
* - 无论请求是否 ajax,固定返回 JSON;
* - 统一字段:code(0) / msg / data / timestamp;
* - 若本实例持有 token,自动附加 access_token / refresh_token 等字段;
* - 若设置了 count,自动附加 count(分页场景)。
*
* @param mixed $data 业务数据,可为数组 / 模型 / null
* @param string $message 成功提示语
* @param int $count 分页总数(可选,>0 时附加 count 字段)
*
* @return void 通过 Response 直接输出并终止
*/
public function apiSuccess($data = null, string $message = 'success', int $count = 0): void
{
$result = ['code' => 0, 'msg' => $message, 'timestamp' => time()];
if ($data !== null) {
$result['data'] = $data;
}
if ($count > 0) {
$result['count'] = $count;
}
if ($this->AccessToken !== null) {
$result['access_token'] = $this->AccessToken;
$result['access_expired'] = $this->AccessExpired;
}
if ($this->RefreshToken !== null) {
$result['refresh_token'] = $this->RefreshToken;
$result['refresh_expired'] = $this->RefreshExpired;
}
$response = Response::create($result, 'json', $this->StatusCode)->header($this->Headers);
$this->applyTokenCookies($response);
$response->send();
app()->http->end($response);
exit;
}
/**
* API 失败响应(强制 JSON,业务 code 与 HTTP 状态解耦).
*
* 与 apiSuccess 配套,专供客户端调用:
* - 固定返回 JSON;
* - 统一字段:code / msg / data(可选) / timestamp;
* - 业务 code 原样透传(如 401 失败返回 code=401,而非被覆盖);
* - HTTP 状态码由 $httpStatus 控制(默认 200,鉴权失败传 401)。
*
* @param string $message 失败提示语
* @param int $code 业务状态码(默认 1,鉴权失败建议 401)
* @param mixed $data 附加数据(可选)
* @param int $httpStatus HTTP 状态码(默认 200)
*
* @return void 通过 Response 直接输出并终止
*/
public function apiError(string $message = 'Error', int $code = 1, $data = null, int $httpStatus = 200): void
{
$result = ['code' => $code, 'msg' => $message, 'timestamp' => time()];
if ($data !== null) {
$result['data'] = $data;
}
if ($this->AccessToken !== null) {
$result['access_token'] = $this->AccessToken;
$result['access_expired'] = $this->AccessExpired;
}
if ($this->RefreshToken !== null) {
$result['refresh_token'] = $this->RefreshToken;
$result['refresh_expired'] = $this->RefreshExpired;
}
$response = Response::create($result, 'json', $httpStatus)->header($this->Headers);
$this->applyTokenCookies($response);
$response->send();
app()->http->end($response);
exit;
}
/**
* URL 重定向.
*
+14 -4
View File
@@ -1,4 +1,13 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-06 22:05:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-19 22:12:47
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\library\Sms.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
@@ -9,13 +18,13 @@
namespace ywxapp\library;
use ywxapp\utils\Random;
use think\facade\Event;
use ywxapp\model\Sms as SmsModel;
use think\facade\Event;
use ywxapp\model\CommonSms as SmsModel;
/**
* 短信验证码类
*/
class Sms
class Sms
{
/**
@@ -53,7 +62,8 @@ class Sms
* @return boolean
*/
public static function send($mobile, $code = null, $event = 'default')
{
{
$code = is_null($code) ? Random::numeric(6) : $code;
$time = time();
$ip = request()->ip();
+2 -2
View File
@@ -51,8 +51,8 @@ class SpiderStat
protected function record($request, $response, string $spider, string $ua): void
{
$prefix = config('database.connections.mysql.prefix', 'wxapp_');
$logTable = $prefix . 'spider_log';
$statTable = $prefix . 'spider_stat';
$logTable = $prefix . 'common_spider_log';
$statTable = $prefix . 'common_spider_stat';
$data = [
'spider' => $spider,
-69
View File
@@ -1,69 +0,0 @@
<?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;
// }
}
-1
View File
@@ -24,7 +24,6 @@ class BackendAdmin extends BaseModel
{
return [
'strict' => false,
'name' => 'backend_admin',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
+11 -3
View File
@@ -1,4 +1,13 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-06 22:05:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-19 20:51:06
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\model\BackendLog.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
@@ -15,8 +24,7 @@ use ywxapp\model\BaseModel;
*
* @author ywxapp <admin@ywxapp.cn>
*/
class BackendLog extends BaseModel{
protected $name = 'backend_log';
class BackendLog extends BaseModel{
protected $updateTime = false;
/**
* 获取用户的角色
@@ -31,7 +39,7 @@ class BackendLog extends BaseModel{
public function roles()
{
return $this->belongsToMany(Role::class, 'access', 'rid','aid');
return $this->belongsToMany(BackendRole::class, 'access', 'rid','aid');
}
+1 -2
View File
@@ -23,8 +23,7 @@ class BackendPower extends BaseModel
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'backend_power',
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
+16 -17
View File
@@ -11,6 +11,7 @@ namespace ywxapp\model;
use think\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
/**
* BackendRole 类
*
@@ -19,24 +20,22 @@ use ywxapp\model\BaseModel;
class BackendRole extends BaseModel
{
use SoftDelete;
// 设置表名(think-orm 4.0:name 不带前缀,自动拼 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'];
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['password', 'delete_at'],
'readonly' => ['id'],
];
}
// 角色拥有的用户
public function admins()
{
return $this->belongsToMany(BackendAdmin::class, BackendRoleAccess::class, 'admin_id', 'role_id');
@@ -52,7 +51,7 @@ class BackendRole extends BaseModel
}
// 【推荐】角色 → 菜单模型(通过中间表 role_permissions 关联 menus)
public function menus()
{
return $this->belongsToMany(BackendPower::class, BackendRolePower::class, 'power_id', 'role_id', 'permission');
+17 -16
View File
@@ -4,34 +4,35 @@
* @Date: 2026-08-06 22:05:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-10 09:44:50
* @Description:
* @Description: ywxapp/model/BackendPower.php
* @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;
class BackendRoleAccess extends Pivot
{
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at'],
];
}
/**
* 表结构自愈:建表 + 关键列兜底(install.sql 为事实源)。
@@ -41,4 +42,4 @@ class BackendRoleAccess extends Pivot{
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'backend_role_access');
}
}
}
+13 -8
View File
@@ -17,14 +17,19 @@ use think\model\Pivot;
* @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;
{
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => false,
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
];
}
/**
+1 -1
View File
@@ -23,7 +23,7 @@ use think\Model;
*/
class BaseModel extends Model
{
protected $autoWriteTimestamp = 'int';
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
-48
View File
@@ -1,48 +0,0 @@
<?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.0:name 不带前缀,自动拼 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
@@ -1,18 +0,0 @@
<?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
{
}
@@ -4,9 +4,8 @@ namespace ywxapp\model;
use think\facade\Db;
use ywxapp\model\BaseModel;
class Ad extends BaseModel
{
protected $name = 'ad';
class CommonAd extends BaseModel
{
// 广告类型
const TYPE_IMAGE = 1; // 图片
@@ -29,7 +28,6 @@ class Ad extends BaseModel
public function getOptions(): array
{
return [
'name' => 'ad',
'strict' => true,
'autoWriteTimestamp' => true,
'createTime' => 'create_at',
@@ -82,9 +80,9 @@ class Ad extends BaseModel
try {
$p = self::currentPrefix();
$prefix = $p;
$table = $prefix . 'ad';
// 缺表建表(install.sql L638 同款 DDL,运行时前缀兜底)
BaseModel::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$p}ad` (
$table = $prefix . 'common_ad';
// 缺表建表(install.sql 同款 DDL,运行时前缀兜底)
BaseModel::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$p}common_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代码',
@@ -116,7 +114,7 @@ class Ad extends BaseModel
public static function getByPosition(string $position): array
{
self::ensureSchema();
$rows = Db::name('ad')
$rows = Db::name('common_ad')
->where('status', 1)
->where('position', $position)
->where('delete_at', 0)
@@ -140,7 +138,7 @@ class Ad extends BaseModel
self::ensureSchema();
$positions = array_keys(self::positionList());
$slots = array_fill_keys($positions, []);
$rows = Db::name('ad')
$rows = Db::name('common_ad')
->where('status', 1)
->where('position', 'in', $positions)
->where('delete_at', 0)
+48
View File
@@ -0,0 +1,48 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-06 22:05:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-19 20:51:29
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\model\AddonModel.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
class CommonAddon extends BaseModel
{
protected function getOptions(): array
{
return [
'strict' => true,
'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',
]
];
}
}
@@ -13,8 +13,9 @@ namespace ywxapp\model;
use think\Model;
class Attachment extends BaseModel
{
class CommonAttachment extends BaseModel
{
// 设置字段信息
protected $schema = [
//文件ID
@@ -90,7 +91,7 @@ class Attachment extends BaseModel
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'attachment');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'common_attachment');
}
/**
@@ -1,5 +1,15 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-07 10:22:47
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-20 00:36:36
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\model\CommonCard.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
@@ -10,18 +20,23 @@ use think\model\concern\SoftDelete;
/**
* 充值卡密
*/
class Card extends BaseModel
class CommonCard 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';
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
// 卡密状态
const STATUS_UNSOLD = 0; // 未售
@@ -33,7 +48,7 @@ class Card extends BaseModel
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'card');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'common_card');
}
/**
@@ -88,9 +103,9 @@ class Card extends BaseModel
$card->save();
// 2) 会员钱包加余额 + 累计充值(无钱包则自动创建)
$wallet = Db::name('member_wallets')->where('uid', $uid)->find();
$wallet = Db::name('member_wallet')->where('uid', $uid)->find();
if (empty($wallet)) {
Db::name('member_wallets')->insert([
Db::name('member_wallet')->insert([
'uid' => $uid,
'balance' => $amount,
'total_recharge' => $amount,
@@ -98,7 +113,7 @@ class Card extends BaseModel
'update_at' => time(),
]);
} else {
Db::name('member_wallets')
Db::name('member_wallet')
->where('uid', $uid)
->inc('balance', $amount)
->inc('total_recharge', $amount)
@@ -14,15 +14,27 @@ namespace ywxapp\model;
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Configure extends BaseModel
class CommonConfigure extends BaseModel
{
protected function getOptions(): array
{
return [
'strict' => false,
'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 function getTitleAttr($value)
{
return lang($value);
}
public function getGroupAttr($value)
{
return lang($value);
@@ -33,7 +45,7 @@ class Configure extends BaseModel
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'configure');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'common_configure');
}
/**
@@ -14,8 +14,21 @@ namespace ywxapp\model;
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Email extends BaseModel{
class CommonEmail extends BaseModel{
// 设置表名(think-orm 4.0:name 不带前缀,自动拼 wxapp_)
protected $name = 'email';
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
}
@@ -1,5 +1,16 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-07 10:22:47
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-19 21:08:06
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\model\Help.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
@@ -9,25 +20,29 @@ use think\model\concern\SoftDelete;
/**
* 站点帮助
*/
class Help extends BaseModel
class CommonHelp 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';
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'readonly' => ['id'],
];
}
/**
* 运行时自愈:确保 help 主表存在(install.sql 已含 category/view_count 等列,为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'help');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'common_help');
}
/**
@@ -39,4 +54,4 @@ class Help extends BaseModel
parent::initialize();
self::ensureSchema();
}
}
}
@@ -14,7 +14,7 @@ use think\model\concern\SoftDelete;
use ywxapp\model\BaseModel;
class Links extends BaseModel
class CommonLinks extends BaseModel
{
use SoftDelete;
@@ -29,7 +29,6 @@ class Links extends BaseModel
{
return [
'strict' => true,
'name' => 'links',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
@@ -41,7 +40,7 @@ class Links extends BaseModel
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'links');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'common_links');
}
/**
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
@@ -10,18 +12,24 @@ use think\model\concern\SoftDelete;
/**
* 勋章中心
*/
class Medal extends BaseModel
class CommonMedal 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 getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 控制器初始化(模型实例方法,非静态)
@@ -74,7 +82,7 @@ class Medal extends BaseModel
}
return Db::name('member_medal')
->alias('um')
->join('medal m', 'm.id = um.medal_id')
->join('common_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')
@@ -89,7 +97,7 @@ class Medal extends BaseModel
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'medal');
BaseModel::ensureTableFromInstall($prefix, 'common_medal');
BaseModel::ensureTableFromInstall($prefix, 'member_medal');
}
}
}
+133
View File
@@ -0,0 +1,133 @@
<?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;
/**
* 前台主导航菜单模型(支持两级下拉).
*/
class CommonNavMenu extends BaseModel
{
use SoftDelete;
protected $deleteTime = 'delete_at';
protected function getOptions(): array
{
return [
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'append' => ['target'],
'hidden' => ['create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
];
}
/**
* 运行时自愈:确保 common_nav_menu 主表存在(install.sql 为事实源),
* 老库补充 app 字段(跨应用链接,见 ensureColumn).
*/
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'common_nav_menu');
BaseModel::ensureColumn(
$prefix . 'common_nav_menu',
'app',
"varchar(30) NOT NULL DEFAULT '' COMMENT '所属应用(跨应用链接用,空=当前应用)' AFTER `url`"
);
}
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
/**
* 获取启用的导航树(两级:父 + 子)。
* @return array [{id,title,url,icon,child:[...]}]
*/
public static function getNavTree(): array
{
$list = self::where('status', 1)
->where('delete_at', 0)
->order('sort', 'asc')
->order('id', 'asc')
->field('id,parent_id,title,url,app,icon,sort')
->select()
->toArray();
$parents = [];
$childrenMap = [];
foreach ($list as $item) {
$item['ctrl'] = strtolower(strtok($item['url'], '/') ?: '');
if ((int) $item['parent_id'] === 0) {
$item['child'] = [];
$parents[] = $item;
} else {
$childrenMap[$item['parent_id']][] = $item;
}
}
foreach ($parents as &$p) {
if (isset($childrenMap[$p['id']])) {
$p['child'] = $childrenMap[$p['id']];
}
}
return $parents;
}
/**
* 后台列表:返回两级扁平树(含隐藏项).
*
* @param string $title 可选:按菜单名称模糊筛选(命中子项时自动带上其父级)
*/
public static function getAdminTree(string $title = ''): array
{
$query = self::where('delete_at', 0);
if ($title !== '') {
$matchIds = self::where('delete_at', 0)->whereLike('title', "%{$title}%")->column('id');
if (empty($matchIds)) {
return [];
}
$parentIds = self::where('delete_at', 0)->whereIn('parent_id', $matchIds)->column('parent_id');
$ids = array_values(array_unique(array_merge($matchIds, $parentIds)));
$query->whereIn('id', $ids);
}
$list = $query->order('sort', 'asc')
->order('id', 'asc')
->field('id,parent_id,title,url,app,icon,sort,status')
->select()
->toArray();
$parents = [];
$childrenMap = [];
foreach ($list as $item) {
$item['ctrl'] = strtolower(strtok($item['url'], '/') ?: '');
if ((int) $item['parent_id'] === 0) {
$item['child'] = [];
$parents[] = $item;
} else {
$childrenMap[$item['parent_id']][] = $item;
}
}
foreach ($parents as &$p) {
if (isset($childrenMap[$p['id']])) {
$p['child'] = $childrenMap[$p['id']];
}
}
return $parents;
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
@@ -8,13 +10,10 @@ use think\model\concern\SoftDelete;
/**
* 站点公告
*/
class Notice extends BaseModel
class CommonNotice 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;
@@ -35,9 +34,6 @@ class Notice extends BaseModel
'delete_at' => 'integer',
];
// 只读字段
protected $readonly = ['view_count'];
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
@@ -45,10 +41,10 @@ class Notice extends BaseModel
{
return [
'strict' => true,
'name' => 'notice',
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'readonly' => ['view_count']
];
}
@@ -121,9 +117,9 @@ class Notice extends BaseModel
try {
$p = self::currentPrefix();
$prefix = $p;
$table = $prefix . 'notice';
// 缺表建表(install.sql L618 同款 DDL,运行时前缀兜底)
self::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$p}notice` (
$table = $prefix . 'common_notice';
// 缺表建表(install.sql 同款 DDL,运行时前缀兜底)
self::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$p}common_notice` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL DEFAULT '' COMMENT '公告标题',
`content` text COMMENT '公告内容',
@@ -152,4 +148,4 @@ class Notice extends BaseModel
// 忽略自愈失败,由业务暴露
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare (strict_types = 1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
// use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class CommonProp extends BaseModel
{
//use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
//'deleteTime' => 'delete_at',
//'hidden' => ['password', 'delete_at'],
//'append' => ['status_text', 'last_login'],
];
}
}
@@ -8,19 +8,25 @@ use think\model\concern\SoftDelete;
/**
* 电子商务
*/
class Shop extends BaseModel
class CommonShop 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';
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'readonly' => ['view_count']
];
}
/**
* 运行时自愈:确保 shop 主表存在(install.sql 为事实源)。
*/
@@ -8,18 +8,25 @@ use think\model\concern\SoftDelete;
/**
* 短信服务
*/
class Sms extends BaseModel
class CommonSms 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';
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'readonly' => ['view_count']
];
}
/**
* 运行时自愈:确保 sms 主表存在(install.sql 为事实源),并幂等补 delete_at 软删除列。
@@ -27,8 +34,8 @@ class Sms extends BaseModel
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'sms');
self::ensureColumn($prefix . 'sms', 'delete_at', "int NOT NULL DEFAULT 0 COMMENT '删除时间(软删除)'");
BaseModel::ensureTableFromInstall($prefix, 'common_sms');
self::ensureColumn($prefix . 'common_sms', 'delete_at', "int NOT NULL DEFAULT 0 COMMENT '删除时间(软删除)'");
}
/**
+28
View File
@@ -0,0 +1,28 @@
<?php
declare (strict_types = 1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
// use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class CommonSmsTemplate extends BaseModel
{
//use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'name' => 'sms_template',
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
//'deleteTime' => 'delete_at',
//'hidden' => ['password', 'delete_at'],
//'append' => ['status_text', 'last_login'],
];
}
}
@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use ywxapp\model\Medal;
use ywxapp\model\CommonMedal;
use think\facade\Db;
use think\model\concern\SoftDelete;
@@ -11,18 +11,25 @@ use think\model\concern\SoftDelete;
/**
* 站点任务
*/
class Task extends BaseModel
class CommonTask 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';
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'deleteTime' => 'delete_at',
'defaultSoftDelete' => 0,
'readonly' => ['view_count']
];
}
// 奖励类型
const REWARD_SCORE = 1; // 积分
@@ -100,19 +107,19 @@ class Task extends BaseModel
// 发放奖励
switch ((int) $task['reward_type']) {
case self::REWARD_SCORE:
[$ok, $msg] = \ywxapp\model\Score::change($uid, \ywxapp\model\Score::TYPE_GAIN, $num, '完成任务:' . $task['title']);
[$ok, $msg] = \ywxapp\model\MebmberScore::change($uid, \ywxapp\model\MebmberScore::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_wallet')->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,
'balance' => Db::name('member_wallet')->where('uid', $uid)->value('coins') ?: $num,
'remark' => '完成任务:' . $task['title'],
'create_at' => time(),
]);
@@ -127,7 +134,7 @@ class Task extends BaseModel
break;
case self::REWARD_MEDAL:
// reward_num 存勋章ID
$grant = Medal::grant($uid, $num);
$grant = CommonMedal::grant($uid, $num);
if (!$grant[0]) {
throw new \Exception($grant[1]);
}
@@ -159,10 +166,10 @@ class Task extends BaseModel
public static function ensureSchema(): void
{
$prefix = BaseModel::currentPrefix();
BaseModel::ensureTableFromInstall($prefix, 'task');
BaseModel::ensureTableFromInstall($prefix, 'common_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 '道具数量(奖励类型为道具时生效)'");
BaseModel::ensureColumn("{$prefix}member_user", 'score', "int unsigned NOT NULL DEFAULT 0 COMMENT '积分(做任务/互动获得)'");
BaseModel::ensureColumn("{$prefix}common_task", 'prop_num', "int NOT NULL DEFAULT 1 COMMENT '道具数量(奖励类型为道具时生效)'");
}
}
@@ -1,4 +1,13 @@
<?php
/*
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-07 22:53:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-19 21:12:33
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\model\Score.php
* @CustomString: Copyright (c) 2026 YwxApp
*/
declare(strict_types=1);
namespace ywxapp\model;
@@ -10,17 +19,23 @@ use think\model\concern\SoftDelete;
/**
* 积分规则 & 积分流水
*/
class Score extends BaseModel
class MebmberScore 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';
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'readonly' => ['view_count']
];
}
// 规则类型
const TYPE_GAIN = 1; // 获取
@@ -40,9 +55,9 @@ class Score extends BaseModel
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 '积分(做任务/互动获得)'");
BaseModel::ensureTableFromInstall($prefix, 'member_score_rule');
BaseModel::ensureTableFromInstall($prefix, 'member_score_log');
BaseModel::ensureColumn("{$prefix}member_user", 'score', "int unsigned NOT NULL DEFAULT 0 COMMENT '积分(做任务/互动获得)'");
}
/**
@@ -66,7 +81,7 @@ class Score extends BaseModel
Db::startTrans();
try {
$user = Db::name('member')->where('uid', $uid)->lock(true)->find();
$user = Db::name('member_user')->where('uid', $uid)->lock(true)->find();
if (!$user) {
throw new \Exception('用户不存在');
}
@@ -77,9 +92,9 @@ class Score extends BaseModel
? $user['score'] - $value
: $user['score'] + $value;
Db::name('member')->where('uid', $uid)->update(['score' => $balance]);
Db::name('member_user')->where('uid', $uid)->update(['score' => $balance]);
Db::name('score_log')->insert([
Db::name('member_score_log')->insert([
'uid' => $uid,
'rule_id' => $ruleId,
'type' => $type,
@@ -103,7 +118,7 @@ class Score extends BaseModel
public static function logList(int $uid = 0, int $page = 1, int $limit = 15): array
{
self::ensureSchema();
$query = Db::name('score_log');
$query = Db::name('member_score_log');
if ($uid > 0) {
$query->where('uid', $uid);
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare (strict_types = 1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
// use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class MemberBill extends BaseModel
{
//use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
//'deleteTime' => 'delete_at',
//'hidden' => ['password', 'delete_at'],
//'append' => ['status_text', 'last_login'],
];
}
}
+1 -2
View File
@@ -23,8 +23,7 @@ class MemberGroup extends BaseModel
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_group',
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
+1 -2
View File
@@ -22,8 +22,7 @@ class MemberGroupAccess extends Pivot
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_group_access',
'strict' => false,
'autoWriteTimestamp' => 'int', // create_at 已统一为 int 时间戳
'createTime' => 'create_at',
'updateTime' => false,
+2 -6
View File
@@ -20,14 +20,10 @@ class MemberGroupRule extends Model
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_group_rule',
'strict' => false,
'autoWriteTimestamp' => 'int', // create_at 已统一为 int 时间戳
'createTime' => 'create_at',
'updateTime' => false,
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'updateTime' => false,
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
+2 -6
View File
@@ -19,14 +19,10 @@ use ywxapp\model\BaseModel;
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'member_log',
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => false,
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'updateTime' => false,
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
+27
View File
@@ -0,0 +1,27 @@
<?php
declare (strict_types = 1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
// use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class MemberMedal extends BaseModel
{
//use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
//'deleteTime' => 'delete_at',
//'hidden' => ['password', 'delete_at'],
//'append' => ['status_text', 'last_login'],
];
}
}
-4
View File
@@ -19,13 +19,9 @@ class MemberProfile extends BaseModel
{
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'],
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
use think\model\concern\SoftDelete;
/**
* 用户持有道具(member_prop)
*/
class MemberProp extends BaseModel
{
use SoftDelete;
/**
* 模型配置(BaseModel::applyOptions 约定)
*/
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
'readonly' => ['view_count']
];
}
/**
* 运行时自愈:确保 member_prop 表存在(install.sql 为事实源)。
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_prop');
}
/**
* 初始化:自愈建表,避免远程库缺失 member_prop 导致 1146。
*/
protected function initialize()
{
parent::initialize();
self::ensureSchema();
}
}
-2
View File
@@ -24,13 +24,11 @@ class MemberRule extends BaseModel
{
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'],
@@ -18,25 +18,21 @@ use ywxapp\model\BaseModel;
*
* @author ywxapp <admin@ywxapp.cn>
*/
class ScoreLog extends BaseModel
class MemberScoreLog extends BaseModel
{
protected function initialize()
{
parent::initialize();
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'score_log');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_score_log');
}
protected function getOptions(): array
{
return [
'strict' => false,
'name' => 'score_log',
'strict' => false,
'autoWriteTimestamp' => 'int',
'createTime' => 'create_at',
'updateTime' => 'update_at',
// 'deleteTime' => 'delete_at',
// 'defaultSoftDelete' => 0,
// 'dateFormat' => 'Y-m-d H:i:s',
'updateTime' => 'update_at',
'append' => ['is_parent'],
'hidden' => ['password', 'create_at', 'update_at', 'delete_at'],
'readonly' => ['id'],
@@ -49,6 +45,6 @@ class ScoreLog extends BaseModel
*/
public static function ensureSchema(): void
{
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'score_log');
BaseModel::ensureTableFromInstall(BaseModel::currentPrefix(), 'member_score_log');
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare (strict_types = 1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
// use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class MemberTask extends BaseModel
{
//use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
//'deleteTime' => 'delete_at',
//'hidden' => ['password', 'delete_at'],
//'append' => ['status_text', 'last_login'],
];
}
}
+10 -21
View File
@@ -26,7 +26,6 @@ class MemberUser extends BaseModel
{
return [
'strict' => true,
'name' => 'member_user',
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
@@ -82,31 +81,21 @@ class MemberUser extends BaseModel
->column('rid');
}
// 密码加密
// 密码加密:bcrypt 自带盐,无需独立 salt 列
// 兼容旧账户(若库里是 password_hash(value.salt) 的老哈希,
// 这里统一改为 password_hash(value),老账户需要先重置密码)。
public function setPasswordAttr($value)
{
$this->salt = $this->generateSalt();
return password_hash($value . $this->salt, PASSWORD_DEFAULT);
return password_hash($value, 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;
}
// 验证密码
// 验证密码:bcrypt 哈希内已含盐,直接验证即可
public function checkPassword($password)
{
return password_verify($password . $this->salt, $this->password);
if (empty($this->password)) {
return false;
}
return password_verify($password, $this->password);
}
// 获取状态文本
@@ -194,7 +183,7 @@ class MemberUser extends BaseModel
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) {
foreach (['member_user', 'member_bill', 'member_group', 'member_group_access', 'member_group_rule', 'member_log', 'member_profile', 'member_rule', 'member_wallet'] as $t) {
BaseModel::ensureTableFromInstall($prefix, $t);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare (strict_types = 1);
namespace ywxapp\model;
use ywxapp\model\BaseModel;
// use think\model\concern\SoftDelete;
/**
* @mixin \think\Model
*/
class MemberWallet extends BaseModel
{
//use SoftDelete;
protected function getOptions(): array
{
return [
'strict' => true,
'autoWriteTimestamp' => 'int',
'readonly' => ['uid'],
'createTime' => 'create_at',
'updateTime' => 'update_at',
//'deleteTime' => 'delete_at',
//'hidden' => ['password', 'delete_at'],
//'append' => ['status_text', 'last_login'],
];
}
}
-40
View File
@@ -1,40 +0,0 @@
<?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();
}
}
+6
View File
@@ -214,6 +214,12 @@ class AddonDevService
$data[] = $item;
}
$this->writeReturnArray($this->addonDir . 'config.php', $data);
// 设计器改了 config.php 静态默认值后,清运行时配置缓存与插件加载缓存,
// 使下次 readConfig() 回源重新合并(文件默认值 + 数据库已保存值),
// 否则仅改了文件、DB/缓存未动,运行时 config('插件名') 仍读旧值。
\ywxapp\service\AddonService::clearConfigCache($this->addon);
\ywxapp\service\AppService::clearAddonCache();
}
/**
+16 -8
View File
@@ -1334,7 +1334,7 @@ class AddonService
if ($config === null || !empty($changed)) {
self::ensureConfigTable();
// 从统一配置表读取「独立项」(addon + name 唯一,每行一个配置项)
$rows = Db::name('addon_config')->where('addon', $name)->column('value', 'name');
$rows = Db::name('common_addonconf')->where('addon', $name)->column('value', 'name');
$config = $rows ?: [];
// 平滑迁移:若该插件此前用 .addonrc 文件存过配置,则一次性导入数据库
if (empty($config)) {
@@ -1372,14 +1372,14 @@ class AddonService
/**
* 自愈建表:确保统一插件配置表存在(CREATE TABLE IF NOT EXISTS)
* 表结构:wxapp_addon_config,每个配置项独立成行(addon + name 唯一)
* 表结构:{prefix}common_addonconf,每个配置项独立成行(addon + name 唯一)
*/
private static function ensureConfigTable(): void
{
try {
$defaultDb = Config::get('database.default');
$prefix = Config::get("database.connections.{$defaultDb}.prefix");
$table = $prefix . 'addon_config';
$table = $prefix . 'common_addonconf';
$exists = Db::query("SHOW TABLES LIKE '{$table}'");
if (empty($exists)) {
Db::execute("CREATE TABLE IF NOT EXISTS `{$table}` (
@@ -1407,7 +1407,7 @@ class AddonService
{
$defaultDb = Config::get('database.default');
$prefix = Config::get("database.connections.{$defaultDb}.prefix");
$table = $prefix . 'addon_config';
$table = $prefix . 'common_addonconf';
$time = time();
foreach ($values as $key => $val) {
$val = is_array($val) ? json_encode($val, JSON_UNESCAPED_UNICODE) : (string) $val;
@@ -1508,7 +1508,7 @@ class AddonService
public function upgrade(string $currentVersion = ''): bool
{
if ($currentVersion === '') {
$installed = \ywxapp\model\AddonModel::where('name', $this->addon)->value('version');
$installed = \ywxapp\model\CommonAddon::where('name', $this->addon)->value('version');
$currentVersion = $installed ?: '';
}
$this->callAddonHook('upgrade', [$currentVersion]);
@@ -1951,6 +1951,13 @@ class AddonService
} catch (\PDOException $e) {
Log::error("Addon {$operation} error (Database): " . $e->getMessage());
throw new Exception(lang('数据库操作失败'));
} catch (\Error $e) {
Log::error("Addon {$operation} error (Error): " . $e->getMessage());
// 开发环境透传原始错误信息便于定位;生产环境统一返回通用提示,避免泄露路径等内部细节
if (Env::get('app_debug')) {
throw new Exception('插件操作失败:' . $e->getMessage());
}
throw new Exception('插件操作失败,请稍后重试或联系管理员');
} catch (\Exception $e) {
Log::error("Addon {$operation} error: " . $e->getMessage());
// 开发环境透传原始异常信息(如 Addon already exists / version incorrect)便于定位;
@@ -1958,7 +1965,7 @@ class AddonService
if (Env::get('app_debug')) {
throw new Exception('插件操作失败:' . $e->getMessage());
}
throw new Exception('插件操作失败,请稍后重试或联系管理员' . $e->getMessage());
throw new Exception('插件操作失败,请稍后重试或联系管理员');
}
}
@@ -2565,8 +2572,9 @@ class AddonService
// 不存在(即未指定可用顶级菜单)则回退到「以插件名为标题的顶级菜单」。
// 2) menu.json 未声明 superior:直接生成一个以插件名为标题的顶级菜单(pid=0),
// 所有菜单都注入到这个插件名顶级菜单下。
$superior = $menu['superior'] ?? '';
if ($superior !== '') {
$superior = $menu['parent'] ?? 'app';
$isRoot = $menu['is_root'] ?? false;
if ($isRoot== false) {
$appPid = $this->getOrCreateAppMenuId($superior);
if ($appPid > 0) {
// 指定顶级菜单存在:在其下挂「插件名分组」,子项再挂该分组下
+38 -17
View File
@@ -3,7 +3,7 @@
* @Author: YwxApp <ywx@ywxapp.cn>
* @Date: 2026-08-06 22:05:48
* @LastEditors: YwxApp <ywx@ywxapp.cn>
* @LastEditTime: 2026-08-13 16:46:11
* @LastEditTime: 2026-08-20 11:19:15
* @Description:
* @FilePath: \ywxapp_dev\ywxapp\service\AppService.php
* @CustomString: Copyright (c) 2026 YwxApp
@@ -74,7 +74,7 @@ class AppService extends Service
continue;
}
try {
\think\facade\Config::set(\ywxapp\addon::readConfig($name), $name);
\think\facade\Config::set(\ywxapp\AddonBase::readConfig($name), $name);
} catch (\Throwable $e) {
// 单个插件预热失败不影响主流程
}
@@ -82,23 +82,29 @@ class AppService extends Service
});
// 默认短信发送监听:开发环境不实际下发(仅记录验证码到日志并回传前端用于调试),
// 生产环境返回 null,交由短信插件(监听 SmsSend 事件)实际发送。
\think\facade\Event::listen('SmsSend', function ($sms) {
if (Env::get('app_debug')) {
\think\facade\Log::info('[DEV] 短信验证码(未实际下发): ' . ($sms['code'] ?? '') . ' -> ' . ($sms['mobile'] ?? ''));
return true;
}
return null;
});
// \think\facade\Event::listen('SmsSend', function ($sms) {
// if (Env::get('app_debug')) {
// \think\facade\Log::info('[DEV] 短信验证码(未实际下发): ' . ($sms['code'] ?? '') . ' -> ' . ($sms['mobile'] ?? ''));
// return true;
// }
// return null;
// });
}
public function boot()
{
$this->app->middleware->import([MultiApp::class], 'global');
// 2. 替换 url() 使用的 UrlBuild 类
$this->app->bind(\think\route\Url::class, UrlBuild::class);
$this->commands([
// 插件命令自动注册约定:
// 插件在 info.php 声明 'commands' => ['插件名:动作' => \addon\<插件名>\command\Xxx::class]
// 命令类 extends think\console\Command,psr-4 (addon\ => addon) 自动加载。
// 执行链:Console 构造 → app->initialize() → BootService 触发本 boot() →
// $this->commands() 挂 Console::starting 回调 → Console::run() 统一 addCommands。
// 坏类名/非 Command 子类会被 addCommands 跳过,单个插件声明错误不影响整体。
$commands = [
'app' => \ywxapp\command\AppCommand::class,
'addon:manage' => \ywxapp\command\AddonManage::class,
'addon:make' => \ywxapp\command\AddonMake::class,
@@ -106,7 +112,14 @@ class AppService extends Service
'addon:license-check' => \ywxapp\command\AddonLicenseCheck::class,
'addon:repair' => \ywxapp\command\AddonRepair::class,
'ywxapp:upgrade' => \ywxapp\command\Upgrade::class,
]);
];
foreach ($this->getEnabledaddon() as $info) {
$addonCmds = $info['commands'] ?? [];
if (is_array($addonCmds) && $addonCmds) {
$commands = array_merge($commands, $addonCmds);
}
}
$this->commands($commands);
$this->loadAddonRelevant();
// $this->loadAddonRoutes();
Route::rule('ueditor', function () {
@@ -252,10 +265,18 @@ class AppService extends Service
public static function clearAddonCache(): void
{
\think\facade\Cache::delete('addon_loaded_config');
// 同时清除配置缓存
$addon = \think\facade\Config::get('addon', []);
foreach ($addon as $addon) {
\think\facade\Cache::delete('addon_config_' . $addon['name']);
// 同时清除各插件的配置缓存。
// 注意:Config::get('addon') 返回的是框架 addon 组件自身配置(含 multi_app/priority 等),
// 并非插件列表,切勿直接 foreach 它(遍历到 multi_app=true 会触发 "array offset on true")。
// 正确的插件名来源是 addon 主表的 name 列。
try {
$names = \ywxapp\model\CommonAddon::column('name');
foreach ($names as $name) {
\think\facade\Cache::delete('addon_config_' . $name);
}
} catch (\Throwable $e) {
// 数据表可能尚未创建,忽略
\think\facade\Log::warning('clearAddonCache 获取插件列表失败:' . $e->getMessage());
}
}
@@ -270,7 +291,7 @@ class AppService extends Service
return true; // 无市场插件 → 无授权域,放行
}
try {
$addonId = \ywxapp\model\AddonModel::where('name', $addon)->value('id');
$addonId = \ywxapp\model\CommonAddon::where('name', $addon)->value('id');
if (!$addonId) {
return false;
}
+6 -6
View File
@@ -23,9 +23,9 @@ class WalletService
*/
public static function ensure(int $uid): void
{
$exist = Db::name('member_wallets')->where('uid', $uid)->value('id');
$exist = Db::name('member_wallet')->where('uid', $uid)->value('id');
if (! $exist) {
Db::name('member_wallets')->insert([
Db::name('member_wallet')->insert([
'uid' => $uid,
'balance' => 0,
'coins' => 0,
@@ -43,7 +43,7 @@ class WalletService
public static function getCoins(int $uid): int
{
self::ensure($uid);
return (int) Db::name('member_wallets')->where('uid', $uid)->value('coins');
return (int) Db::name('member_wallet')->where('uid', $uid)->value('coins');
}
/**
@@ -57,7 +57,7 @@ class WalletService
self::ensure($uid);
Db::startTrans();
try {
Db::name('member_wallets')->where('uid', $uid)->inc('coins', $num)->update(['update_at' => time()]);
Db::name('member_wallet')->where('uid', $uid)->inc('coins', $num)->update(['update_at' => time()]);
Db::name('member_bill')->insert([
'uid' => $uid,
'type' => 1,
@@ -85,13 +85,13 @@ class WalletService
return true;
}
self::ensure($uid);
$cur = Db::name('member_wallets')->where('uid', $uid)->lock(true)->value('coins');
$cur = Db::name('member_wallet')->where('uid', $uid)->lock(true)->value('coins');
if ($cur < $num) {
throw new \Exception('金币不足');
}
Db::startTrans();
try {
Db::name('member_wallets')->where('uid', $uid)
Db::name('member_wallet')->where('uid', $uid)
->dec('coins', $num)
->inc('total_consume', $num)
->update(['update_at' => time()]);