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
+201
View File
@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace addon\download\controller;
use think\facade\Config;
use think\facade\Lang;
use think\facade\Cookie;
use think\Response;
use ywxapp\controller\FrontendBase;
use addon\download\model\Category;
use addon\download\model\Resource;
use addon\download\service\DownloadService;
class Index extends FrontendBase
{
protected $noNeedLogin = ['*'];
protected $noNeedVerify = ['*'];
public function initialize()
{
// 多语言:支持 ?lang=zh-cn|en-us 切换并写 cookie;插件级 lang 显式加载兜底
$lang = $this->request->param('lang', '');
if (in_array($lang, ['zh-cn', 'en-us'], true)) {
Lang::setLangSet($lang);
Cookie::set('download_lang', $lang);
} elseif ($cookie = Cookie::get('download_lang')) {
Lang::setLangSet($cookie);
}
// 固定页面文案注入模板
$this->view->assign([
'site_name' => lang('site_name'),
'all_category_text' => lang('all_category'),
'hot_text' => lang('hot'),
'latest_text' => lang('latest'),
'list_text' => lang('list'),
'detail_text' => lang('detail'),
'search_text' => lang('search'),
'search_placeholder' => lang('search_placeholder'),
'download_btn_text' => lang('download_btn'),
]);
// 插件前台接入「全站动态布局」:视图只写正文,由核心 frontend 的
// common/layout.html 包裹全站 header / footer(与开发文档约定一致)。
// 注意:layout_name 必须带 .html 扩展名,否则 think-template 的
// parseTemplateFile() 会把无扩展名的绝对路径里的盘符冒号(D:)当成
// 模板分隔符替换成当前 viewPath,导致「模板文件不存在」。
$this->view->config([
'layout_on' => true,
'layout_name' => $this->app->getRootPath() . 'app' . DIRECTORY_SEPARATOR
. 'frontend' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR
. 'common' . DIRECTORY_SEPARATOR . 'layout.html',
]);
}
/**
* 读取本插件配置(运行时由 AppInit 注入 config('download')
*/
protected function pluginConfig(): array
{
return Config::get('download', []);
}
/**
* 首页:分类导航 + 热门/最新(按 config 开关)
*/
public function index()
{
$cfg = $this->pluginConfig();
$categories = Category::with(['resources' => function ($q) {
$q->order('downloads', 'desc')->limit(8);
}])
->where('pid', 0)
->where('status', 1)
->order('sort', 'desc')
->select();
$hot = [];
$news = [];
if (!empty($cfg['show_hot'])) {
$hot = Resource::scope('visible')->order('downloads', 'desc')->limit(10)->select();
}
if (!empty($cfg['show_new'])) {
$news = Resource::scope('visible')->order('id', 'desc')->limit(10)->select();
}
$this->view->assign('categories', $categories);
$this->view->assign('hot', $hot);
$this->view->assign('news', $news);
$this->view->assign('config', $cfg);
return $this->view->fetch();
}
/**
* 分类列表页
*/
public function list()
{
$cid = $this->request->param('cid/d', 0);
$page = $this->request->param('page/d', 1);
$cfg = $this->pluginConfig();
$size = (int)($cfg['page_size'] ?? 20);
$cat = $cid ? Category::find($cid) : null;
if ($cid && !$cat) {
$this->error('分类不存在');
}
$list = Resource::scope('visible');
if ($cid) {
$list = $list->where('cid', $cid);
}
$list = $list->order('downloads', 'desc')
->paginate(['page' => $page, 'list_rows' => $size]);
$this->view->assign('cat', $cat);
$this->view->assign('list', $list);
$this->view->assign('pager', $list->render());
$this->view->assign('config', $cfg);
return $this->view->fetch();
}
/**
* 详情页
*/
public function detail()
{
$id = $this->request->param('id/d', 0);
$resource = Resource::scope('visible')->find($id);
if (!$resource) {
$this->error('资源不存在或已下架');
}
// 查看计数(防刷交给 service 内的简单锁,此处仅点击+1)
DownloadService::incClicks($id);
$cat = $resource['cid'] ? Category::find($resource['cid']) : null;
$this->view->assign('resource', $resource);
$this->view->assign('cat', $cat);
$this->view->assign('config', $this->pluginConfig());
return $this->view->fetch();
}
/**
* 搜索
*/
public function search()
{
$q = $this->request->param('q', $this->request->param('kw', ''));
$page = $this->request->param('page/d', 1);
$cfg = $this->pluginConfig();
$size = (int)($cfg['page_size'] ?? 20);
$list = Resource::scope('visible');
if ($q) {
$list = $list->whereLike('title', "%{$q}%");
}
$list = $list->order('downloads', 'desc')
->paginate(['page' => $page, 'list_rows' => $size]);
$this->view->assign('keyword', $q);
$this->view->assign('list', $list);
$this->view->assign('pager', $list->render());
$this->view->assign('config', $cfg);
return $this->view->fetch();
}
/**
* 触发下载:外链 302 跳转 / 本地文件流式输出
*/
public function down()
{
$id = $this->request->param('id/d', 0);
$ip = $this->request->ip();
try {
$ret = DownloadService::dispatch($id, $ip);
} catch (\think\Exception $e) {
$this->error($e->getMessage());
return;
}
if ($ret['type'] === 'redirect') {
return redirect($ret['url']);
}
// 本地文件流式输出(避免大文件占用内存)
$path = $ret['path'];
$name = ($ret['name'] ?? 'download') . '.' . pathinfo($path, PATHINFO_EXTENSION);
return Response::create()->data(file_get_contents($path))->header([
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . rawurlencode($name) . '"',
'Content-Length' => filesize($path),
]);
}
}
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace addon\download\controller\backend;
use think\exception\ValidateException;
use think\facade\Db;
use addon\download\model\Category as CategoryModel;
use addon\download\model\Resource;
use ywxapp\controller\BackendBase;
class Category extends BackendBase
{
protected $noNeedVerify = ['*'];
protected function initialize()
{
$this->model = new CategoryModel();
}
public function index()
{
if ($this->request->isAjax()) {
$title = $this->request->param('title', '');
$all = $this->model->order('sort', 'desc')->order('id', 'asc')->select()->toArray();
//$tree = CategoryModel::toNestedTree($all);
// 关键字过滤:命中节点保留,并保留其祖先链
// if ($title) {
// $tree = $this->filterTree($tree, $title);
// }
$this->result->success($all);
}
return $this->view->fetch('category/index');
}
/**
* 按标题关键字过滤树,保留命中节点及其祖先链
*/
protected function filterTree(array $tree, string $keyword): array
{
$result = [];
foreach ($tree as $node) {
$children = !empty($node['children']) ? $this->filterTree($node['children'], $keyword) : [];
$hit = stripos($node['title'], $keyword) !== false;
if ($hit || !empty($children)) {
$node['children'] = $children;
$result[] = $node;
}
}
return $result;
}
public function create()
{
if ($this->request->isAjax()) {
$data = CategoryModel::cateTree($this->model->select()->toArray());
$this->result->success(['data' => $data]);
}
}
public function save()
{
if ($this->request->isPost()) {
$params = $this->request->post();
$this->validateSave($params);
Db::startTrans();
try {
$this->model->save($params);
Db::commit();
$this->result->success($this->model, '保存成功');
} catch (\Exception $e) {
Db::rollback();
$this->result->error('保存失败: ' . $e->getMessage());
}
}
}
public function edit($id = null)
{
$id = $this->request->param('id');
$model = $this->model->find($id);
if (!$model) {
$this->result->error('数据不存在');
}
if ($this->request->isAjax()) {
$tree = CategoryModel::cateTree($this->model->select()->toArray());
$this->result->success(['power' => $tree, 'info' => $model]);
}
}
public function update()
{
$id = $this->request->param('id');
if ($this->request->isAjax() && $this->request->isPut()) {
$params = $this->request->param();
$this->validateSave($params);
Db::startTrans();
try {
$model = $this->model->find($id);
if (!$model) {
throw new ValidateException('数据不存在');
}
$model->save($params);
Db::commit();
$this->result->success($model, '更新成功');
} catch (\Exception $e) {
Db::rollback();
$this->result->error('更新失败: ' . $e->getMessage());
}
}
}
public function delete()
{
if ($this->request->isAjax() && $this->request->isDelete()) {
$ids = $this->request->param('ids', '');
$force = $this->request->param('force/d', 0);
if (empty($ids)) {
$this->result->error('请选择要删除的数据');
}
$idArr = array_filter(array_map('intval', explode(',', $ids)));
try {
Db::transaction(function () use ($idArr, $force) {
foreach ($idArr as $id) {
$this->deleteCascade($id, (bool) $force);
}
});
$this->result->success();
} catch (\think\exception\HttpResponseException $e) {
throw $e;
} catch (\Throwable $th) {
$this->result->error('删除失败: ' . $th->getMessage());
}
}
}
/**
* 递归级联删除:force=true 时先删子分类下的资源与子分类,再删自身
*/
protected function deleteCascade($id, bool $force): void
{
$model = $this->model->find($id);
if (!$model) {
return;
}
if ($force) {
Resource::where('cid', $id)->delete();
$children = $this->model->where('pid', $id)->select();
foreach ($children as $child) {
$this->deleteCascade($child['id'], true);
}
}
// 非 force 时若仍有子分类/资源,onBeforeDelete 会抛出异常阻止删除
$model->delete();
}
protected function validateSave(array $params): void
{
if (empty($params['title'])) {
throw new ValidateException('分类名称不能为空');
}
}
}
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace addon\download\controller\backend;
use think\exception\ValidateException;
use think\facade\Db;
use addon\download\model\Resource as ResourceModel;
use addon\download\model\Category as CategoryModel;
use ywxapp\controller\BackendBase;
class Resource extends BackendBase
{
protected $noNeedVerify = ['*'];
protected function initialize()
{
$this->model = new ResourceModel();
}
public function index()
{
if ($this->request->isAjax()) {
$title = $this->request->param('title', '');
$cid = $this->request->param('cid/d', 0);
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 20);
$data = $this->model
->with(['category'])
->when($title, fn($q, $t) => $q->whereLike('title', "%{$t}%"))
->when($cid, fn($q, $c) => $q->where('cid', $c))
->order('id', 'desc')
->paginate(['page' => $page, 'list_rows' => $limit]);
$this->result->setCount($data->total());
$this->result->success($data->items());
}
$categories = (new CategoryModel())
->where('status', 1)
->order('sort', 'desc')
->select();
$this->view->assign('categories', $categories);
return $this->view->fetch('resource/index');
}
public function create()
{
if ($this->request->isAjax()) {
$categories = (new CategoryModel())
->where('status', 1)
->order('sort', 'desc')
->select();
$this->result->success(['categories' => $categories]);
}
}
public function save()
{
if ($this->request->isPost()) {
$params = $this->request->post();
$this->validateSave($params);
Db::startTrans();
try {
$this->model->save($params);
Db::commit();
$this->result->success($this->model, '保存成功');
} catch (\Exception $e) {
Db::rollback();
$this->result->error('保存失败: ' . $e->getMessage());
}
}
}
public function edit($id = null)
{
$id = $this->request->param('id');
$model = $this->model->find($id);
if (!$model) {
$this->result->error('数据不存在');
}
if ($this->request->isAjax()) {
$categories = (new CategoryModel())
->where('status', 1)
->order('sort', 'desc')
->select();
$this->result->success(['categories' => $categories, 'info' => $model]);
}
}
public function update()
{
$id = $this->request->param('id');
if ($this->request->isAjax() && $this->request->isPut()) {
$params = $this->request->param();
$this->validateSave($params);
Db::startTrans();
try {
$model = $this->model->find($id);
if (!$model) {
throw new ValidateException('数据不存在');
}
$model->save($params);
Db::commit();
$this->result->success($model, '更新成功');
} catch (\Exception $e) {
Db::rollback();
$this->result->error('更新失败: ' . $e->getMessage());
}
}
}
public function delete()
{
if ($this->request->isAjax() && $this->request->isDelete()) {
$ids = $this->request->param('ids', '');
if (empty($ids)) {
$this->result->error('请选择要删除的数据');
}
try {
Db::transaction(function () use ($ids) {
$this->model->destroy($ids);
});
$this->result->success();
} catch (\think\exception\HttpResponseException $e) {
throw $e;
} catch (\Throwable $th) {
$this->result->error('删除失败: ' . $th->getMessage());
}
}
}
protected function validateSave(array $params): void
{
if (empty($params['title'])) {
throw new ValidateException('资源标题不能为空');
}
if (empty($params['cid'])) {
throw new ValidateException('请选择分类');
}
}
}