更新
This commit is contained in:
-1
Submodule backend deleted from 49917ae7c0
-1
Submodule backend-tp deleted from 49917ae7c0
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
*.log
|
||||||
|
.env
|
||||||
|
composer.phar
|
||||||
|
composer.lock
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
/vendor
|
||||||
|
/.settings
|
||||||
|
/.buildpath
|
||||||
|
/.project
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
deny from all
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
use think\Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用服务类
|
||||||
|
*/
|
||||||
|
class AppService extends Service
|
||||||
|
{
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
// 服务注册
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
// 服务启动
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
use think\App;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制器基础类
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
use think\db\exception\DataNotFoundException;
|
||||||
|
use think\db\exception\ModelNotFoundException;
|
||||||
|
use think\exception\Handle;
|
||||||
|
use think\exception\HttpException;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\Response;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用异常处理类
|
||||||
|
*/
|
||||||
|
class ExceptionHandle extends Handle
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 不需要记录信息(日志)的异常类列表
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $ignoreReport = [
|
||||||
|
HttpException::class,
|
||||||
|
HttpResponseException::class,
|
||||||
|
ModelNotFoundException::class,
|
||||||
|
DataNotFoundException::class,
|
||||||
|
ValidateException::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录异常信息(包括日志或者其它方式记录)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param Throwable $exception
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function report(Throwable $exception): void
|
||||||
|
{
|
||||||
|
// 使用内置的方式记录异常日志
|
||||||
|
parent::report($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an exception into an HTTP response.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param \think\Request $request
|
||||||
|
* @param Throwable $e
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function render($request, Throwable $e): Response
|
||||||
|
{
|
||||||
|
// 添加自定义异常处理机制
|
||||||
|
|
||||||
|
// 其他错误交给系统处理
|
||||||
|
return parent::render($request, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
// 应用请求对象类
|
||||||
|
class Request extends \think\Request
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
// 应用公共文件
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller;
|
||||||
|
|
||||||
|
use app\BaseController;
|
||||||
|
|
||||||
|
class Index extends BaseController
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return '<style>*{ padding: 0; margin: 0; }</style><iframe src="https://www.thinkphp.cn/welcome?version=' . \think\facade\App::version() . '" width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hello($name = 'ThinkPHP8')
|
||||||
|
{
|
||||||
|
return 'hello,' . $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
// 事件定义文件
|
||||||
|
return [
|
||||||
|
'bind' => [
|
||||||
|
],
|
||||||
|
|
||||||
|
'listen' => [
|
||||||
|
'AppInit' => [],
|
||||||
|
'HttpRun' => [],
|
||||||
|
'HttpEnd' => [],
|
||||||
|
'LogLevel' => [],
|
||||||
|
'LogWrite' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'subscribe' => [
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
// 全局中间件定义文件
|
||||||
|
return [
|
||||||
|
// 全局请求缓存
|
||||||
|
// \think\middleware\CheckRequestCache::class,
|
||||||
|
// 多语言加载
|
||||||
|
// \think\middleware\LoadLangPack::class,
|
||||||
|
// Session初始化
|
||||||
|
// \think\middleware\SessionInit::class
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
use app\ExceptionHandle;
|
||||||
|
use app\Request;
|
||||||
|
|
||||||
|
// 容器Provider定义文件
|
||||||
|
return [
|
||||||
|
'think\Request' => Request::class,
|
||||||
|
'think\exception\Handle' => ExceptionHandle::class,
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use app\AppService;
|
||||||
|
|
||||||
|
// 系统服务定义文件
|
||||||
|
// 服务在完成全局初始化之后执行
|
||||||
|
return [
|
||||||
|
AppService::class,
|
||||||
|
];
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repositories": [{
|
||||||
|
"name": "packagist",
|
||||||
|
"type": "composer",
|
||||||
|
"url": "https://repo.packagist.org"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 应用设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 应用的命名空间
|
||||||
|
'app_namespace' => '',
|
||||||
|
// 是否启用路由
|
||||||
|
'with_route' => true,
|
||||||
|
// 默认应用
|
||||||
|
'default_app' => 'index',
|
||||||
|
// 默认时区
|
||||||
|
'default_timezone' => 'Asia/Shanghai',
|
||||||
|
|
||||||
|
// 应用映射(自动多应用模式有效)
|
||||||
|
'app_map' => [],
|
||||||
|
// 域名绑定(自动多应用模式有效)
|
||||||
|
'domain_bind' => [],
|
||||||
|
// 禁止URL访问的应用列表(自动多应用模式有效)
|
||||||
|
'deny_app_list' => [],
|
||||||
|
|
||||||
|
// 异常页面的模板文件
|
||||||
|
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
|
||||||
|
|
||||||
|
// 错误显示信息,非调试模式有效
|
||||||
|
'error_message' => '页面错误!请稍后再试~',
|
||||||
|
// 显示错误信息
|
||||||
|
'show_error_msg' => false,
|
||||||
|
];
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 缓存设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 默认缓存驱动
|
||||||
|
'default' => 'file',
|
||||||
|
|
||||||
|
// 缓存连接方式配置
|
||||||
|
'stores' => [
|
||||||
|
'file' => [
|
||||||
|
// 驱动方式
|
||||||
|
'type' => 'File',
|
||||||
|
// 缓存保存目录
|
||||||
|
'path' => '',
|
||||||
|
// 缓存前缀
|
||||||
|
'prefix' => '',
|
||||||
|
// 缓存有效期 0表示永久缓存
|
||||||
|
'expire' => 0,
|
||||||
|
// 缓存标签前缀
|
||||||
|
'tag_prefix' => 'tag:',
|
||||||
|
// 序列化机制 例如 ['serialize', 'unserialize']
|
||||||
|
'serialize' => [],
|
||||||
|
],
|
||||||
|
// 更多的缓存连接
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 控制台配置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
return [
|
||||||
|
// 指令定义
|
||||||
|
'commands' => [
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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' => '',
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
],
|
||||||
|
|
||||||
|
// 更多的数据库配置信息
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
],
|
||||||
|
// 更多的磁盘配置信息
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
],
|
||||||
|
// 其它日志通道配置
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
// 中间件配置
|
||||||
|
return [
|
||||||
|
// 别名或分组
|
||||||
|
'alias' => [],
|
||||||
|
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
|
||||||
|
'priority' => [],
|
||||||
|
];
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 路由设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// pathinfo分隔符
|
||||||
|
'pathinfo_depr' => '/',
|
||||||
|
// 是否开启路由延迟解析
|
||||||
|
'url_lazy_route' => false,
|
||||||
|
// 是否强制使用路由
|
||||||
|
'url_route_must' => false,
|
||||||
|
// 是否区分大小写
|
||||||
|
'url_case_sensitive' => false,
|
||||||
|
// 自动扫描子目录分组
|
||||||
|
'route_auto_group' => false,
|
||||||
|
// 合并路由规则
|
||||||
|
'route_rule_merge' => false,
|
||||||
|
// 路由是否完全匹配
|
||||||
|
'route_complete_match' => false,
|
||||||
|
// 去除斜杠
|
||||||
|
'remove_slash' => false,
|
||||||
|
// 默认的路由变量规则
|
||||||
|
'default_route_pattern' => '[\w\.]+',
|
||||||
|
// URL伪静态后缀
|
||||||
|
'url_html_suffix' => 'html',
|
||||||
|
// 访问控制器层名称
|
||||||
|
'controller_layer' => 'controller',
|
||||||
|
// 空控制器名
|
||||||
|
'empty_controller' => 'Error',
|
||||||
|
// 是否使用控制器后缀
|
||||||
|
'controller_suffix' => false,
|
||||||
|
// 默认模块名(开启自动多模块有效)
|
||||||
|
'default_module' => 'index',
|
||||||
|
// 默认控制器名
|
||||||
|
'default_controller' => 'Index',
|
||||||
|
// 默认操作名
|
||||||
|
'default_action' => 'index',
|
||||||
|
// 操作方法后缀
|
||||||
|
'action_suffix' => '',
|
||||||
|
// 非路由变量是否使用普通参数方式(用于URL生成)
|
||||||
|
'url_common_param' => true,
|
||||||
|
// 操作方法的参数绑定方式 route get param
|
||||||
|
'action_bind_param' => 'get',
|
||||||
|
// 请求缓存规则 true为自动规则
|
||||||
|
'request_cache_key' => true,
|
||||||
|
// 请求缓存有效期
|
||||||
|
'request_cache_expire' => null,
|
||||||
|
// 全局请求缓存排除规则
|
||||||
|
'request_cache_except' => [],
|
||||||
|
// 请求缓存的Tag
|
||||||
|
'request_cache_tag' => '',
|
||||||
|
// API版本header变量
|
||||||
|
'api_version' => 'Api-Version',
|
||||||
|
];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 会话设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// session name
|
||||||
|
'name' => 'PHPSESSID',
|
||||||
|
// SESSION_ID的提交变量,解决flash上传跨域
|
||||||
|
'var_session_id' => '',
|
||||||
|
// 驱动方式 支持file cache
|
||||||
|
'type' => 'file',
|
||||||
|
// 存储连接标识 当type使用cache的时候有效
|
||||||
|
'store' => null,
|
||||||
|
// 过期时间
|
||||||
|
'expire' => 1440,
|
||||||
|
// 前缀
|
||||||
|
'prefix' => '',
|
||||||
|
];
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | Trace设置 开启调试模式后有效
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
return [
|
||||||
|
// 内置Html和Console两种方式 支持扩展
|
||||||
|
'type' => 'Html',
|
||||||
|
// 读取的日志通道名
|
||||||
|
'channel' => '',
|
||||||
|
];
|
||||||
@@ -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' => '}',
|
||||||
|
];
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
Options +FollowSymlinks -Multiviews
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
|
||||||
|
</IfModule>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
<?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>
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
// [ 应用入口文件 ]
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// 执行HTTP应用并响应
|
||||||
|
$http = (new App())->http;
|
||||||
|
|
||||||
|
$response = $http->run();
|
||||||
|
|
||||||
|
$response->send();
|
||||||
|
|
||||||
|
$http->end($response);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?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>
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
use think\facade\Route;
|
||||||
|
|
||||||
|
Route::get('think', function () {
|
||||||
|
return 'hello,ThinkPHP8!';
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get('hello/:name', 'index/hello');
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use think\App;
|
||||||
|
|
||||||
|
// 命令行入口文件
|
||||||
|
// 加载基础文件
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// 应用初始化
|
||||||
|
(new App())->console->run();
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
如果不使用模板,可以删除该目录
|
||||||
-1
Submodule backend-tp8 deleted from 49917ae7c0
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
*.log
|
||||||
|
.env
|
||||||
|
composer.phar
|
||||||
|
composer.lock
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
/vendor
|
||||||
|
/.settings
|
||||||
|
/.buildpath
|
||||||
|
/.project
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
deny from all
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
use think\Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用服务类
|
||||||
|
*/
|
||||||
|
class AppService extends Service
|
||||||
|
{
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
// 服务注册
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
// 服务启动
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
declare (strict_types = 1);
|
||||||
|
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
use think\App;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制器基础类
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
use think\db\exception\DataNotFoundException;
|
||||||
|
use think\db\exception\ModelNotFoundException;
|
||||||
|
use think\exception\Handle;
|
||||||
|
use think\exception\HttpException;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\Response;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用异常处理类
|
||||||
|
*/
|
||||||
|
class ExceptionHandle extends Handle
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 不需要记录信息(日志)的异常类列表
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $ignoreReport = [
|
||||||
|
HttpException::class,
|
||||||
|
HttpResponseException::class,
|
||||||
|
ModelNotFoundException::class,
|
||||||
|
DataNotFoundException::class,
|
||||||
|
ValidateException::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录异常信息(包括日志或者其它方式记录)
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param Throwable $exception
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function report(Throwable $exception): void
|
||||||
|
{
|
||||||
|
// 使用内置的方式记录异常日志
|
||||||
|
parent::report($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an exception into an HTTP response.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param \think\Request $request
|
||||||
|
* @param Throwable $e
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function render($request, Throwable $e): Response
|
||||||
|
{
|
||||||
|
// 添加自定义异常处理机制
|
||||||
|
|
||||||
|
// 其他错误交给系统处理
|
||||||
|
return parent::render($request, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace app;
|
||||||
|
|
||||||
|
// 应用请求对象类
|
||||||
|
class Request extends \think\Request
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
// 应用公共文件
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller;
|
||||||
|
|
||||||
|
use app\BaseController;
|
||||||
|
|
||||||
|
class Index extends BaseController
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return '<style>*{ padding: 0; margin: 0; }</style><iframe src="https://www.thinkphp.cn/welcome?version=' . \think\facade\App::version() . '" width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hello($name = 'ThinkPHP8')
|
||||||
|
{
|
||||||
|
return 'hello,' . $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\model\AiModel;
|
||||||
|
use app\model\Conversation as ConversationModel;
|
||||||
|
use app\model\MembershipLevel;
|
||||||
|
use app\model\Message;
|
||||||
|
use app\model\SystemSetting;
|
||||||
|
use app\model\User;
|
||||||
|
use app\model\UserDailyStat;
|
||||||
|
use app\service\SettingsService;
|
||||||
|
|
||||||
|
class Admin extends BaseApi
|
||||||
|
{
|
||||||
|
public function users()
|
||||||
|
{
|
||||||
|
$page = max(1, (int) $this->request->get('page', 1));
|
||||||
|
$limit = min(50, max(1, (int) $this->request->get('limit', 20)));
|
||||||
|
|
||||||
|
$query = User::alias('u')
|
||||||
|
->leftJoin('membership_levels ml', 'u.membership_level_id = ml.id')
|
||||||
|
->field('u.id,u.username,u.email,u.nickname,u.role,u.status,u.membership_level_id,u.created_at,u.last_login_at,ml.name as membership_name')
|
||||||
|
->order('u.id', 'desc');
|
||||||
|
|
||||||
|
$total = (clone $query)->count();
|
||||||
|
$list = $query->page($page, $limit)->select();
|
||||||
|
|
||||||
|
return $this->success(['list' => $list, 'total' => $total]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateUser($id)
|
||||||
|
{
|
||||||
|
$input = $this->request->put();
|
||||||
|
$allowed = ['nickname', 'role', 'status', 'membership_level_id'];
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($allowed as $field) {
|
||||||
|
if (array_key_exists($field, $input)) {
|
||||||
|
$data[$field] = $input[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return $this->error('无更新内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
User::where('id', $id)->update($data);
|
||||||
|
return $this->success(null, '更新成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function conversations()
|
||||||
|
{
|
||||||
|
$page = max(1, (int) $this->request->get('page', 1));
|
||||||
|
$limit = min(50, max(1, (int) $this->request->get('limit', 20)));
|
||||||
|
$userId = $this->request->get('user_id');
|
||||||
|
|
||||||
|
$query = ConversationModel::alias('c')
|
||||||
|
->join('users u', 'c.user_id = u.id')
|
||||||
|
->whereNull('c.deleted_at')
|
||||||
|
->field('c.*,u.username,u.email')
|
||||||
|
->order('c.updated_at', 'desc');
|
||||||
|
|
||||||
|
if ($userId) {
|
||||||
|
$query->where('c.user_id', $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($query->page($page, $limit)->select());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stats()
|
||||||
|
{
|
||||||
|
return $this->success([
|
||||||
|
'users' => User::count(),
|
||||||
|
'conversations' => ConversationModel::whereNull('deleted_at')->count(),
|
||||||
|
'messages' => Message::count(),
|
||||||
|
'today_messages' => (int) UserDailyStat::where('stat_date', date('Y-m-d'))->sum('message_count'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function settings()
|
||||||
|
{
|
||||||
|
$rows = SystemSetting::select();
|
||||||
|
$settings = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$decoded = json_decode($row->setting_value, true);
|
||||||
|
$settings[$row->setting_key] = [
|
||||||
|
'value' => json_last_error() === JSON_ERROR_NONE ? $decoded : $row->setting_value,
|
||||||
|
'description' => $row->description,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateSettings()
|
||||||
|
{
|
||||||
|
$input = $this->request->put();
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
SettingsService::set($key, $value);
|
||||||
|
}
|
||||||
|
return $this->success(null, '设置已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function models()
|
||||||
|
{
|
||||||
|
$list = AiModel::field('id,name,model_id,api_base_url,max_tokens,temperature,is_default,enabled,sort_order')
|
||||||
|
->order('sort_order')
|
||||||
|
->select();
|
||||||
|
return $this->success($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createModel()
|
||||||
|
{
|
||||||
|
$input = $this->request->post();
|
||||||
|
$model = AiModel::create([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'model_id' => $input['model_id'],
|
||||||
|
'api_base_url' => $input['api_base_url'] ?? 'https://api.openai.com/v1',
|
||||||
|
'api_key' => $input['api_key'],
|
||||||
|
'max_tokens' => $input['max_tokens'] ?? 4096,
|
||||||
|
'temperature' => $input['temperature'] ?? 0.7,
|
||||||
|
'is_default' => (int) ($input['is_default'] ?? 0),
|
||||||
|
'enabled' => (int) ($input['enabled'] ?? 1),
|
||||||
|
'sort_order' => $input['sort_order'] ?? 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($input['is_default'])) {
|
||||||
|
AiModel::where('id', '<>', $model->id)->update(['is_default' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(['id' => $model->id], '创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateModel($id)
|
||||||
|
{
|
||||||
|
$input = $this->request->put();
|
||||||
|
$allowed = ['name', 'model_id', 'api_base_url', 'api_key', 'max_tokens', 'temperature', 'is_default', 'enabled', 'sort_order'];
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($allowed as $field) {
|
||||||
|
if (array_key_exists($field, $input)) {
|
||||||
|
$data[$field] = $input[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return $this->error('无更新内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
AiModel::where('id', $id)->update($data);
|
||||||
|
|
||||||
|
if (!empty($input['is_default'])) {
|
||||||
|
AiModel::where('id', '<>', $id)->update(['is_default' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success(null, '更新成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteModel($id)
|
||||||
|
{
|
||||||
|
AiModel::destroy($id);
|
||||||
|
return $this->success(null, '删除成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function memberships()
|
||||||
|
{
|
||||||
|
$levels = MembershipLevel::order('sort_order')->select();
|
||||||
|
return $this->success($levels);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateMembership($id)
|
||||||
|
{
|
||||||
|
$input = $this->request->put();
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach (['name', 'max_conversations', 'max_messages_per_day', 'max_upload_size_mb', 'sort_order'] as $field) {
|
||||||
|
if (isset($input[$field])) {
|
||||||
|
$data[$field] = $input[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($input['permissions'])) {
|
||||||
|
$data['permissions'] = $input['permissions'];
|
||||||
|
}
|
||||||
|
if (isset($input['allowed_models'])) {
|
||||||
|
$data['allowed_models'] = $input['allowed_models'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return $this->error('无更新内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
MembershipLevel::where('id', $id)->update($data);
|
||||||
|
return $this->success(null, '更新成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\model\User;
|
||||||
|
use app\service\JwtService;
|
||||||
|
use app\service\SettingsService;
|
||||||
|
|
||||||
|
class Auth extends BaseApi
|
||||||
|
{
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
$allow = SettingsService::get('allow_register', true);
|
||||||
|
if ($allow !== true && $allow !== 'true') {
|
||||||
|
return $this->error('当前不允许注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = $this->request->post();
|
||||||
|
$username = trim($input['username'] ?? '');
|
||||||
|
$email = trim($input['email'] ?? '');
|
||||||
|
$password = $input['password'] ?? '';
|
||||||
|
|
||||||
|
if (strlen($username) < 3 || strlen($username) > 50) {
|
||||||
|
return $this->error('用户名长度需 3-50 个字符');
|
||||||
|
}
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
return $this->error('邮箱格式不正确');
|
||||||
|
}
|
||||||
|
if (strlen($password) < 6) {
|
||||||
|
return $this->error('密码至少 6 位');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (User::where('username', $username)->whereOr('email', $email)->find()) {
|
||||||
|
return $this->error('用户名或邮箱已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'username' => $username,
|
||||||
|
'email' => $email,
|
||||||
|
'password_hash' => password_hash($password, PASSWORD_BCRYPT),
|
||||||
|
'nickname' => $username,
|
||||||
|
'membership_level_id' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = JwtService::generateToken(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'token' => $token,
|
||||||
|
'user' => $this->formatUser($user->id),
|
||||||
|
], '注册成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login()
|
||||||
|
{
|
||||||
|
$input = $this->request->post();
|
||||||
|
$account = trim($input['account'] ?? '');
|
||||||
|
$password = $input['password'] ?? '';
|
||||||
|
|
||||||
|
if (!$account || !$password) {
|
||||||
|
return $this->error('请输入账号和密码');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::where(function ($query) use ($account) {
|
||||||
|
$query->where('username', $account)->whereOr('email', $account);
|
||||||
|
})->where('status', 'active')->find();
|
||||||
|
|
||||||
|
if (!$user || !password_verify($password, $user->password_hash)) {
|
||||||
|
return $this->error('账号或密码错误', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->save(['last_login_at' => date('Y-m-d H:i:s')]);
|
||||||
|
$token = JwtService::generateToken(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'token' => $token,
|
||||||
|
'user' => $this->formatUser($user->id),
|
||||||
|
], '登录成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function me()
|
||||||
|
{
|
||||||
|
return $this->success($this->authUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateProfile()
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
$nickname = trim($this->request->put('nickname', ''));
|
||||||
|
|
||||||
|
if ($nickname) {
|
||||||
|
User::where('id', $user['id'])->update(['nickname' => $nickname]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($this->formatUser($user['id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
return $this->success(null, '已退出');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatUser(int $userId): array
|
||||||
|
{
|
||||||
|
$user = User::with(['membership'])->find($userId);
|
||||||
|
$level = $user->membership;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $user->id,
|
||||||
|
'username' => $user->username,
|
||||||
|
'email' => $user->email,
|
||||||
|
'nickname' => $user->nickname,
|
||||||
|
'avatar' => $user->avatar,
|
||||||
|
'role' => $user->role,
|
||||||
|
'membership_name' => $level?->name,
|
||||||
|
'membership_slug' => $level?->slug,
|
||||||
|
'max_conversations' => $level?->max_conversations,
|
||||||
|
'max_messages_per_day' => $level?->max_messages_per_day,
|
||||||
|
'membership_permissions' => $level?->permissions ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\BaseController;
|
||||||
|
use think\response\Json;
|
||||||
|
|
||||||
|
class BaseApi extends BaseController
|
||||||
|
{
|
||||||
|
protected function success(mixed $data = null, string $message = 'success', int $httpCode = 200): Json
|
||||||
|
{
|
||||||
|
return json([
|
||||||
|
'code' => 0,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => $data,
|
||||||
|
], $httpCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function error(string $message, int $httpCode = 400, int $code = 1): Json
|
||||||
|
{
|
||||||
|
return json([
|
||||||
|
'code' => $code,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => null,
|
||||||
|
], $httpCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function authUser(): array
|
||||||
|
{
|
||||||
|
return $this->request->authUser ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\model\Conversation as ConversationModel;
|
||||||
|
use app\model\Message;
|
||||||
|
use app\service\OpenAIService;
|
||||||
|
use app\service\PermissionService;
|
||||||
|
use app\service\SettingsService;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
|
||||||
|
class Chat extends BaseApi
|
||||||
|
{
|
||||||
|
public function completions()
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
PermissionService::checkDailyLimit($user);
|
||||||
|
|
||||||
|
$input = $this->request->post();
|
||||||
|
$conversationId = (int) ($input['conversation_id'] ?? 0);
|
||||||
|
$content = trim($input['content'] ?? '');
|
||||||
|
$attachments = $input['attachments'] ?? [];
|
||||||
|
$stream = ($input['stream'] ?? true) !== false;
|
||||||
|
|
||||||
|
if (!$conversationId) {
|
||||||
|
return $this->error('缺少 conversation_id');
|
||||||
|
}
|
||||||
|
if (!$content && empty($attachments)) {
|
||||||
|
return $this->error('消息内容不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation = ConversationModel::where('id', $conversationId)
|
||||||
|
->where('user_id', $user['id'])
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
return $this->error('会话不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentType = SettingsService::isFeatureEnabled('markdown') ? 'markdown' : 'text';
|
||||||
|
if (!empty($attachments)) {
|
||||||
|
$contentType = 'mixed';
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $content,
|
||||||
|
'content_type' => $contentType,
|
||||||
|
'attachments' => $attachments,
|
||||||
|
]);
|
||||||
|
|
||||||
|
PermissionService::incrementDailyCount((int) $user['id']);
|
||||||
|
|
||||||
|
$history = Message::where('conversation_id', $conversationId)
|
||||||
|
->field('role,content,attachments')
|
||||||
|
->order('created_at', 'asc')
|
||||||
|
->limit(50)
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$apiMessages = $this->buildApiMessages($history);
|
||||||
|
$model = OpenAIService::getModel($conversation->model_id ? (int) $conversation->model_id : null);
|
||||||
|
|
||||||
|
if ((int) $conversation->message_count === 0 && $content) {
|
||||||
|
$conversation->save(['title' => mb_substr($content, 0, 30)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation->inc('message_count')->update(['updated_at' => date('Y-m-d H:i:s')]);
|
||||||
|
|
||||||
|
if ($stream) {
|
||||||
|
$this->streamResponse($conversationId, $model, $apiMessages, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->syncResponse($conversationId, $model, $apiMessages, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function streamResponse(int $conversationId, $model, array $apiMessages, array $user): void
|
||||||
|
{
|
||||||
|
OpenAIService::sseHeaders();
|
||||||
|
$fullContent = '';
|
||||||
|
|
||||||
|
OpenAIService::streamChat($model, $apiMessages, function ($chunk) use (&$fullContent) {
|
||||||
|
$delta = $chunk['choices'][0]['delta']['content'] ?? '';
|
||||||
|
if ($delta) {
|
||||||
|
$fullContent .= $delta;
|
||||||
|
OpenAIService::sseEvent('message', ['content' => $delta]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
'role' => 'assistant',
|
||||||
|
'content' => $fullContent,
|
||||||
|
'content_type' => 'markdown',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ConversationModel::where('id', $conversationId)->inc('message_count')->update([
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
PermissionService::incrementDailyCount((int) $user['id']);
|
||||||
|
OpenAIService::sseEvent('done', ['content' => $fullContent]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncResponse(int $conversationId, $model, array $apiMessages, array $user)
|
||||||
|
{
|
||||||
|
$result = OpenAIService::chat($model, $apiMessages);
|
||||||
|
$content = $result['choices'][0]['message']['content'] ?? '';
|
||||||
|
$tokens = $result['usage']['total_tokens'] ?? 0;
|
||||||
|
|
||||||
|
Message::create([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
'role' => 'assistant',
|
||||||
|
'content' => $content,
|
||||||
|
'content_type' => 'markdown',
|
||||||
|
'tokens_used' => $tokens,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ConversationModel::where('id', $conversationId)->inc('message_count')->update([
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
PermissionService::incrementDailyCount((int) $user['id']);
|
||||||
|
|
||||||
|
return $this->success(['content' => $content, 'tokens' => $tokens]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildApiMessages(array $history): array
|
||||||
|
{
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
foreach ($history as $msg) {
|
||||||
|
$content = $msg['content'];
|
||||||
|
$attachments = $msg['attachments'] ?? [];
|
||||||
|
if (is_string($attachments)) {
|
||||||
|
$attachments = json_decode($attachments, true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($attachments) && SettingsService::isFeatureEnabled('image')) {
|
||||||
|
$parts = [];
|
||||||
|
if ($content) {
|
||||||
|
$parts[] = ['type' => 'text', 'text' => $content];
|
||||||
|
}
|
||||||
|
foreach ($attachments as $att) {
|
||||||
|
if (($att['type'] ?? '') === 'image') {
|
||||||
|
$parts[] = [
|
||||||
|
'type' => 'image_url',
|
||||||
|
'image_url' => ['url' => $this->absoluteUrl($att['url'])],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$messages[] = ['role' => $msg['role'], 'content' => $parts];
|
||||||
|
} else {
|
||||||
|
$messages[] = ['role' => $msg['role'], 'content' => $content];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function absoluteUrl(string $url): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($url, 'http')) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
$scheme = $this->request->scheme();
|
||||||
|
$host = $this->request->host();
|
||||||
|
return "{$scheme}://{$host}{$url}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\model\Conversation as ConversationModel;
|
||||||
|
use app\model\Message;
|
||||||
|
use app\service\PermissionService;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
|
||||||
|
class Conversation extends BaseApi
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
$page = max(1, (int) $this->request->get('page', 1));
|
||||||
|
$limit = min(50, max(1, (int) $this->request->get('limit', 20)));
|
||||||
|
|
||||||
|
$query = ConversationModel::alias('c')
|
||||||
|
->leftJoin('ai_models m', 'c.model_id = m.id')
|
||||||
|
->where('c.user_id', $user['id'])
|
||||||
|
->whereNull('c.deleted_at')
|
||||||
|
->field('c.*,m.name as model_name')
|
||||||
|
->order(['c.is_pinned' => 'desc', 'c.updated_at' => 'desc']);
|
||||||
|
|
||||||
|
$total = (clone $query)->count();
|
||||||
|
$list = $query->page($page, $limit)->select();
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'list' => $list,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
PermissionService::checkConversationLimit($user);
|
||||||
|
|
||||||
|
$input = $this->request->post();
|
||||||
|
$conversation = ConversationModel::create([
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'title' => trim($input['title'] ?? '新对话'),
|
||||||
|
'model_id' => $input['model_id'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success($this->findConversation((int) $conversation->id, (int) $user['id']), '创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
return $this->success($this->findConversation((int) $id, (int) $user['id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id)
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
$conversation = ConversationModel::where('id', $id)
|
||||||
|
->where('user_id', $user['id'])
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
return $this->error('会话不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = $this->request->put();
|
||||||
|
$data = [];
|
||||||
|
if (isset($input['title'])) {
|
||||||
|
$data['title'] = trim($input['title']);
|
||||||
|
}
|
||||||
|
if (isset($input['is_pinned'])) {
|
||||||
|
$data['is_pinned'] = (int) $input['is_pinned'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('model_id', $input)) {
|
||||||
|
$data['model_id'] = $input['model_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return $this->error('无更新内容');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation->save($data);
|
||||||
|
return $this->success($this->findConversation((int) $id, (int) $user['id']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
$conversation = ConversationModel::where('id', $id)
|
||||||
|
->where('user_id', $user['id'])
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
return $this->error('会话不存在', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation->save(['deleted_at' => date('Y-m-d H:i:s')]);
|
||||||
|
return $this->success(null, '删除成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages($id)
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
$this->findConversation((int) $id, (int) $user['id']);
|
||||||
|
|
||||||
|
$messages = Message::where('conversation_id', $id)
|
||||||
|
->field('id,role,content,content_type,attachments,created_at')
|
||||||
|
->order('created_at', 'asc')
|
||||||
|
->select()
|
||||||
|
->each(function ($item) {
|
||||||
|
if (is_string($item->attachments)) {
|
||||||
|
$item->attachments = json_decode($item->attachments, true) ?: [];
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->success($messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findConversation(int $id, int $userId): array
|
||||||
|
{
|
||||||
|
$conversation = ConversationModel::alias('c')
|
||||||
|
->leftJoin('ai_models m', 'c.model_id = m.id')
|
||||||
|
->where('c.id', $id)
|
||||||
|
->where('c.user_id', $userId)
|
||||||
|
->whereNull('c.deleted_at')
|
||||||
|
->field('c.*,m.name as model_name')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$conversation) {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => '会话不存在',
|
||||||
|
'data' => null,
|
||||||
|
], 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conversation->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\model\AiModel;
|
||||||
|
use app\service\SettingsService;
|
||||||
|
|
||||||
|
class Settings extends BaseApi
|
||||||
|
{
|
||||||
|
public function features()
|
||||||
|
{
|
||||||
|
return $this->success(SettingsService::getFeatures());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function publicSettings()
|
||||||
|
{
|
||||||
|
$allow = SettingsService::get('allow_register', true);
|
||||||
|
return $this->success([
|
||||||
|
'site_name' => SettingsService::get('site_name', 'AI Chat'),
|
||||||
|
'allow_register' => $allow === true || $allow === 'true',
|
||||||
|
'features' => SettingsService::getFeatures(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function models()
|
||||||
|
{
|
||||||
|
$list = AiModel::where('enabled', 1)
|
||||||
|
->field('id,name,model_id,is_default')
|
||||||
|
->order('sort_order,id')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
return $this->success($list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\controller\api;
|
||||||
|
|
||||||
|
use app\model\UploadFile;
|
||||||
|
use app\service\PermissionService;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
|
||||||
|
class Upload extends BaseApi
|
||||||
|
{
|
||||||
|
public function upload()
|
||||||
|
{
|
||||||
|
$user = $this->authUser();
|
||||||
|
$file = $this->request->file('file');
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
return $this->error('请选择文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mime = $file->getMime() ?: mime_content_type($file->getPathname());
|
||||||
|
$fileType = $this->detectFileType($mime);
|
||||||
|
|
||||||
|
if (!PermissionService::canUpload($user, $fileType)) {
|
||||||
|
return $this->error('您没有权限上传此类型文件或功能未开启', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxSize = PermissionService::getMaxUploadSizeMb($user) * 1024 * 1024;
|
||||||
|
if ($file->getSize() > $maxSize) {
|
||||||
|
return $this->error('文件大小超出限制');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isAllowedMime($mime, $fileType)) {
|
||||||
|
return $this->error('不支持的文件类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $file->getOriginalName();
|
||||||
|
$fileSize = $file->getSize();
|
||||||
|
|
||||||
|
$uploadPath = config('upload.path');
|
||||||
|
if (!is_dir($uploadPath)) {
|
||||||
|
mkdir($uploadPath, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = $file->extension() ?: 'bin';
|
||||||
|
$subdir = date('Y/m/d');
|
||||||
|
$storedBase = uniqid() . '.' . $ext;
|
||||||
|
$storedName = $subdir . '/' . $storedBase;
|
||||||
|
$fullDir = $uploadPath . '/' . $subdir;
|
||||||
|
|
||||||
|
if (!is_dir($fullDir)) {
|
||||||
|
mkdir($fullDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file->move($fullDir, $storedBase);
|
||||||
|
|
||||||
|
$record = UploadFile::create([
|
||||||
|
'user_id' => $user['id'],
|
||||||
|
'original_name' => $originalName,
|
||||||
|
'stored_name' => $storedBase,
|
||||||
|
'file_path' => $storedName,
|
||||||
|
'mime_type' => $mime,
|
||||||
|
'file_size' => $fileSize,
|
||||||
|
'file_type' => $fileType,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'id' => $record->id,
|
||||||
|
'url' => '/api/uploads/' . urlencode($storedBase),
|
||||||
|
'name' => $originalName,
|
||||||
|
'type' => $fileType,
|
||||||
|
'mime' => $mime,
|
||||||
|
'size' => $fileSize,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serve($filename)
|
||||||
|
{
|
||||||
|
$filename = basename($filename);
|
||||||
|
$upload = UploadFile::where('stored_name', $filename)
|
||||||
|
->whereOr('file_path', 'like', '%/' . $filename)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$upload) {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => '文件不存在',
|
||||||
|
'data' => null,
|
||||||
|
], 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = config('upload.path') . '/' . $upload->file_path;
|
||||||
|
if (!is_file($path)) {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => '文件不存在',
|
||||||
|
'data' => null,
|
||||||
|
], 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
return download($path, $upload->original_name, true)
|
||||||
|
->mimeType($upload->mime_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectFileType(string $mime): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($mime, 'image/')) {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
if (str_starts_with($mime, 'video/')) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
if (str_starts_with($mime, 'audio/')) {
|
||||||
|
return 'audio';
|
||||||
|
}
|
||||||
|
if (in_array($mime, config('upload.allowed_documents'), true)) {
|
||||||
|
return 'document';
|
||||||
|
}
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAllowedMime(string $mime, string $fileType): bool
|
||||||
|
{
|
||||||
|
$config = config('upload');
|
||||||
|
return match ($fileType) {
|
||||||
|
'image' => in_array($mime, $config['allowed_images'], true),
|
||||||
|
'video' => in_array($mime, $config['allowed_videos'], true),
|
||||||
|
'audio' => in_array($mime, $config['allowed_audios'], true),
|
||||||
|
'document' => in_array($mime, $config['allowed_documents'], true),
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
// 事件定义文件
|
||||||
|
return [
|
||||||
|
'bind' => [
|
||||||
|
],
|
||||||
|
|
||||||
|
'listen' => [
|
||||||
|
'AppInit' => [],
|
||||||
|
'HttpRun' => [],
|
||||||
|
'HttpEnd' => [],
|
||||||
|
'LogLevel' => [],
|
||||||
|
'LogWrite' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'subscribe' => [
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
// 全局中间件定义文件
|
||||||
|
return [
|
||||||
|
// 全局请求缓存
|
||||||
|
// \think\middleware\CheckRequestCache::class,
|
||||||
|
// 多语言加载
|
||||||
|
// \think\middleware\LoadLangPack::class,
|
||||||
|
// Session初始化
|
||||||
|
// \think\middleware\SessionInit::class
|
||||||
|
];
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\middleware;
|
||||||
|
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
|
||||||
|
class AdminAuth
|
||||||
|
{
|
||||||
|
public function handle($request, \Closure $next)
|
||||||
|
{
|
||||||
|
$user = $request->authUser ?? null;
|
||||||
|
if (!$user || ($user['role'] ?? '') !== 'admin') {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => '无管理员权限',
|
||||||
|
'data' => null,
|
||||||
|
], 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\middleware;
|
||||||
|
|
||||||
|
use app\service\JwtService;
|
||||||
|
use think\Response;
|
||||||
|
|
||||||
|
class Cors
|
||||||
|
{
|
||||||
|
public function handle($request, \Closure $next)
|
||||||
|
{
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||||
|
|
||||||
|
if ($request->method(true) === 'OPTIONS') {
|
||||||
|
return Response::create('', 'html', 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\middleware;
|
||||||
|
|
||||||
|
use app\model\User;
|
||||||
|
use app\service\JwtService;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
use think\Response;
|
||||||
|
|
||||||
|
class JwtAuth
|
||||||
|
{
|
||||||
|
public function handle($request, \Closure $next)
|
||||||
|
{
|
||||||
|
$token = $request->header('Authorization', '');
|
||||||
|
$payload = JwtService::verifyToken($token);
|
||||||
|
|
||||||
|
if (!$payload || empty($payload['user_id'])) {
|
||||||
|
$this->abort('请先登录', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::with(['membership'])->find($payload['user_id']);
|
||||||
|
if (!$user || $user->status !== 'active') {
|
||||||
|
$this->abort('请先登录', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->authUser = $this->formatUser($user);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatUser(User $user): array
|
||||||
|
{
|
||||||
|
$level = $user->membership;
|
||||||
|
return [
|
||||||
|
'id' => $user->id,
|
||||||
|
'username' => $user->username,
|
||||||
|
'email' => $user->email,
|
||||||
|
'nickname' => $user->nickname,
|
||||||
|
'avatar' => $user->avatar,
|
||||||
|
'role' => $user->role,
|
||||||
|
'status' => $user->status,
|
||||||
|
'membership_level_id' => $user->membership_level_id,
|
||||||
|
'membership_name' => $level?->name,
|
||||||
|
'membership_slug' => $level?->slug,
|
||||||
|
'max_conversations' => $level?->max_conversations ?? 20,
|
||||||
|
'max_messages_per_day' => $level?->max_messages_per_day ?? 50,
|
||||||
|
'max_upload_size_mb' => $level?->max_upload_size_mb ?? 5,
|
||||||
|
'membership_permissions' => $level?->permissions ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function abort(string $message, int $httpCode = 400): void
|
||||||
|
{
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => null,
|
||||||
|
], $httpCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class AiModel extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'ai_models';
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'created_at';
|
||||||
|
protected $updateTime = 'updated_at';
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class Conversation extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'conversations';
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'created_at';
|
||||||
|
protected $updateTime = 'updated_at';
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function aiModel()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AiModel::class, 'model_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class MembershipLevel extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'membership_levels';
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'created_at';
|
||||||
|
protected $updateTime = 'updated_at';
|
||||||
|
|
||||||
|
protected $type = [
|
||||||
|
'permissions' => 'json',
|
||||||
|
'allowed_models' => 'json',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class Message extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'messages';
|
||||||
|
protected $autoWriteTimestamp = 'created_at';
|
||||||
|
protected $createTime = 'created_at';
|
||||||
|
protected $updateTime = false;
|
||||||
|
|
||||||
|
protected $type = [
|
||||||
|
'attachments' => 'json',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class SystemSetting extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'system_settings';
|
||||||
|
protected $autoWriteTimestamp = 'updated_at';
|
||||||
|
protected $updateTime = 'updated_at';
|
||||||
|
protected $createTime = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class UploadFile extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'uploads';
|
||||||
|
protected $autoWriteTimestamp = 'created_at';
|
||||||
|
protected $createTime = 'created_at';
|
||||||
|
protected $updateTime = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class User extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'users';
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'created_at';
|
||||||
|
protected $updateTime = 'updated_at';
|
||||||
|
|
||||||
|
public function membership()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(MembershipLevel::class, 'membership_level_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class UserDailyStat extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'user_daily_stats';
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
use app\ExceptionHandle;
|
||||||
|
use app\Request;
|
||||||
|
|
||||||
|
// 容器Provider定义文件
|
||||||
|
return [
|
||||||
|
'think\Request' => Request::class,
|
||||||
|
'think\exception\Handle' => ExceptionHandle::class,
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use app\AppService;
|
||||||
|
|
||||||
|
// 系统服务定义文件
|
||||||
|
// 服务在完成全局初始化之后执行
|
||||||
|
return [
|
||||||
|
AppService::class,
|
||||||
|
];
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\service;
|
||||||
|
|
||||||
|
class JwtService
|
||||||
|
{
|
||||||
|
public static function generateToken(array $payload): string
|
||||||
|
{
|
||||||
|
$secret = config('jwt.secret');
|
||||||
|
$expire = config('jwt.expire');
|
||||||
|
|
||||||
|
$header = self::base64UrlEncode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
|
||||||
|
$payload['exp'] = time() + $expire;
|
||||||
|
$payload['iat'] = time();
|
||||||
|
$body = self::base64UrlEncode(json_encode($payload));
|
||||||
|
$signature = self::base64UrlEncode(hash_hmac('sha256', "{$header}.{$body}", $secret, true));
|
||||||
|
|
||||||
|
return "{$header}.{$body}.{$signature}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verifyToken(?string $token): ?array
|
||||||
|
{
|
||||||
|
if (!$token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($token, 'Bearer ')) {
|
||||||
|
$token = substr($token, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('.', $token);
|
||||||
|
if (count($parts) !== 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$header, $body, $signature] = $parts;
|
||||||
|
$secret = config('jwt.secret');
|
||||||
|
$expected = self::base64UrlEncode(hash_hmac('sha256', "{$header}.{$body}", $secret, true));
|
||||||
|
|
||||||
|
if (!hash_equals($expected, $signature)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = json_decode(self::base64UrlDecode($body), true);
|
||||||
|
if (!$payload || ($payload['exp'] ?? 0) < time()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function base64UrlEncode(string $data): string
|
||||||
|
{
|
||||||
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function base64UrlDecode(string $data): string
|
||||||
|
{
|
||||||
|
return base64_decode(strtr($data, '-_', '+/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\service;
|
||||||
|
|
||||||
|
use app\model\AiModel;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
|
||||||
|
class OpenAIService
|
||||||
|
{
|
||||||
|
public static function getModel(?int $modelId = null): AiModel
|
||||||
|
{
|
||||||
|
if ($modelId) {
|
||||||
|
$model = AiModel::where('id', $modelId)->where('enabled', 1)->find();
|
||||||
|
} else {
|
||||||
|
$model = AiModel::where('is_default', 1)->where('enabled', 1)->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$model) {
|
||||||
|
$model = AiModel::where('enabled', 1)->order('sort_order')->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$model) {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => '未配置可用的 AI 模型',
|
||||||
|
'data' => null,
|
||||||
|
], 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function streamChat(AiModel $model, array $messages, callable $onChunk): void
|
||||||
|
{
|
||||||
|
$url = rtrim($model->api_base_url, '/') . '/chat/completions';
|
||||||
|
$payload = [
|
||||||
|
'model' => $model->model_id,
|
||||||
|
'messages' => $messages,
|
||||||
|
'stream' => true,
|
||||||
|
'max_tokens' => (int) $model->max_tokens,
|
||||||
|
'temperature' => (float) $model->temperature,
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $model->api_key,
|
||||||
|
],
|
||||||
|
CURLOPT_RETURNTRANSFER => false,
|
||||||
|
CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($onChunk) {
|
||||||
|
$lines = explode("\n", $data);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line === 'data: [DONE]') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_starts_with($line, 'data: ')) {
|
||||||
|
$json = json_decode(substr($line, 6), true);
|
||||||
|
if ($json) {
|
||||||
|
$onChunk($json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strlen($data);
|
||||||
|
},
|
||||||
|
CURLOPT_TIMEOUT => 120,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
if ($result === false) {
|
||||||
|
$error = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
self::sseEvent('error', ['message' => 'AI 请求失败: ' . $error]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
curl_close($ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function chat(AiModel $model, array $messages): array
|
||||||
|
{
|
||||||
|
$url = rtrim($model->api_base_url, '/') . '/chat/completions';
|
||||||
|
$payload = [
|
||||||
|
'model' => $model->model_id,
|
||||||
|
'messages' => $messages,
|
||||||
|
'stream' => false,
|
||||||
|
'max_tokens' => (int) $model->max_tokens,
|
||||||
|
'temperature' => (float) $model->temperature,
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $model->api_key,
|
||||||
|
],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 120,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 200) {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => 'AI 请求失败: HTTP ' . $httpCode,
|
||||||
|
'data' => null,
|
||||||
|
], 502));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
if (!$data) {
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => 'AI 响应解析失败',
|
||||||
|
'data' => null,
|
||||||
|
], 502));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sseHeaders(): void
|
||||||
|
{
|
||||||
|
header('Content-Type: text/event-stream');
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
header('Connection: keep-alive');
|
||||||
|
header('X-Accel-Buffering: no');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sseEvent(string $event, mixed $data): void
|
||||||
|
{
|
||||||
|
echo "event: {$event}\n";
|
||||||
|
echo 'data: ' . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
|
||||||
|
if (ob_get_level() > 0) {
|
||||||
|
ob_flush();
|
||||||
|
}
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\service;
|
||||||
|
|
||||||
|
use app\model\UserDailyStat;
|
||||||
|
use think\exception\HttpResponseException;
|
||||||
|
|
||||||
|
class PermissionService
|
||||||
|
{
|
||||||
|
public static function checkDailyLimit(array $user): void
|
||||||
|
{
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$stats = UserDailyStat::where('user_id', $user['id'])
|
||||||
|
->where('stat_date', $today)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
$count = (int) ($stats->message_count ?? 0);
|
||||||
|
if ($count >= (int) $user['max_messages_per_day']) {
|
||||||
|
self::abort('今日消息数量已达上限', 429);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function incrementDailyCount(int $userId): void
|
||||||
|
{
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$stats = UserDailyStat::where('user_id', $userId)
|
||||||
|
->where('stat_date', $today)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if ($stats) {
|
||||||
|
$stats->inc('message_count')->save();
|
||||||
|
} else {
|
||||||
|
UserDailyStat::create([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'stat_date' => $today,
|
||||||
|
'message_count' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function checkConversationLimit(array $user): void
|
||||||
|
{
|
||||||
|
$count = \app\model\Conversation::where('user_id', $user['id'])
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($count >= (int) $user['max_conversations']) {
|
||||||
|
self::abort('会话数量已达上限', 429);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canUpload(array $user, string $type): bool
|
||||||
|
{
|
||||||
|
$permissions = $user['membership_permissions'] ?? [];
|
||||||
|
$map = [
|
||||||
|
'image' => ['upload_image', 'can_upload_image'],
|
||||||
|
'video' => ['upload_video', 'can_upload_video'],
|
||||||
|
'document' => ['upload_file', 'can_upload_file'],
|
||||||
|
'audio' => ['voice', 'can_use_voice'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($map[$type])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$featureKey, $permKey] = $map[$type];
|
||||||
|
if (!SettingsService::isFeatureEnabled($featureKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !empty($permissions[$permKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMaxUploadSizeMb(array $user): int
|
||||||
|
{
|
||||||
|
return min((int) $user['max_upload_size_mb'], (int) config('upload.max_size_mb'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function abort(string $message, int $httpCode = 400): void
|
||||||
|
{
|
||||||
|
throw new HttpResponseException(json([
|
||||||
|
'code' => 1,
|
||||||
|
'message' => $message,
|
||||||
|
'data' => null,
|
||||||
|
], $httpCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\service;
|
||||||
|
|
||||||
|
use app\model\SystemSetting;
|
||||||
|
|
||||||
|
class SettingsService
|
||||||
|
{
|
||||||
|
public static function get(string $key, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
$row = SystemSetting::where('setting_key', $key)->find();
|
||||||
|
if (!$row) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($row->setting_value, true);
|
||||||
|
return json_last_error() === JSON_ERROR_NONE ? $decoded : $row->setting_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function set(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$stored = is_array($value) || is_object($value)
|
||||||
|
? json_encode($value, JSON_UNESCAPED_UNICODE)
|
||||||
|
: (string) $value;
|
||||||
|
|
||||||
|
$setting = SystemSetting::where('setting_key', $key)->find();
|
||||||
|
if ($setting) {
|
||||||
|
$setting->save(['setting_value' => $stored]);
|
||||||
|
} else {
|
||||||
|
SystemSetting::create([
|
||||||
|
'setting_key' => $key,
|
||||||
|
'setting_value' => $stored,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getFeatures(): array
|
||||||
|
{
|
||||||
|
return self::get('features', [
|
||||||
|
'markdown' => true,
|
||||||
|
'image' => true,
|
||||||
|
'video' => true,
|
||||||
|
'voice' => true,
|
||||||
|
'document' => true,
|
||||||
|
'emoji' => true,
|
||||||
|
'upload_image' => true,
|
||||||
|
'upload_video' => true,
|
||||||
|
'upload_file' => true,
|
||||||
|
'paste_image' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isFeatureEnabled(string $feature): bool
|
||||||
|
{
|
||||||
|
$features = self::getFeatures();
|
||||||
|
return !empty($features[$feature]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 应用设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 应用的命名空间
|
||||||
|
'app_namespace' => '',
|
||||||
|
// 是否启用路由
|
||||||
|
'with_route' => true,
|
||||||
|
// 默认应用
|
||||||
|
'default_app' => 'index',
|
||||||
|
// 默认时区
|
||||||
|
'default_timezone' => 'Asia/Shanghai',
|
||||||
|
|
||||||
|
// 应用映射(自动多应用模式有效)
|
||||||
|
'app_map' => [],
|
||||||
|
// 域名绑定(自动多应用模式有效)
|
||||||
|
'domain_bind' => [],
|
||||||
|
// 禁止URL访问的应用列表(自动多应用模式有效)
|
||||||
|
'deny_app_list' => [],
|
||||||
|
|
||||||
|
// 异常页面的模板文件
|
||||||
|
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
|
||||||
|
|
||||||
|
// 错误显示信息,非调试模式有效
|
||||||
|
'error_message' => '页面错误!请稍后再试~',
|
||||||
|
// 显示错误信息
|
||||||
|
'show_error_msg' => false,
|
||||||
|
];
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 缓存设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 默认缓存驱动
|
||||||
|
'default' => 'file',
|
||||||
|
|
||||||
|
// 缓存连接方式配置
|
||||||
|
'stores' => [
|
||||||
|
'file' => [
|
||||||
|
// 驱动方式
|
||||||
|
'type' => 'File',
|
||||||
|
// 缓存保存目录
|
||||||
|
'path' => '',
|
||||||
|
// 缓存前缀
|
||||||
|
'prefix' => '',
|
||||||
|
// 缓存有效期 0表示永久缓存
|
||||||
|
'expire' => 0,
|
||||||
|
// 缓存标签前缀
|
||||||
|
'tag_prefix' => 'tag:',
|
||||||
|
// 序列化机制 例如 ['serialize', 'unserialize']
|
||||||
|
'serialize' => [],
|
||||||
|
],
|
||||||
|
// 更多的缓存连接
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 控制台配置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
return [
|
||||||
|
// 指令定义
|
||||||
|
'commands' => [
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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' => '',
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
],
|
||||||
|
|
||||||
|
// 更多的数据库配置信息
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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',
|
||||||
|
],
|
||||||
|
// 更多的磁盘配置信息
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'secret' => env('JWT_SECRET', 'change-this-secret-key-in-production'),
|
||||||
|
'expire' => (int) env('JWT_EXPIRE', 86400 * 7),
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
@@ -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,
|
||||||
|
],
|
||||||
|
// 其它日志通道配置
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
// 中间件配置
|
||||||
|
return [
|
||||||
|
// 别名或分组
|
||||||
|
'alias' => [],
|
||||||
|
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
|
||||||
|
'priority' => [],
|
||||||
|
];
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 路由设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// pathinfo分隔符
|
||||||
|
'pathinfo_depr' => '/',
|
||||||
|
// 是否开启路由延迟解析
|
||||||
|
'url_lazy_route' => false,
|
||||||
|
// 是否强制使用路由
|
||||||
|
'url_route_must' => false,
|
||||||
|
// 是否区分大小写
|
||||||
|
'url_case_sensitive' => false,
|
||||||
|
// 自动扫描子目录分组
|
||||||
|
'route_auto_group' => false,
|
||||||
|
// 合并路由规则
|
||||||
|
'route_rule_merge' => false,
|
||||||
|
// 路由是否完全匹配
|
||||||
|
'route_complete_match' => false,
|
||||||
|
// 去除斜杠
|
||||||
|
'remove_slash' => false,
|
||||||
|
// 默认的路由变量规则
|
||||||
|
'default_route_pattern' => '[\w\.]+',
|
||||||
|
// URL伪静态后缀
|
||||||
|
'url_html_suffix' => 'html',
|
||||||
|
// 访问控制器层名称
|
||||||
|
'controller_layer' => 'controller',
|
||||||
|
// 空控制器名
|
||||||
|
'empty_controller' => 'Error',
|
||||||
|
// 是否使用控制器后缀
|
||||||
|
'controller_suffix' => false,
|
||||||
|
// 默认模块名(开启自动多模块有效)
|
||||||
|
'default_module' => 'index',
|
||||||
|
// 默认控制器名
|
||||||
|
'default_controller' => 'Index',
|
||||||
|
// 默认操作名
|
||||||
|
'default_action' => 'index',
|
||||||
|
// 操作方法后缀
|
||||||
|
'action_suffix' => '',
|
||||||
|
// 非路由变量是否使用普通参数方式(用于URL生成)
|
||||||
|
'url_common_param' => true,
|
||||||
|
// 操作方法的参数绑定方式 route get param
|
||||||
|
'action_bind_param' => 'get',
|
||||||
|
// 请求缓存规则 true为自动规则
|
||||||
|
'request_cache_key' => true,
|
||||||
|
// 请求缓存有效期
|
||||||
|
'request_cache_expire' => null,
|
||||||
|
// 全局请求缓存排除规则
|
||||||
|
'request_cache_except' => [],
|
||||||
|
// 请求缓存的Tag
|
||||||
|
'request_cache_tag' => '',
|
||||||
|
// API版本header变量
|
||||||
|
'api_version' => 'Api-Version',
|
||||||
|
];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 会话设置
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
return [
|
||||||
|
// session name
|
||||||
|
'name' => 'PHPSESSID',
|
||||||
|
// SESSION_ID的提交变量,解决flash上传跨域
|
||||||
|
'var_session_id' => '',
|
||||||
|
// 驱动方式 支持file cache
|
||||||
|
'type' => 'file',
|
||||||
|
// 存储连接标识 当type使用cache的时候有效
|
||||||
|
'store' => null,
|
||||||
|
// 过期时间
|
||||||
|
'expire' => 1440,
|
||||||
|
// 前缀
|
||||||
|
'prefix' => '',
|
||||||
|
];
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | Trace设置 开启调试模式后有效
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
return [
|
||||||
|
// 内置Html和Console两种方式 支持扩展
|
||||||
|
'type' => 'Html',
|
||||||
|
// 读取的日志通道名
|
||||||
|
'channel' => '',
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'path' => root_path() . 'uploads',
|
||||||
|
'max_size_mb' => 50,
|
||||||
|
'allowed_images' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||||
|
'allowed_videos' => ['video/mp4', 'video/webm', 'video/quicktime'],
|
||||||
|
'allowed_audios' => ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm'],
|
||||||
|
'allowed_documents' => [
|
||||||
|
'application/pdf',
|
||||||
|
'text/plain',
|
||||||
|
'text/markdown',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -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' => '}',
|
||||||
|
];
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据库脚本
|
||||||
|
* 用法: php database/install.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
$sqlFile = __DIR__ . '/schema.sql';
|
||||||
|
$sql = file_get_contents($sqlFile);
|
||||||
|
|
||||||
|
$host = '127.0.0.1';
|
||||||
|
$user = 'root';
|
||||||
|
$pass = 'root';
|
||||||
|
$port = 3306;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("mysql:host={$host};port={$port};charset=utf8mb4", $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statements = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
if ($statement !== '') {
|
||||||
|
$pdo->exec($statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Database installed successfully.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
-- AI Chat Database Schema
|
||||||
|
CREATE DATABASE IF NOT EXISTS ai_chat DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE ai_chat;
|
||||||
|
|
||||||
|
-- 会员等级
|
||||||
|
CREATE TABLE IF NOT EXISTS membership_levels (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
max_conversations INT UNSIGNED DEFAULT 50,
|
||||||
|
max_messages_per_day INT UNSIGNED DEFAULT 100,
|
||||||
|
max_upload_size_mb INT UNSIGNED DEFAULT 10,
|
||||||
|
allowed_models JSON NULL COMMENT '允许使用的模型ID列表,null表示全部',
|
||||||
|
permissions JSON NULL COMMENT '额外权限配置',
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 用户
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
nickname VARCHAR(50) DEFAULT NULL,
|
||||||
|
avatar VARCHAR(255) DEFAULT NULL,
|
||||||
|
role ENUM('user', 'admin') DEFAULT 'user',
|
||||||
|
membership_level_id INT UNSIGNED DEFAULT 1,
|
||||||
|
status ENUM('active', 'disabled') DEFAULT 'active',
|
||||||
|
last_login_at TIMESTAMP NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (membership_level_id) REFERENCES membership_levels(id)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 系统功能开关
|
||||||
|
CREATE TABLE IF NOT EXISTS system_settings (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
setting_key VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
setting_value TEXT NOT NULL,
|
||||||
|
description VARCHAR(255) DEFAULT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- AI 模型配置(OpenAI 格式)
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_models (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
model_id VARCHAR(100) NOT NULL COMMENT 'API model 参数',
|
||||||
|
api_base_url VARCHAR(255) NOT NULL DEFAULT 'https://api.openai.com/v1',
|
||||||
|
api_key VARCHAR(255) NOT NULL,
|
||||||
|
max_tokens INT UNSIGNED DEFAULT 4096,
|
||||||
|
temperature DECIMAL(3,2) DEFAULT 0.70,
|
||||||
|
is_default TINYINT(1) DEFAULT 0,
|
||||||
|
enabled TINYINT(1) DEFAULT 1,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 会话
|
||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT UNSIGNED NOT NULL,
|
||||||
|
title VARCHAR(200) DEFAULT '新对话',
|
||||||
|
model_id INT UNSIGNED NULL,
|
||||||
|
is_pinned TINYINT(1) DEFAULT 0,
|
||||||
|
message_count INT UNSIGNED DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (model_id) REFERENCES ai_models(id) ON DELETE SET NULL,
|
||||||
|
INDEX idx_user_updated (user_id, updated_at DESC)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 消息
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
conversation_id INT UNSIGNED NOT NULL,
|
||||||
|
role ENUM('user', 'assistant', 'system') NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
content_type ENUM('text', 'markdown', 'mixed') DEFAULT 'text',
|
||||||
|
attachments JSON NULL COMMENT '[{type,url,name,size,mime}]',
|
||||||
|
tokens_used INT UNSIGNED DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_conversation (conversation_id, created_at)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 上传文件
|
||||||
|
CREATE TABLE IF NOT EXISTS uploads (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT UNSIGNED NOT NULL,
|
||||||
|
original_name VARCHAR(255) NOT NULL,
|
||||||
|
stored_name VARCHAR(255) NOT NULL,
|
||||||
|
file_path VARCHAR(500) NOT NULL,
|
||||||
|
mime_type VARCHAR(100) NOT NULL,
|
||||||
|
file_size INT UNSIGNED NOT NULL,
|
||||||
|
file_type ENUM('image', 'video', 'audio', 'document', 'other') NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 每日消息统计
|
||||||
|
CREATE TABLE IF NOT EXISTS user_daily_stats (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT UNSIGNED NOT NULL,
|
||||||
|
stat_date DATE NOT NULL,
|
||||||
|
message_count INT UNSIGNED DEFAULT 0,
|
||||||
|
UNIQUE KEY uk_user_date (user_id, stat_date),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
-- 初始会员等级
|
||||||
|
INSERT INTO membership_levels (name, slug, max_conversations, max_messages_per_day, max_upload_size_mb, permissions) VALUES
|
||||||
|
('免费用户', 'free', 20, 50, 5, '{"can_upload_image":true,"can_upload_video":false,"can_upload_file":false,"can_use_voice":false}'),
|
||||||
|
('高级会员', 'premium', 200, 500, 50, '{"can_upload_image":true,"can_upload_video":true,"can_upload_file":true,"can_use_voice":true}'),
|
||||||
|
('管理员', 'admin', 9999, 9999, 100, '{"can_upload_image":true,"can_upload_video":true,"can_upload_file":true,"can_use_voice":true}');
|
||||||
|
|
||||||
|
-- 默认系统设置
|
||||||
|
INSERT INTO system_settings (setting_key, setting_value, description) VALUES
|
||||||
|
('features', '{"markdown":true,"image":true,"video":true,"voice":true,"document":true,"emoji":true,"upload_image":true,"upload_video":true,"upload_file":true,"paste_image":true}', '功能开关'),
|
||||||
|
('site_name', 'AI Chat', '站点名称'),
|
||||||
|
('allow_register', 'true', '是否允许注册');
|
||||||
|
|
||||||
|
-- 默认管理员 (密码: admin123)
|
||||||
|
INSERT INTO users (username, email, password_hash, nickname, role, membership_level_id) VALUES
|
||||||
|
('admin', 'admin@example.com', '$2y$10$VJCJHjJoAdUAENQZzLgKO.DrJaVsquBXGWSg5ok3u2FHAKRkVMWX.', '管理员', 'admin', 3);
|
||||||
|
|
||||||
|
-- 示例 AI 模型(需替换 api_key)
|
||||||
|
INSERT INTO ai_models (name, model_id, api_base_url, api_key, is_default, enabled) VALUES
|
||||||
|
('GPT-4o Mini', 'gpt-4o-mini', 'https://api.openai.com/v1', 'your-api-key-here', 1, 1);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user