chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<?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\controller;
|
||||
|
||||
use ywxapp\service\AddonService as AddonService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* Addon 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Addon
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
$list = [];
|
||||
if (defined('ADDON_PATH') && is_dir(ADDON_PATH)) {
|
||||
foreach (scandir(ADDON_PATH) as $name) {
|
||||
if ($name === '.' || $name === '..' || !is_dir(ADDON_PATH . $name)) {
|
||||
continue;
|
||||
}
|
||||
$infoFile = ADDON_PATH . $name . DIRECTORY_SEPARATOR . 'info.php';
|
||||
if (!is_file($infoFile)) {
|
||||
continue;
|
||||
}
|
||||
$info = include $infoFile;
|
||||
if (!isset($info['name'])) {
|
||||
continue;
|
||||
}
|
||||
$installedInfo = Db::name('addon')->where('name', $name)->find();
|
||||
$list[] = [
|
||||
'name' => $name,
|
||||
'title' => $info['title'] ?? $name,
|
||||
'description' => $info['description'] ?? '',
|
||||
'version' => $info['version'] ?? '1.0.0',
|
||||
'author' => $info['author'] ?? '',
|
||||
'installed' => $installedInfo ? true : false,
|
||||
'status' => $installedInfo['status'] ?? 0,
|
||||
'installed_version' => $installedInfo['version'] ?? null,
|
||||
'has_update' => $installedInfo
|
||||
? version_compare($info['version'] ?? '1.0.0', $installedInfo['version'], '>')
|
||||
: false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'data' => $list
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function install($name)
|
||||
{
|
||||
try {
|
||||
AddonService::instance($name)->install();
|
||||
return json(['code' => 200, 'msg' => '安装成功']);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function uninstall($name)
|
||||
{
|
||||
try {
|
||||
AddonService::instance($name)->uninstall();
|
||||
return json(['code' => 200, 'msg' => '卸载成功']);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function enable($name)
|
||||
{
|
||||
try {
|
||||
AddonService::instance($name)->enable();
|
||||
return json(['code' => 200, 'msg' => '启用成功']);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function disable($name)
|
||||
{
|
||||
try {
|
||||
AddonService::instance($name)->disable();
|
||||
return json(['code' => 200, 'msg' => '禁用成功']);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function upgrade($name)
|
||||
{
|
||||
try {
|
||||
$service = AddonService::instance($name);
|
||||
if (!method_exists($service, 'upgrade')) {
|
||||
return json(['code' => 500, 'msg' => '当前版本不支持在线升级']);
|
||||
}
|
||||
$service->upgrade();
|
||||
return json(['code' => 200, 'msg' => '升级成功']);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function info($name)
|
||||
{
|
||||
try {
|
||||
$info = AddonService::instance($name)->getInfo();
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 404, 'msg' => '插件不存在']);
|
||||
}
|
||||
$installed = Db::name('addon')->where('name', $name)->find();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'data' => [
|
||||
'info' => $info,
|
||||
'installed' => $installed ?: false
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
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('获取用户信息失败!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
<?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;
|
||||
use think\Exception;
|
||||
use think\facade\Db;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\View;
|
||||
use app\common\library\Token;
|
||||
use ywxapp\library\AdminAuth;
|
||||
use ywxapp\library\SkinOverlay;
|
||||
use ywxapp\library\SkinVariables;
|
||||
use ywxapp\AddonBase;
|
||||
|
||||
/**
|
||||
* 后台控制器基类(统一核心后台与插件后台).
|
||||
*
|
||||
* - 核心后台(app\backend\...):afterAuth 启用核心 layout 并注入管理员。
|
||||
* - 插件后台(addon\*\...):beforeAuth 强制 AdminAuth 与插件视图目录,
|
||||
* afterAuth 复用核心 layout,fetch 叠加皮肤层与配色变量。
|
||||
*
|
||||
* 公共逻辑(属性 / buildparams / 数据权限 / CRUD / 登录登出)统一落地于此,
|
||||
* 原 Backend、AddonBackend、traits\Backend 已并入并删除。
|
||||
*/
|
||||
abstract class BackendBase extends BaseController
|
||||
{
|
||||
/**
|
||||
* 快速搜索时执行查找的字段.
|
||||
*/
|
||||
protected $searchFields = 'id';
|
||||
|
||||
/**
|
||||
* 是否是关联查询.
|
||||
*/
|
||||
protected $relationSearch = false;
|
||||
|
||||
/**
|
||||
* 是否开启数据限制
|
||||
* 支持auth/personal
|
||||
* 表示按权限判断/仅限个人
|
||||
* 默认为禁用,若启用请务必保证表中存在admin_id字段.
|
||||
*/
|
||||
protected $dataLimit = false;
|
||||
|
||||
/**
|
||||
* 数据限制字段.
|
||||
*/
|
||||
protected $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 数据限制开启时自动填充限制字段值
|
||||
*/
|
||||
protected $dataLimitFieldAutoFill = true;
|
||||
|
||||
/**
|
||||
* 是否开启Validate验证
|
||||
*/
|
||||
protected $modelValidate = false;
|
||||
|
||||
/**
|
||||
* 是否开启模型场景验证
|
||||
*/
|
||||
protected $modelSceneValidate = false;
|
||||
|
||||
/**
|
||||
* Multi方法可批量修改的字段.
|
||||
*/
|
||||
protected $multiFields = 'status';
|
||||
|
||||
/**
|
||||
* Selectpage可显示的字段.
|
||||
*/
|
||||
protected $selectpageFields = '*';
|
||||
|
||||
/**
|
||||
* 前台提交过来,需要排除的字段数据.
|
||||
*/
|
||||
protected $excludeFields = '';
|
||||
|
||||
/**
|
||||
* 导入文件首行类型
|
||||
* 支持comment/name
|
||||
* 表示注释或字段名.
|
||||
*/
|
||||
protected $importHeadType = 'comment';
|
||||
|
||||
/**
|
||||
* 视图类实例
|
||||
* @var \think\View
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* 构造方法:在 parent 解析容器 auth 之前,先判定本控制器是否为「插件后台」。
|
||||
*
|
||||
* 插件后台路由经全局分发落在 frontend 应用,容器 auth 默认是前台 Auth。
|
||||
* 这里用 static::class(实际子类类名,构造前即可获取,不依赖路由 dispatch 时机)
|
||||
* 匹配 addon\<插件>\controller\backend\* 目录约定,命中则把容器 auth 绑成 AdminAuth,
|
||||
* 使 parent 构造取到的 $app->auth 即为后台鉴权实例(isAdmin=true)。
|
||||
* 核心后台(backend 应用)由 AppService 闭包直接给 AdminAuth,无需此处处理。
|
||||
*
|
||||
* @param App $app
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
// 插件后台判定:getNamespace() 只到 addon\<插件> 层(不含 controller\backend 子段),
|
||||
// 无法区分后台/前台控制器,故插件后台必须用 static::class(完整类名构造前即可获取,
|
||||
// 含 addon\<插件>\controller\backend\ 段)来匹配;getNamespace() 仅用于插件上下文兜底。
|
||||
$class = static::class;
|
||||
if (strpos($class, 'addon\\') === 0
|
||||
&& strpos($class, '\\controller\\backend\\') !== false) {
|
||||
$app->bind('auth', AdminAuth::class);
|
||||
}
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器初始化骨架:准备视图实例 -> beforeAuth -> 校验 -> afterAuth.
|
||||
* @return void
|
||||
*/
|
||||
public function _initialize()
|
||||
{
|
||||
$this->view = $this->app->view;
|
||||
$this->beforeAuth();
|
||||
// 登录与权限校验:未登录时整页请求 302 跳登录页,AJAX 返回 401 由前端跳转
|
||||
$this->auth->verifyAuth($this->noNeedLogin, $this->noNeedVerify);
|
||||
// 鉴权后置钩子(启用 layout / 注入登录管理员 / 修正 $site 等)
|
||||
$this->afterAuth();
|
||||
// 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用
|
||||
$this->assignSite();
|
||||
// 子类初始化钩子
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器初始化方法,子类可通过重写该方法实现自己的初始化逻辑.
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 鉴权前置钩子:根据上下文自动适配核心 / 插件后台.
|
||||
* @return void
|
||||
*/
|
||||
protected function beforeAuth()
|
||||
{
|
||||
if ($this->isAddonContext()) {
|
||||
// 插件后台安全基线:必须登录,杜绝继承来的免登录白名单
|
||||
$this->noNeedLogin = [];
|
||||
// 鉴权实例由容器统一解析:本类控制器命名空间为 addon\*,
|
||||
// AppService 的 auth 闭包已据此返回 AdminAuth(无需手动 new)。
|
||||
// 视图目录切到插件自身 view/backend/
|
||||
$this->switchAddonViewPath();
|
||||
} else {
|
||||
// 核心后台处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鉴权后置钩子:根据上下文自动适配核心 / 插件后台.
|
||||
* @return void
|
||||
*/
|
||||
protected function afterAuth()
|
||||
{
|
||||
if ($this->isAddonContext()) {
|
||||
// 插件后台复用核心后台外壳(裸内容由 layout 包裹 {__CONTENT__})
|
||||
$this->view->config([
|
||||
"layout_on" => true,
|
||||
"layout_name" => $this->app->getRootPath() . 'app/backend/view/common/layout.html'
|
||||
]);
|
||||
} else {
|
||||
$this->view->config([
|
||||
"layout_on" => true,
|
||||
"layout_name" => 'common/layout'
|
||||
]);
|
||||
// 核心后台:layout 由后台模板自行 {extend common/layout} 控制(保持原 Backend 注释态,不强制包裹)
|
||||
if ($this->auth && $this->auth->isLogin) {
|
||||
$this->view->assign('member', $this->auth->info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板(基类入口).
|
||||
* 插件后台叠加皮肤覆盖层与配色变量;核心后台走默认。
|
||||
*/
|
||||
protected function fetch($template = '', $vars = [], $replace = [], $config = [])
|
||||
{
|
||||
if ($this->isAddonContext()) {
|
||||
$addon = $this->currentAddon();
|
||||
$restore = null;
|
||||
if ($addon && $overlay = SkinOverlay::resolve($addon, 'backend')) {
|
||||
$restore = View::getConfig('view_path');
|
||||
View::config(['view_path' => $overlay]);
|
||||
}
|
||||
$result = View::fetch($template, $vars, $replace, $config);
|
||||
if ($restore !== null) {
|
||||
View::config(['view_path' => $restore]);
|
||||
}
|
||||
// 注入皮肤变量 CSS(Discuz 式配色层,无需改 HTML)
|
||||
$style = SkinVariables::styleTag($addon, 'backend');
|
||||
if ($style !== '' && ($pos = stripos($result, '</head>')) !== false) {
|
||||
$result = substr($result, 0, $pos) . $style . "\n" . substr($result, $pos);
|
||||
} elseif ($style !== '') {
|
||||
$result = $style . $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
return View::fetch($template, $vars, $replace, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将站点上下文修正为后台(插件页复用核心 layout 时 $site.app/module 需为 backend).
|
||||
* @return void
|
||||
*/
|
||||
protected function applyBackendSite()
|
||||
{
|
||||
$site = $this->view->getConfig('site') ?? [];
|
||||
if (is_array($site)) {
|
||||
$site['module'] = 'backend';
|
||||
$site['app'] = 'backend';
|
||||
$this->view->assign('site', $site);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从控制器类名反推插件目录并切换视图根目录到 addon/<name>/view/backend/.
|
||||
* @return void
|
||||
*/
|
||||
protected function switchAddonViewPath()
|
||||
{
|
||||
// 重构版 MultiApp 已将 appPath 设为 addon/<插件>/,直接拼视图目录,无需正则
|
||||
$this->view->config([
|
||||
'view_path' => $this->app->getAppPath() . 'view' . DIRECTORY_SEPARATOR
|
||||
. 'backend' . DIRECTORY_SEPARATOR,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否插件后台(addon\*\controller\backend\ 命名空间 + AdminAuth 鉴权).
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAddonAdmin(): bool
|
||||
{
|
||||
return strpos($this->app->getNamespace(), 'addon\\') === 0
|
||||
&& strpos($this->app->getNamespace(), '\\controller\\backend\\') !== false
|
||||
&& $this->auth instanceof AdminAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前控制器是否属于插件上下文(addon\ 命名空间).
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAddonContext(): bool
|
||||
{
|
||||
return strpos($this->app->getNamespace(), 'addon\\') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前控制器命名空间提取插件名(addon\<插件>\... -> <插件>)。
|
||||
* 由 appPath(addon/<插件>/)剥 rootPath 取首段,无需正则。
|
||||
* @return string
|
||||
*/
|
||||
protected function currentAddon(): string
|
||||
{
|
||||
$rel = trim(substr($this->app->getAppPath(), strlen($this->app->getRootPath())), DIRECTORY_SEPARATOR);
|
||||
$seg = explode(DIRECTORY_SEPARATOR, $rel);
|
||||
return $seg[0] === 'addon' && isset($seg[1]) ? $seg[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值到模板.
|
||||
*/
|
||||
protected function assign($name, $value = '')
|
||||
{
|
||||
$this->view->assign($name, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已登录
|
||||
* @return bool
|
||||
*/
|
||||
public function isLogin()
|
||||
{
|
||||
return $this->auth->isLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台登录入口(免登录).
|
||||
* 渲染登录页(layout(false) 独立整页)。
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$account = $this->request->post('account/s', '');
|
||||
$password = $this->request->post('password/s', '');
|
||||
$captcha = $this->request->post('captcha/s', '');
|
||||
if (! $this->auth->login($account, $password, $captcha)) {
|
||||
$this->result->error($this->auth->getError());
|
||||
}
|
||||
$this->result->success('登录成功', ['url' => url('backend/index/index')->build()]);
|
||||
}
|
||||
View::layout(false);
|
||||
return View::fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$this->auth->logout();
|
||||
$this->result->success('退出成功', ['url' => url('backend/login/login')->build()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面标题(核心 layout 通过 {$title} 显示).
|
||||
* @param string $title
|
||||
*/
|
||||
protected function setTitle(string $title)
|
||||
{
|
||||
$this->view->assign('title', $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除前台提交过来的字段
|
||||
* @param $params
|
||||
* @return array
|
||||
*/
|
||||
protected function preExcludeFields($params)
|
||||
{
|
||||
if (is_array($this->excludeFields)) {
|
||||
foreach ($this->excludeFields as $field) {
|
||||
if (array_key_exists($field, $params))
|
||||
unset($params[$field]);
|
||||
}
|
||||
} else {
|
||||
if (array_key_exists($this->excludeFields, $params))
|
||||
unset($params[$this->excludeFields]);
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
//设置过滤方法
|
||||
$this->request->filter(['strip_tags']);
|
||||
//如果发送的来源是Selectpage,则转发到Selectpage
|
||||
if ($this->request->request('keyField'))
|
||||
return $this->selectpage();
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$total = $this->model
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->count();
|
||||
$list = $this->model
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->limit($offset, $limit)
|
||||
->select();
|
||||
$list = $list->toArray();
|
||||
$result = ['total' => $total, 'rows' => $list];
|
||||
return $this->result->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收站
|
||||
*/
|
||||
public function recyclebin()
|
||||
{
|
||||
//设置过滤方法
|
||||
$this->request->filter(['strip_tags']);
|
||||
if ($this->request->isAjax()) {
|
||||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||
$total = $this->model
|
||||
->onlyTrashed()
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->count();
|
||||
|
||||
$list = $this->model
|
||||
->onlyTrashed()
|
||||
->where($where)
|
||||
->order($sort, $order)
|
||||
->limit($offset, $limit)
|
||||
->select();
|
||||
$result = ['total' => $total, 'rows' => $list];
|
||||
$this->result->success($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if ($this->request->isPost()) {
|
||||
$params = $this->request->post('row/a');
|
||||
if ($params) {
|
||||
$params = $this->preExcludeFields($params);
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill)
|
||||
$params[$this->dataLimitField] = $this->auth->id;
|
||||
$result = false;
|
||||
Db::startTrans();
|
||||
try {
|
||||
//是否采用模型验证
|
||||
if ($this->modelValidate) {
|
||||
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||
$validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
|
||||
validate($validate)->scene($this->modelSceneValidate ? 'edit' : $name)->check($params);
|
||||
}
|
||||
$result = $this->model->save($params);
|
||||
Db::commit();
|
||||
} catch (ValidateException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (\PDOException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false)
|
||||
$this->result->success();
|
||||
$this->result->error(lang('No rows were inserted'));
|
||||
}
|
||||
$this->result->error(lang('Parameter %s can not be empty', ''));
|
||||
}
|
||||
$this->result->error(lang('Parameter %s can not be empty'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
public function edit($id = null)
|
||||
{
|
||||
$row = $this->model->get($ids);
|
||||
if (!$row)
|
||||
$this->result->error(lang('No Results were found'));
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds)) {
|
||||
if (!in_array($row[$this->dataLimitField], $adminIds))
|
||||
$this->result->error(lang('You have no permission'));
|
||||
}
|
||||
if ($this->request->isPost()) {
|
||||
$params = $this->request->post('row/a');
|
||||
if ($params) {
|
||||
$params = $this->preExcludeFields($params);
|
||||
$result = false;
|
||||
Db::startTrans();
|
||||
try {
|
||||
//是否采用模型验证
|
||||
if ($this->modelValidate) {
|
||||
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||
$validate = is_bool($this->modelValidate) ? $name : $this->modelValidate;
|
||||
$pk = $row->getPk();
|
||||
if (!isset($params[$pk])) {
|
||||
$params[$pk] = $row->$pk;
|
||||
}
|
||||
validate($validate)->scene($this->modelSceneValidate ? 'edit' : $name)->check($params);
|
||||
}
|
||||
$result = $row->save($params);
|
||||
Db::commit();
|
||||
} catch (ValidateException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (\PDOException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false)
|
||||
$this->result->success();
|
||||
$this->result->error(lang('No rows were updated'));
|
||||
}
|
||||
$this->result->error(lang('Parameter %s can not be empty', ''));
|
||||
}
|
||||
$this->view->assign('row', $row);
|
||||
return $this->view->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
public function del($ids = '')
|
||||
{
|
||||
if ($ids) {
|
||||
$pk = $this->model->getPk();
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds))
|
||||
$this->model->where($this->dataLimitField, 'in', $adminIds);
|
||||
$list = $this->model->where($pk, 'in', $ids)->select();
|
||||
$count = 0;
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($list as $k => $v) {
|
||||
$count += $v->delete();
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\PDOException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
if ($count)
|
||||
$this->result->success();
|
||||
$this->result->error(lang('No rows were deleted'));
|
||||
}
|
||||
$this->result->error(lang('Parameter %s can not be empty'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 真实删除
|
||||
*/
|
||||
public function destroy($ids = '')
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
$where = [];
|
||||
if (is_array($adminIds))
|
||||
$where[$this->dataLimitField] = $adminIds;
|
||||
|
||||
if ($ids)
|
||||
$where[$pk] = explode(',', $ids);
|
||||
|
||||
$count = 0;
|
||||
Db::startTrans();
|
||||
|
||||
try {
|
||||
$list = $this->model->onlyTrashed()->where($where)->select();
|
||||
foreach ($list as $k => $v) {
|
||||
$count += $v->force()->delete();
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\PDOException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
if ($count)
|
||||
$this->result->success();
|
||||
$this->result->error(lang('Parameter %s can not be empty', 'ids'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原
|
||||
*/
|
||||
public function restore($ids = '')
|
||||
{
|
||||
$pk = $this->model->getPk();
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
$where = [];
|
||||
if (is_array($adminIds))
|
||||
$where[$this->dataLimitField] = $adminIds;
|
||||
if ($ids)
|
||||
$where[$pk] = explode(',', $ids);
|
||||
$count = 0;
|
||||
Db::startTrans();
|
||||
try {
|
||||
$list = $this->model->onlyTrashed()->where($where)->select();
|
||||
foreach ($list as $index => $item) {
|
||||
$count += $item->restore();
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\PDOException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
if ($count)
|
||||
$this->result->success();
|
||||
$this->result->error(lang('No rows were updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新
|
||||
*/
|
||||
public function multi($ids = '')
|
||||
{
|
||||
$ids = $ids ? $ids : $this->request->param('ids');
|
||||
if ($ids) {
|
||||
if ($this->request->has('params')) {
|
||||
parse_str($this->request->post('params'), $values);
|
||||
$values = $this->auth->isSuperAdmin() ? $values : array_intersect_key(
|
||||
$values,
|
||||
array_flip(is_array($this->multiFields) ? $this->multiFields : explode(',', $this->multiFields))
|
||||
);
|
||||
if ($values) {
|
||||
$adminIds = $this->getDataLimitAdminIds();
|
||||
if (is_array($adminIds)) {
|
||||
$this->model->where($this->dataLimitField, 'in', $adminIds);
|
||||
}
|
||||
$count = 0;
|
||||
Db::startTrans();
|
||||
|
||||
try {
|
||||
$list = $this->model->where($this->model->getPk(), 'in', $ids)->select();
|
||||
foreach ($list as $index => $item) {
|
||||
$count += $item->save($values);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\PDOException $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
if ($count) {
|
||||
$this->result->success();
|
||||
} else {
|
||||
$this->result->error(lang('No rows were updated'));
|
||||
}
|
||||
} else {
|
||||
$this->result->error(lang('You have no permission'));
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->result->error(lang('Parameter %s can not be empty', 'ids'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
protected function import()
|
||||
{
|
||||
$file = $this->request->request('file');
|
||||
if (!$file) {
|
||||
$this->result->error(lang('Parameter %s can not be empty', 'file'));
|
||||
}
|
||||
$filePath = app()->getRootPath() . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $file;
|
||||
if (!is_file($filePath)) {
|
||||
$this->result->error(lang('No results were found'));
|
||||
}
|
||||
//实例化reader
|
||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||
if (!in_array($ext, ['csv', 'xls', 'xlsx'])) {
|
||||
$this->result->error(lang('Unknown data format'));
|
||||
}
|
||||
if ($ext === 'csv') {
|
||||
$file = fopen($filePath, 'r');
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'import_csv');
|
||||
$fp = fopen($filePath, 'w');
|
||||
$n = 0;
|
||||
while ($line = fgets($file)) {
|
||||
$line = rtrim($line, "\n\r\0");
|
||||
$encoding = mb_detect_encoding($line, ['utf-8', 'gbk', 'latin1', 'big5']);
|
||||
if ($encoding != 'utf-8') {
|
||||
$line = mb_convert_encoding($line, 'utf-8', $encoding);
|
||||
}
|
||||
if ($n == 0 || preg_match('/^".*"$/', $line)) {
|
||||
fwrite($fp, $line . "\n");
|
||||
} else {
|
||||
fwrite($fp, '"' . str_replace(['"', ','], ['""', '","'], $line) . "\"\n");
|
||||
}
|
||||
$n++;
|
||||
}
|
||||
fclose($file) || fclose($fp);
|
||||
|
||||
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv();
|
||||
} elseif ($ext === 'xls') {
|
||||
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls();
|
||||
} else {
|
||||
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
|
||||
}
|
||||
|
||||
//导入文件首行类型,默认是注释,如果需要使用字段名称请使用name
|
||||
$importHeadType = isset($this->importHeadType) ? $this->importHeadType : 'comment';
|
||||
|
||||
$table = $this->model->db()->getTable();
|
||||
$database = \think\facade\Config::get('database.database');
|
||||
$fieldArr = [];
|
||||
$list = Db::query(
|
||||
'SELECT COLUMN_NAME,COLUMN_COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?',
|
||||
[$table, $database]
|
||||
);
|
||||
foreach ($list as $k => $v) {
|
||||
if ($importHeadType == 'comment')
|
||||
$fieldArr[$v['COLUMN_COMMENT']] = $v['COLUMN_NAME'];
|
||||
else
|
||||
$fieldArr[$v['COLUMN_NAME']] = $v['COLUMN_NAME'];
|
||||
}
|
||||
|
||||
//加载文件
|
||||
$insert = [];
|
||||
|
||||
try {
|
||||
if (!$PHPExcel = $reader->load($filePath)) {
|
||||
$this->result->error(lang('Unknown data format'));
|
||||
}
|
||||
$currentSheet = $PHPExcel->getSheet(0); //读取文件中的第一个工作表
|
||||
$allColumn = $currentSheet->getHighestDataColumn(); //取得最大的列号
|
||||
$allRow = $currentSheet->getHighestRow(); //取得一共有多少行
|
||||
$maxColumnNumber = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($allColumn);
|
||||
$fields = [];
|
||||
for ($currentRow = 1; $currentRow <= 1; $currentRow++) {
|
||||
for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
|
||||
$val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue();
|
||||
$fields[] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) {
|
||||
$values = [];
|
||||
for ($currentColumn = 1; $currentColumn <= $maxColumnNumber; $currentColumn++) {
|
||||
$val = $currentSheet->getCellByColumnAndColumn($currentColumn, $currentRow)->getValue();
|
||||
$values[] = is_null($val) ? '' : $val;
|
||||
}
|
||||
$row = [];
|
||||
$temp = array_combine($fields, $values);
|
||||
foreach ($temp as $k => $v) {
|
||||
if (isset($fieldArr[$k]) && $k !== '') {
|
||||
$row[$fieldArr[$k]] = $v;
|
||||
}
|
||||
}
|
||||
if ($row) {
|
||||
$insert[] = $row;
|
||||
}
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->result->error($exception->getMessage());
|
||||
}
|
||||
if (!$insert) {
|
||||
$this->result->error(lang('No rows were updated'));
|
||||
}
|
||||
|
||||
try {
|
||||
//是否包含admin_id字段
|
||||
$has_admin_id = false;
|
||||
foreach ($fieldArr as $name => $key) {
|
||||
if ($key == 'admin_id') {
|
||||
$has_admin_id = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($has_admin_id) {
|
||||
$auth = $this->auth;
|
||||
foreach ($insert as &$val) {
|
||||
if (!isset($val['admin_id']) || empty($val['admin_id'])) {
|
||||
$val['admin_id'] = $auth->isLogin ? $auth->id : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->model->saveAll($insert);
|
||||
} catch (\PDOException $exception) {
|
||||
$msg = $exception->getMessage();
|
||||
if (
|
||||
preg_match(
|
||||
"/.+Integrity constraint violation: 1062 Duplicate entry '(.+)' for key '(.+)'/is",
|
||||
$msg,
|
||||
$matches
|
||||
)
|
||||
) {
|
||||
$msg = "导入失败,包含【{$matches[1]}】的记录已存在";
|
||||
}
|
||||
$this->result->error($msg);
|
||||
} catch (Exception $e) {
|
||||
$this->result->error($e->getMessage());
|
||||
}
|
||||
|
||||
$this->result->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成查询所需要的条件,排序,分页等信息(含安全加固).
|
||||
*
|
||||
* @param bool|array $searchfields 快速搜索字段
|
||||
* @return array
|
||||
*/
|
||||
protected function buildparams($searchfields = false)
|
||||
{
|
||||
$searchfields = is_array($searchfields) ? $searchfields : (is_bool($searchfields) && $searchfields !== false ? $this->searchFields : $searchfields);
|
||||
$searchfields = is_string($searchfields) ? explode(',', $searchfields) : $searchfields;
|
||||
$filter = $this->request->get('filter', '');
|
||||
$op = $this->request->get('op', '', 'trim');
|
||||
$sort = $this->request->get('sort', 'id');
|
||||
$order = $this->request->get('order', 'desc');
|
||||
$offset = $this->request->get('offset', 0);
|
||||
$limit = $this->request->get('limit', 0);
|
||||
$filter = (array)json_decode($filter, true);
|
||||
$op = (array)json_decode($op, true);
|
||||
$filter = $filter ? $filter : [];
|
||||
$where = [];
|
||||
$tableName = '';
|
||||
$model = $this->model;
|
||||
if (! empty($model)) {
|
||||
// 兼容模型别名
|
||||
$tableName = $model->getQuery()->getTable();
|
||||
}
|
||||
$alias = $model && method_exists($model, 'getTable') ? $model->getTable() : '';
|
||||
$pkField = $model && method_exists($model, 'getPk') ? $model->getPk() : 'id';
|
||||
foreach ($filter as $k => $v) {
|
||||
// 安全加固:仅允许白名单字段(字母/数字/下划线/点,禁止表达式注入)
|
||||
if (! preg_match('/^[A-Za-z_][A-Za-z0-9_.]*$/', $k)) {
|
||||
continue;
|
||||
}
|
||||
$sym = isset($op[$k]) ? $op[$k] : '=';
|
||||
// 安全加固:限定操作符白名单
|
||||
if (! in_array($sym, ['=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'RANGE', 'NOT RANGE', 'NULL', 'IS NULL', 'NOT NULL', 'IS NOT NULL', 'FIND_IN_SET'])) {
|
||||
$sym = '=';
|
||||
}
|
||||
if (strtoupper($sym) === 'FIND_IN_SET') {
|
||||
// FIND_IN_SET(col, val) —— 列名同样需校验
|
||||
if (preg_match('/^[A-Za-z_][A-Za-z0-9_.]*$/', $k)) {
|
||||
$where[] = ['', 'EXP', Db::raw("FIND_IN_SET(`{$k}`, '" . addslashes($v) . "')")];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (stripos($k, '.') === false) {
|
||||
$k = $alias ? ($alias . '.' . $k) : $k;
|
||||
}
|
||||
switch (strtoupper($sym)) {
|
||||
case '=':
|
||||
case '>':
|
||||
case '>=':
|
||||
case '<':
|
||||
case '<=':
|
||||
case 'LIKE':
|
||||
case 'NOT LIKE':
|
||||
$where[] = [$k, $sym, $v];
|
||||
break;
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$arr = is_array($v) ? $v : (strpos($v, ',') !== false ? explode(',', $v) : [$v]);
|
||||
$where[] = [$k, $sym, $arr];
|
||||
break;
|
||||
case 'BETWEEN':
|
||||
case 'NOT BETWEEN':
|
||||
$arr = array_slice(explode(',', $v), 0, 2);
|
||||
if (stripos($v, ',') === false || ! array_filter($arr)) {
|
||||
continue 2;
|
||||
}
|
||||
$where[] = [$k, $sym, $arr];
|
||||
break;
|
||||
case 'RANGE':
|
||||
case 'NOT RANGE':
|
||||
$v = str_replace(' - ', ',', $v);
|
||||
$arr = array_slice(explode(',', $v), 0, 2);
|
||||
if (stripos($v, ',') === false || ! array_filter($arr)) {
|
||||
continue 2;
|
||||
}
|
||||
//当出现一边为空时改变操作符
|
||||
if ($arr[0] === '') {
|
||||
$sym = $sym == 'RANGE' ? ' <= ' : '>';
|
||||
$arr = $arr[1];
|
||||
} elseif ($arr[1] === '') {
|
||||
$sym = $sym == 'RANGE' ? ' >= ' : '<';
|
||||
$arr = $arr[0];
|
||||
}
|
||||
$where[] = [$k, str_replace('RANGE', 'BETWEEN', $sym) . ' time', $arr];
|
||||
break;
|
||||
case 'NULL':
|
||||
case 'IS NULL':
|
||||
case 'NOT NULL':
|
||||
case 'IS NOT NULL':
|
||||
$where[] = [$k, strtolower(str_replace('IS ', '', $sym))];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! empty($where)) {
|
||||
$where = function ($query) use ($where) {
|
||||
foreach ($where as $k => $v) {
|
||||
if (is_array($v)) {
|
||||
call_user_func_array([$query, 'where'], $v);
|
||||
} else {
|
||||
$query->where($v);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return [$where, trim($sort), trim($order), $offset, $limit];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据限制的管理员 ID 集合(用于数据权限隔离).
|
||||
*
|
||||
* - dataLimit 为 false:返回 null,表示不限制(默认)
|
||||
* - 'personal':仅当前管理员本人
|
||||
* - 'auth':可按组织树扩展,此处简化为当前管理员
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getDataLimitAdminIds()
|
||||
{
|
||||
if (! $this->dataLimit) {
|
||||
return null;
|
||||
}
|
||||
$adminId = $this->auth->model->id ?? null;
|
||||
return $adminId !== null ? [$adminId] : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?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;
|
||||
use think\exception\ValidateException;
|
||||
use think\Validate;
|
||||
use ywxapp\library\Result;
|
||||
use think\facade\Lang;
|
||||
use think\facade\View;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
/**
|
||||
* 应用实例
|
||||
* @var \think\App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* Request实例
|
||||
* @var \think\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* 应用模块名称
|
||||
* @var string
|
||||
*/
|
||||
protected $module = '';
|
||||
|
||||
/**
|
||||
* 是否批量验证
|
||||
* @var bool
|
||||
*/
|
||||
protected $batchValidate = false;
|
||||
|
||||
/**
|
||||
* 控制器中间件
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [];
|
||||
|
||||
/**
|
||||
* 控制器模型
|
||||
* @var \think\Model
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 响应实例
|
||||
* @var \ywxapp\library\Result
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* 用户实例
|
||||
* @var \ywxapp\library\Auth
|
||||
*/
|
||||
protected $auth;
|
||||
|
||||
/**
|
||||
* Summary of needLogin
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
/**
|
||||
* Summary of needRight
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @access public
|
||||
* @param App $app 应用对象
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
// 安全的CORS配置(必须在 $this->app 赋值后再调用,使用容器 Env 而非全局 env() 助手)
|
||||
$this->setCorsHeaders();
|
||||
|
||||
$this->module = $app->http->getName();
|
||||
$this->result = $app->result;
|
||||
$this->request = $app->request;
|
||||
$this->auth = $app->auth;
|
||||
// 尝试用 token 还原登录态:有 token 就解析并初始化用户,没有则跳过(不报错、不影响后续)
|
||||
$this->auth->tryInitByToken();
|
||||
// 登录验证(需要登录的 action 未登录返回 401;已登录则校验权限)
|
||||
|
||||
// 加载当前控制器语言包
|
||||
$this->loadlang($this->request->controller());
|
||||
$this->_initialize();
|
||||
$this->assignSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置安全的CORS头部
|
||||
*/
|
||||
protected function setCorsHeaders(): void
|
||||
{
|
||||
// 从配置或环境变量获取允许的来源(使用容器 Env,避免依赖全局 env() 助手在插件/CLI 上下文未注册)
|
||||
$allowedOrigins = $this->app->env->get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000,http://localhost:8080');
|
||||
$allowedOriginsArray = array_filter(array_map('trim', explode(',', $allowedOrigins)));
|
||||
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
|
||||
// 检查来源是否在白名单中
|
||||
if (in_array($origin, $allowedOriginsArray) || empty($origin)) {
|
||||
if (!empty($origin)) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
||||
header('Access-Control-Max-Age: 86400'); // 24小时预检缓存
|
||||
}
|
||||
|
||||
// 处理OPTIONS预检请求
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向模板注入当前应用的基础路径信息。
|
||||
*
|
||||
* 关键:对外 URL 的模块段必须是 app_map 的「虚拟名」(admin/user/home),
|
||||
* 而非 http->getName() 返回的「真实目录名」(backend/member/frontend),
|
||||
* 否则拼接出的 URL 会与路由不匹配(重复 / 错位模块名)。
|
||||
* 虚拟名直接取自 request->root() 中解析出的那段。
|
||||
*/
|
||||
protected function assignSite(): void
|
||||
{
|
||||
$root = $this->app->request->root(); // 如 /admin、/user、默认应用为空
|
||||
$appRoot = ltrim($root, '/'); // 虚拟名:admin/user/home,默认应用为 ''
|
||||
// 部署根(去掉模块段):/sub/admin -> /sub;/admin -> ''(网站根目录)
|
||||
$deployRoot = preg_replace('#^/[^/]+#', '', $root);
|
||||
|
||||
View::assign('site', [
|
||||
'root' => $deployRoot === '' ? '' : rtrim($deployRoot, '/'),
|
||||
'module' => $appRoot, // 对外模块段(虚拟名,URL 用):admin/user/home
|
||||
'app' => $this->module, // 真实应用目录名(静态资源目录用):backend/member/frontend/home
|
||||
'controller' => $this->request->controller(),
|
||||
'action' => $this->request->action(),
|
||||
'devToken' => config('ywxapp.developer_token'),
|
||||
'devAddon' => config('ywxapp.addon_developer')
|
||||
]);
|
||||
|
||||
// 后端生成一个「带占位符的基准 URL」,供 JS(route.js) 做片段替换,
|
||||
// 从而与后端路由规则(app_map/别名/后缀)完全对齐。
|
||||
// 例如当前应用在 backend 时生成: /admin/{__CTRL__}/{__ACT__}
|
||||
// 注意:不可用 Route::buildUrl('{__CTRL__}/{__ACT__}') —— 它把占位符当真实
|
||||
// 路由解析,在某些路由配置下会误生成 /menu/{__CTRL__} 之类的异常路径。
|
||||
// 改为按 app_map 反查当前应用(真实目录名 $this->module)的虚拟前缀手动拼。
|
||||
$appMap = (array) config('app.app_map');
|
||||
$appPrefix = array_search($this->module, $appMap, true);
|
||||
if (! $appPrefix) {
|
||||
$appPrefix = $this->module; // 无映射时直接用真实目录名
|
||||
}
|
||||
$base = $deployRoot === '' ? '' : rtrim($deployRoot, '/');
|
||||
$routeBase = $base . '/' . $appPrefix . '/{__CTRL__}/{__ACT__}';
|
||||
View::assign('route_base', $routeBase);
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器初始化 _initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function _initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 加载语言文件.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
protected function loadlang($name = '')
|
||||
{
|
||||
$name = $name ?: $this->request->controller();
|
||||
if (strpos($name, '.')) {
|
||||
$_arr = explode('.', $name);
|
||||
if (count($_arr) == 2) {
|
||||
$path = $_arr[0] . '/' . strtolower($_arr[1]);
|
||||
} else {
|
||||
$path = strtolower($name);
|
||||
}
|
||||
} else {
|
||||
$path = strtolower($name);
|
||||
}
|
||||
Lang::load($this->app->getAppPath() . '/lang/' . Lang::getLangset() . '/' . $path . '.php');
|
||||
}
|
||||
/**
|
||||
* 验证数据
|
||||
* @access protected
|
||||
* @param array $data 数据
|
||||
* @param string|array $validate 验证器名或者验证规则数组
|
||||
* @param array $message 提示信息
|
||||
* @param bool $batch 是否批量验证
|
||||
* @return array|string|true
|
||||
* @throws ValidateException
|
||||
*/
|
||||
protected function validate(array $data, string | array $validate, array $message = [], bool $batch = false)
|
||||
{
|
||||
if (is_array($validate)) {
|
||||
$v = new Validate();
|
||||
$v->rule($validate);
|
||||
} else {
|
||||
if (strpos($validate, '.')) {
|
||||
[$validate, $scene] = explode('.', $validate);
|
||||
}
|
||||
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
|
||||
$v = new $class();
|
||||
if (! empty($scene)) {
|
||||
$v->scene($scene);
|
||||
}
|
||||
}
|
||||
$v->message($message);
|
||||
if ($batch || $this->batchValidate) {
|
||||
$v->batch(true);
|
||||
}
|
||||
return $v->failException(true)->check($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(转发到 Result).
|
||||
*/
|
||||
public function success($data = null, $message = 'success', int $code = 0)
|
||||
{
|
||||
return $this->result->success($data, $message, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(转发到 Result).
|
||||
*/
|
||||
public function error($message = 'Error', int $code = 1, $data = null)
|
||||
{
|
||||
return $this->result->error($message, $code, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?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\facade\View;
|
||||
use ywxapp\model\Configure as ConfModel;
|
||||
use ywxapp\model\Links;
|
||||
use ywxapp\model\Notice;
|
||||
use ywxapp\model\Ad;
|
||||
use ywxapp\library\SkinOverlay;
|
||||
use ywxapp\library\SkinVariables;
|
||||
use ywxapp\library\TemplateManager;
|
||||
|
||||
/**
|
||||
* 前台控制器基类(统一核心前台与插件前台).
|
||||
*
|
||||
* - 核心前台(app\frontend\...):afterAuth 不启用后台 layout,渲染视图走应用默认视图目录。
|
||||
* - 插件前台(addon\*\...):视图根切到 addon/<name>/view/frontend/,
|
||||
* fetch 叠加皮肤覆盖层、配色变量与风格切换器。
|
||||
*
|
||||
* 公共逻辑(站点配置 / 友情链接 / user / success / error)统一落地于此,
|
||||
* 原 Frontend、AddonFrontend 已并入并删除。
|
||||
*/
|
||||
abstract class FrontendBase extends BaseController
|
||||
{
|
||||
/**
|
||||
* 视图类实例
|
||||
* @var \think\View
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* 前台默认放行登录:前台本质是「开放展示」,故默认不拦截(游客可访问)。
|
||||
* 需要登录 + 用户组权限的控制器(如插件用户中心、开发者中心)把本属性
|
||||
* 设为 [] 或具体 action 数组,即复用 BaseController 同一份 verifyAuth 守卫。
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化骨架.
|
||||
* @return void
|
||||
*/
|
||||
public function _initialize()
|
||||
{
|
||||
$this->view = $this->app->view;
|
||||
if ($this->isAddonContext()) {
|
||||
// 插件前台:视图根切到插件自身 view/frontend/
|
||||
$this->switchAddonViewPath();
|
||||
} else {
|
||||
// 核心前台(如 app/wxapp):统一走全局 layout 外壳,
|
||||
// 子模板写裸内容,由 common/layout.html 的 {__CONTENT__} 包裹。
|
||||
$this->view->config([
|
||||
'layout_on' => true,
|
||||
'layout_name' => 'common/layout',
|
||||
]);
|
||||
// 默认页面标题(子模板可通过 view() 的变量覆盖)
|
||||
$this->view->assign('title', 'YwxApp 框架展示');
|
||||
}
|
||||
|
||||
// 登录与用户组权限校验:默认放行(noNeedLogin=['*']),需登录的子类自行收窄。
|
||||
// verifyAuth 内部 tokenParse() 还原登录态并做权限校验,未登录整页 302 / AJAX 返 401,
|
||||
// 与后台、会员中心共用同一份 Auth::verifyAuth 逻辑。
|
||||
$this->auth->verifyAuth($this->noNeedLogin, $this->noNeedVerify);
|
||||
|
||||
if ($this->auth && $this->auth->isLogin) {
|
||||
$this->view->assign('member', $this->auth->info);
|
||||
}
|
||||
$siteConf = ConfModel::cache(true)->column('value', 'name');
|
||||
$this->view->assign('siteConf', $siteConf);
|
||||
$links = Links::cache(true)->where('status', 1)->order('sort desc')->select();
|
||||
$this->view->assign('links', $links);
|
||||
// 全站置顶公告条(方案 A):仅取当前生效 + 置顶 + 启用的公告
|
||||
$typeList = Notice::typeList();
|
||||
$topNotices = array_map(function ($n) use ($typeList) {
|
||||
$n['type_text'] = $typeList[$n['type']] ?? '公告';
|
||||
return $n;
|
||||
}, Notice::getActiveTopNotices());
|
||||
$this->view->assign('topNotices', $topNotices);
|
||||
// 全站广告位:按 position 分组的启用广告,模板可随时调用 {notempty name="adSlots.home_top"}
|
||||
$this->view->assign('adSlots', Ad::getSlots());
|
||||
// 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用
|
||||
$this->assignSite();
|
||||
// 前台主导航菜单(后台可配置,两级下拉),供 header.html 渲染
|
||||
$this->view->assign('navMenus', \app\backend\model\NavMenu::getNavTree());
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize(子类重写).
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值到模板.
|
||||
*/
|
||||
public function assign($name, $value = null)
|
||||
{
|
||||
View::assign($name, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从控制器类名反推插件目录并切换视图根目录到 addon/<name>/view/frontend/.
|
||||
* @return void
|
||||
*/
|
||||
protected function switchAddonViewPath()
|
||||
{
|
||||
// 重构版 MultiApp 已将 appPath 设为 addon/<插件>/,直接拼视图目录,无需正则
|
||||
$this->view->config([
|
||||
'view_path' => $this->app->getAppPath() . 'view' . DIRECTORY_SEPARATOR
|
||||
. 'frontend' . DIRECTORY_SEPARATOR,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前插件名(从控制器命名空间反推 addon\<name>\...).
|
||||
* 由 appPath(addon/<插件>/)剥 rootPath 取首段,无需正则。
|
||||
* @return string
|
||||
*/
|
||||
protected function currentAddon(): string
|
||||
{
|
||||
$rel = trim(substr($this->app->getAppPath(), strlen($this->app->getRootPath())), DIRECTORY_SEPARATOR);
|
||||
$seg = explode(DIRECTORY_SEPARATOR, $rel);
|
||||
return $seg[0] === 'addon' && isset($seg[1]) ? $seg[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前控制器是否属于插件上下文(addon\ 命名空间).
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAddonContext(): bool
|
||||
{
|
||||
return strpos($this->app->getNamespace(), 'addon\\') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息(前台会员).
|
||||
* @return mixed
|
||||
*/
|
||||
protected function user()
|
||||
{
|
||||
if ($this->auth && $this->auth->isLogin) {
|
||||
return $this->auth->info;
|
||||
}
|
||||
$this->result->error('获取用户信息失败!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板:插件前台叠加皮肤覆盖层 / 配色变量 / 风格切换器.
|
||||
*/
|
||||
protected function fetch($template = '', $vars = [], $replace = [], $config = [])
|
||||
{
|
||||
if ($this->isAddonContext()) {
|
||||
$addon = $this->currentAddon();
|
||||
$restore = null;
|
||||
if ($addon && $overlay = SkinOverlay::resolve($addon, 'frontend')) {
|
||||
// 把视图根目录切到「插件视图 + 皮肤覆盖」合并层,使 extend 的 layout 也走皮肤
|
||||
$restore = View::getConfig('view_path');
|
||||
View::config(['view_path' => $overlay]);
|
||||
}
|
||||
$result = View::fetch($template, $vars, $replace, $config);
|
||||
if ($restore !== null) {
|
||||
View::config(['view_path' => $restore]);
|
||||
}
|
||||
// 注入皮肤变量 CSS(Discuz 式配色层,无需改 HTML)
|
||||
$style = SkinVariables::styleTag($addon, 'frontend');
|
||||
if ($style !== '' && ($pos = stripos($result, '</head>')) !== false) {
|
||||
$result = substr($result, 0, $pos) . $style . "\n" . substr($result, $pos);
|
||||
} elseif ($style !== '') {
|
||||
$result = $style . $result;
|
||||
}
|
||||
// 注入前台风格切换器(界面设置开启 allow_member_select 时自动出现)
|
||||
$switcher = TemplateManager::switcherHtml($addon, 'frontend');
|
||||
if ($switcher !== '') {
|
||||
if (($pos = stripos($result, '</body>')) !== false) {
|
||||
$result = substr($result, 0, $pos) . $switcher . "\n" . substr($result, $pos);
|
||||
} else {
|
||||
$result .= $switcher;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
return View::fetch($template, $vars, $replace, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* success/error 已统一提升到 BaseController(success/error 代理 Result),此处不再重复定义。
|
||||
*/
|
||||
}
|
||||
@@ -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 ywxapp\library\SkinVariables;
|
||||
use ywxapp\model\MemberUser;
|
||||
|
||||
/**
|
||||
* 会员中心控制器基类(MemberBase)
|
||||
*
|
||||
* 同时服务于两类场景:
|
||||
* 1) 会员中心本体(app/member 应用):view_path = app/member/view/,layout = common/layout
|
||||
* 2) 插件用户中心(addon/<插件>/controller/member/):view_path = addon/<插件>/view/member/,layout = member/layout
|
||||
*
|
||||
* 鉴权:容器 auth 在 AppService 中默认已绑定 \ywxapp\library\Auth(会员鉴权),
|
||||
* 故会员登录态由 BaseController::verifyAuth 自动校验(设置 $noNeedLogin 可放行)。
|
||||
*
|
||||
* 用法:
|
||||
* // 会员中心本体控制器
|
||||
* namespace app\member\controller;
|
||||
* use ywxapp\controller\MemberBase;
|
||||
* class Index extends MemberBase { ... }
|
||||
*
|
||||
* // 插件用户中心控制器
|
||||
* namespace addon\blog\controller\member;
|
||||
* use ywxapp\controller\MemberBase;
|
||||
* class Index extends MemberBase { ... }
|
||||
*/
|
||||
class MemberBase extends BaseController
|
||||
{
|
||||
/**
|
||||
* 视图类实例
|
||||
* @var \think\View
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* 控制器初始化
|
||||
*/
|
||||
public function _initialize()
|
||||
{
|
||||
MemberUser::ensureSchema(); // 用户中心核心表自愈(缺表/缺列兜底)
|
||||
$this->view = $this->app->view;
|
||||
$appName = $this->app->http->getName();
|
||||
// 全局 layout 模式:会员中心本体套 view/layout,插件用户中心套各自的 view/member/layout。
|
||||
// 不需要套 layout 的页面(如会员中心 iframe 主框架页)可在控制器内调用 $this->view->layout(false) 关闭。
|
||||
$layout = 'common/layout';
|
||||
if ($appName === 'member') {
|
||||
// 会员中心本体:视图根目录为 app/member/view/,layout 为 view/layout.html
|
||||
$viewPath = $this->app->getAppPath() . 'view' . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
// 插件用户中心:视图根目录为 addon/<插件>/view/member/
|
||||
$addon = $this->currentAddon();
|
||||
$viewPath = $addon
|
||||
? root_path() . 'addon' . DIRECTORY_SEPARATOR . $addon . DIRECTORY_SEPARATOR
|
||||
. 'view' . DIRECTORY_SEPARATOR . 'member' . DIRECTORY_SEPARATOR
|
||||
: $this->app->getAppPath() . 'view' . DIRECTORY_SEPARATOR . 'member' . DIRECTORY_SEPARATOR;
|
||||
// 插件内使用插件自己的 view/member/layout.html(view_path 已含 member/)
|
||||
$layout = 'common/layout';
|
||||
}
|
||||
$this->view->config([
|
||||
'view_suffix' => 'html',
|
||||
'view_path' => $viewPath,
|
||||
'layout_on' => true,
|
||||
'layout_name' => $layout,
|
||||
]);
|
||||
|
||||
// 登录与权限校验:统一走 Base 的 verifyAuth + $noNeedLogin 机制
|
||||
// (未登录且非免登录 action 时整页 302 跳登录页,AJAX 返回 401,与后台一致)。
|
||||
// 注意:verifyAuth 内部会 tokenParse() 还原登录态,无需在中间件重复处理。
|
||||
$this->auth->verifyAuth($this->noNeedLogin, $this->noNeedVerify);
|
||||
|
||||
if ($this->auth && $this->auth->isLogin) {
|
||||
$this->view->assign('member', $this->auth->info);
|
||||
}
|
||||
$siteConf = \ywxapp\model\Configure::cache(true)->column('value', 'name');
|
||||
$this->view->assign('siteConf', $siteConf);
|
||||
$links = \ywxapp\model\Links::cache(true)->where('status', 1)->order('sort desc')->select();
|
||||
$this->view->assign('links', $links);
|
||||
// 注入站点信息(route_base / site.module / site.controller 等),供前端 route.js 使用
|
||||
$this->assignSite();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 子类初始化钩子
|
||||
*/
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 获取当前登录会员信息(未登录直接报错)
|
||||
* @return \ywxapp\model\Member
|
||||
*/
|
||||
protected function user()
|
||||
{
|
||||
if ($this->auth && $this->auth->isLogin) {
|
||||
return $this->auth->info;
|
||||
}
|
||||
$this->result->error('请先登录会员中心');
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前插件名(addon\<name>\...)
|
||||
* 由 appPath(addon/<插件>/)剥 rootPath 取首段,无需正则。
|
||||
*/
|
||||
protected function currentAddon(): string
|
||||
{
|
||||
$rel = trim(substr($this->app->getAppPath(), strlen($this->app->getRootPath())), DIRECTORY_SEPARATOR);
|
||||
$seg = explode(DIRECTORY_SEPARATOR, $rel);
|
||||
return $seg[0] === 'addon' && isset($seg[1]) ? $seg[1] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板:优先使用已激活模板的覆盖页(Discuz 式局部覆盖),未命中回退插件默认视图。
|
||||
*/
|
||||
protected function fetch($template = '', $vars = [], $replace = [], $config = [])
|
||||
{
|
||||
$addon = $this->currentAddon();
|
||||
$restore = null;
|
||||
if ($addon && $overlay = \ywxapp\library\SkinOverlay::resolve($addon, 'member')) {
|
||||
// 把视图根目录切到「插件视图 + 皮肤覆盖」合并层,使 extend 的 layout 也走皮肤
|
||||
$restore = \think\facade\View::getConfig('view_path');
|
||||
\think\facade\View::config(['view_path' => $overlay]);
|
||||
}
|
||||
$result = \think\facade\View::fetch($template, $vars, $replace, $config);
|
||||
if ($restore !== null) {
|
||||
\think\facade\View::config(['view_path' => $restore]);
|
||||
}
|
||||
// 注入皮肤变量 CSS(Discuz 式配色层,无需改 HTML)
|
||||
$style = SkinVariables::styleTag($addon, 'member');
|
||||
if ($style !== '' && ($pos = stripos($result, '</head>')) !== false) {
|
||||
$result = substr($result, 0, $pos) . $style . "\n" . substr($result, $pos);
|
||||
} elseif ($style !== '') {
|
||||
$result = $style . $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?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\controller;
|
||||
|
||||
use ywxapp\model\Attachment;
|
||||
use ywxapp\service\FileStorageService;
|
||||
|
||||
/**
|
||||
* UeditorPlus 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class UeditorPlus
|
||||
{
|
||||
|
||||
private $user;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (app('auth')) {
|
||||
$this->user = app()->auth->info;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置 config
|
||||
* GET action config 请求动作名称
|
||||
*/
|
||||
public function config()
|
||||
{
|
||||
return json(config('ueditor-plus'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片 image
|
||||
* GET action image 请求动作名称,可通过 imageActionName 配置修改名称
|
||||
* FILE file file 上传的文件,文件表单名称可通过 imageFieldName 配置修改名称
|
||||
*return { "state":"SUCCESS", "url":"upload/demo.jpg", "title":"demo.jpg", "original":"demo.jpg"; }
|
||||
*/
|
||||
public function image()
|
||||
{
|
||||
$file = request()->file('file');
|
||||
// return json(['data'=> app()->auth->info]);
|
||||
try {
|
||||
$storage = new FileStorageService();
|
||||
$data = $storage->upload($file, 'images');
|
||||
$info = $this->toSaveData($data);
|
||||
return json([
|
||||
'mime' => $data['mime'],
|
||||
"state" => "SUCCESS",
|
||||
"url" => $info->url,
|
||||
"title" => $info->name,
|
||||
"original" => $info->original,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片抓取 catch
|
||||
* GET action catch 请求动作名称,可通过 catcherActionName 配置修改名称
|
||||
* POST source url 抓取的图片地址,文件表单名称可通过 catcherFieldName 配置修改名称
|
||||
*/
|
||||
public function catch ()
|
||||
{
|
||||
$file = request()->file('file');
|
||||
try {
|
||||
// 实例化上传服务
|
||||
$storage = new FileStorageService();
|
||||
|
||||
// 执行上传
|
||||
// 这里的逻辑不需要关心具体是上传到哪里,由配置决定
|
||||
$result = $storage->upload($file, 'avatar/' . date('Ymd'));
|
||||
return json(['code' => 0, 'msg' => '上传成功', 'data' => $result]);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频上传 video
|
||||
* GET action video 请求动作名称,可通过 videoActionName 配置修改名称
|
||||
* FILE file file 上传的文件,文件表单名称可通过 videoFieldName 配置修改名称
|
||||
*/
|
||||
public function video()
|
||||
{
|
||||
$file = request()->file('file');
|
||||
|
||||
$data = [
|
||||
'size' => $file->getSize(),
|
||||
'Mime' => $file->getOriginalMime(),
|
||||
];
|
||||
return json(['code' => 0, 'msg' => '上传成功', 'data' => $data]);
|
||||
try {
|
||||
// 实例化上传服务
|
||||
$storage = new FileStorageService();
|
||||
|
||||
// 执行上传
|
||||
// 这里的逻辑不需要关心具体是上传到哪里,由配置决定
|
||||
$result = $storage->upload($file, 'avatar/' . date('Ymd'));
|
||||
return json(['code' => 0, 'msg' => '上传成功', 'data' => $result]);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传 file
|
||||
* GET action file 请求动作名称,可通过 fileActionName 配置修改名称
|
||||
* FILE file file 上传的文件,文件表单名称可通过 fileFieldName 配置修改名称
|
||||
*/
|
||||
public function file()
|
||||
{
|
||||
$file = request()->file('file');
|
||||
try {
|
||||
// 实例化上传服务
|
||||
$storage = new FileStorageService();
|
||||
|
||||
// 执行上传
|
||||
// 这里的逻辑不需要关心具体是上传到哪里,由配置决定
|
||||
$result = $storage->upload($file, 'avatar/' . date('Ymd'));
|
||||
return json(['code' => 0, 'msg' => '上传成功', 'data' => $result]);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片列表 listImage
|
||||
* GET action listImage 请求动作名称,可通过 imageManagerActionName 配置修改名称
|
||||
*/
|
||||
public function listImage()
|
||||
{
|
||||
try {
|
||||
$user = app()->auth->info;
|
||||
$datas = Attachment::where('uid', $user->uid)->where('mime', 'like', 'image%')->field('name,url')->select();
|
||||
return json([
|
||||
"state" => "SUCCESS",
|
||||
"list" => $datas,
|
||||
"start" => 0,
|
||||
"total" => count($datas),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录日志,但不要因为入库失败而中断上传流程(或者根据需求决定)
|
||||
\think\facade\Log::error( $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件列表 listFile
|
||||
* GET action listFile 请求动作名称,可通过 fileManagerActionName 配置修改名称
|
||||
*/
|
||||
public function listFile()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件信息保存到数据库
|
||||
*/
|
||||
protected function toSaveData(array $data, array $options = [])
|
||||
{
|
||||
try {
|
||||
$user = app()->auth->info;
|
||||
$data['uid'] = $user->uid; // 根据你的登录逻辑获取用户ID
|
||||
$info = Attachment::create($data);
|
||||
return $info;
|
||||
} catch (\Exception $e) {
|
||||
// 记录日志,但不要因为入库失败而中断上传流程(或者根据需求决定)
|
||||
\think\facade\Log::error('文件记录入库失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?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\facade\Config;
|
||||
use think\facade\Request;
|
||||
use ywxapp\exceptions\UploadException;
|
||||
use ywxapp\service\FileStorageService;
|
||||
|
||||
/**
|
||||
* UploadController 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class UploadController extends BaseController
|
||||
{
|
||||
|
||||
/**
|
||||
* 不需要登录验证的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['*'];
|
||||
|
||||
/**
|
||||
* 不需要权限验证的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 控制器初始化 initialize
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 单文件上传
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
try {
|
||||
// 获取上传的文件
|
||||
$file = Request::file('file');
|
||||
if (! $file || ! $file->isValid()) {
|
||||
throw UploadException::fileNotExists();
|
||||
}
|
||||
|
||||
// 获取存储驱动(可从前端指定)
|
||||
$driver = Request::param('driver', Config::get('upload.default'));
|
||||
$path = Request::param('type', '');
|
||||
$filename = Request::param('filename', '');
|
||||
|
||||
$storage = new FileStorageService();
|
||||
$result = $storage->upload($file, $path . '/' . date('Ymd'));
|
||||
$this->result->success($result);
|
||||
} catch (\Exception $e) {
|
||||
$this->result->error($e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多文件上传
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
// public function batchUpload()
|
||||
// {
|
||||
// try {
|
||||
// $files = Request::file('files');
|
||||
// if (empty($files)) {
|
||||
// throw UploadException::fileNotExists();
|
||||
// }
|
||||
|
||||
// $driver = Request::param('driver', Config::get('upload.default'));
|
||||
// $path = Request::param('path', '');
|
||||
|
||||
// $uploadService = new UploadService($driver);
|
||||
// $results = $uploadService->batchUpload($files, $path);
|
||||
|
||||
// // 检查是否有失败的上传
|
||||
// $hasError = collect($results)->some(function ($item) {
|
||||
// return isset($item['error']);
|
||||
// });
|
||||
|
||||
// if ($hasError) {
|
||||
// return json($this->result->error('部分文件上传失败', 200, $results));
|
||||
// }
|
||||
|
||||
// return json($this->result->success($results));
|
||||
// } catch (UploadException $e) {
|
||||
// return json($this->result->error($e->getMessage(), 400));
|
||||
// } catch (\Exception $e) {
|
||||
// return json($this->result->error($e->getMessage(), 500));
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Base64上传
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
// public function base64Upload()
|
||||
// {
|
||||
// try {
|
||||
// $base64 = Request::param('base64', '');
|
||||
// if (empty($base64)) {
|
||||
// throw UploadException::fileNotExists();
|
||||
// }
|
||||
|
||||
// // 解析base64
|
||||
// if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64, $matches)) {
|
||||
// $ext = $matches[2];
|
||||
// $data = base64_decode(str_replace($matches[1], '', $base64));
|
||||
|
||||
// // 创建临时文件
|
||||
// $tempFile = tmpfile();
|
||||
// fwrite($tempFile, $data);
|
||||
// $meta = stream_get_meta_data($tempFile);
|
||||
// $tempPath = $meta['uri'];
|
||||
|
||||
// // 创建UploadedFile对象
|
||||
// $file = new \think\file\UploadedFile(
|
||||
// $tempPath,
|
||||
// 'base64_image.' . $ext,
|
||||
// mime_content_type($tempPath),
|
||||
// filesize($tempPath),
|
||||
// UPLOAD_ERR_OK
|
||||
// );
|
||||
|
||||
// $driver = Request::param('driver', Config::get('upload.default'));
|
||||
// $path = Request::param('path', '');
|
||||
|
||||
// $uploadService = new UploadService($driver);
|
||||
// $result = $uploadService->upload($file, $path);
|
||||
|
||||
// fclose($tempFile);
|
||||
|
||||
// return json($this->result->success($result));
|
||||
// }
|
||||
|
||||
// throw new \Exception('无效的Base64数据');
|
||||
// } catch (UploadException $e) {
|
||||
// return json($this->result->error($e->getMessage(), 400));
|
||||
// } catch (\Exception $e) {
|
||||
// return json($this->result->error($e->getMessage(), 500));
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user