first commot
11
.env
Normal file
@ -0,0 +1,11 @@
|
||||
APP_DEBUG = true
|
||||
|
||||
DB_TYPE = mysql
|
||||
DB_HOST = 10.168.1.239
|
||||
DB_NAME = official_website
|
||||
DB_USER = official
|
||||
DB_PASS = meitian@#!
|
||||
DB_PORT = 3306
|
||||
DB_CHARSET = utf8
|
||||
|
||||
DEFAULT_LANG = zh-cn
|
||||
11
.example.env
Normal file
@ -0,0 +1,11 @@
|
||||
APP_DEBUG = true
|
||||
|
||||
DB_TYPE = mysql
|
||||
DB_HOST = 127.0.0.1
|
||||
DB_NAME = test
|
||||
DB_USER = username
|
||||
DB_PASS = password
|
||||
DB_PORT = 3306
|
||||
DB_CHARSET = utf8
|
||||
|
||||
DEFAULT_LANG = zh-cn
|
||||
42
.travis.yml
Normal file
@ -0,0 +1,42 @@
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
branches:
|
||||
only:
|
||||
- stable
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
before_install:
|
||||
- composer self-update
|
||||
|
||||
install:
|
||||
- composer install --no-dev --no-interaction --ignore-platform-reqs
|
||||
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
|
||||
- composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
|
||||
- composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
|
||||
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
|
||||
|
||||
script:
|
||||
- php think unit
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
|
||||
file:
|
||||
- ThinkPHP_Core.zip
|
||||
- ThinkPHP_Full.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
32
LICENSE.txt
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
|
||||
版权所有Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn)
|
||||
All rights reserved。
|
||||
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
|
||||
|
||||
Apache Licence是著名的非盈利开源组织Apache采用的协议。
|
||||
该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
|
||||
允许代码修改,再作为开源或商业软件发布。需要满足
|
||||
的条件:
|
||||
1. 需要给代码的用户一份Apache Licence ;
|
||||
2. 如果你修改了代码,需要在被修改的文件中说明;
|
||||
3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
|
||||
带有原来代码中的协议,商标,专利声明和其他原来作者规
|
||||
定需要包含的说明;
|
||||
4. 如果再发布的产品中包含一个Notice文件,则在Notice文
|
||||
件中需要带有本协议内容。你可以在Notice中增加自己的
|
||||
许可,但不可以表现为对Apache Licence构成更改。
|
||||
具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
77
README.md
Normal file
@ -0,0 +1,77 @@
|
||||

