918 lines
33 KiB
PHP
918 lines
33 KiB
PHP
<?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;
|
||
}
|
||
}
|