|
||||
|
||||
ThinkPHP 8
|
||||
===============
|
||||
|
||||
## 特性
|
||||
|
||||
* 基于PHP`8.0+`重构
|
||||
* 升级`PSR`依赖
|
||||
* 依赖`think-orm`3.0+版本
|
||||
* 全新的`think-dumper`服务,支持远程调试
|
||||
* 支持`6.0`/`6.1`无缝升级
|
||||
|
||||
> ThinkPHP8的运行环境要求PHP8.0+
|
||||
|
||||
现在开始,你可以使用官方提供的[ThinkChat](https://chat.topthink.com/),让你在学习ThinkPHP的旅途中享受私人AI助理服务!
|
||||
|
||||

|
||||
|
||||
ThinkPHP生态服务由[顶想云](https://www.topthink.com)(TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。
|
||||
|
||||
## 文档
|
||||
|
||||
[完全开发手册](https://doc.thinkphp.cn)
|
||||
|
||||
|
||||
## 赞助
|
||||
|
||||
全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及GIT仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障ThinkPHP的可持续发展。
|
||||
|
||||
[](https://www.thinkphp.cn/sponsor/special)
|
||||
|
||||
[](https://www.thinkphp.cn/sponsor)
|
||||
|
||||
## 安装
|
||||
|
||||
~~~
|
||||
composer create-project topthink/think tp
|
||||
~~~
|
||||
|
||||
启动服务
|
||||
|
||||
~~~
|
||||
cd tp
|
||||
php think run
|
||||
~~~
|
||||
|
||||
然后就可以在浏览器中访问
|
||||
|
||||
~~~
|
||||
http://localhost:8000
|
||||
~~~
|
||||
|
||||
如果需要更新框架使用
|
||||
~~~
|
||||
composer update topthink/framework
|
||||
~~~
|
||||
|
||||
## 命名规范
|
||||
|
||||
`ThinkPHP`遵循PSR-2命名规范和PSR-4自动加载规范。
|
||||
|
||||
## 参与开发
|
||||
|
||||
直接提交PR或者Issue即可
|
||||
|
||||
## 版权信息
|
||||
|
||||
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
|
||||
|
||||
本项目包含的第三方源码和二进制文件之版权信息另行标注。
|
||||
|
||||
版权所有Copyright © 2006-2024 by ThinkPHP (http://thinkphp.cn) All rights reserved。
|
||||
|
||||
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
|
||||
|
||||
更多细节参阅 [LICENSE.txt](LICENSE.txt)
|
||||
100
REDIS_SETUP.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Redis 配置说明
|
||||
|
||||
## 1. 安装 Redis 扩展
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
composer require predis/predis
|
||||
```
|
||||
|
||||
或者如果使用 phpredis 扩展(需要 PHP Redis 扩展):
|
||||
|
||||
```bash
|
||||
# 确保已安装 PHP Redis 扩展
|
||||
# 然后直接使用,无需 composer 安装
|
||||
```
|
||||
|
||||
## 2. 配置环境变量
|
||||
|
||||
在项目根目录创建 `.env` 文件(如果不存在),添加以下配置:
|
||||
|
||||
```env
|
||||
# Redis 配置
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
REDIS_PREFIX=
|
||||
|
||||
# 缓存配置
|
||||
CACHE_DRIVER=redis
|
||||
|
||||
# Session 配置
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_PREFIX=
|
||||
```
|
||||
|
||||
## 3. 配置文件说明
|
||||
|
||||
### cache.php
|
||||
- 默认缓存驱动已设置为 `redis`
|
||||
- Redis 连接配置已添加
|
||||
|
||||
### session.php
|
||||
- Session 驱动已设置为 `redis`
|
||||
- 使用 Redis 存储 Session
|
||||
|
||||
## 4. 使用说明
|
||||
|
||||
### 缓存使用
|
||||
```php
|
||||
use think\facade\Cache;
|
||||
|
||||
// 设置缓存
|
||||
Cache::set('key', 'value', 3600);
|
||||
|
||||
// 获取缓存
|
||||
$value = Cache::get('key');
|
||||
|
||||
// 删除缓存
|
||||
Cache::delete('key');
|
||||
```
|
||||
|
||||
### Session 使用
|
||||
```php
|
||||
use think\facade\Session;
|
||||
|
||||
// 设置 Session
|
||||
Session::set('key', 'value');
|
||||
|
||||
// 获取 Session
|
||||
$value = Session::get('key');
|
||||
```
|
||||
|
||||
## 5. 验证 Redis 连接
|
||||
|
||||
确保 Redis 服务已启动:
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
redis-server
|
||||
|
||||
# Linux/Mac
|
||||
sudo systemctl start redis
|
||||
# 或
|
||||
redis-server
|
||||
```
|
||||
|
||||
测试连接:
|
||||
```bash
|
||||
redis-cli ping
|
||||
# 应该返回 PONG
|
||||
```
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
1. 如果 Redis 未安装或连接失败,可以临时将 `CACHE_DRIVER` 和 `SESSION_DRIVER` 改回 `file`
|
||||
2. Redis 密码如果为空,可以不设置 `REDIS_PASSWORD`
|
||||
3. `REDIS_PREFIX` 用于区分不同项目的缓存,建议设置
|
||||
|
||||
165
app/admin/BaseController.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\admin;
|
||||
|
||||
use think\App;
|
||||
use think\exception\ValidateException;
|
||||
use think\Validate;
|
||||
use think\facade\Request;
|
||||
use think\facade\Cache;
|
||||
use app\admin\controller\OperationLog\OperationLogger;
|
||||
use app\service\JwtService;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
/**
|
||||
* Request实例
|
||||
* @var \think\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* 应用实例
|
||||
* @var \think\App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* 是否批量验证
|
||||
* @var bool
|
||||
*/
|
||||
protected $batchValidate = false;
|
||||
|
||||
/**
|
||||
* 控制器中间件
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [];
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @access public
|
||||
* @param App $app 应用对象
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->request = $this->app->request;
|
||||
|
||||
// 控制器初始化
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
protected function initialize()
|
||||
{}
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作日志
|
||||
*
|
||||
* @param string $module 模块名称(如:文件管理)
|
||||
* @param string $action 操作名称(如:创建文件分组)
|
||||
* @param array $responseData 响应数据(可选)
|
||||
* @param array $userInfo 用户信息(可选,如果不提供则从 Session 获取)
|
||||
* @return bool
|
||||
*/
|
||||
protected function logSuccess(string $module, string $action, array $responseData = [], array $userInfo = []): bool
|
||||
{
|
||||
return OperationLogger::success($module, $action, $responseData, $userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败日志
|
||||
*
|
||||
* @param string $module 模块名称
|
||||
* @param string $action 操作名称
|
||||
* @param string $errorMessage 错误信息
|
||||
* @param array $userInfo 用户信息(可选,如果不提供则从 Session 获取)
|
||||
* @return bool
|
||||
*/
|
||||
protected function logFail(string $module, string $action, string $errorMessage, array $userInfo = []): bool
|
||||
{
|
||||
return OperationLogger::fail($module, $action, $errorMessage, $userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作日志
|
||||
*
|
||||
* @param string $module 操作模块
|
||||
* @param string $action 操作动作
|
||||
* @param array $requestData 请求数据(为空则自动获取)
|
||||
* @param array $responseData 响应数据
|
||||
* @param int $status 操作状态:1-成功,0-失败
|
||||
* @param string $errorMessage 错误信息
|
||||
* @return bool
|
||||
*/
|
||||
protected function logOperation(
|
||||
string $module,
|
||||
string $action,
|
||||
array $requestData = [],
|
||||
array $responseData = [],
|
||||
int $status = 1,
|
||||
string $errorMessage = ''
|
||||
): bool {
|
||||
return OperationLogger::record(
|
||||
$module,
|
||||
$action,
|
||||
$requestData,
|
||||
$responseData,
|
||||
$status,
|
||||
$errorMessage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录管理员信息
|
||||
*
|
||||
* @return array 用户信息数组,包含 id、account、name 等字段
|
||||
*/
|
||||
protected function getAdminUserInfo(): array
|
||||
{
|
||||
return JwtService::getUserFromHeader($this->request->header('Authorization', ''));
|
||||
}
|
||||
|
||||
}
|
||||
2
app/admin/common.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// 这是系统自动生成的公共文件
|
||||
197
app/admin/controller/Article/ArticleCategoryController.php
Normal file
@ -0,0 +1,197 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\admin\controller\Article;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
|
||||
class ArticleCategoryController extends BaseController
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取文章分类列表接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getAllArticleCategories(string $keyword)
|
||||
{
|
||||
try {
|
||||
//判断keyword是否存在
|
||||
$keyword = $keyword ?? '';
|
||||
|
||||
// 获取文章分类列表
|
||||
$categories = Db::name('mete_articles_category')
|
||||
->where('delete_time', null)
|
||||
->where('status', 1)
|
||||
->where('name', 'like', '%' . $keyword . '%')
|
||||
->order('sort', 'desc')
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray(); // 转换为数组
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $categories
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章分类列表接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getArticleCategories()
|
||||
{
|
||||
try {
|
||||
// 获取文章分类列表
|
||||
$categories = Db::name('mete_articles_category')
|
||||
->where('delete_time', null)
|
||||
->where('status', 1)
|
||||
->field('id,cid,name,image,sort')
|
||||
->order('sort', 'desc')
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray(); // 转换为数组
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $categories
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增文章分类
|
||||
* @return Json
|
||||
*/
|
||||
public function createCategory(): Json
|
||||
{
|
||||
try {
|
||||
$data = request()->only(['cid','name','image','desc','sort','status']);
|
||||
// 基础校验
|
||||
if (empty($data['name'])) {
|
||||
throw new ValidateException('分类名称不能为空');
|
||||
}
|
||||
$data['cid'] = isset($data['cid']) ? (int)$data['cid'] : 0;
|
||||
$data['sort'] = isset($data['sort']) ? (int)$data['sort'] : 0;
|
||||
$data['status']= isset($data['status']) ? (int)$data['status'] : 1;
|
||||
$data['create_time'] = date('Y-m-d H:i:s');
|
||||
$data['update_time'] = $data['create_time'];
|
||||
|
||||
$id = Db::name('mete_articles_category')->insertGetId($data);
|
||||
return json(['code'=>200,'msg'=>'success','data'=>['id'=>$id]]);
|
||||
} catch (ValidateException $ve) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章分类管理', '新增文章分类', $ve->getMessage());
|
||||
return json(['code'=>422,'msg'=>$ve->getMessage(),'data'=>null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章分类管理', '新增文章分类', $e->getMessage());
|
||||
return json(['code'=>500,'msg'=>'fail:'.$e->getMessage(),'data'=>$e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文章分类
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function editCategory(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = request()->only(['cid','name','image','desc','sort','status']);
|
||||
if (empty($data)) {
|
||||
return json(['code'=>400,'msg'=>'无更新数据','data'=>null]);
|
||||
}
|
||||
$data['update_time'] = date('Y-m-d H:i:s');
|
||||
$affected = Db::name('mete_articles_category')->where('id',$id)->update($data);
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章分类管理', '更新文章分类', ['id' => $id]);
|
||||
return json(['code'=>200,'msg'=>'success','data'=>null]);
|
||||
}
|
||||
return json(['code'=>404,'msg'=>'not found','data'=>null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章分类管理', '更新文章分类', $e->getMessage());
|
||||
return json(['code'=>500,'msg'=>'fail:'.$e->getMessage(),'data'=>$e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章分类(软删除)
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteCategory(int $id): Json
|
||||
{
|
||||
try {
|
||||
$affected = Db::name('mete_articles_category')
|
||||
->where('id', $id)
|
||||
->update(['delete_time' => date('Y-m-d H:i:s')]);
|
||||
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章分类管理', '删除文章分类', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => 'success', 'data' => null]);
|
||||
}
|
||||
return json(['code' => 404, 'msg' => 'not found', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章分类管理', '删除文章分类', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分类状态
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function updateCategoryStatus(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = request()->only(['status']);
|
||||
if (empty($data)) {
|
||||
return json(['code'=>400,'msg'=>'无更新数据','data'=>null]);
|
||||
}
|
||||
$data['status'] = isset($data['status']) ? (int)$data['status'] : 1;
|
||||
$data['update_time'] = date('Y-m-d H:i:s');
|
||||
$affected = Db::name('mete_articles_category')->where('id',$id)->update($data);
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章分类管理', '更新分类状态', ['id' => $id]);
|
||||
return json(['code'=>200,'msg'=>'success','data'=>null]);
|
||||
}
|
||||
return json(['code'=>404,'msg'=>'not found','data'=>null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章分类管理', '更新分类状态', $e->getMessage());
|
||||
return json(['code'=>500,'msg'=>'fail:'.$e->getMessage(),'data'=>$e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
605
app/admin/controller/Article/ArticleController.php
Normal file
@ -0,0 +1,605 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\admin\controller\Article;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
|
||||
class ArticleController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取文章列表
|
||||
* @param string $keyword 文章标题关键词(默认空)
|
||||
* @param string $cate 文章分类(默认空)
|
||||
* @param int $page 页码(默认1)
|
||||
* @param int $pageSize 每页条数(默认10)
|
||||
* @return Json
|
||||
*/
|
||||
public function getArticlesList(string $keyword = '', string $cate = '', int $page = 1, int $pageSize = 10): Json
|
||||
{
|
||||
try {
|
||||
// 安全处理参数(防止非法参数)
|
||||
$page = max(1, $page); // 页码最小为1
|
||||
$pageSize = max(1, min(100, $pageSize)); // 每页条数限制1-100条
|
||||
$keyword = trim($keyword);
|
||||
$cate = trim($cate);
|
||||
|
||||
// 输出参数值
|
||||
trace('查询参数:', 'debug');
|
||||
trace([
|
||||
'keyword' => $keyword,
|
||||
'cate' => $cate,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
], 'debug');
|
||||
|
||||
// 构建查询条件,合并表连接
|
||||
$query = Db::name('mete_articles')->alias('a')
|
||||
->where('a.delete_time', null)
|
||||
->leftJoin('mete_admin_user u', 'a.publisher = u.id')
|
||||
->leftJoin('mete_articles_category c', 'a.cate = c.id');
|
||||
|
||||
// 关键词筛选
|
||||
if ($keyword) {
|
||||
$query->where('a.title', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
if ($cate) {
|
||||
$query->where('a.cate', $cate);
|
||||
}
|
||||
|
||||
// 获取总条数
|
||||
$total = $query->count();
|
||||
|
||||
// 执行分页查询,合并order语句
|
||||
$articles = $query
|
||||
->field('a.id, a.title, c.name as cate, a.author, a.status, a.views, a.likes, a.publishdate,a.recommend,a.top,a.update_time, u.name as publisher')
|
||||
->order('a.status, a.top desc, a.recommend desc, a.sort desc, a.id desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => [
|
||||
'list' => $articles,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 简化异常处理
|
||||
trace('文章列表查询失败: ' . $e->getMessage(), 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '文章列表查询失败,请稍后重试',
|
||||
'data' => ['list' => [], 'total' => 0, 'page' => $page, 'pageSize' => $pageSize]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查标题相似度
|
||||
* @param string $title 新标题
|
||||
* @return array|null 相似文章列表,null表示没有相似标题
|
||||
*/
|
||||
private function checkTitleSimilarity(string $title)
|
||||
{
|
||||
$newTitleLower = strtolower($title);
|
||||
$newTitleLength = strlen($newTitleLower);
|
||||
|
||||
// 获取所有文章标题
|
||||
$allTitles = Db::name('mete_articles')
|
||||
->field('id, title')
|
||||
->select();
|
||||
|
||||
$similarArticles = [];
|
||||
|
||||
// 计算每个标题的相似度
|
||||
foreach ($allTitles as $article) {
|
||||
$existingTitleLower = strtolower($article['title']);
|
||||
$existingTitleLength = strlen($existingTitleLower);
|
||||
|
||||
// 使用Levenshtein距离计算相似度
|
||||
$distance = levenshtein($newTitleLower, $existingTitleLower);
|
||||
$maxLength = max($newTitleLength, $existingTitleLength);
|
||||
$similarity = $maxLength > 0 ? (1 - $distance / $maxLength) * 100 : 0;
|
||||
|
||||
// 相似度超过50%才返回
|
||||
if ($similarity >= 50) {
|
||||
$similarArticles[] = [
|
||||
'id' => $article['id'],
|
||||
'title' => $article['title'],
|
||||
'similarity' => round($similarity, 2) // 保留两位小数
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 按相似度降序排序
|
||||
usort($similarArticles, function($a, $b) {
|
||||
return $b['similarity'] - $a['similarity'];
|
||||
});
|
||||
|
||||
return !empty($similarArticles) ? $similarArticles : null;
|
||||
}
|
||||
|
||||
public function createArticle(): Json
|
||||
{
|
||||
try {
|
||||
$data = request()->only(['title','cate','image','desc','author','content','sort','status','is_trans','transurl','ignore_similarity']);
|
||||
// 基础校验
|
||||
if (empty($data['title'])) {
|
||||
throw new ValidateException('文章标题不能为空');
|
||||
} elseif (empty($data['cate'])) {
|
||||
throw new ValidateException('请选择文章分类');
|
||||
} elseif (empty($data['author'])) {
|
||||
throw new ValidateException('请填写作者');
|
||||
} elseif (empty($data['content'])) {
|
||||
throw new ValidateException('请填写内容');
|
||||
}
|
||||
|
||||
// 标题相似度检测(仅新增时检测)
|
||||
if (empty($data['ignore_similarity'])) {
|
||||
$similarArticles = $this->checkTitleSimilarity($data['title']);
|
||||
if ($similarArticles) {
|
||||
return json([
|
||||
'code' => 409,
|
||||
'msg' => '检测到相似标题',
|
||||
'data' => [
|
||||
'similar_articles' => $similarArticles
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前登录用户ID
|
||||
$userId = Request::middleware('user_id', '');
|
||||
|
||||
// 移除 ignore_similarity,不保存到数据库
|
||||
unset($data['ignore_similarity']);
|
||||
|
||||
// 准备写入数据库的数据
|
||||
$insertData = [
|
||||
'title' => $data['title'],
|
||||
'cate' => $data['cate'],
|
||||
'image' => $data['image'] ?? '',
|
||||
'desc' => $data['desc'] ?? '',
|
||||
'author' => $data['author'],
|
||||
'content' => $data['content'],
|
||||
'sort' => isset($data['sort']) ? (int)$data['sort'] : 0,
|
||||
'status' => isset($data['status']) ? (int)$data['status'] : 0,
|
||||
'is_trans' => $data['is_trans'] ?? 0,
|
||||
'transurl' => $data['is_trans'] == 1 ? ($data['transurl'] ?? '') : '',
|
||||
'publisher' => $userId,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$id = Db::name('mete_articles')->insertGetId($insertData);
|
||||
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '创建文章', ['id' => $id]);
|
||||
|
||||
return json(['code'=>200,'msg'=>'success','data'=>['id'=>$id]]);
|
||||
} catch (ValidateException $ve) {
|
||||
return json(['code'=>422,'msg'=>$ve->getMessage(),'data'=>null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '创建文章', $e->getMessage());
|
||||
return json(['code'=>500,'msg'=>'fail:'.$e->getMessage(),'data'=>$e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章详情
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function getArticle(int $id): Json
|
||||
{
|
||||
try {
|
||||
$article = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->field('id,title,cate,image,desc,author,content,is_trans,transurl,views,likes,publisher,create_time,publishdate,update_time')
|
||||
->find();
|
||||
|
||||
if (!$article) {
|
||||
return json(['code' => 404, 'msg' => '文章不存在', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 200, 'msg' => 'success', 'data' => $article]);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 500, 'msg' => 'fail:' . $e->getMessage(), 'data' => $e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文章
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function editArticle(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = request()->only(['title','cate','image','desc','author','content','sort','status','is_trans','transurl']);
|
||||
if (empty($data)) {
|
||||
return json(['code'=>400,'msg'=>'无更新数据','data'=>null]);
|
||||
}
|
||||
$data['update_time'] = date('Y-m-d H:i:s');
|
||||
$affected = Db::name('mete_articles')->where('id',$id)->update($data);
|
||||
if ($affected) {
|
||||
return json(['code'=>200,'msg'=>'success','data'=>null]);
|
||||
}
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '编辑文章', ['id' => $id]);
|
||||
return json(['code'=>404,'msg'=>'not found','data'=>null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '编辑文章', $e->getMessage());
|
||||
return json(['code'=>500,'msg'=>'fail:'.$e->getMessage(),'data'=>$e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章分类(软删除)
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteArticle(int $id): Json
|
||||
{
|
||||
try {
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update(['delete_time' => date('Y-m-d H:i:s')]);
|
||||
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '删除文章', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => 'success', 'data' => null]);
|
||||
}
|
||||
return json(['code' => 404, 'msg' => 'not found', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '删除文章', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分类状态
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function updateCategoryStatus(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = request()->only(['status']);
|
||||
if (empty($data)) {
|
||||
return json(['code'=>400,'msg'=>'无更新数据','data'=>null]);
|
||||
}
|
||||
$data['update_time'] = date('Y-m-d H:i:s');
|
||||
$affected = Db::name('mete_articles')->where('id',$id)->update($data);
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '更新文章分类状态', ['id' => $id]);
|
||||
return json(['code'=>200,'msg'=>'success','data'=>null]);
|
||||
}
|
||||
return json(['code'=>404,'msg'=>'not found','data'=>null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '更新文章分类状态', $e->getMessage());
|
||||
return json(['code'=>500,'msg'=>'fail:'.$e->getMessage(),'data'=>$e->getTraceAsString()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核发布文章
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function publishArticle(int $id): Json
|
||||
{
|
||||
try {
|
||||
// 获取请求对象
|
||||
$request = Request::instance();
|
||||
$uid = $request->post('uid');
|
||||
|
||||
$currentTime = date('Y-m-d H:i:s');
|
||||
if (!isset($uid) || empty($uid)){
|
||||
return json(['code'=>400,'msg'=>'用户ID不能为空','data'=>null]);
|
||||
}else{
|
||||
$user_id = $uid;
|
||||
}
|
||||
$data = [
|
||||
'status' => 2, // 已发布状态
|
||||
'publishdate' => $currentTime,
|
||||
'update_time' => $currentTime,
|
||||
'publisher' => $user_id,
|
||||
];
|
||||
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '审核发布文章', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => '发布成功', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文章不存在或更新失败', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '审核发布文章', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '发布失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下架文章
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function unPublishArticle(int $id): Json
|
||||
{
|
||||
try {
|
||||
$currentTime = date('Y-m-d H:i:s');
|
||||
$data = [
|
||||
'status' => 1, // 已发布状态
|
||||
'publishdate' => null,
|
||||
'update_time' => $currentTime
|
||||
];
|
||||
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '下架文章', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => '下架成功', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文章不存在或更新失败', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '下架文章', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '发布失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章推荐
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function articleRecommend(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = [
|
||||
'recommend' => 1, // 推荐状态
|
||||
];
|
||||
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($affected) {
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文章管理', '文章推荐', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => '推荐成功', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文章不存在或更新失败', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文章管理', '文章推荐', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '推荐失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章取消推荐
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function unArticleRecommend(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = [
|
||||
'recommend' => 0, // 取消推荐状态
|
||||
];
|
||||
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($affected) {
|
||||
return json(['code' => 200, 'msg' => '取消推荐成功', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文章不存在或更新失败', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '取消推荐失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章置顶
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function articleTop(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = [
|
||||
'top' => 1, // 置顶状态
|
||||
];
|
||||
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($affected) {
|
||||
return json(['code' => 200, 'msg' => '置顶成功', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文章不存在或更新失败', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '置顶失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章取消置顶
|
||||
* @param int $id 文章ID
|
||||
* @return Json
|
||||
*/
|
||||
public function unArticleTop(int $id): Json
|
||||
{
|
||||
try {
|
||||
$data = [
|
||||
'top' => 0, // 取消置顶状态
|
||||
];
|
||||
|
||||
$affected = Db::name('mete_articles')
|
||||
->where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($affected) {
|
||||
return json(['code' => 200, 'msg' => '取消置顶成功', 'data' => null]);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文章不存在或更新失败', 'data' => null]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '取消置顶失败:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章列表接口(带条件筛选和分页)
|
||||
* @param string $keyword 文章标题关键词(默认空)
|
||||
* @param string $cate 文章分类(默认空)
|
||||
* @param int $page 页码(默认1)
|
||||
* @param int $pageSize 每页条数(默认10)
|
||||
* @return Json
|
||||
*/
|
||||
public function getAllArticles(string $keyword = '', string $cate = '', int $page = 1, int $pageSize = 10): Json
|
||||
{
|
||||
try {
|
||||
// 安全处理参数(防止非法参数)
|
||||
$page = max(1, $page); // 页码最小为1
|
||||
$pageSize = max(1, min(100, $pageSize)); // 每页条数限制1-100条
|
||||
$keyword = trim($keyword);
|
||||
$cate = trim($cate);
|
||||
|
||||
// 输出参数值
|
||||
trace('查询参数:', 'debug');
|
||||
trace([
|
||||
'keyword' => $keyword,
|
||||
'cate' => $cate,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
], 'debug');
|
||||
|
||||
// 构建查询条件
|
||||
$query = Db::name('mete_articles')
|
||||
->alias('a')
|
||||
->where('a.delete_time', null);
|
||||
|
||||
// 关键词筛选(标题模糊匹配)
|
||||
if (!empty($keyword)) {
|
||||
$query->where('a.title', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
if (!empty($cate)) {
|
||||
$query->where('a.cate', $cate);
|
||||
}
|
||||
|
||||
// 克隆查询对象用于获取总数
|
||||
$countQuery = clone $query;
|
||||
|
||||
// 获取总条数(必须在分页前获取)
|
||||
$total = $countQuery->count();
|
||||
|
||||
// 执行分页查询
|
||||
$articles = $query
|
||||
->alias('a')
|
||||
->leftJoin('mete_admin_user u', 'a.publisher = u.id')
|
||||
->leftJoin('mete_articles_category c', 'a.cate = c.id')
|
||||
->field('a.*, u.name as publisher, c.name as cate')
|
||||
->order('a.status', 'asc')
|
||||
->order('a.top', 'desc')
|
||||
->order('a.recommend', 'desc')
|
||||
->order('a.sort', 'desc')
|
||||
->order('a.id', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => [
|
||||
'list' => $articles, // 分页数据列表
|
||||
'total' => $total, // 总条数
|
||||
'page' => $page, // 当前页码
|
||||
'pageSize' => $pageSize // 每页条数
|
||||
]
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 数据库异常,记录详细日志但返回友好提示
|
||||
trace('文章查询失败: ' . $e->getMessage() . ' | ' . $e->getTraceAsString(), 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '文章列表查询失败,请稍后重试',
|
||||
'data' => []
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 其他异常处理
|
||||
trace('系统异常: ' . $e->getMessage() . ' | ' . $e->getTraceAsString(), 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '服务器内部错误,请稍后重试',
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
232
app/admin/controller/BannerController.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
use app\model\Banner;
|
||||
|
||||
class BannerController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有Banner信息
|
||||
* @return Json
|
||||
*/
|
||||
public function getAllBanners()
|
||||
{
|
||||
try {
|
||||
$banners = Banner::where('delete_time', null)
|
||||
->order('sort', 'asc')
|
||||
->field('id, title, desc, url, image, sort, create_time, update_time')
|
||||
->select()
|
||||
->toArray();
|
||||
// 记录操作日志
|
||||
$this->logSuccess('Banner管理', '获取所有Banner', ['data' => $banners]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $banners
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('Banner管理', '获取所有Banner', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取失败:' . $e->getMessage(),
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Banner
|
||||
* @return Json
|
||||
*/
|
||||
public function createBanner()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|标题' => 'require|max:255',
|
||||
'desc|简介' => 'max:65535',
|
||||
'url|跳转地址' => 'max:65535',
|
||||
'image|图片地址' => 'max:65535',
|
||||
'sort|排序号' => 'integer',
|
||||
]);
|
||||
|
||||
// 准备Banner数据
|
||||
$bannerData = [
|
||||
'title' => $data['title'],
|
||||
'desc' => $data['desc'] ?? '',
|
||||
'url' => $data['url'] ?? '',
|
||||
'image' => $data['image'] ?? '',
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 创建Banner
|
||||
$result = Banner::insertGetId($bannerData);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('Banner管理', '创建Banner', ['id' => $result]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '创建成功',
|
||||
'data' => ['id' => $result]
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('Banner管理', '创建Banner', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('Banner管理', '创建Banner', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑Banner
|
||||
* @param int $id Banner ID
|
||||
* @return Json
|
||||
*/
|
||||
public function editBanner(int $id)
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|标题' => 'require|max:255',
|
||||
'desc|简介' => 'max:65535',
|
||||
'url|跳转地址' => 'max:65535',
|
||||
'image|图片地址' => 'max:65535',
|
||||
'sort|排序号' => 'integer',
|
||||
]);
|
||||
|
||||
// 检查Banner是否存在
|
||||
$banner = Banner::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$banner) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => 'Banner不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [
|
||||
'title' => $data['title'],
|
||||
'desc' => $data['desc'] ?? null,
|
||||
'url' => $data['url'] ?? null,
|
||||
'image' => $data['image'] ?? null,
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'update_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 执行更新
|
||||
Banner::where('id', $id)->update($updateData);
|
||||
|
||||
// 获取更新后的Banner信息
|
||||
$updatedBanner = Banner::where('id', $id)->find();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('Banner管理', '更新Banner', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '更新成功',
|
||||
'data' => $updatedBanner ? $updatedBanner->toArray() : []
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('Banner管理', '更新Banner', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('Banner管理', '更新Banner', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '更新失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Banner
|
||||
* @param int $id Banner ID
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteBanner(int $id)
|
||||
{
|
||||
try {
|
||||
// 检查Banner是否存在
|
||||
$banner = Banner::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$banner) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => 'Banner不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 逻辑删除Banner
|
||||
$result = Banner::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->update([
|
||||
'delete_time' => time(),
|
||||
'update_time' => time()
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除失败'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('Banner管理', '删除Banner', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除成功'
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('Banner管理', '删除Banner', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
390
app/admin/controller/FileController.php
Normal file
@ -0,0 +1,390 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\facade\Filesystem;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
use think\Response;
|
||||
|
||||
use app\model\FilesCategory;
|
||||
use app\model\Files;
|
||||
|
||||
class FileController extends BaseController
|
||||
{
|
||||
// 文件类型映射
|
||||
protected $fileTypes = [
|
||||
'image' => 1,
|
||||
'document' => 2,
|
||||
'video' => 3,
|
||||
'audio' => 4
|
||||
];
|
||||
|
||||
// 允许的文件类型
|
||||
protected $allowedExtensions = [
|
||||
'image' => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'],
|
||||
'document' => ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'],
|
||||
'video' => ['mp4', 'webm', 'mov'],
|
||||
'audio' => ['mp3', 'wav', 'ogg']
|
||||
];
|
||||
|
||||
// 获取所有文件
|
||||
public function getAllFiles()
|
||||
{
|
||||
try {
|
||||
$page = Request::param('page/d', 1);
|
||||
$pageSize = Request::param('pageSize/d', 10);
|
||||
$cate = Request::param('cate/d', 0);
|
||||
$keyword = Request::param('keyword/s', '');
|
||||
|
||||
$query = Files::where('delete_time', null);
|
||||
|
||||
if ($cate) {
|
||||
$query->where('cate', $cate);
|
||||
}
|
||||
|
||||
if ($keyword) {
|
||||
$query->whereLike('name', "%{$keyword}%");
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$list = $query->page($page, $pageSize)
|
||||
->order('create_time', 'desc')
|
||||
->select();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取文件列表失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//获取用户分类
|
||||
public function getUserCate()
|
||||
{
|
||||
try {
|
||||
$cate = FilesCategory::where('delete_time', null)->field('id,name')->select();
|
||||
|
||||
// 获取每个分类下的文件数量
|
||||
foreach ($cate as &$c) {
|
||||
$c['total'] = Files::where('cate', $c['id'])->where('delete_time', null)->count();
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $cate
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取用户分类失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//新建文件分组
|
||||
public function createFileCate()
|
||||
{
|
||||
try {
|
||||
$data = Request::param();
|
||||
$data['create_time'] = date('Y-m-d H:i:s');
|
||||
$id = FilesCategory::insertGetId($data);
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '创建文件分组', ['id' => $id]);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '新建文件分组成功',
|
||||
'data' => [
|
||||
'id' => $id
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '创建文件分组', $e->getMessage());
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '新建文件分组失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//重命名文件分组
|
||||
public function renameFileCate($id)
|
||||
{
|
||||
try {
|
||||
$data = Request::param();
|
||||
$result = FilesCategory::where('id', $id)->update(['name' => $data['name']]);
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '重命名文件分组', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '重命名文件分组成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '重命名文件分组', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '重命名文件分组失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//删除文件分组
|
||||
public function deleteFileCate($id)
|
||||
{
|
||||
try {
|
||||
$result = FilesCategory::where('id', $id)->update(['delete_time' => date('Y-m-d H:i:s')]);
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '删除文件分组', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除文件分组成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '删除文件分组', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除文件分组失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
//获取分类文件
|
||||
public function getCateFiles(int $id)
|
||||
{
|
||||
try {
|
||||
$page = Request::param('page/d', 1);
|
||||
$pageSize = Request::param('pageSize/d', 10);
|
||||
$keyword = Request::param('keyword/s', '');
|
||||
|
||||
$query = Files::where('cate', $id)->where('delete_time', null);
|
||||
|
||||
if ($keyword) {
|
||||
$query->whereLike('name', "%{$keyword}%");
|
||||
}
|
||||
|
||||
$list = $query->page($page, $pageSize)
|
||||
->order('create_time', 'desc')
|
||||
->select();
|
||||
|
||||
// 格式化文件信息
|
||||
foreach ($list as &$file) {
|
||||
$file['createTime'] = $file['create_time'];
|
||||
$file['groupId'] = $file['cate'];
|
||||
$file['url'] = $file['src'];
|
||||
}
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'categoryId' => $id
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取分类文件失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadFile()
|
||||
{
|
||||
try {
|
||||
$file = Request::file('file');
|
||||
if (!$file) {
|
||||
return json(['code' => 400, 'msg' => '请选择要上传的文件']);
|
||||
}
|
||||
|
||||
// 验证文件大小和类型
|
||||
$maxSize = 50 * 1024 * 1024; // 50MB
|
||||
$fileExt = strtolower($file->getOriginalExtension());
|
||||
|
||||
if ($file->getSize() > $maxSize) {
|
||||
return json(['code' => 400, 'msg' => '文件大小不能超过50MB']);
|
||||
}
|
||||
|
||||
// 计算文件MD5
|
||||
$fileMd5 = md5_file($file->getRealPath());
|
||||
|
||||
// 检查是否已存在相同文件
|
||||
$existFile = Files::where('md5', $fileMd5)->where('delete_time', null)->find();
|
||||
|
||||
if ($existFile) {
|
||||
return json([
|
||||
'code' => 201,
|
||||
'msg' => '文件已存在',
|
||||
'data' => [
|
||||
'url' => $existFile['src'],
|
||||
'id' => $existFile['id'],
|
||||
'name' => $existFile['name']
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// 确定文件类型
|
||||
$fileType = 2; // 默认为文件
|
||||
foreach ($this->allowedExtensions as $type => $extensions) {
|
||||
if (in_array($fileExt, $extensions)) {
|
||||
$fileType = $this->fileTypes[$type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$cate = Request::param('cate/d', 0);
|
||||
// 生成按日期分类的目录结构
|
||||
$datePath = date('Y/m/d');
|
||||
$saveName = $datePath . '/' . uniqid() . '.' . $fileExt;
|
||||
$fullPath = Filesystem::disk('public')->putFileAs('uploads', $file, $saveName);
|
||||
$fileUrl = '/storage/' . str_replace('\\', '/', $fullPath);
|
||||
|
||||
// 获取当前登录用户ID
|
||||
$userId = Request::middleware('user_id', '');
|
||||
|
||||
// 保存文件信息到数据库
|
||||
$fileData = [
|
||||
'name' => $file->getOriginalName(),
|
||||
'type' => $fileType,
|
||||
'cate' => $cate,
|
||||
'size' => $file->getSize(),
|
||||
'src' => $fileUrl,
|
||||
'md5' => $fileMd5,
|
||||
'uploader' => $userId,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$fileId = Files::insertGetId($fileData);
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '上传文件', ['id' => $fileId]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '上传成功',
|
||||
'data' => [
|
||||
'url' => $fileUrl,
|
||||
'id' => $fileId,
|
||||
'name' => $fileData['name']
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '上传文件', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '上传失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新文件信息
|
||||
public function updateFile($id)
|
||||
{
|
||||
try {
|
||||
$data = Request::only(['name', 'cate']);
|
||||
if (empty($data)) {
|
||||
return json(['code' => 400, 'msg' => '无更新数据']);
|
||||
}
|
||||
|
||||
$data['update_time'] = date('Y-m-d H:i:s');
|
||||
|
||||
$result = Files::where('id', $id)->where('delete_time', null)->update($data);
|
||||
|
||||
if ($result) {
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '更新文件', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => '更新成功']);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文件不存在']);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '更新文件', $e->getMessage());
|
||||
return json(['code' => 500, 'msg' => '更新失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
public function deleteFile($id)
|
||||
{
|
||||
try {
|
||||
$result = Files::where('id', $id)->update(['delete_time' => date('Y-m-d H:i:s')]);
|
||||
|
||||
if ($result) {
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '删除文件', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => '删除成功']);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文件不存在']);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '删除文件', $e->getMessage());
|
||||
return json(['code' => 500, 'msg' => '删除失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
public function download($id)
|
||||
{
|
||||
try {
|
||||
$file = Files::where('id', $id)->where('delete_time', null)->find();
|
||||
|
||||
if (!$file) {
|
||||
return json(['code' => 404, 'msg' => '文件不存在']);
|
||||
}
|
||||
|
||||
$filePath = public_path() . $file['src'];
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
return json(['code' => 404, 'msg' => '文件不存在']);
|
||||
}
|
||||
|
||||
return download($filePath, $file['name']);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '下载文件', $e->getMessage());
|
||||
return json(['code' => 500, 'msg' => '下载失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动文件
|
||||
public function moveFile($id)
|
||||
{
|
||||
try {
|
||||
$cate = Request::param('cate/d', 0);
|
||||
$result = Files::where('id', $id)->update(['cate' => $cate]);
|
||||
if ($result) {
|
||||
// 记录操作日志
|
||||
$this->logSuccess('文件管理', '移动文件', ['id' => $id]);
|
||||
return json(['code' => 200, 'msg' => '移动成功']);
|
||||
}
|
||||
|
||||
return json(['code' => 404, 'msg' => '文件不存在']);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '移动文件', $e->getMessage());
|
||||
return json(['code' => 500, 'msg' => '移动失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
261
app/admin/controller/FrontMenuController.php
Normal file
@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
use app\model\FrontMenu;
|
||||
|
||||
class FrontMenuController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取前端导航接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getFrontMenus()
|
||||
{
|
||||
try {
|
||||
// 获取所有未删除的菜单
|
||||
$frontMenus = FrontMenu::where('delete_time', null)
|
||||
->field('id,pid,title,type,image,path,component_path,sort,desc')
|
||||
->order('sort', 'desc')
|
||||
->select()
|
||||
->toArray(); // 转换为数组
|
||||
|
||||
// 使用 buildFrontMenuTree 构建树形结构
|
||||
$treeFrontMenus = $this->buildFrontMenuTree($frontMenus);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $treeFrontMenus
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新菜单
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function createFrontMenu()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|菜单名称' => 'require|max:50',
|
||||
'pid|上级菜单ID' => 'integer',
|
||||
'type|菜单类型' => 'require|in:1,2,3,4',
|
||||
'sort|排序号' => 'integer',
|
||||
'path|路由路径' => 'max:200',
|
||||
'component_path|组件路径' => 'max:200',
|
||||
'desc|描述' => 'max:500',
|
||||
]);
|
||||
|
||||
// 准备菜单数据
|
||||
$menuData = [
|
||||
'pid' => $data['pid'] ?? 0,
|
||||
'title' => $data['title'],
|
||||
'type' => $data['type'],
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'path' => $data['path'] ?? '',
|
||||
'component_path' => $data['component_path'] ?? '',
|
||||
'image' => $data['image'] ?? '',
|
||||
'desc' => $data['desc'] ?? '',
|
||||
'create_time' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 创建菜单
|
||||
$result = FrontMenu::insertGetId($menuData);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('前端导航管理', '创建前端导航', ['id' => $result]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success'
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('前端导航管理', '创建前端导航', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑前端导航
|
||||
* @param int $id 前端导航ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function editFrontMenu(int $id)
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|菜单名称' => 'require|max:50',
|
||||
'pid|上级菜单ID' => 'integer',
|
||||
'type|菜单类型' => 'require|in:1,2,3,4',
|
||||
'sort|排序号' => 'integer',
|
||||
'path|路由路径' => 'max:200',
|
||||
'component_path|组件路径' => 'max:200',
|
||||
'desc|描述' => 'max:500',
|
||||
]);
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [
|
||||
'title' => $data['title'],
|
||||
'pid' => $data['pid'] ?? null,
|
||||
'type' => $data['type'],
|
||||
'path' => $data['path'] ?? null,
|
||||
'component_path' => $data['component_path'] ?? null,
|
||||
'image' => $data['image'] ?? null,
|
||||
'desc' => $data['desc'] ?? null,
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 执行更新
|
||||
FrontMenu::where('id', $id)->update($updateData);
|
||||
|
||||
// 获取更新后的菜单信息
|
||||
$menu = FrontMenu::where('id', $id)->find();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('前端导航管理', '更新前端导航', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $menu ? $menu->toArray() : []
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('前端导航管理', '更新前端导航', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('前端导航管理', '更新前端导航', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function deleteFrontMenu(int $id)
|
||||
{
|
||||
try {
|
||||
// 检查是否有子前端导航
|
||||
$hasChildren = FrontMenu::where('pid', $id)
|
||||
->where('pid', $id)
|
||||
->where('delete_time', null)
|
||||
->count() > 0;
|
||||
|
||||
if ($hasChildren) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '该前端导航下有子导航,无法删除'
|
||||
]);
|
||||
}
|
||||
|
||||
// 逻辑删除前端导航
|
||||
$result = FrontMenu::where('id', $id)
|
||||
->where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->update([
|
||||
'delete_time' => date('Y-m-d H:i:s'),
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('前端导航管理', '删除前端导航', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success'
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('前端导航管理', '删除前端导航', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('前端导航管理', '删除前端导航', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建前端导航树形结构
|
||||
* @param array $frontMenus 前端导航列表
|
||||
* @param int $pid 父前端导航ID
|
||||
* @return array
|
||||
*/
|
||||
private function buildFrontMenuTree(array $frontMenus, int $pid = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
foreach ($frontMenus as $frontMenu) {
|
||||
// 将null的pid视为0处理
|
||||
$menuPid = is_null($frontMenu['pid']) ? 0 : $frontMenu['pid'];
|
||||
if ($menuPid == $pid) {
|
||||
$children = $this->buildFrontMenuTree($frontMenus, $frontMenu['id']);
|
||||
if (!empty($children)) {
|
||||
$frontMenu['children'] = $children;
|
||||
}
|
||||
$tree[] = $frontMenu;
|
||||
}
|
||||
}
|
||||
|
||||
// 按 sort 字段降序排序
|
||||
usort($tree, function ($a, $b) {
|
||||
$sortA = $a['sort'] ?? 0;
|
||||
$sortB = $b['sort'] ?? 0;
|
||||
return $sortB - $sortA;
|
||||
});
|
||||
|
||||
return $tree;
|
||||
}
|
||||
}
|
||||
18
app/admin/controller/Index.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
use app\admin\BaseController;
|
||||
|
||||
class Index extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return '后端运行中...';
|
||||
}
|
||||
|
||||
public function hello($name = 'ThinkPHP8')
|
||||
{
|
||||
return 'hello,' . $name;
|
||||
}
|
||||
}
|
||||
191
app/admin/controller/LoginController.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Cache;
|
||||
use think\response\Json;
|
||||
use app\model\AdminUser;
|
||||
use app\service\JwtService;
|
||||
|
||||
class LoginController extends BaseController
|
||||
{
|
||||
private function generateToken($userInfo): string
|
||||
{
|
||||
return JwtService::generateToken($userInfo);
|
||||
}
|
||||
|
||||
private function verifyToken($token): ?array
|
||||
{
|
||||
return JwtService::verifyToken($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @return Json
|
||||
*/
|
||||
public function login(): Json
|
||||
{
|
||||
$data = $this->request->param();
|
||||
|
||||
if (isset($data['email'])) {
|
||||
$data['account'] = $data['email'];
|
||||
} elseif (isset($data['phone'])) {
|
||||
$data['account'] = $data['phone'];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->validate($data, [
|
||||
'account|账号' => 'require|length:3,32',
|
||||
'password|密码' => 'require|length:6,32'
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
$this->logFail('登录管理', '登录', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
}
|
||||
|
||||
$user = AdminUser::where('account', $data['account'])
|
||||
->where('status', 1)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '账号不存在或已禁用'
|
||||
]);
|
||||
}
|
||||
|
||||
if (md5($data['password']) !== $user['password']) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '密码错误'
|
||||
]);
|
||||
}
|
||||
|
||||
AdminUser::where('id', $user['id'])->update([
|
||||
'login_count' => $user['login_count'] + 1,
|
||||
'last_login_ip' => $this->request->ip()
|
||||
]);
|
||||
|
||||
$userInfo = [
|
||||
'id' => $user['id'],
|
||||
'account' => $user['account'],
|
||||
'name' => $user['name'],
|
||||
'group_id' => $user['group_id']
|
||||
];
|
||||
|
||||
$token = $this->generateToken($userInfo);
|
||||
|
||||
$this->logSuccess('登录管理', '登录', ['id' => $user['id']], $userInfo);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '登录成功',
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'user' => $userInfo
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @return Json
|
||||
*/
|
||||
public function logout(): Json
|
||||
{
|
||||
$authHeader = $this->request->header('Authorization', '');
|
||||
$userInfo = null;
|
||||
|
||||
if (preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) {
|
||||
$tokenData = $this->verifyToken($matches[1]);
|
||||
if ($tokenData && isset($tokenData['user'])) {
|
||||
$userInfo = (array)$tokenData['user'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($userInfo && isset($userInfo['id'])) {
|
||||
$this->logSuccess('登录管理', '退出登录', ['result' => 'success'], $userInfo);
|
||||
} else {
|
||||
\app\model\OperationLog::create([
|
||||
'user_id' => 0,
|
||||
'user_account' => '',
|
||||
'user_name' => '未知用户',
|
||||
'module' => '登录管理',
|
||||
'action' => '退出登录',
|
||||
'method' => 'POST',
|
||||
'url' => $this->request->url(true),
|
||||
'ip' => $this->request->ip(),
|
||||
'user_agent' => $this->request->header('user-agent', ''),
|
||||
'request_data' => null,
|
||||
'response_data' => json_encode(['result' => 'success'], JSON_UNESCAPED_UNICODE),
|
||||
'status' => 1,
|
||||
'error_message' => '',
|
||||
'execution_time' => 0.0,
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '退出成功'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
* @return Json
|
||||
*/
|
||||
public function userInfo(): Json
|
||||
{
|
||||
$authHeader = $this->request->header('Authorization', '');
|
||||
|
||||
if (!preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => '未登录'
|
||||
]);
|
||||
}
|
||||
|
||||
$tokenData = $this->verifyToken($matches[1]);
|
||||
|
||||
if (!$tokenData || !isset($tokenData['user'])) {
|
||||
return json([
|
||||
'code' => 401,
|
||||
'msg' => 'Token无效'
|
||||
]);
|
||||
}
|
||||
|
||||
$user = (array)$tokenData['user'];
|
||||
$user_id = $user['id'];
|
||||
|
||||
$userData = AdminUser::where('id', $user_id)
|
||||
->where('delete_time', null)
|
||||
->field('id, account, name, phone, qq, sex, group_id, status, create_time, update_time')
|
||||
->find();
|
||||
|
||||
if (!$userData) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '用户不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $userData->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
public function getAdminUserFromToken(): array
|
||||
{
|
||||
return JwtService::getUserFromHeader($this->request->header('Authorization', ''));
|
||||
}
|
||||
}
|
||||
418
app/admin/controller/MenuController.php
Normal file
@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
use app\model\SystemMenu;
|
||||
use app\model\AdminUser;
|
||||
use app\model\AdminUserGroup;
|
||||
|
||||
class MenuController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取用户菜单接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getMenus(int $id)
|
||||
{
|
||||
try {
|
||||
$user = AdminUser::where('id', $id)
|
||||
->where('id', $id)
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '用户不存在',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 获取用户组权限信息
|
||||
$userGroup = AdminUserGroup::where('id', $user['group_id'])
|
||||
->find();
|
||||
|
||||
// var_dump($userGroup);
|
||||
|
||||
if (!$userGroup) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '用户组不存在',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
// 解析权限数组
|
||||
$menuIds = [];
|
||||
if (!empty($userGroup['rights'])) {
|
||||
$menuIds = is_array($userGroup['rights']) ? $userGroup['rights'] : json_decode($userGroup['rights'], true);
|
||||
}
|
||||
// var_dump($menuIds);
|
||||
|
||||
// 如果权限为空,返回空数组
|
||||
if (empty($menuIds)) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
|
||||
// 获取有权限的菜单
|
||||
$menus = SystemMenu::where('delete_time', null)
|
||||
->where('status', 1)
|
||||
->whereIn('id', $menuIds)
|
||||
->field('id,pid,title,path,component_path,icon,sort')
|
||||
->order('sort', 'asc')
|
||||
->select();
|
||||
|
||||
// 将菜单转换为树形结构
|
||||
$treeMenus = $this->buildMenuTree($menus->toArray());
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $treeMenus
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有菜单接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getAllMenus()
|
||||
{
|
||||
try {
|
||||
// 获取所有未删除的菜单
|
||||
$menus = SystemMenu::where('delete_time', null)
|
||||
->order('sort', 'asc')
|
||||
->select()
|
||||
->toArray(); // 转换为数组
|
||||
|
||||
// 构建树形结构
|
||||
$tree = [];
|
||||
$map = [];
|
||||
|
||||
// 首先创建所有菜单的映射
|
||||
foreach ($menus as $menu) {
|
||||
$menu['children'] = []; // 初始化子菜单数组
|
||||
$map[$menu['id']] = $menu;
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
foreach ($menus as $menu) {
|
||||
$pid = $menu['pid'];
|
||||
if ($pid == 0) {
|
||||
// 顶级菜单
|
||||
$tree[] = &$map[$menu['id']];
|
||||
} else {
|
||||
// 子菜单
|
||||
if (isset($map[$pid])) {
|
||||
$map[$pid]['children'][] = &$map[$menu['id']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归函数:对所有层级的菜单按 sort 降序排序
|
||||
$sortChildren = function (&$items) use (&$sortChildren) {
|
||||
// 再递归处理每个节点的子菜单
|
||||
foreach ($items as &$item) {
|
||||
if (!empty($item['children'])) {
|
||||
$sortChildren($item['children']);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 对整个树形结构进行排序(包括顶级菜单)
|
||||
$sortChildren($tree);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $tree
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单状态
|
||||
* @param int $id 菜单ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function updateMenuStatus(int $id)
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'status|菜单状态' => 'require|in:0,1'
|
||||
]);
|
||||
|
||||
// 更新菜单状态
|
||||
$result = SystemMenu::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->update([
|
||||
'status' => $data['status'],
|
||||
'update_time' => time()
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('菜单管理', '更新菜单状态', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success'
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('菜单管理', '更新菜单状态', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('菜单管理', '更新菜单状态', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新菜单
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function createMenu()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|菜单名称' => 'require|max:50',
|
||||
'type|菜单类型' => 'require|in:1,2,3',
|
||||
'status|菜单状态' => 'require|in:0,1',
|
||||
'sort|排序号' => 'integer',
|
||||
'path|路由路径' => 'max:200',
|
||||
'icon|菜单图标' => 'max:100',
|
||||
'permission|权限标识' => 'max:100',
|
||||
'remark|备注' => 'max:500'
|
||||
]);
|
||||
|
||||
// 准备菜单数据
|
||||
$menuData = [
|
||||
'pid' => $data['pid'] ?? 0,
|
||||
'title' => $data['title'],
|
||||
'type' => $data['type'],
|
||||
'status' => $data['status'] ?? 1,
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'path' => $data['path'] ?? '',
|
||||
'component_path' => $data['component_path'] ?? '',
|
||||
'icon' => $data['icon'] ?? '',
|
||||
'permission' => $data['permission'] ?? '',
|
||||
'remark' => $data['remark'] ?? '',
|
||||
'creater' => Session::get('name') ?? 'system',
|
||||
'create_time' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 创建菜单
|
||||
$result = SystemMenu::insertGetId($menuData);
|
||||
|
||||
if ($result) {
|
||||
// 记录操作日志
|
||||
$this->logSuccess('菜单管理', '创建菜单', ['id' => $result]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '创建成功',
|
||||
'data' => ['id' => $result]
|
||||
]);
|
||||
} else {
|
||||
throw new \Exception('创建菜单失败');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('菜单管理', '创建菜单', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败:' . $e->getMessage(),
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单信息
|
||||
* @param int $id 菜单ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function updateMenu(int $id)
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|菜单名称' => 'require|max:50',
|
||||
'pid|上级菜单ID' => 'integer',
|
||||
'type|菜单类型' => 'require|in:1,2,3',
|
||||
'status|菜单状态' => 'require|in:0,1',
|
||||
'sort|排序号' => 'integer',
|
||||
'path|路由路径' => 'max:200',
|
||||
'icon|菜单图标' => 'max:100',
|
||||
'permission|权限标识' => 'max:100',
|
||||
'remark|备注' => 'max:500'
|
||||
]);
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [
|
||||
'title' => $data['title'],
|
||||
'pid' => $data['pid'] ?? null,
|
||||
'type' => $data['type'],
|
||||
'path' => $data['path'] ?? null,
|
||||
'component_path' => $data['component_path'] ?? null,
|
||||
'icon' => $data['icon'] ?? null,
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'status' => $data['status'],
|
||||
'permission' => $data['permission'] ?? null,
|
||||
'remark' => $data['remark'] ?? null,
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 执行更新
|
||||
SystemMenu::where('id', $id)->update($updateData);
|
||||
|
||||
// 获取更新后的菜单信息
|
||||
$menu = SystemMenu::where('id', $id)->find();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('菜单管理', '更新菜单信息', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $menu ? $menu->toArray() : []
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('菜单管理', '更新菜单信息', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('菜单管理', '更新菜单信息', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* @param int $id 菜单ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function deleteMenu(int $id)
|
||||
{
|
||||
try {
|
||||
// 检查是否有子菜单
|
||||
$hasChildren = SystemMenu::where('pid', $id)
|
||||
->where('delete_time', null)
|
||||
->count() > 0;
|
||||
|
||||
if ($hasChildren) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '该菜单下有子菜单,无法删除'
|
||||
]);
|
||||
}
|
||||
|
||||
// 逻辑删除菜单
|
||||
$result = SystemMenu::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->update([
|
||||
'delete_time' => time(),
|
||||
'update_time' => time()
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('菜单管理', '删除菜单', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success'
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('菜单管理', '删除菜单', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树形结构
|
||||
* @param array $menus 菜单列表
|
||||
* @param int $pid 父菜单ID
|
||||
* @return array
|
||||
*/
|
||||
private function buildMenuTree(array $menus, int $pid = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
foreach ($menus as $menu) {
|
||||
// 将null的pid视为0处理
|
||||
$menuPid = is_null($menu['pid']) ? 0 : $menu['pid'];
|
||||
if ($menuPid == $pid) {
|
||||
$children = $this->buildMenuTree($menus, $menu['id']);
|
||||
if (!empty($children)) {
|
||||
$menu['children'] = $children;
|
||||
}
|
||||
$tree[] = $menu;
|
||||
}
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
}
|
||||
268
app/admin/controller/OnePageController.php
Normal file
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
use app\model\OnePage;
|
||||
|
||||
class OnePageController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有单页信息
|
||||
* @return Json
|
||||
*/
|
||||
public function getAllOnePages()
|
||||
{
|
||||
try {
|
||||
$onePages = OnePage::where('delete_time', null)
|
||||
->order('sort', 'asc')
|
||||
->field('id, title, content, path, sort, status, create_time, update_time')
|
||||
->select()
|
||||
->toArray();
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $onePages
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取失败:' . $e->getMessage(),
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单页
|
||||
* @return Json
|
||||
*/
|
||||
public function createOnePage()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|标题' => 'require|max:255',
|
||||
'content|内容' => 'require',
|
||||
'path|路由' => 'require|max:200',
|
||||
'sort|排序号' => 'integer',
|
||||
'status|状态' => 'integer|in:0,1',
|
||||
]);
|
||||
|
||||
// 验证路由格式(必须以 / 开头)
|
||||
if (!preg_match('/^\/[a-zA-Z0-9\/_-]*$/', $data['path'])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '路由必须以 / 开头,只能包含字母、数字、下划线、横线和斜线'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查路由是否已存在
|
||||
$exists = OnePage::where('path', $data['path'])
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if ($exists) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '路由已存在,请使用其他路由'
|
||||
]);
|
||||
}
|
||||
|
||||
// 准备单页数据
|
||||
$onePageData = [
|
||||
'title' => $data['title'],
|
||||
'content' => $data['content'],
|
||||
'path' => $data['path'],
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'status' => $data['status'] ?? 1,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 创建单页
|
||||
$result = OnePage::insertGetId($onePageData);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('单页管理', '创建单页', ['id' => $result]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '创建成功',
|
||||
'data' => ['id' => $result]
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('单页管理', '创建单页', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('单页管理', '创建单页', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑单页
|
||||
* @param int $id 单页ID
|
||||
* @return Json
|
||||
*/
|
||||
public function editOnePage(int $id)
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'title|标题' => 'require|max:255',
|
||||
'content|内容' => 'require',
|
||||
'path|路由' => 'require|max:200',
|
||||
'sort|排序号' => 'integer',
|
||||
'status|状态' => 'integer|in:0,1',
|
||||
]);
|
||||
|
||||
// 验证路由格式(必须以 / 开头)
|
||||
if (!preg_match('/^\/[a-zA-Z0-9\/_-]*$/', $data['path'])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '路由必须以 / 开头,只能包含字母、数字、下划线、横线和斜线'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查单页是否存在
|
||||
$onePage = OnePage::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$onePage) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '单页不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查路由是否被其他单页使用
|
||||
$exists = OnePage::where('path', $data['path'])
|
||||
->where('id', '<>', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if ($exists) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '路由已被其他单页使用,请使用其他路由'
|
||||
]);
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [
|
||||
'title' => $data['title'],
|
||||
'content' => $data['content'],
|
||||
'path' => $data['path'],
|
||||
'sort' => $data['sort'] ?? 0,
|
||||
'status' => $data['status'] ?? 1,
|
||||
'update_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 执行更新
|
||||
OnePage::where('id', $id)->update($updateData);
|
||||
|
||||
// 获取更新后的单页信息
|
||||
$updatedOnePage = OnePage::where('id', $id)->find();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('单页管理', '更新单页', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '更新成功',
|
||||
'data' => $updatedOnePage ? $updatedOnePage->toArray() : []
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('单页管理', '更新单页', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('单页管理', '更新单页', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '更新失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单页
|
||||
* @param int $id 单页ID
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteOnePage(int $id)
|
||||
{
|
||||
try {
|
||||
// 检查单页是否存在
|
||||
$onePage = OnePage::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$onePage) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '单页不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 逻辑删除单页
|
||||
$result = OnePage::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->update([
|
||||
'delete_time' => date('Y-m-d H:i:s'),
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
if ($result === false) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除失败'
|
||||
]);
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('单页管理', '删除单页', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除成功'
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('单页管理', '删除单页', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
256
app/admin/controller/OperationLog/OperationLogController.php
Normal file
@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller\OperationLog;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\facade\Request;
|
||||
use think\response\Json;
|
||||
use app\model\OperationLog;
|
||||
use app\model\AdminUser;
|
||||
|
||||
class OperationLogController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取操作日志列表
|
||||
* @return Json
|
||||
*/
|
||||
public function getOperationLogs()
|
||||
{
|
||||
try {
|
||||
$page = Request::param('page/d', 1);
|
||||
$pageSize = Request::param('pageSize/d', 20);
|
||||
$keyword = Request::param('keyword/s', '');
|
||||
$module = Request::param('module/s', '');
|
||||
$action = Request::param('action/s', '');
|
||||
$status = Request::param('status/s', '');
|
||||
$startTime = Request::param('startTime/s', '');
|
||||
$endTime = Request::param('endTime/s', '');
|
||||
|
||||
$query = OperationLog::where('delete_time', null);
|
||||
|
||||
// 关键词搜索(用户姓名、URL)
|
||||
if ($keyword) {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$q
|
||||
->whereOr('user_name', 'like', "%{$keyword}%")
|
||||
->whereOr('url', 'like', "%{$keyword}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 模块筛选
|
||||
if ($module) {
|
||||
$query->where('module', $module);
|
||||
}
|
||||
|
||||
// 操作动作筛选
|
||||
if ($action) {
|
||||
$query->where('action', $action);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if ($startTime) {
|
||||
$query->where('create_time', '>=', $startTime);
|
||||
}
|
||||
if ($endTime) {
|
||||
$query->where('create_time', '<=', $endTime);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
$total = $query->count();
|
||||
|
||||
// 分页查询
|
||||
$list = $query->order('id', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 获取所有唯一的 user_id
|
||||
$userIds = array_unique(array_column($list, 'user_id'));
|
||||
$userIds = array_filter($userIds); // 过滤掉0和null
|
||||
|
||||
// 批量查询用户信息
|
||||
$users = [];
|
||||
if (!empty($userIds)) {
|
||||
$userList = AdminUser::whereIn('id', $userIds)
|
||||
->where('delete_time', null)
|
||||
->field('id, name')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 转换为以 id 为键的数组,方便查找
|
||||
foreach ($userList as $user) {
|
||||
$users[$user['id']] = [
|
||||
'name' => $user['name']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 将用户信息合并到日志列表中
|
||||
foreach ($list as &$item) {
|
||||
if (isset($users[$item['user_id']])) {
|
||||
$item['user_name'] = $users[$item['user_id']]['name'];
|
||||
} else {
|
||||
$item['user_name'] = $item['user_name'] ?? '';
|
||||
}
|
||||
}
|
||||
unset($item); // 释放引用
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取操作日志失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志详情
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function getOperationLogDetail(int $id)
|
||||
{
|
||||
try {
|
||||
$log = OperationLog::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$log) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '操作日志不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
$logData = $log->toArray();
|
||||
$this->logSuccess('操作日志', '查看操作日志详情', $logData);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $logData
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logFail('操作日志', '查看操作日志详情', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取操作日志详情失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteOperationLog(int $id)
|
||||
{
|
||||
try {
|
||||
$log = OperationLog::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$log) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '操作日志不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$log->delete();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除操作日志
|
||||
* @return Json
|
||||
*/
|
||||
public function batchDeleteOperationLogs()
|
||||
{
|
||||
try {
|
||||
$ids = Request::param('ids/a', []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '请选择要删除的操作日志'
|
||||
]);
|
||||
}
|
||||
|
||||
OperationLog::whereIn('id', $ids)
|
||||
->where('delete_time', null)
|
||||
->delete();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '批量删除成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '批量删除失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作统计信息(模块、动作等)
|
||||
* @return Json
|
||||
*/
|
||||
public function getOperationStatistics()
|
||||
{
|
||||
try {
|
||||
// 获取模块列表
|
||||
$modules = OperationLog::where('delete_time', null)
|
||||
->group('module')
|
||||
->column('module');
|
||||
|
||||
// 获取动作列表
|
||||
$actions = OperationLog::where('delete_time', null)
|
||||
->group('action')
|
||||
->column('action');
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'modules' => $modules,
|
||||
'actions' => $actions
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '获取统计信息失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
app/admin/controller/OperationLog/OperationLogHelper.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\OperationLog;
|
||||
|
||||
use app\model\OperationLog;
|
||||
use think\facade\Request;
|
||||
use think\facade\Session;
|
||||
|
||||
/**
|
||||
* 操作日志记录助手类
|
||||
*/
|
||||
class OperationLogHelper
|
||||
{
|
||||
/**
|
||||
* 记录操作日志
|
||||
* @param string $module 操作模块
|
||||
* @param string $action 操作动作
|
||||
* @param array $requestData 请求数据
|
||||
* @param array $responseData 响应数据
|
||||
* @param int $status 操作状态:1-成功,0-失败
|
||||
* @param string $errorMessage 错误信息
|
||||
* @param float $executionTime 执行时间(秒)
|
||||
* @return bool
|
||||
*/
|
||||
public static function log(
|
||||
string $module,
|
||||
string $action,
|
||||
array $requestData = [],
|
||||
array $responseData = [],
|
||||
int $status = 1,
|
||||
string $errorMessage = '',
|
||||
float $executionTime = 0.0
|
||||
): bool {
|
||||
try {
|
||||
// 获取用户信息
|
||||
$userInfo = Session::get('user');
|
||||
$userId = $userInfo['id'] ?? 0;
|
||||
$userAccount = $userInfo['account'] ?? '';
|
||||
$userName = $userInfo['name'] ?? '';
|
||||
|
||||
// 获取请求信息
|
||||
$method = Request::method();
|
||||
$url = Request::url(true);
|
||||
$ip = Request::ip();
|
||||
$userAgent = Request::header('user-agent', '');
|
||||
|
||||
// 过滤敏感信息(如密码)
|
||||
$filteredRequestData = self::filterSensitiveData($requestData);
|
||||
|
||||
// 记录日志
|
||||
OperationLog::create([
|
||||
'user_id' => $userId,
|
||||
'user_account' => $userAccount,
|
||||
'user_name' => $userName,
|
||||
'module' => $module,
|
||||
'action' => $action,
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'ip' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
'request_data' => !empty($filteredRequestData) ? json_encode($filteredRequestData, JSON_UNESCAPED_UNICODE) : null,
|
||||
'response_data' => !empty($responseData) ? json_encode($responseData, JSON_UNESCAPED_UNICODE) : null,
|
||||
'status' => $status,
|
||||
'error_message' => $errorMessage,
|
||||
'execution_time' => $executionTime,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
// 记录日志失败不应该影响主流程,只记录错误
|
||||
error_log('操作日志记录失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤敏感数据
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private static function filterSensitiveData(array $data): array
|
||||
{
|
||||
$sensitiveKeys = ['password', 'pwd', 'token', 'api_key', 'secret'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array(strtolower($key), $sensitiveKeys)) {
|
||||
$data[$key] = '***';
|
||||
} elseif (is_array($value)) {
|
||||
$data[$key] = self::filterSensitiveData($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
97
app/admin/controller/OperationLog/OperationLogger.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\OperationLog;
|
||||
|
||||
use think\facade\Request;
|
||||
use app\model\OperationLog;
|
||||
use app\service\JwtService;
|
||||
|
||||
/**
|
||||
* 操作日志记录器(JWT版)
|
||||
*/
|
||||
class OperationLogger
|
||||
{
|
||||
private static function getUserFromToken(): array
|
||||
{
|
||||
return JwtService::getUserFromHeader(Request::header('Authorization', ''));
|
||||
}
|
||||
|
||||
public static function record(
|
||||
string $module,
|
||||
string $action,
|
||||
array $requestData = [],
|
||||
array $responseData = [],
|
||||
int $status = 1,
|
||||
string $errorMessage = '',
|
||||
array $userInfo = []
|
||||
): bool {
|
||||
try {
|
||||
if (empty($userInfo)) {
|
||||
$userInfo = self::getUserFromToken();
|
||||
}
|
||||
|
||||
$userId = $userInfo['id'] ?? 0;
|
||||
$userAccount = $userInfo['account'] ?? '';
|
||||
$userName = $userInfo['name'] ?? '';
|
||||
|
||||
if (empty($requestData)) {
|
||||
$requestData = Request::param();
|
||||
}
|
||||
|
||||
$method = Request::method();
|
||||
$url = Request::url(true);
|
||||
$ip = Request::ip();
|
||||
$userAgent = Request::header('user-agent', '');
|
||||
|
||||
$filteredRequestData = self::filterSensitiveData($requestData);
|
||||
|
||||
OperationLog::create([
|
||||
'user_id' => $userId,
|
||||
'user_account' => $userAccount,
|
||||
'user_name' => $userName,
|
||||
'module' => $module,
|
||||
'action' => $action,
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'ip' => $ip,
|
||||
'user_agent' => $userAgent,
|
||||
'request_data' => !empty($filteredRequestData) ? json_encode($filteredRequestData, JSON_UNESCAPED_UNICODE) : null,
|
||||
'response_data' => !empty($responseData) ? json_encode($responseData, JSON_UNESCAPED_UNICODE) : null,
|
||||
'status' => $status,
|
||||
'error_message' => $errorMessage,
|
||||
'execution_time' => 0.0,
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
error_log('操作日志记录失败: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function success(string $module, string $action, array $responseData = [], array $userInfo = []): bool
|
||||
{
|
||||
return self::record($module, $action, [], $responseData, 1, '', $userInfo);
|
||||
}
|
||||
|
||||
public static function fail(string $module, string $action, string $errorMessage, array $userInfo = []): bool
|
||||
{
|
||||
return self::record($module, $action, [], [], 0, $errorMessage, $userInfo);
|
||||
}
|
||||
|
||||
private static function filterSensitiveData(array $data): array
|
||||
{
|
||||
$sensitiveKeys = ['password', 'pwd', 'token', 'api_key', 'secret'];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array(strtolower($key), $sensitiveKeys)) {
|
||||
$data[$key] = '***';
|
||||
} elseif (is_array($value)) {
|
||||
$data[$key] = self::filterSensitiveData($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
249
app/admin/controller/RoleController.php
Normal file
@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
|
||||
use app\model\AdminUser;
|
||||
use app\model\AdminUserGroup;
|
||||
|
||||
class RoleController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有角色列表
|
||||
* @return Json
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
$roles = AdminUserGroup::where('delete_time', null)
|
||||
->order('id', 'asc')
|
||||
->select();
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $roles->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色详情
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function getRoleById(int $id)
|
||||
{
|
||||
$role = AdminUserGroup::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if (!$role) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '角色不存在',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $role->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
* @return Json
|
||||
*/
|
||||
public function createRole()
|
||||
{
|
||||
try {
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'name|角色名称' => 'require|max:50',
|
||||
'status|状态' => 'in:0,1',
|
||||
'rights|权限' => 'array'
|
||||
]);
|
||||
|
||||
// 检查角色名称是否已存在
|
||||
$exists = AdminUserGroup::where('name', $data['name'])
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if ($exists) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '角色名称已存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 准备数据
|
||||
$roleData = [
|
||||
'name' => $data['name'],
|
||||
'status' => $data['status'] ?? 1,
|
||||
'rights' => !empty($data['rights']) ? json_encode($data['rights']) : null,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// 创建角色
|
||||
$roleModel = new AdminUserGroup();
|
||||
$roleModel->save($roleData);
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('角色管理', '创建角色', ['id' => $roleModel->id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '创建成功',
|
||||
'data' => $roleModel->toArray()
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('角色管理', '创建角色', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('角色管理', '创建角色', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function updateRole(int $id)
|
||||
{
|
||||
try {
|
||||
$data = $this->request->param();
|
||||
|
||||
// 验证参数
|
||||
$this->validate($data, [
|
||||
'name|角色名称' => 'require|max:50',
|
||||
'status|状态' => 'in:0,1',
|
||||
'rights|权限' => 'array'
|
||||
]);
|
||||
|
||||
// 查找角色
|
||||
$role = AdminUserGroup::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if (!$role) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '角色不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查角色名称是否已被其他角色使用
|
||||
$exists = AdminUserGroup::where('name', $data['name'])
|
||||
->where('id', '<>', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if ($exists) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '角色名称已存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
$role->name = $data['name'];
|
||||
$role->status = $data['status'] ?? 1;
|
||||
$role->rights = !empty($data['rights']) ? json_encode($data['rights']) : null;
|
||||
$role->update_time = date('Y-m-d H:i:s');
|
||||
$role->save();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('角色管理', '更新角色', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '更新成功',
|
||||
'data' => $role->toArray()
|
||||
]);
|
||||
} catch (ValidateException $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('角色管理', '更新角色', $e->getMessage());
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => $e->getError()
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('角色管理', '更新角色', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '更新失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param int $id
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteRole(int $id)
|
||||
{
|
||||
try {
|
||||
// 不允许删除ID为1的超级管理员角色
|
||||
if ($id == 1) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '不允许删除超级管理员角色'
|
||||
]);
|
||||
}
|
||||
|
||||
// 查找角色
|
||||
$role = AdminUserGroup::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if (!$role) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '角色不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查是否有用户正在使用该角色
|
||||
$userCount = AdminUser::where('group_id', $id)
|
||||
->where('delete_time', null)
|
||||
->count();
|
||||
if ($userCount > 0) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => "该角色下还有 {$userCount} 个用户,无法删除"
|
||||
]);
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$role->delete();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('角色管理', '删除角色', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('角色管理', '删除角色', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '删除失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
162
app/admin/controller/SiteSettingsController.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\response\Json;
|
||||
use app\model\SystemSiteSettings;
|
||||
|
||||
class SiteSettingsController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有站点设置列表
|
||||
* @return Json
|
||||
*/
|
||||
public function getNormalInfos()
|
||||
{
|
||||
$siteSettings = SystemSiteSettings::where('delete_time', null)
|
||||
->field('id, label, value, sort')
|
||||
->order('id', 'asc')
|
||||
->select();
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $siteSettings->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存基本信息
|
||||
* @return Json
|
||||
*/
|
||||
public function saveNormalInfos()
|
||||
{
|
||||
$params = $this->request->param();
|
||||
|
||||
if (!isset($params['data']) || empty($params['data'])) {
|
||||
return json(['code' => 400, 'msg' => '数据不能为空', 'data' => null]);
|
||||
}
|
||||
|
||||
$data = json_decode($params['data'], true);
|
||||
if (!is_array($data)) {
|
||||
return json(['code' => 400, 'msg' => '数据格式错误', 'data' => null]);
|
||||
}
|
||||
|
||||
foreach ($data as $item) {
|
||||
if (!isset($item['label']) || !isset($item['value'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $item['label'];
|
||||
$value = $item['value'] ?? '';
|
||||
|
||||
$setting = SystemSiteSettings::where('label', $label)->find();
|
||||
if ($setting) {
|
||||
$setting->value = $value;
|
||||
$setting->save();
|
||||
} else {
|
||||
SystemSiteSettings::create([
|
||||
'label' => $label,
|
||||
'value' => $value,
|
||||
'create_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$allSettings = SystemSiteSettings::column('value', 'label');
|
||||
$allSettings['type'] = 'normal';
|
||||
|
||||
$labels = array_column($data, 'label');
|
||||
$userInfo = $this->getAdminUserInfo();
|
||||
$this->logSuccess('站点设置管理', '保存基本信息', ['labels' => $labels], $userInfo);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '保存成功',
|
||||
'data' => $allSettings
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有站点设置列表
|
||||
* @return Json
|
||||
*/
|
||||
public function getLegalInfos()
|
||||
{
|
||||
$siteSettings = SystemSiteSettings::where('delete_time', null)
|
||||
->field('id, label, value, sort')
|
||||
->order('id', 'asc')
|
||||
->select();
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $siteSettings->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存法律声明和隐私条款
|
||||
* @return Json
|
||||
*/
|
||||
public function saveLegalInfos()
|
||||
{
|
||||
$params = $this->request->param();
|
||||
|
||||
if (!isset($params['data']) || empty($params['data'])) {
|
||||
return json(['code' => 400, 'msg' => '数据不能为空', 'data' => null]);
|
||||
}
|
||||
|
||||
$data = json_decode($params['data'], true);
|
||||
if (!is_array($data)) {
|
||||
return json(['code' => 400, 'msg' => '数据格式错误', 'data' => null]);
|
||||
}
|
||||
|
||||
foreach ($data as $item) {
|
||||
if (!isset($item['label']) || !isset($item['value'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $item['label'];
|
||||
$value = $item['value'] ?? '';
|
||||
|
||||
$setting = SystemSiteSettings::where('label', $label)->find();
|
||||
if ($setting) {
|
||||
$setting->value = $value;
|
||||
$setting->save();
|
||||
} else {
|
||||
SystemSiteSettings::create([
|
||||
'label' => $label,
|
||||
'value' => $value,
|
||||
'create_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$allSettings = SystemSiteSettings::column('value', 'label');
|
||||
$allSettings['type'] = 'legal_notice';
|
||||
|
||||
$labels = array_column($data, 'label');
|
||||
|
||||
$userId = Session::get('user_id', 0);
|
||||
$userFromSession = Session::get('user', []);
|
||||
$userFromCache = Cache::get('userInfo_' . $userId, []);
|
||||
$account = Session::get('account', '');
|
||||
$name = Session::get('name', '');
|
||||
|
||||
\think\facade\Log::record('SiteSettings Debug - user_id: ' . $userId . ', userFromSession: ' . json_encode($userFromSession) . ', userFromCache: ' . json_encode($userFromCache) . ', account: ' . $account . ', name: ' . $name);
|
||||
|
||||
$this->logSuccess('站点设置管理', '保存法律声明和隐私条款', ['labels' => $labels]);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '保存成功',
|
||||
'data' => $allSettings
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
69
app/admin/controller/StorageController.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\facade\Request;
|
||||
use think\Response;
|
||||
|
||||
class StorageController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取存储文件
|
||||
* 用于访问 /storage/ 路径下的静态文件
|
||||
* @param string $path 文件路径
|
||||
*/
|
||||
public function index($path = '')
|
||||
{
|
||||
// 如果没有传递路径参数,从pathinfo获取
|
||||
if (empty($path)) {
|
||||
$pathinfo = Request::pathinfo();
|
||||
$path = str_replace('storage/', '', $pathinfo);
|
||||
}
|
||||
|
||||
// 防止路径遍历攻击
|
||||
$path = str_replace('..', '', $path);
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
// 构建文件完整路径
|
||||
$filePath = public_path() . 'storage/' . $path;
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!file_exists($filePath) || !is_file($filePath)) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '文件不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 获取文件MIME类型
|
||||
$mimeType = mime_content_type($filePath);
|
||||
if (!$mimeType) {
|
||||
// 根据文件扩展名判断MIME类型
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
$mimeTypes = [
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'mp4' => 'video/mp4',
|
||||
'mp3' => 'audio/mpeg',
|
||||
'pdf' => 'application/pdf',
|
||||
];
|
||||
$mimeType = $mimeTypes[$ext] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
// 返回文件响应
|
||||
return Response::create($content, 'file', 200)
|
||||
->header([
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Length' => filesize($filePath),
|
||||
'Cache-Control' => 'public, max-age=31536000',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
133
app/admin/controller/UserController.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\admin\BaseController;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
|
||||
use app\model\AdminUser;
|
||||
|
||||
class UserController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有用户信息
|
||||
* @return Json
|
||||
*/
|
||||
public function getAllUsers()
|
||||
{
|
||||
$users = AdminUser::where('delete_time', null)->field('id, account, name, phone,email, qq, sex, group_id, status, last_login_ip, login_count, create_time, update_time')->select()->toArray();
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $users,
|
||||
'total' => count($users)
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @return Json
|
||||
*/
|
||||
public function getUserInfo(int $id)
|
||||
{
|
||||
$user = AdminUser::where('id', $id)
|
||||
->field('id, account, name, phone,email, qq, sex, group_id, status')
|
||||
->find();
|
||||
|
||||
// 记录操作日志
|
||||
$this->logSuccess('用户管理', '获取用户信息', ['id' => $id]);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @return Json
|
||||
*/
|
||||
public function changePassword(int $id, string $password)
|
||||
{
|
||||
try {
|
||||
AdminUser::where('id', $id)->update([
|
||||
'password' => md5($password),
|
||||
'update_time' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
// 记录操作日志
|
||||
$this->logSuccess('用户管理', '修改密码', ['id' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '修改成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('用户管理', '修改密码', $e->getMessage());
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '修改失败'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加用户
|
||||
* @return Json
|
||||
*/
|
||||
public function addUser()
|
||||
{
|
||||
$data = request()->param();
|
||||
$data['password'] = md5($data['password']);
|
||||
$data['create_time'] = date('Y-m-d H:i:s');
|
||||
$data['update_time'] = $data['create_time'];
|
||||
$data['group_id'] = 2;
|
||||
$id = AdminUser::insertGetId($data);
|
||||
// 记录操作日志
|
||||
$this->logSuccess('用户管理', '添加用户', ['data' => $data]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '添加成功',
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户
|
||||
* @return Json
|
||||
*/
|
||||
public function editUser(int $id)
|
||||
{
|
||||
$data = request()->param();
|
||||
$data['update_time'] = date('Y-m-d H:i:s');
|
||||
AdminUser::where('id', $id)->update($data);
|
||||
// 记录操作日志
|
||||
$this->logSuccess('用户管理', '编辑用户', ['data' => $data]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '编辑成功'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteUser(int $id)
|
||||
{
|
||||
AdminUser::where('id', $id)->delete();
|
||||
// 记录操作日志
|
||||
$this->logSuccess('用户管理', '删除用户', ['data' => $id]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除成功'
|
||||
]);
|
||||
}
|
||||
}
|
||||
5
app/admin/event.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
// 这是系统自动生成的event定义文件
|
||||
return [
|
||||
|
||||
];
|
||||
11
app/admin/middleware.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
// 全局中间件定义文件
|
||||
return [
|
||||
// 全局请求缓存
|
||||
// \think\middleware\CheckRequestCache::class,
|
||||
// 多语言加载
|
||||
// \think\middleware\LoadLangPack::class,
|
||||
// Session初始化
|
||||
\think\middleware\SessionInit::class,
|
||||
];
|
||||
|
||||
61
app/admin/middleware/CustomCors.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\middleware;
|
||||
|
||||
use think\Response;
|
||||
use think\Request;
|
||||
|
||||
/**
|
||||
* 自定义跨域中间件
|
||||
* 解决 AJAX 请求 Cookie 传递问题
|
||||
*/
|
||||
class CustomCors
|
||||
{
|
||||
public function handle(Request $request, \Closure $next): Response
|
||||
{
|
||||
$origin = $request->header('origin', '');
|
||||
|
||||
// 允许的域名列表(根据实际情况修改)
|
||||
$allowedDomains = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:8000',
|
||||
'http://127.0.0.1:3000',
|
||||
'http://127.0.0.1:8000',
|
||||
'http://backapi.yunzer.cn',
|
||||
'http://localhost:5173',
|
||||
'http://www.yunzer.cn',
|
||||
'https://www.yunzer.cn',
|
||||
'http://yunzer.cn',
|
||||
'https://yunzer.cn',
|
||||
];
|
||||
|
||||
// 检查是否为允许的域名
|
||||
if (in_array($origin, $allowedDomains)) {
|
||||
$header['Access-Control-Allow-Origin'] = $origin;
|
||||
} else {
|
||||
// 对于同源请求,允许当前域名
|
||||
if (!empty($origin)) {
|
||||
$header['Access-Control-Allow-Origin'] = $origin;
|
||||
} else {
|
||||
$header['Access-Control-Allow-Origin'] = '*';
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 OPTIONS 预检请求,直接返回
|
||||
if ($request->method() === 'OPTIONS') {
|
||||
$header['Access-Control-Allow-Credentials'] = 'true';
|
||||
$header['Access-Control-Max-Age'] = 1800;
|
||||
$header['Access-Control-Allow-Methods'] = 'GET, POST, PATCH, PUT, DELETE, OPTIONS';
|
||||
$header['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With';
|
||||
\think\facade\Log::record('CORS OPTIONS preflight for: ' . $origin);
|
||||
return response('', 200, $header);
|
||||
}
|
||||
|
||||
$header['Access-Control-Allow-Credentials'] = 'true';
|
||||
$header['Access-Control-Max-Age'] = 1800;
|
||||
$header['Access-Control-Allow-Methods'] = 'GET, POST, PATCH, PUT, DELETE, OPTIONS';
|
||||
$header['Access-Control-Allow-Headers'] = 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With';
|
||||
|
||||
return $next($request)->header($header);
|
||||
}
|
||||
}
|
||||
100
app/admin/route/app.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
use think\facade\Route;
|
||||
|
||||
// 首页路由
|
||||
Route::get('/', 'app\\admin\\controller\\Index@index');
|
||||
Route::get('index/index', 'app\\admin\\controller\\Index@index');
|
||||
|
||||
|
||||
// 静态文件路由 - 必须在其他路由之前定义,使用通配符匹配所有storage路径
|
||||
Route::get('storage/:path', 'app\\admin\\controller\\StorageController@index')->pattern(['path' => '.*']);
|
||||
|
||||
// 登录路由
|
||||
Route::post('login', 'app\\admin\\controller\\LoginController@login');
|
||||
Route::post('logout', 'app\\admin\\controller\\LoginController@logout');
|
||||
Route::get('user/info', 'app\\admin\\controller\\LoginController@userInfo');
|
||||
|
||||
// 菜单路由
|
||||
Route::get('allmenu', 'app\\admin\\controller\\MenuController@getAllMenus');
|
||||
Route::get('menu/:id', 'app\\admin\\controller\\MenuController@getMenus');
|
||||
Route::post('createMenu', 'app\\admin\\controller\\MenuController@createMenu');
|
||||
Route::put('updateMenu/:id', 'app\\admin\\controller\\MenuController@updateMenu');
|
||||
Route::delete('deleteMenu/:id', 'app\\admin\\controller\\MenuController@deleteMenu');
|
||||
Route::patch('menu/status/:id', 'app\\admin\\controller\\MenuController@updateMenuStatus');
|
||||
|
||||
// 前端导航路由
|
||||
Route::get('frontmenus', 'app\\admin\\controller\\FrontMenuController@getFrontMenus');
|
||||
Route::post('createfrontmenu', 'app\\admin\\controller\\FrontMenuController@createFrontMenu');
|
||||
Route::post('editfrontmenu/:id', 'app\\admin\\controller\\FrontMenuController@editFrontMenu');
|
||||
Route::delete('deletefrontmenu/:id', 'app\\admin\\controller\\FrontMenuController@deleteFrontMenu');
|
||||
|
||||
// Banner路由
|
||||
Route::get('allbanners', 'app\\admin\\controller\\BannerController@getAllBanners');
|
||||
Route::post('createbanner', 'app\\admin\\controller\\BannerController@createBanner');
|
||||
Route::post('editbanner/:id', 'app\\admin\\controller\\BannerController@editBanner');
|
||||
Route::delete('deletebanner/:id', 'app\\admin\\controller\\BannerController@deleteBanner');
|
||||
|
||||
// 单页路由
|
||||
Route::get('allonepages', 'app\\admin\\controller\\OnePageController@getAllOnePages');
|
||||
Route::post('createonepage', 'app\\admin\\controller\\OnePageController@createOnePage');
|
||||
Route::post('editonepage/:id', 'app\\admin\\controller\\OnePageController@editOnePage');
|
||||
Route::delete('deleteonepage/:id', 'app\\admin\\controller\\OnePageController@deleteOnePage');
|
||||
|
||||
// 文章路由
|
||||
Route::get('articlesList', 'app\\admin\\controller\\Article\\ArticleController@getArticlesList');
|
||||
Route::get('allarticles', 'app\\admin\\controller\\Article\\ArticleController@getAllArticles');
|
||||
Route::get('articles/:id', 'app\\admin\\controller\\Article\\ArticleController@getArticle');
|
||||
Route::post('createarticle', 'app\\admin\\controller\\Article\\ArticleController@createArticle');
|
||||
Route::post('editarticle/:id', 'app\\admin\\controller\\Article\\ArticleController@editArticle');
|
||||
Route::delete('deletearticle/:id', 'app\\admin\\controller\\Article\\ArticleController@deleteArticle');
|
||||
Route::post('publisharticle/:id', 'app\\admin\\controller\\Article\\ArticleController@publishArticle');
|
||||
Route::post('unPublisharticle/:id', 'app\\admin\\controller\\Article\\ArticleController@unPublishArticle');
|
||||
Route::post('articleRecommend/:id', 'app\\admin\\controller\\Article\\ArticleController@articleRecommend');
|
||||
Route::post('unArticleRecommend/:id', 'app\\admin\\controller\\Article\\ArticleController@unArticleRecommend');
|
||||
Route::post('articleTop/:id', 'app\\admin\\controller\\Article\\ArticleController@articleTop');
|
||||
Route::post('unArticleTop/:id', 'app\\admin\\controller\\Article\\ArticleController@unArticleTop');
|
||||
|
||||
// 文章分类路由
|
||||
Route::get('allcategories', 'app\\admin\\controller\\Article\\ArticleCategoryController@getAllArticleCategories');
|
||||
Route::get('categories', 'app\\admin\\controller\\Article\\ArticleCategoryController@getArticleCategories');
|
||||
Route::delete('categories/:id', 'app\\admin\\controller\\Article\\ArticleCategoryController@deleteCategory');
|
||||
Route::post('createCategory', 'app\\admin\\controller\\Article\\ArticleCategoryController@createCategory');
|
||||
Route::post('editCategory/:id', 'app\\admin\\controller\\Article\\ArticleCategoryController@editCategory');
|
||||
|
||||
// 文件路由
|
||||
Route::get('usercate', 'app\\admin\\controller\\FileController@getUserCate');
|
||||
Route::get('allfiles', 'app\\admin\\controller\\FileController@getAllFiles');
|
||||
Route::get('catefiles/:id', 'app\\admin\\controller\\FileController@getCateFiles');
|
||||
Route::post('uploadfile', 'app\\admin\\controller\\FileController@uploadFile');
|
||||
Route::post('updatefile/:id', 'app\\admin\\controller\\FileController@updateFile');
|
||||
Route::delete('deletefile/:id', 'app\\admin\\controller\\FileController@deleteFile');
|
||||
Route::get('movefile/:id', 'app\\admin\\controller\\FileController@moveFile');
|
||||
Route::post('createfilecate', 'app\\admin\\controller\\FileController@createFileCate');
|
||||
Route::post('renamefilecate/:id', 'app\\admin\\controller\\FileController@renameFileCate');
|
||||
Route::delete('deletefilecate/:id', 'app\\admin\\controller\\FileController@deleteFileCate');
|
||||
|
||||
// 用户路由
|
||||
Route::get('getAllUsers', 'app\\admin\\controller\\UserController@getAllUsers');
|
||||
Route::get('getUserInfo/:id', 'app\\admin\\controller\\UserController@getUserInfo');
|
||||
Route::post('addUser', 'app\\admin\\controller\\UserController@addUser');
|
||||
Route::post('editUser/:id', 'app\\admin\\controller\\UserController@editUser');
|
||||
Route::delete('deleteUser/:id', 'app\\admin\\controller\\UserController@deleteUser');
|
||||
Route::post('changePassword', 'app\\admin\\controller\\UserController@changePassword');
|
||||
|
||||
// 角色路由
|
||||
Route::get('allRoles', 'app\\admin\\controller\\RoleController@getAllRoles');
|
||||
Route::get('roles/:id', 'app\\admin\\controller\\RoleController@getRoleById');
|
||||
Route::post('roles', 'app\\admin\\controller\\RoleController@createRole');
|
||||
Route::put('roles/:id', 'app\\admin\\controller\\RoleController@updateRole');
|
||||
Route::delete('roles/:id', 'app\\admin\\controller\\RoleController@deleteRole');
|
||||
|
||||
// 操作日志路由(注意:具体路径要在动态路径之前)
|
||||
Route::get('operationLogs/statistics', 'app\\admin\\controller\\OperationLog\\OperationLogController@getOperationStatistics');
|
||||
Route::get('operationLogs/:id', 'app\\admin\\controller\\OperationLog\\OperationLogController@getOperationLogDetail');
|
||||
Route::get('operationLogs', 'app\\admin\\controller\\OperationLog\\OperationLogController@getOperationLogs');
|
||||
|
||||
// 站点设置路由
|
||||
Route::get('normalInfos', 'app\\admin\\controller\\SiteSettingsController@getNormalInfos');
|
||||
Route::post('saveNormalInfos', 'app\\admin\\controller\\SiteSettingsController@saveNormalInfos');
|
||||
Route::get('legalInfos', 'app\\admin\\controller\\SiteSettingsController@getLegalInfos');
|
||||
Route::post('saveLegalInfos', 'app\\admin\\controller\\SiteSettingsController@saveLegalInfos');
|
||||
100
app/index/BaseController.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// 关键修正:命名空间对应控制器目录
|
||||
namespace app\index;
|
||||
|
||||
use think\App;
|
||||
use think\exception\ValidateException;
|
||||
use think\Validate;
|
||||
use think\Request;
|
||||
|
||||
// 在控制器方法中添加
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
*/
|
||||
abstract class BaseController
|
||||
{
|
||||
/**
|
||||
* Request实例
|
||||
* @var Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* 应用实例
|
||||
* @var App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* 是否批量验证
|
||||
* @var bool
|
||||
*/
|
||||
protected $batchValidate = false;
|
||||
|
||||
/**
|
||||
* 控制器中间件
|
||||
* @var array
|
||||
*/
|
||||
protected $middleware = [];
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
* @access public
|
||||
* @param App $app 应用对象
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->request = $this->app->request;
|
||||
|
||||
// 控制器初始化
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
protected function initialize() {}
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
2
app/index/common.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// 这是系统自动生成的公共文件
|
||||
249
app/index/controller/Article/ArticleController.php
Normal file
@ -0,0 +1,249 @@
|
||||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\index\controller\Article;
|
||||
|
||||
use app\index\BaseController;
|
||||
use app\model\Articles;
|
||||
use app\model\ArticlesCategory;
|
||||
use Symfony\Component\VarDumper\VarDumper;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Request;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
|
||||
class ArticleController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取新闻中心顶部4篇文章
|
||||
* @return Json
|
||||
*/
|
||||
public function getNewsCenterTop4(): Json
|
||||
{
|
||||
try {
|
||||
// 1. 查询新闻中心主分类
|
||||
$newsCenterCategory = ArticlesCategory::where('name', '新闻中心')
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$newsCenterCategory) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
// 2. 获取新闻中心及其子分类的ID列表
|
||||
$mainCategoryId = $newsCenterCategory['id'];
|
||||
|
||||
// 查询子分类
|
||||
$subCategories = ArticlesCategory::where('cid', $mainCategoryId)
|
||||
->where('delete_time', null)
|
||||
->column('id');
|
||||
|
||||
// 合并主分类和子分类ID
|
||||
$categoryIds = array_merge([$mainCategoryId], $subCategories);
|
||||
|
||||
if (empty($categoryIds)) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. 查询符合条件的文章,按指定优先级排序,取最新4篇
|
||||
$articles = Articles::published()
|
||||
->whereIn('cate', $categoryIds)
|
||||
->order([
|
||||
'recommend' => 'desc',
|
||||
'publishdate' => 'desc',
|
||||
'sort' => 'desc',
|
||||
'id' => 'desc'
|
||||
])
|
||||
->field([
|
||||
'id',
|
||||
'title',
|
||||
'content',
|
||||
'publishdate',
|
||||
'DAY(publishdate) AS `date`',
|
||||
'DATE_FORMAT(publishdate, "%Y-%m") AS `month`',
|
||||
'image',
|
||||
'recommend'
|
||||
])
|
||||
->limit(4)
|
||||
->select();
|
||||
|
||||
// 4. 处理文章数据,去除HTML标签并生成desc字段
|
||||
foreach ($articles as &$article) {
|
||||
// 使用PHP的strip_tags函数去除HTML标签,然后截取前100个字符
|
||||
$article['desc'] = mb_substr(trim(strip_tags($article['content'])), 0, 100, 'UTF-8');
|
||||
// 移除原始content字段,减少返回数据大小
|
||||
unset($article['content']);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => $articles,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 打印完整错误信息到日志
|
||||
$errorMsg = '错误信息:' . $e->getMessage() . ' | 错误行号:' . $e->getLine() . ' | 执行SQL:' . Articles::getLastSql();
|
||||
trace('新闻中心文章查询失败: ' . $errorMsg, 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '新闻中心文章查询失败,请稍后重试',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 游客文章阅读量函数
|
||||
* @return Json
|
||||
*/
|
||||
public function articleViews(): Json
|
||||
{
|
||||
$id = Request::param('id');
|
||||
if (!$id) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '没有文章id',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$article = Articles::where('id', $id)->where('delete_time', null)->find();
|
||||
|
||||
if (!$article) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '文章不存在',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
$article->inc('views');
|
||||
$article->save();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '阅读量更新成功',
|
||||
'data' => [
|
||||
'views' => $article->views
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
trace('阅读量更新失败: ' . $e->getMessage(), 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '阅读量更新失败,请稍后重试',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 游客文章点赞函数
|
||||
* @return Json
|
||||
*/
|
||||
public function articleLikes(): Json
|
||||
{
|
||||
$id = Request::param('id');
|
||||
if (!$id) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '没有文章id',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$article = Articles::where('id', $id)->where('delete_time', null)->find();
|
||||
|
||||
if (!$article) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '文章不存在',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
$article->inc('likes');
|
||||
$article->save();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '点赞成功',
|
||||
'data' => [
|
||||
'likes' => $article->likes
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
trace('点赞失败: ' . $e->getMessage(), 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '点赞失败,请稍后重试',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 游客文章取消点赞函数
|
||||
* @return Json
|
||||
*/
|
||||
public function articleUnlikes(): Json
|
||||
{
|
||||
$id = Request::param('id');
|
||||
if (!$id) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '没有文章id',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$article = Articles::where('id', $id)->where('delete_time', null)->find();
|
||||
|
||||
if (!$article) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '文章不存在',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
if ($article->likes <= 0) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '文章点赞数为0,不能取消点赞',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
|
||||
$article->dec('likes');
|
||||
$article->save();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '取消点赞成功',
|
||||
'data' => [
|
||||
'likes' => $article->likes
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
trace('取消点赞失败: ' . $e->getMessage(), 'error');
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '取消点赞失败,请稍后重试',
|
||||
'list' => []
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
app/index/controller/Index.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\index\controller;
|
||||
|
||||
use app\model\Banner;
|
||||
use app\index\BaseController;
|
||||
use app\model\FrontMenu;
|
||||
use app\model\OnePage;
|
||||
use think\db\exception\DbException;
|
||||
|
||||
class Index extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return '您好!这是一个[index]示例应用';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端导航接口
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getHeadMenu()
|
||||
{
|
||||
try {
|
||||
// 获取所有未删除的菜单
|
||||
$frontMenus = FrontMenu::where('delete_time', null)
|
||||
->field('id,pid,title,type,image,path,component_path,sort,desc')
|
||||
->order('sort', 'desc')
|
||||
->select()
|
||||
->toArray(); // 转换为数组
|
||||
|
||||
// 使用 buildFrontMenuTree 构建树形结构
|
||||
$treeFrontMenus = $this->buildFrontMenuTree($frontMenus);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $treeFrontMenus
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建前端导航树形结构
|
||||
* @param array $frontMenus 前端导航列表
|
||||
* @param int $pid 父前端导航ID
|
||||
* @return array
|
||||
*/
|
||||
private function buildFrontMenuTree(array $frontMenus, int $pid = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
foreach ($frontMenus as $frontMenu) {
|
||||
// 将null的pid视为0处理
|
||||
$menuPid = is_null($frontMenu['pid']) ? 0 : $frontMenu['pid'];
|
||||
if ($menuPid == $pid) {
|
||||
$children = $this->buildFrontMenuTree($frontMenus, $frontMenu['id']);
|
||||
if (!empty($children)) {
|
||||
$frontMenu['children'] = $children;
|
||||
}
|
||||
$tree[] = $frontMenu;
|
||||
}
|
||||
}
|
||||
|
||||
// 按 sort 字段降序排序
|
||||
usort($tree, function ($a, $b) {
|
||||
$sortA = $a['sort'] ?? 0;
|
||||
$sortB = $b['sort'] ?? 0;
|
||||
return $sortB - $sortA;
|
||||
});
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据路径获取单页内容
|
||||
* @param string $path 路由路径
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getOnePageByPath($path = '')
|
||||
{
|
||||
try {
|
||||
// 从路由参数获取路径
|
||||
if (empty($path)) {
|
||||
// 如果没有路由参数,尝试从 pathinfo 获取
|
||||
$requestPath = $this->request->pathinfo();
|
||||
$path = str_replace('onepage/', '', $requestPath);
|
||||
}
|
||||
|
||||
// 确保路径以 / 开头
|
||||
if (empty($path) || $path[0] !== '/') {
|
||||
$path = '/' . $path;
|
||||
}
|
||||
|
||||
// 解码路径
|
||||
$path = urldecode($path);
|
||||
|
||||
// 查找对应路径的单页
|
||||
$onePage = OnePage::where('path', $path)
|
||||
->where('status', 1)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$onePage) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '单页不存在或已禁用',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $onePage->toArray()
|
||||
]);
|
||||
} catch (DbException $e) {
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => 'fail:' . $e->getMessage(),
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
434
app/index/controller/NewsCenterController.php
Normal file
@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\index\controller;
|
||||
|
||||
use app\index\BaseController;
|
||||
use Symfony\Component\VarDumper\VarDumper;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Request;
|
||||
use think\facade\Session;
|
||||
use think\response\Json;
|
||||
use think\db\exception\DbException;
|
||||
|
||||
use app\model\Articles;
|
||||
use app\model\ArticlesCategory;
|
||||
|
||||
|
||||
class NewsCenterController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 根据分类获取文章
|
||||
* @param string $category 分类名称
|
||||
* @return Json
|
||||
*/
|
||||
public function getNewsByCategory(string $category): Json
|
||||
{
|
||||
// 查询分类
|
||||
$categoryInfo = ArticlesCategory::where('name', $category)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$categoryInfo) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
// 查询文章
|
||||
$articles = Articles::published()
|
||||
->where('cate', $categoryInfo['id'])
|
||||
->order('publishdate', 'desc')
|
||||
->limit(4)
|
||||
->select();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => $articles,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业新闻
|
||||
* @return Json
|
||||
*/
|
||||
public function getCompanyNews(): Json
|
||||
{
|
||||
// 获取分页参数
|
||||
$page = Request::param('page', 1);
|
||||
$limit = Request::param('limit', 10);
|
||||
$page = max(1, intval($page));
|
||||
$limit = max(1, min(50, intval($limit))); // 限制每页最多50条
|
||||
|
||||
// 查询分类
|
||||
$categoryInfo = ArticlesCategory::where('name', '企业新闻')
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$categoryInfo) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
$total = Articles::where('cate', $categoryInfo['id'])
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->count();
|
||||
|
||||
// 查询文章
|
||||
$articles = Articles::where('cate', $categoryInfo['id'])
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->order('top', 'desc')
|
||||
->order('recommend', 'desc')
|
||||
->order('sort', 'desc')
|
||||
->order('publishdate', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => $articles,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取金蝶新闻
|
||||
* @return Json
|
||||
*/
|
||||
public function getKingdeeNews(): Json
|
||||
{
|
||||
// 获取分页参数
|
||||
$page = Request::param('page', 1);
|
||||
$limit = Request::param('limit', 10);
|
||||
$page = max(1, intval($page));
|
||||
$limit = max(1, min(50, intval($limit))); // 限制每页最多50条
|
||||
|
||||
// 查询分类
|
||||
$categoryInfo = ArticlesCategory::where('name', '金蝶新闻')
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$categoryInfo) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
$total = Articles::where('cate', $categoryInfo['id'])
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->count();
|
||||
|
||||
// 查询文章
|
||||
$articles = Articles::where('cate', $categoryInfo['id'])
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->order('top', 'desc')
|
||||
->order('recommend', 'desc')
|
||||
->order('sort', 'desc')
|
||||
->order('publishdate', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => $articles,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取技术中心子分类
|
||||
* @return Json
|
||||
*/
|
||||
public function getTechnologyCategories(): Json
|
||||
{
|
||||
// 获取"技术中心"主分类
|
||||
$parentCategory = ArticlesCategory::where('name', '技术中心')
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$parentCategory) {
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
|
||||
// 查找所有子分类
|
||||
$subCategories = ArticlesCategory::where('cid', $parentCategory['id'])
|
||||
->where('delete_time', null)
|
||||
->field('id,name,desc,sort,image')
|
||||
->order('sort', 'desc')
|
||||
->select();
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'data' => $subCategories
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取技术中心
|
||||
* @return Json
|
||||
*/
|
||||
public function getTechnologyCenter(): Json
|
||||
{
|
||||
// 1. 分页参数规范化
|
||||
$page = max(1, intval(Request::param('page', 1)));
|
||||
$limit = max(1, min(50, intval(Request::param('limit', 10))));
|
||||
|
||||
// 获取分类ID参数(可选)
|
||||
$categoryId = Request::param('category_id', null);
|
||||
|
||||
// 2. 第一步:获取“技术中心”主分类的 id
|
||||
$parentCategory = ArticlesCategory::where('name', '技术中心')
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
// print_r($parentCategory['id']);
|
||||
|
||||
if (!$parentCategory) {
|
||||
return $this->emptyResponse($page, $limit);
|
||||
}
|
||||
|
||||
// 3. 第二步:查找 cid 等于主分类 id 的所有子分类 id
|
||||
$subCategoryQuery = ArticlesCategory::where('cid', $parentCategory['id'])
|
||||
->where('delete_time', null);
|
||||
|
||||
// print_r($subCategoryQuery->select());
|
||||
|
||||
// 如果指定了分类ID,则只查询该分类
|
||||
if ($categoryId) {
|
||||
$subCategoryQuery->where('id', $categoryId);
|
||||
}
|
||||
|
||||
$subCategoryIds = $subCategoryQuery->column('id');
|
||||
|
||||
// print_r($subCategoryIds);
|
||||
|
||||
if (empty($subCategoryIds)) {
|
||||
return $this->emptyResponse($page, $limit);
|
||||
}
|
||||
|
||||
// 5. 第三步:查询 Articles 表,并限定字段
|
||||
$articleQuery = Articles::whereIn('cate', $subCategoryIds)
|
||||
->where('delete_time', null)
|
||||
->where('status', 2);
|
||||
|
||||
// 获取总数
|
||||
$total = (clone $articleQuery)->count();
|
||||
|
||||
// 获取列表:限定返回 id, title, desc, publishdate, image, likes, views
|
||||
$articles = $articleQuery->field('id,title,desc,publishdate,image,likes,views,cate')
|
||||
->order([
|
||||
'top' => 'desc',
|
||||
'recommend' => 'desc',
|
||||
'sort' => 'desc',
|
||||
'publishdate' => 'desc'
|
||||
])
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 如果文章没有image,查询对应分类的image
|
||||
$categoryImages = ArticlesCategory::whereIn('id', $subCategoryIds)->column('image', 'id');
|
||||
|
||||
foreach ($articles as &$article) {
|
||||
if (empty($article['image']) && isset($categoryImages[$article['cate']])) {
|
||||
$article['image'] = $categoryImages[$article['cate']];
|
||||
}
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => $articles,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一空返回
|
||||
*/
|
||||
private function emptyResponse($page, $limit): Json
|
||||
{
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取金蝶新闻详情
|
||||
*/
|
||||
public function getKingdeeNewsDetail(int $id): Json
|
||||
{
|
||||
$article = Articles::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->field('id,title,cate,image,desc,author,content,publisher,publishdate,views,likes,is_trans,transurl')
|
||||
->find();
|
||||
|
||||
if (!$article) {
|
||||
return json(['code' => 404, 'msg' => '文章不存在', 'data' => null]);
|
||||
}
|
||||
|
||||
$cate = (int) $article['cate'];
|
||||
|
||||
// 转换为数组再处理
|
||||
$articleData = $article->toArray();
|
||||
// $articleData['catename'] = $this->getCategoryName($cate);
|
||||
|
||||
// 获取相关文章
|
||||
$articleData['relatedArticles'] = $this->getRelatedArticles($id, $cate);
|
||||
|
||||
// 获取上一篇下一篇
|
||||
$articleData['nextPreviousArticles'] = $this->getNextPreviousArticles($id, $cate);
|
||||
|
||||
// 增加浏览量
|
||||
Articles::where('id', $id)->inc('views')->update();
|
||||
|
||||
return json(['code' => 200, 'msg' => 'success', 'data' => $articleData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业新闻详情
|
||||
*/
|
||||
public function getCompanyNewsDetail(int $id): Json
|
||||
{
|
||||
$article = Articles::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->find();
|
||||
|
||||
if (!$article) {
|
||||
return json(['code' => 404, 'msg' => '文章不存在', 'data' => null]);
|
||||
}
|
||||
|
||||
$cate = (int) $article['cate'];
|
||||
|
||||
// 转换为数组再处理
|
||||
$articleData = $article->toArray();
|
||||
$articleData['catename'] = $this->getCategoryName($cate);
|
||||
|
||||
// 获取相关文章
|
||||
$articleData['relatedArticles'] = $this->getRelatedArticles($id, $cate);
|
||||
|
||||
// 获取上一篇下一篇
|
||||
$articleData['nextPreviousArticles'] = $this->getNextPreviousArticles($id, $cate);
|
||||
|
||||
// 增加浏览量
|
||||
Articles::where('id', $id)->inc('views')->update();
|
||||
|
||||
return json(['code' => 200, 'msg' => 'success', 'data' => $articleData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一篇下一篇
|
||||
* @param int $id 文章ID
|
||||
* @param int $cate 分类ID
|
||||
* @return array
|
||||
*/
|
||||
private function getNextPreviousArticles(int $id, int $cate): array
|
||||
{
|
||||
$nextArticle = Articles::where('id', '<', $id)
|
||||
->where('cate', $cate)
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->field('id,title')
|
||||
->find();
|
||||
|
||||
$previousArticle = Articles::where('id', '>', $id)
|
||||
->where('cate', $cate)
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->field('id,title')
|
||||
->find();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'next' => $nextArticle,
|
||||
'previous' => $previousArticle,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过分类 ID 获取分类名称
|
||||
* @param int $cateId 分类ID
|
||||
* @return string
|
||||
*/
|
||||
private function getCategoryName(int $id): string
|
||||
{
|
||||
$categoryInfo = ArticlesCategory::where('id', $id)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
|
||||
if (!$categoryInfo) {
|
||||
return '未分类';
|
||||
}
|
||||
|
||||
return $categoryInfo['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相关文章
|
||||
*/
|
||||
private function getRelatedArticles(int $id, int $cate): array
|
||||
{
|
||||
$articles = Articles::where('id', '<>', $id)
|
||||
->where('cate', $cate)
|
||||
->where('delete_time', null)
|
||||
->where('status', 2)
|
||||
->order('top', 'desc')
|
||||
->order('recommend', 'desc')
|
||||
->order('sort', 'desc')
|
||||
->order('id', 'desc')
|
||||
->limit(5)
|
||||
->select();
|
||||
|
||||
foreach ($articles as &$article) {
|
||||
$article['cate'] = $this->getCategoryName($article['cate']);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'success',
|
||||
'list' => $articles,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
5
app/index/event.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
// 这是系统自动生成的event定义文件
|
||||
return [
|
||||
|
||||
];
|
||||
12
app/index/middleware.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// 全局中间件定义文件
|
||||
return [
|
||||
// 全局跨域中间件
|
||||
\think\middleware\AllowCrossDomain::class,
|
||||
// 全局请求缓存
|
||||
// \think\middleware\CheckRequestCache::class,
|
||||
// 多语言加载
|
||||
// \think\middleware\LoadLangPack::class,
|
||||
// Session初始化
|
||||
\think\middleware\SessionInit::class,
|
||||
];
|
||||
27
app/index/route/app.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
use think\facade\Route;
|
||||
|
||||
// 首页路由
|
||||
Route::get('/', 'app\index\controller\Index@index');
|
||||
Route::get('index/index', 'app\index\controller\Index@index');
|
||||
|
||||
// --- 文章详情路由 ---
|
||||
Route::get('kingdeenews/detail/:id', 'app\index\controller\NewsCenterController@getKingdeeNewsDetail');
|
||||
Route::get('companynews/detail/:id', 'app\index\controller\NewsCenterController@getCompanyNewsDetail');
|
||||
|
||||
// --- 文章列表路由 ---
|
||||
Route::get('kingdeenews$', 'app\index\controller\NewsCenterController@getKingdeeNews');
|
||||
Route::get('companynews$', 'app\index\controller\NewsCenterController@getCompanyNews');
|
||||
Route::get('technologyCenter$', 'app\index\controller\NewsCenterController@getTechnologyCenter');
|
||||
Route::get('technologyCategories$', 'app\index\controller\NewsCenterController@getTechnologyCategories');
|
||||
Route::get('newscentertop4', 'app\index\controller\Article\ArticleController@getNewsCenterTop4');
|
||||
Route::get('newsbycategory/:category', 'app\index\controller\NewsCenterController@getNewsByCategory');
|
||||
|
||||
// --- 文章互动路由 ---
|
||||
Route::post('articleViews/:id', 'app\index\controller\Article\ArticleController@articleViews');
|
||||
Route::post('articleLikes/:id', 'app\index\controller\Article\ArticleController@articleLikes');
|
||||
Route::post('articleUnlikes/:id', 'app\index\controller\Article\ArticleController@articleUnlikes');
|
||||
|
||||
// --- 前端导航与单页路由 ---
|
||||
Route::get('headmenu', 'app\index\controller\Index@getHeadMenu');
|
||||
Route::rule('onepage/:path', 'app\index\controller\Index@getOnePageByPath', 'GET')->pattern(['path' => '.*']);
|
||||
42
app/model/AdminUser.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class AdminUser extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_admin_user';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'sex' => 'integer',
|
||||
'group_id' => 'integer',
|
||||
'login_count' => 'integer',
|
||||
'status' => 'integer',
|
||||
'api_key_status' => 'integer',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
40
app/model/AdminUserGroup.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 用户组模型
|
||||
*/
|
||||
class AdminUserGroup extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_admin_user_group';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'name' => 'string',
|
||||
'status' => 'integer',
|
||||
'rights' => 'string',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
129
app/model/Articles.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章模型
|
||||
*/
|
||||
class Articles extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_articles';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 时间戳字段名
|
||||
protected $createTime = 'create_time';
|
||||
protected $updateTime = 'update_time';
|
||||
protected $deleteTime = 'delete_time';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'cate' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
'is_trans' => 'integer',
|
||||
'views' => 'integer',
|
||||
'likes' => 'integer',
|
||||
'recommend' => 'integer',
|
||||
'top' => 'integer',
|
||||
'publisher' => 'integer',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'publishdate' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联文章分类
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo('app\index\model\ArticleCategory', 'cate', 'id', [], 'LEFT')->bind([
|
||||
'cate_name' => 'name',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联发布者
|
||||
*/
|
||||
public function publisher()
|
||||
{
|
||||
return $this->belongsTo('app\index\model\AdminUser', 'publisher', 'id', [], 'LEFT')->bind([
|
||||
'publisher_name' => 'name',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已发布的文章
|
||||
*/
|
||||
public static function published()
|
||||
{
|
||||
return self::where('status', 2)->where('delete_time', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推荐文章
|
||||
*/
|
||||
public static function recommended()
|
||||
{
|
||||
return self::published()->where('recommend', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取置顶文章
|
||||
*/
|
||||
public static function top()
|
||||
{
|
||||
return self::published()->where('top', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分类获取文章
|
||||
*/
|
||||
public static function byCategory($cateId)
|
||||
{
|
||||
return self::published()->where('cate', $cateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按关键词搜索文章
|
||||
*/
|
||||
public static function search($keyword)
|
||||
{
|
||||
return self::published()->where('title', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新阅读量
|
||||
*/
|
||||
public function incrementViews()
|
||||
{
|
||||
return $this->where('id', $this->id)->inc('views')->update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新点赞量
|
||||
*/
|
||||
public function incrementLikes()
|
||||
{
|
||||
return $this->where('id', $this->id)->inc('likes')->update();
|
||||
}
|
||||
}
|
||||
80
app/model/ArticlesCategory.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class ArticlesCategory extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_articles_category';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
// 时间戳字段名
|
||||
protected $createTime = 'create_time';
|
||||
protected $updateTime = 'update_time';
|
||||
protected $deleteTime = 'delete_time';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联文章
|
||||
*/
|
||||
public function articles()
|
||||
{
|
||||
return $this->hasMany('app\index\model\Articles', 'cate', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分类
|
||||
*/
|
||||
public static function allCategories()
|
||||
{
|
||||
return self::where('delete_time', null)->order('sort asc, id asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的分类
|
||||
*/
|
||||
public static function enabled()
|
||||
{
|
||||
return self::where('status', 1)->where('delete_time', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类下的文章数量
|
||||
*/
|
||||
public function articleCount()
|
||||
{
|
||||
return $this->hasMany('app\index\model\Articles', 'cate', 'id')
|
||||
->where('status', 1)
|
||||
->where('delete_time', null)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
42
app/model/Banner.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class Banner extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_banner';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'title' => 'string',
|
||||
'desc' => 'string',
|
||||
'url' => 'string',
|
||||
'image' => 'string',
|
||||
'sort' => 'integer',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
44
app/model/Files.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class Files extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_files';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'name' => 'string',
|
||||
'type' => 'integer',
|
||||
'cate' => 'integer',
|
||||
'size' => 'integer',
|
||||
'src' => 'string',
|
||||
'uploader' => 'integer',
|
||||
'md5' => 'string',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
38
app/model/FilesCategory.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class FilesCategory extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_files_category';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'name' => 'string',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
43
app/model/FrontMenu.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class FrontMenu extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_front_menu';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'pid' => 'integer',
|
||||
'title' => 'string',
|
||||
'type' => 'integer',
|
||||
'path' => 'string',
|
||||
'component_path' => 'string',
|
||||
'desc' => 'string',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
41
app/model/OnePage.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 单页模型
|
||||
*/
|
||||
class OnePage extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_onepage';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'title' => 'string',
|
||||
'content' => 'string',
|
||||
'path' => 'string',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
40
app/model/OperationLog.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 操作日志模型
|
||||
*/
|
||||
class OperationLog extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_operation_log';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'status' => 'integer',
|
||||
'execution_time' => 'float',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
48
app/model/SystemMenu.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 系统菜单模型
|
||||
*/
|
||||
class SystemMenu extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_system_menu';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'pid' => 'integer',
|
||||
'title' => 'string',
|
||||
'path' => 'string',
|
||||
'component_path' => 'string',
|
||||
'icon' => 'string',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
'type' => 'integer',
|
||||
'permission' => 'string',
|
||||
'remark' => 'string',
|
||||
'creater' => 'string',
|
||||
'create_time' => 'datetime',
|
||||
'update_time' => 'datetime',
|
||||
'delete_time' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
}
|
||||
36
app/model/SystemSiteSettings.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: Liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
/**
|
||||
* 文章分类模型
|
||||
*/
|
||||
class SystemSiteSettings extends Model
|
||||
{
|
||||
// 启用软删除
|
||||
use SoftDelete;
|
||||
|
||||
// 数据库表名
|
||||
protected $name = 'mete_system_site_settings';
|
||||
|
||||
// 字段类型转换
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'label' => 'string',
|
||||
'value' => 'string',
|
||||
'sort' => 'integer',
|
||||
'remark' => 'string',
|
||||
];
|
||||
}
|
||||
60
app/service/JwtService.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
class JwtService
|
||||
{
|
||||
private static string $secret = 'meitian@#!';
|
||||
private static int $expire = 86400;
|
||||
|
||||
public static function generateToken(array $userInfo): string
|
||||
{
|
||||
$payload = [
|
||||
'iss' => 'backapi.yunzer.cn',
|
||||
'sub' => $userInfo['id'],
|
||||
'iat' => time(),
|
||||
'exp' => time() + self::$expire,
|
||||
'user' => $userInfo
|
||||
];
|
||||
return JWT::encode($payload, self::$secret, 'HS256');
|
||||
}
|
||||
|
||||
public static function verifyToken(string $token): ?array
|
||||
{
|
||||
try {
|
||||
$decoded = JWT::decode($token, new Key(self::$secret, 'HS256'));
|
||||
return (array)$decoded;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getUserFromHeader(string $authHeader): array
|
||||
{
|
||||
if (!preg_match('/Bearer\s+(.+)/i', $authHeader, $matches)) {
|
||||
return ['id' => 0, 'account' => '', 'name' => ''];
|
||||
}
|
||||
|
||||
$tokenData = self::verifyToken($matches[1]);
|
||||
if (!$tokenData || !isset($tokenData['user'])) {
|
||||
return ['id' => 0, 'account' => '', 'name' => ''];
|
||||
}
|
||||
|
||||
return (array)$tokenData['user'];
|
||||
}
|
||||
|
||||
public static function getSecret(): string
|
||||
{
|
||||
return self::$secret;
|
||||
}
|
||||
|
||||
public static function setSecret(string $secret): void
|
||||
{
|
||||
self::$secret = $secret;
|
||||
}
|
||||
}
|
||||
51
composer.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "topthink/think",
|
||||
"description": "the new thinkphp framework",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"thinkphp",
|
||||
"ORM"
|
||||
],
|
||||
"homepage": "https://www.thinkphp.cn/",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.0.0",
|
||||
"topthink/framework": "^8.0",
|
||||
"topthink/think-orm": "^3.0|^4.0",
|
||||
"topthink/think-filesystem": "^2.0|^3.0",
|
||||
"topthink/think-multi-app": "^1.1",
|
||||
"firebase/php-jwt": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"topthink/think-dumper": "^1.0",
|
||||
"topthink/think-trace": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"app\\": "app"
|
||||
},
|
||||
"psr-0": {
|
||||
"": "extend/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist"
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"@php think service:discover",
|
||||
"@php think vendor:publish"
|
||||
]
|
||||
}
|
||||
}
|
||||
1290
composer.lock
generated
Normal file
35
config/app.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 应用设置
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
// 应用的命名空间
|
||||
'app_namespace' => 'app',
|
||||
// 是否启用路由
|
||||
'with_route' => true,
|
||||
// 默认应用
|
||||
'default_app' => 'admin',
|
||||
// 默认时区
|
||||
'default_timezone' => 'Asia/Shanghai',
|
||||
// 自动多应用模式
|
||||
'auto_multi_app' => true,
|
||||
// 应用快速访问(当访问的应用不存在时,使用默认应用)
|
||||
'app_express' => true,
|
||||
// 控制器后缀
|
||||
'controller_suffix' => false,
|
||||
// 应用映射(自动多应用模式有效)
|
||||
'app_map' => [],
|
||||
// 域名绑定(自动多应用模式有效)
|
||||
'domain_bind' => [],
|
||||
// 禁止URL访问的应用列表(自动多应用模式有效)
|
||||
'deny_app_list' => [],
|
||||
|
||||
// 异常页面的模板文件
|
||||
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
|
||||
|
||||
// 错误显示信息,非调试模式有效
|
||||
'error_message' => '页面错误!请稍后再试~',
|
||||
// 显示错误信息
|
||||
'show_error_msg' => false,
|
||||
];
|
||||
48
config/cache.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | 缓存设置
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
// 默认缓存驱动
|
||||
'default' => 'file',
|
||||
|
||||
// 缓存连接方式配置
|
||||
'stores' => [
|
||||
'file' => [
|
||||
// 驱动方式
|
||||
'type' => 'File',
|
||||
// 缓存保存目录
|
||||
'path' => '',
|
||||
// 缓存前缀
|
||||
'prefix' => '',
|
||||
// 缓存有效期 0表示永久缓存
|
||||
'expire' => 0,
|
||||
// 缓存标签前缀
|
||||
'tag_prefix' => 'tag:',
|
||||
// 序列化机制 例如 ['serialize', 'unserialize']
|
||||
'serialize' => [],
|
||||
],
|
||||
'redis' => [
|
||||
// 驱动方式
|
||||
'type' => 'redis',
|
||||
// 服务器地址
|
||||
'host' => '10.168.1.231',
|
||||
// 端口
|
||||
'port' => 6379,
|
||||
// 密码
|
||||
'password' => 'meitian123',
|
||||
// 数据库索引
|
||||
'select' => 0,
|
||||
// 超时时间
|
||||
'timeout' => 0,
|
||||
// 缓存前缀
|
||||
'prefix' => 'cache_',
|
||||
// 缓存有效期 0表示永久缓存
|
||||
'expire' => 0,
|
||||
// 持久化连接
|
||||
'persistent' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
9
config/console.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 控制台配置
|
||||
// +----------------------------------------------------------------------
|
||||
return [
|
||||
// 指令定义
|
||||
'commands' => [
|
||||
],
|
||||
];
|
||||
20
config/cookie.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | Cookie设置
|
||||
// +----------------------------------------------------------------------
|
||||
return [
|
||||
// cookie 保存时间
|
||||
'expire' => 0,
|
||||
// cookie 保存路径
|
||||
'path' => '/',
|
||||
// cookie 有效域名
|
||||
'domain' => '',
|
||||
// cookie 启用安全传输
|
||||
'secure' => false,
|
||||
// httponly设置
|
||||
'httponly' => false,
|
||||
// 是否使用 setcookie
|
||||
'setcookie' => true,
|
||||
// samesite 设置,支持 'strict' 'lax'
|
||||
'samesite' => '',
|
||||
];
|
||||
63
config/database.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 默认使用的数据库连接配置
|
||||
'default' => env('DB_DRIVER', 'mysql'),
|
||||
|
||||
// 自定义时间查询规则
|
||||
'time_query_rule' => [],
|
||||
|
||||
// 自动写入时间戳字段
|
||||
// true为自动识别类型 false关闭
|
||||
// 字符串则明确指定时间字段类型 支持 int timestamp datetime date
|
||||
'auto_timestamp' => true,
|
||||
|
||||
// 时间字段取出后的默认时间格式
|
||||
'datetime_format' => 'Y-m-d H:i:s',
|
||||
|
||||
// 时间字段配置 配置格式:create_time,update_time
|
||||
'datetime_field' => '',
|
||||
|
||||
// 数据库连接配置信息
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
// 数据库类型
|
||||
'type' => env('DB_TYPE', 'mysql'),
|
||||
// 服务器地址
|
||||
'hostname' => env('DB_HOST', '127.0.0.1'),
|
||||
// 数据库名
|
||||
'database' => env('DB_NAME', ''),
|
||||
// 用户名
|
||||
'username' => env('DB_USER', 'root'),
|
||||
// 密码
|
||||
'password' => env('DB_PASS', ''),
|
||||
// 端口
|
||||
'hostport' => env('DB_PORT', '3306'),
|
||||
// 数据库连接参数
|
||||
'params' => [],
|
||||
// 数据库编码
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
// 数据库表前缀
|
||||
'prefix' => env('DB_PREFIX', ''),
|
||||
|
||||
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
|
||||
'deploy' => 0,
|
||||
// 数据库读写是否分离 主从式有效
|
||||
'rw_separate' => false,
|
||||
// 读写分离后 主服务器数量
|
||||
'master_num' => 1,
|
||||
// 指定从服务器序号
|
||||
'slave_no' => '',
|
||||
// 是否严格检查字段是否存在
|
||||
'fields_strict' => true,
|
||||
// 是否需要断线重连
|
||||
'break_reconnect' => false,
|
||||
// 监听SQL
|
||||
'trigger_sql' => env('APP_DEBUG', true),
|
||||
// 开启字段缓存
|
||||
'fields_cache' => false,
|
||||
],
|
||||
|
||||
// 更多的数据库配置信息
|
||||
],
|
||||
];
|
||||
24
config/filesystem.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 默认磁盘
|
||||
'default' => 'local',
|
||||
// 磁盘列表
|
||||
'disks' => [
|
||||
'local' => [
|
||||
'type' => 'local',
|
||||
'root' => app()->getRuntimePath() . 'storage',
|
||||
],
|
||||
'public' => [
|
||||
// 磁盘类型
|
||||
'type' => 'local',
|
||||
// 磁盘路径
|
||||
'root' => app()->getRootPath() . 'public/storage',
|
||||
// 磁盘路径对应的外部URL路径
|
||||
'url' => '/storage',
|
||||
// 可见性
|
||||
'visibility' => 'public',
|
||||
],
|
||||
// 更多的磁盘配置信息
|
||||
],
|
||||
];
|
||||
29
config/lang.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 多语言设置
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
// 默认语言
|
||||
'default_lang' => env('DEFAULT_LANG', 'zh-cn'),
|
||||
// 自动侦测浏览器语言
|
||||
'auto_detect_browser' => true,
|
||||
// 允许的语言列表
|
||||
'allow_lang_list' => [],
|
||||
// 多语言自动侦测变量名
|
||||
'detect_var' => 'lang',
|
||||
// 是否使用Cookie记录
|
||||
'use_cookie' => true,
|
||||
// 多语言cookie变量
|
||||
'cookie_var' => 'think_lang',
|
||||
// 多语言header变量
|
||||
'header_var' => 'think-lang',
|
||||
// 扩展语言包
|
||||
'extend_list' => [],
|
||||
// Accept-Language转义为对应语言包名称
|
||||
'accept_language' => [
|
||||
'zh-hans-cn' => 'zh-cn',
|
||||
],
|
||||
// 是否支持语言分组
|
||||
'allow_group' => false,
|
||||
];
|
||||
45
config/log.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
// +----------------------------------------------------------------------
|
||||
// | 日志设置
|
||||
// +----------------------------------------------------------------------
|
||||
return [
|
||||
// 默认日志记录通道
|
||||
'default' => 'file',
|
||||
// 日志记录级别
|
||||
'level' => [],
|
||||
// 日志类型记录的通道 ['error'=>'email',...]
|
||||
'type_channel' => [],
|
||||
// 关闭全局日志写入
|
||||
'close' => false,
|
||||
// 全局日志处理 支持闭包
|
||||
'processor' => null,
|
||||
|
||||
// 日志通道列表
|
||||
'channels' => [
|
||||
'file' => [
|
||||
// 日志记录方式
|
||||
'type' => 'File',
|
||||
// 日志保存目录
|
||||
'path' => '',
|
||||
// 单文件日志写入
|
||||
'single' => false,
|
||||
// 独立日志级别
|
||||
'apart_level' => [],
|
||||
// 最大日志文件数量
|
||||
'max_files' => 0,
|
||||
// 使用JSON格式记录
|
||||
'json' => false,
|
||||
// 日志处理
|
||||
'processor' => null,
|
||||
// 关闭通道日志写入
|
||||
'close' => false,
|
||||
// 日志输出格式化
|
||||
'format' => '[%s][%s] %s',
|
||||
// 是否实时写入
|
||||
'realtime_write' => false,
|
||||
],
|
||||
// 其它日志通道配置
|
||||
],
|
||||
|
||||
];
|
||||
12
config/middleware.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// 中间件配置
|
||||
return [
|
||||
// 别名或分组
|
||||
'alias' => [
|
||||
'multi_app' => \think\app\MultiApp::class,
|
||||
],
|
||||
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
|
||||
'priority' => [
|
||||
\think\app\MultiApp::class,
|
||||
],
|
||||
];
|
||||
47
config/route.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 路由设置
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
// pathinfo分隔符
|
||||
'pathinfo_depr' => '/',
|
||||
// URL伪静态后缀
|
||||
'url_html_suffix' => '',
|
||||
// URL普通方式参数 用于自动生成
|
||||
'url_common_param' => true,
|
||||
// 是否开启路由延迟解析
|
||||
'url_lazy_route' => false,
|
||||
// 是否强制使用路由
|
||||
'url_route_must' => false,
|
||||
// 合并路由规则
|
||||
'route_rule_merge' => false,
|
||||
// 路由是否完全匹配
|
||||
'route_complete_match' => false,
|
||||
// 访问控制器层名称
|
||||
'controller_layer' => 'controller',
|
||||
// 空控制器名
|
||||
'empty_controller' => 'Error',
|
||||
// 是否使用控制器后缀
|
||||
'controller_suffix' => false,
|
||||
// 默认的路由变量规则
|
||||
'default_route_pattern' => '[\w\.]+',
|
||||
// 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
|
||||
'request_cache_key' => false,
|
||||
// 请求缓存有效期
|
||||
'request_cache_expire' => null,
|
||||
// 全局请求缓存排除规则
|
||||
'request_cache_except' => [],
|
||||
// 默认控制器名
|
||||
'default_controller' => 'Index',
|
||||
// 默认操作名
|
||||
'default_action' => 'index',
|
||||
// 操作方法后缀
|
||||
'action_suffix' => '',
|
||||
// 默认JSONP格式返回的处理方法
|
||||
'default_jsonp_handler' => 'jsonpReturn',
|
||||
// 默认JSONP处理方法
|
||||
'var_jsonp_handler' => 'callback',
|
||||
// 操作方法的参数绑定方式 route get param
|
||||
'action_bind_param' => 'param',
|
||||
];
|
||||
23
config/session.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 会话设置
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
// session name
|
||||
'name' => 'PHPSESSID',
|
||||
// SESSION_ID的提交变量,解决flash上传跨域
|
||||
'var_session_id' => '',
|
||||
// 驱动方式 支持file cache redis
|
||||
'type' => 'file',
|
||||
// 过期时间
|
||||
'expire' => 1440,
|
||||
// 前缀
|
||||
'prefix' => '',
|
||||
'save_path' => '/www/wwwroot/tp/runtime/admin/session',
|
||||
// 缓存配置
|
||||
'cache' => [
|
||||
'expire' => 1440,
|
||||
'prefix' => '',
|
||||
],
|
||||
];
|
||||
10
config/trace.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | Trace设置 开启调试模式后有效
|
||||
// +----------------------------------------------------------------------
|
||||
return [
|
||||
// 内置Html和Console两种方式 支持扩展
|
||||
'type' => 'Html',
|
||||
// 读取的日志通道名
|
||||
'channel' => '',
|
||||
];
|
||||
25
config/view.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 模板设置
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
// 模板引擎类型使用Think
|
||||
'type' => 'Think',
|
||||
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
|
||||
'auto_rule' => 1,
|
||||
// 模板目录名
|
||||
'view_dir_name' => 'view',
|
||||
// 模板后缀
|
||||
'view_suffix' => 'html',
|
||||
// 模板文件名分隔符
|
||||
'view_depr' => DIRECTORY_SEPARATOR,
|
||||
// 模板引擎普通标签开始标记
|
||||
'tpl_begin' => '{',
|
||||
// 模板引擎普通标签结束标记
|
||||
'tpl_end' => '}',
|
||||
// 标签库标签开始标记
|
||||
'taglib_begin' => '{',
|
||||
// 标签库标签结束标记
|
||||
'taglib_end' => '}',
|
||||
];
|
||||
10
docs/jwt.md
Normal file
@ -0,0 +1,10 @@
|
||||
use app\service\JwtService;
|
||||
|
||||
// 生成 Token
|
||||
$token = JwtService::generateToken($userInfo);
|
||||
|
||||
// 验证 Token
|
||||
$result = JwtService::verifyToken($token);
|
||||
|
||||
// 从 Header 获取用户
|
||||
$user = JwtService::getUserFromHeader($authHeader);
|
||||
338
docs/operation_log_usage.md
Normal file
@ -0,0 +1,338 @@
|
||||
# 操作日志使用说明
|
||||
|
||||
## 1. 数据库表创建
|
||||
|
||||
执行以下 SQL 创建操作日志表:
|
||||
|
||||
```sql
|
||||
-- 操作日志表
|
||||
CREATE TABLE IF NOT EXISTS `mete_operation_log` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
|
||||
`user_account` varchar(50) NOT NULL DEFAULT '' COMMENT '用户账号',
|
||||
`user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户姓名',
|
||||
`module` varchar(50) NOT NULL DEFAULT '' COMMENT '操作模块(如:用户管理、文件管理等)',
|
||||
`action` varchar(50) NOT NULL DEFAULT '' COMMENT '操作动作(如:添加、编辑、删除、查询等)',
|
||||
`method` varchar(10) NOT NULL DEFAULT '' COMMENT '请求方法(GET、POST、PUT、DELETE等)',
|
||||
`url` varchar(255) NOT NULL DEFAULT '' COMMENT '请求URL',
|
||||
`ip` varchar(50) NOT NULL DEFAULT '' COMMENT '操作IP地址',
|
||||
`user_agent` varchar(500) NOT NULL DEFAULT '' COMMENT '用户代理(浏览器信息)',
|
||||
`request_data` text COMMENT '请求参数(JSON格式)',
|
||||
`response_data` text COMMENT '响应数据(JSON格式)',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '操作状态:1-成功,0-失败',
|
||||
`error_message` text COMMENT '错误信息(失败时记录)',
|
||||
`execution_time` decimal(10,3) NOT NULL DEFAULT '0.000' COMMENT '执行时间(秒)',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_module` (`module`),
|
||||
KEY `idx_action` (`action`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
|
||||
```
|
||||
|
||||
## 2. 在控制器中记录操作日志
|
||||
|
||||
### 方法1:使用 BaseController 的便捷方法(推荐)
|
||||
|
||||
所有继承 `BaseController` 的控制器都可以使用 `$this->logOperation()` 方法:
|
||||
|
||||
```php
|
||||
class YourController extends BaseController
|
||||
{
|
||||
public function yourAction()
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
// 你的业务逻辑
|
||||
$result = $this->doSomething();
|
||||
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
// 记录成功日志
|
||||
$this->logOperation(
|
||||
'模块名称', // 如:用户管理、文件管理等
|
||||
'操作动作', // 如:添加、编辑、删除、查询等
|
||||
Request::param(), // 请求参数(可选,默认自动获取)
|
||||
['id' => $result], // 响应数据
|
||||
1, // 状态:1-成功
|
||||
'', // 错误信息(成功时为空)
|
||||
$executionTime // 执行时间(可选)
|
||||
);
|
||||
|
||||
return json(['code' => 200, 'msg' => '操作成功']);
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
// 记录失败日志
|
||||
$this->logOperation(
|
||||
'模块名称',
|
||||
'操作动作',
|
||||
Request::param(),
|
||||
[],
|
||||
0, // 状态:0-失败
|
||||
$e->getMessage(), // 错误信息
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方法2:直接使用 OperationLogHelper
|
||||
|
||||
```php
|
||||
use app\admin\common\OperationLogHelper;
|
||||
use think\facade\Request;
|
||||
|
||||
class YourController extends BaseController
|
||||
{
|
||||
public function yourAction()
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
// 你的业务逻辑
|
||||
$result = $this->doSomething();
|
||||
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
// 记录成功日志
|
||||
OperationLogHelper::log(
|
||||
'模块名称',
|
||||
'操作动作',
|
||||
Request::param(),
|
||||
['result' => 'success'],
|
||||
1,
|
||||
'',
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 200, 'msg' => '操作成功']);
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
OperationLogHelper::log(
|
||||
'模块名称',
|
||||
'操作动作',
|
||||
Request::param(),
|
||||
[],
|
||||
0,
|
||||
$e->getMessage(),
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 使用示例
|
||||
|
||||
### 示例1:用户管理 - 添加用户
|
||||
|
||||
```php
|
||||
public function addUser()
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$data = Request::param();
|
||||
// ... 添加用户的业务逻辑
|
||||
$userId = AdminUser::insertGetId($data);
|
||||
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
// 记录日志
|
||||
$this->logOperation(
|
||||
'用户管理',
|
||||
'添加用户',
|
||||
$data,
|
||||
['id' => $userId],
|
||||
1,
|
||||
'',
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 200, 'msg' => '添加成功']);
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
$this->logOperation(
|
||||
'用户管理',
|
||||
'添加用户',
|
||||
Request::param(),
|
||||
[],
|
||||
0,
|
||||
$e->getMessage(),
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2:文件管理 - 创建文件分组
|
||||
|
||||
```php
|
||||
public function createFileCate()
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
$data = Request::param();
|
||||
$data['create_time'] = date('Y-m-d H:i:s');
|
||||
$id = FilesCategory::insertGetId($data);
|
||||
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
// 记录日志
|
||||
$this->logOperation(
|
||||
'文件管理',
|
||||
'创建文件分组',
|
||||
$data,
|
||||
['id' => $id],
|
||||
1,
|
||||
'',
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '新建文件分组成功',
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
$this->logOperation(
|
||||
'文件管理',
|
||||
'创建文件分组',
|
||||
Request::param(),
|
||||
[],
|
||||
0,
|
||||
$e->getMessage(),
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '新建文件分组失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:文件管理 - 删除文件
|
||||
|
||||
```php
|
||||
public function deleteFile($id)
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
// ... 删除文件的业务逻辑
|
||||
Files::where('id', $id)->delete();
|
||||
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
$this->logOperation(
|
||||
'文件管理',
|
||||
'删除文件',
|
||||
['id' => $id],
|
||||
['result' => 'success'],
|
||||
1,
|
||||
'',
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 200, 'msg' => '删除成功']);
|
||||
} catch (\Exception $e) {
|
||||
$executionTime = microtime(true) - $startTime;
|
||||
|
||||
$this->logOperation(
|
||||
'文件管理',
|
||||
'删除文件',
|
||||
['id' => $id],
|
||||
[],
|
||||
0,
|
||||
$e->getMessage(),
|
||||
$executionTime
|
||||
);
|
||||
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 模块和操作命名规范
|
||||
|
||||
### 模块名称建议:
|
||||
- 用户管理
|
||||
- 角色管理
|
||||
- 文件管理
|
||||
- 文章管理
|
||||
- 菜单管理
|
||||
- 系统设置
|
||||
- Banner管理
|
||||
- 单页管理
|
||||
- 等等...
|
||||
|
||||
### 操作动作建议:
|
||||
- 添加 / 创建
|
||||
- 编辑 / 更新
|
||||
- 删除
|
||||
- 查询 / 查看
|
||||
- 登录
|
||||
- 登出
|
||||
- 上传
|
||||
- 下载
|
||||
- 导出
|
||||
- 导入
|
||||
- 重命名
|
||||
- 移动
|
||||
- 修改密码
|
||||
- 等等...
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
1. **敏感信息过滤**:工具类会自动过滤密码、token 等敏感信息
|
||||
2. **性能影响**:操作日志记录是异步的,不会影响主业务流程
|
||||
3. **错误处理**:即使日志记录失败,也不会影响业务逻辑的执行
|
||||
4. **执行时间**:建议使用 `microtime(true)` 来精确计算执行时间
|
||||
5. **按需记录**:只在需要记录的操作中添加日志代码,避免过度记录
|
||||
|
||||
## 6. 前端查看
|
||||
|
||||
操作日志可以在后台管理系统的"系统管理 -> 操作日志"页面查看,支持:
|
||||
- 关键词搜索
|
||||
- 模块筛选
|
||||
- 操作动作筛选
|
||||
- 状态筛选
|
||||
- 时间范围筛选
|
||||
- 查看详情
|
||||
- 删除和批量删除
|
||||
|
||||
## 7. 总结
|
||||
|
||||
**使用方式**:
|
||||
- ✅ **推荐使用** `$this->logOperation()` 方法(BaseController 提供)
|
||||
- ✅ **或者使用** `OperationLogHelper::log()` 静态方法
|
||||
|
||||
**使用场景**:
|
||||
- 重要的增删改操作
|
||||
- 需要审计的操作
|
||||
- 需要追踪问题的操作
|
||||
|
||||
**不需要记录**:
|
||||
- 简单的查询操作
|
||||
- 频繁的操作(如心跳检测)
|
||||
- 不重要的操作
|
||||
211
docs/operation_log_usage_simple.md
Normal file
@ -0,0 +1,211 @@
|
||||
# 操作日志使用说明(简化版)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 最简单的使用方式
|
||||
|
||||
```php
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文件管理', '创建文件分组', ['id' => $id]);
|
||||
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '创建文件分组', $e->getMessage());
|
||||
```
|
||||
|
||||
就这么简单!只需要两行代码。
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例1:创建文件分组
|
||||
|
||||
```php
|
||||
public function createFileCate()
|
||||
{
|
||||
try {
|
||||
$data = Request::param();
|
||||
$data['create_time'] = date('Y-m-d H:i:s');
|
||||
$id = FilesCategory::insertGetId($data);
|
||||
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文件管理', '创建文件分组', ['id' => $id]);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '新建文件分组成功',
|
||||
'data' => ['id' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '创建文件分组', $e->getMessage());
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '新建文件分组失败: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2:删除文件
|
||||
|
||||
```php
|
||||
public function deleteFile($id)
|
||||
{
|
||||
try {
|
||||
Files::where('id', $id)->delete();
|
||||
|
||||
// 记录成功日志
|
||||
$this->logSuccess('文件管理', '删除文件', ['id' => $id]);
|
||||
|
||||
return json(['code' => 200, 'msg' => '删除成功']);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('文件管理', '删除文件', $e->getMessage());
|
||||
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:添加用户
|
||||
|
||||
```php
|
||||
public function addUser()
|
||||
{
|
||||
try {
|
||||
$data = Request::param();
|
||||
$userId = AdminUser::insertGetId($data);
|
||||
|
||||
// 记录成功日志
|
||||
$this->logSuccess('用户管理', '添加用户', ['id' => $userId]);
|
||||
|
||||
return json(['code' => 200, 'msg' => '添加成功']);
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败日志
|
||||
$this->logFail('用户管理', '添加用户', $e->getMessage());
|
||||
|
||||
return json(['code' => 500, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 方法说明
|
||||
|
||||
### BaseController 提供的方法
|
||||
|
||||
所有继承 `BaseController` 的控制器都可以使用以下方法:
|
||||
|
||||
#### 1. `logSuccess()` - 记录成功日志
|
||||
|
||||
```php
|
||||
$this->logSuccess(string $module, string $action, array $responseData = [])
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `$module`: 模块名称(如:文件管理、用户管理等)
|
||||
- `$action`: 操作名称(如:创建文件分组、删除文件等)
|
||||
- `$responseData`: 响应数据(可选,如:`['id' => 123]`)
|
||||
|
||||
**示例:**
|
||||
```php
|
||||
$this->logSuccess('文件管理', '创建文件分组', ['id' => $id]);
|
||||
```
|
||||
|
||||
#### 2. `logFail()` - 记录失败日志
|
||||
|
||||
```php
|
||||
$this->logFail(string $module, string $action, string $errorMessage)
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `$module`: 模块名称
|
||||
- `$action`: 操作名称
|
||||
- `$errorMessage`: 错误信息
|
||||
|
||||
**示例:**
|
||||
```php
|
||||
$this->logFail('文件管理', '创建文件分组', $e->getMessage());
|
||||
```
|
||||
|
||||
#### 3. `logOperation()` - 完整版(需要更多控制时使用)
|
||||
|
||||
```php
|
||||
$this->logOperation(
|
||||
string $module,
|
||||
string $action,
|
||||
array $requestData = [],
|
||||
array $responseData = [],
|
||||
int $status = 1,
|
||||
string $errorMessage = ''
|
||||
)
|
||||
```
|
||||
|
||||
## 直接使用 OperationLogger 类
|
||||
|
||||
如果不继承 BaseController,可以直接使用:
|
||||
|
||||
```php
|
||||
use app\admin\common\OperationLogger;
|
||||
|
||||
// 记录成功
|
||||
OperationLogger::success('文件管理', '创建文件分组', ['id' => $id]);
|
||||
|
||||
// 记录失败
|
||||
OperationLogger::fail('文件管理', '创建文件分组', $e->getMessage());
|
||||
|
||||
// 完整版
|
||||
OperationLogger::record(
|
||||
'文件管理',
|
||||
'创建文件分组',
|
||||
Request::param(), // 请求数据(可选)
|
||||
['id' => $id], // 响应数据(可选)
|
||||
1, // 状态:1-成功,0-失败
|
||||
'' // 错误信息(可选)
|
||||
);
|
||||
```
|
||||
|
||||
## 模块和操作命名建议
|
||||
|
||||
### 模块名称:
|
||||
- 文件管理
|
||||
- 用户管理
|
||||
- 角色管理
|
||||
- 文章管理
|
||||
- 菜单管理
|
||||
- Banner管理
|
||||
- 单页管理
|
||||
- 系统设置
|
||||
|
||||
### 操作名称:
|
||||
- 创建文件分组
|
||||
- 重命名文件分组
|
||||
- 删除文件分组
|
||||
- 上传文件
|
||||
- 删除文件
|
||||
- 移动文件
|
||||
- 添加用户
|
||||
- 编辑用户
|
||||
- 删除用户
|
||||
- 修改密码
|
||||
- 等等...
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **自动获取**:请求数据会自动获取,无需手动传递
|
||||
2. **敏感信息过滤**:密码、token 等敏感信息会自动过滤
|
||||
3. **错误处理**:日志记录失败不会影响主业务流程
|
||||
4. **按需记录**:只在需要记录的操作中添加日志代码
|
||||
|
||||
## 总结
|
||||
|
||||
**推荐使用方式:**
|
||||
```php
|
||||
// 成功时
|
||||
$this->logSuccess('模块名称', '操作名称', ['响应数据']);
|
||||
|
||||
// 失败时
|
||||
$this->logFail('模块名称', '操作名称', '错误信息');
|
||||
```
|
||||
|
||||
就这么简单!
|
||||
|
||||
2
extend/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
14
public/.htaccess
Normal file
@ -0,0 +1,14 @@
|
||||
# Apache/.htaccess CORS 配置
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# 处理 OPTIONS 预检请求
|
||||
RewriteCond %{REQUEST_METHOD} OPTIONS
|
||||
RewriteRule ^(.*)$ $1 [R=204,L]
|
||||
|
||||
# 添加 CORS 头
|
||||
Header set Access-Control-Allow-Origin "http://localhost:5173"
|
||||
Header set Access-Control-Allow-Credentials "true"
|
||||
Header set Access-Control-Allow-Methods "GET, POST, PATCH, PUT, DELETE, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With"
|
||||
</IfModule>
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
24
public/index.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2019 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
// [ 应用入口文件 ]
|
||||
namespace think;
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// 执行HTTP应用并响应
|
||||
$http = (new App())->http;
|
||||
|
||||
$response = $http->run();
|
||||
|
||||
$response->send();
|
||||
|
||||
$http->end($response);
|
||||
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
19
public/router.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
// $Id$
|
||||
|
||||
if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) {
|
||||
return false;
|
||||
} else {
|
||||
$_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php';
|
||||
|
||||
require __DIR__ . "/index.php";
|
||||
}
|
||||
2
public/static/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
BIN
public/storage/uploads/2026/01/09/696112be534f8.png
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
public/storage/uploads/2026/01/10/6961a51ab7a02.jpeg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/storage/uploads/2026/01/10/6961a586a4e4f.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
public/storage/uploads/2026/01/10/6961a58e1a3e5.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
public/storage/uploads/2026/01/10/6961a634e0e47.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
public/storage/uploads/2026/01/10/6961a6499a336.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/storage/uploads/2026/01/10/6961a67693672.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
public/storage/uploads/2026/01/10/6961a6a3730a6.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/storage/uploads/2026/01/10/6961bfe3e047b.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/storage/uploads/2026/01/10/6961bff803efd.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
public/storage/uploads/2026/01/10/6961c003d1625.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
public/storage/uploads/2026/01/10/6961c1a1dbfe2.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
public/storage/uploads/2026/01/10/6961c1b71fb6a.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/storage/uploads/2026/01/10/6961c2be380f1.png
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
public/storage/uploads/2026/01/10/6961c2ce76d2e.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
public/storage/uploads/2026/01/10/6961c2db37c1f.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
public/storage/uploads/2026/01/10/6961c2ef3bb57.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
public/storage/uploads/2026/01/10/6961c30a1c109.png
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
public/storage/uploads/2026/01/10/6961c310ccbc0.png
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
public/storage/uploads/2026/01/10/6961c3b7c9f0a.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/storage/uploads/2026/01/10/6961c3c4875c1.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/storage/uploads/2026/01/10/6961c3d348300.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
public/storage/uploads/2026/01/10/6961f55722b67.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/storage/uploads/2026/01/10/6961f57a6d8f1.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
public/storage/uploads/2026/01/10/6961f59f3ec8e.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
public/storage/uploads/2026/01/10/6961f5bdf1abe.png
Normal file
|
After Width: | Height: | Size: 211 KiB |