This commit is contained in:
Your Name
2026-07-22 10:18:59 +08:00
parent 2530ddada6
commit 0fb03d0bca
618 changed files with 19445 additions and 3 deletions
Submodule backend deleted from 49917ae7c0
Submodule backend-tp deleted from 49917ae7c0
+11
View File
@@ -0,0 +1,11 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = test
DB_USER = username
DB_PASS = password
DB_PORT = 3306
DB_CHARSET = utf8
DEFAULT_LANG = zh-cn
+12
View File
@@ -0,0 +1,12 @@
*.log
.env
composer.phar
composer.lock
.DS_Store
Thumbs.db
/.idea
/.vscode
/vendor
/.settings
/.buildpath
/.project
+42
View File
@@ -0,0 +1,42 @@
sudo: false
language: php
branches:
only:
- stable
cache:
directories:
- $HOME/.composer/cache
before_install:
- composer self-update
install:
- composer install --no-dev --no-interaction --ignore-platform-reqs
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
- composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
- composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
script:
- php think unit
deploy:
provider: releases
api_key:
secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
file:
- ThinkPHP_Core.zip
- ThinkPHP_Full.zip
skip_cleanup: true
on:
tags: true
+32
View File
@@ -0,0 +1,32 @@
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
版权所有Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn)
All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
Apache Licence是著名的非盈利开源组织Apache采用的协议。
该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
允许代码修改,再作为开源或商业软件发布。需要满足
的条件:
1. 需要给代码的用户一份Apache Licence
2. 如果你修改了代码,需要在被修改的文件中说明;
3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
带有原来代码中的协议,商标,专利声明和其他原来作者规
定需要包含的说明;
4. 如果再发布的产品中包含一个Notice文件,则在Notice文
件中需要带有本协议内容。你可以在Notice中增加自己的
许可,但不可以表现为对Apache Licence构成更改。
具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+77
View File
@@ -0,0 +1,77 @@
![](https://www.thinkphp.cn/uploads/images/20230630/300c856765af4d8ae758c503185f8739.png)
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助理服务!
![](https://www.topthink.com/uploads/assistant/20230630/4d1a3f0ad2958b49bb8189b7ef824cb0.png)
ThinkPHP生态服务由[顶想云](https://www.topthink.com)TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。
## 文档
[完全开发手册](https://doc.thinkphp.cn)
## 赞助
全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及GIT仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障ThinkPHP的可持续发展。
[![](https://www.thinkphp.cn/sponsor/special.svg)](https://www.thinkphp.cn/sponsor/special)
[![](https://www.thinkphp.cn/sponsor.svg)](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)
+1
View File
@@ -0,0 +1 @@
deny from all
+22
View File
@@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}
+94
View File
@@ -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);
}
}
+58
View File
@@ -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);
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}
+2
View File
@@ -0,0 +1,2 @@
<?php
// 应用公共文件
+18
View File
@@ -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;
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];
+10
View File
@@ -0,0 +1,10 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];
+9
View File
@@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];
+9
View File
@@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];
+54
View File
@@ -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"
}]
}
+30
View File
@@ -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,
];
+29
View File
@@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------
return [
// 默认缓存驱动
'default' => 'file',
// 缓存连接方式配置
'stores' => [
'file' => [
// 驱动方式
'type' => 'File',
// 缓存保存目录
'path' => '',
// 缓存前缀
'prefix' => '',
// 缓存有效期 0表示永久缓存
'expire' => 0,
// 缓存标签前缀
'tag_prefix' => 'tag:',
// 序列化机制 例如 ['serialize', 'unserialize']
'serialize' => [],
],
// 更多的缓存连接
],
];
+9
View File
@@ -0,0 +1,9 @@
<?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
return [
// 指令定义
'commands' => [
],
];
+20
View File
@@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | Cookie设置
// +----------------------------------------------------------------------
return [
// cookie 保存时间
'expire' => 0,
// cookie 保存路径
'path' => '/',
// cookie 有效域名
'domain' => '',
// cookie 启用安全传输
'secure' => false,
// httponly设置
'httponly' => false,
// 是否使用 setcookie
'setcookie' => true,
// samesite 设置,支持 'strict' 'lax'
'samesite' => '',
];
+63
View File
@@ -0,0 +1,63 @@
<?php
return [
// 默认使用的数据库连接配置
'default' => env('DB_DRIVER', 'mysql'),
// 自定义时间查询规则
'time_query_rule' => [],
// 自动写入时间戳字段
// true为自动识别类型 false关闭
// 字符串则明确指定时间字段类型 支持 int timestamp datetime date
'auto_timestamp' => true,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
// 时间字段配置 配置格式:create_time,update_time
'datetime_field' => '',
// 数据库连接配置信息
'connections' => [
'mysql' => [
// 数据库类型
'type' => env('DB_TYPE', 'mysql'),
// 服务器地址
'hostname' => env('DB_HOST', '127.0.0.1'),
// 数据库名
'database' => env('DB_NAME', ''),
// 用户名
'username' => env('DB_USER', 'root'),
// 密码
'password' => env('DB_PASS', ''),
// 端口
'hostport' => env('DB_PORT', '3306'),
// 数据库连接参数
'params' => [],
// 数据库编码
'charset' => env('DB_CHARSET', 'utf8mb4'),
// 数据库表前缀
'prefix' => env('DB_PREFIX', ''),
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
// 是否需要断线重连
'break_reconnect' => false,
// 监听SQL
'trigger_sql' => env('APP_DEBUG', true),
// 开启字段缓存
'fields_cache' => false,
],
// 更多的数据库配置信息
],
];
+24
View File
@@ -0,0 +1,24 @@
<?php
return [
// 默认磁盘
'default' => 'local',
// 磁盘列表
'disks' => [
'local' => [
'type' => 'local',
'root' => app()->getRuntimePath() . 'storage',
],
'public' => [
// 磁盘类型
'type' => 'local',
// 磁盘路径
'root' => app()->getRootPath() . 'public/storage',
// 磁盘路径对应的外部URL路径
'url' => '/storage',
// 可见性
'visibility' => 'public',
],
// 更多的磁盘配置信息
],
];
+29
View File
@@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | 多语言设置
// +----------------------------------------------------------------------
return [
// 默认语言
'default_lang' => env('DEFAULT_LANG', 'zh-cn'),
// 自动侦测浏览器语言
'auto_detect_browser' => true,
// 允许的语言列表
'allow_lang_list' => [],
// 多语言自动侦测变量名
'detect_var' => 'lang',
// 是否使用Cookie记录
'use_cookie' => true,
// 多语言cookie变量
'cookie_var' => 'think_lang',
// 多语言header变量
'header_var' => 'think-lang',
// 扩展语言包
'extend_list' => [],
// Accept-Language转义为对应语言包名称
'accept_language' => [
'zh-hans-cn' => 'zh-cn',
],
// 是否支持语言分组
'allow_group' => false,
];
+45
View File
@@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 日志设置
// +----------------------------------------------------------------------
return [
// 默认日志记录通道
'default' => 'file',
// 日志记录级别
'level' => [],
// 日志类型记录的通道 ['error'=>'email',...]
'type_channel' => [],
// 关闭全局日志写入
'close' => false,
// 全局日志处理 支持闭包
'processor' => null,
// 日志通道列表
'channels' => [
'file' => [
// 日志记录方式
'type' => 'File',
// 日志保存目录
'path' => '',
// 单文件日志写入
'single' => false,
// 独立日志级别
'apart_level' => [],
// 最大日志文件数量
'max_files' => 0,
// 使用JSON格式记录
'json' => false,
// 日志处理
'processor' => null,
// 关闭通道日志写入
'close' => false,
// 日志输出格式化
'format' => '[%s][%s] %s',
// 是否实时写入
'realtime_write' => false,
],
// 其它日志通道配置
],
];
+8
View File
@@ -0,0 +1,8 @@
<?php
// 中间件配置
return [
// 别名或分组
'alias' => [],
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
'priority' => [],
];
+55
View File
@@ -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',
];
+19
View File
@@ -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' => '',
];
+10
View File
@@ -0,0 +1,10 @@
<?php
// +----------------------------------------------------------------------
// | Trace设置 开启调试模式后有效
// +----------------------------------------------------------------------
return [
// 内置Html和Console两种方式 支持扩展
'type' => 'Html',
// 读取的日志通道名
'channel' => '',
];
+25
View File
@@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | 模板设置
// +----------------------------------------------------------------------
return [
// 模板引擎类型使用Think
'type' => 'Think',
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
'auto_rule' => 1,
// 模板目录名
'view_dir_name' => 'view',
// 模板后缀
'view_suffix' => 'html',
// 模板文件名分隔符
'view_depr' => DIRECTORY_SEPARATOR,
// 模板引擎普通标签开始标记
'tpl_begin' => '{',
// 模板引擎普通标签结束标记
'tpl_end' => '}',
// 标签库标签开始标记
'taglib_begin' => '{',
// 标签库标签结束标记
'taglib_end' => '}',
];
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+8
View File
@@ -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

+25
View File
@@ -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);
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+19
View File
@@ -0,0 +1,19 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
// $Id$
if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) {
return false;
} else {
$_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php';
require __DIR__ . "/index.php";
}
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+17
View File
@@ -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');
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env php
<?php
use think\App;
// 命令行入口文件
// 加载基础文件
require __DIR__ . '/vendor/autoload.php';
// 应用初始化
(new App())->console->run();
+1
View File
@@ -0,0 +1 @@
如果不使用模板,可以删除该目录
Submodule backend-tp8 deleted from 49917ae7c0
+11
View File
@@ -0,0 +1,11 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = test
DB_USER = username
DB_PASS = password
DB_PORT = 3306
DB_CHARSET = utf8
DEFAULT_LANG = zh-cn
+12
View File
@@ -0,0 +1,12 @@
*.log
.env
composer.phar
composer.lock
.DS_Store
Thumbs.db
/.idea
/.vscode
/vendor
/.settings
/.buildpath
/.project
+42
View File
@@ -0,0 +1,42 @@
sudo: false
language: php
branches:
only:
- stable
cache:
directories:
- $HOME/.composer/cache
before_install:
- composer self-update
install:
- composer install --no-dev --no-interaction --ignore-platform-reqs
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
- composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
- composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
script:
- php think unit
deploy:
provider: releases
api_key:
secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
file:
- ThinkPHP_Core.zip
- ThinkPHP_Full.zip
skip_cleanup: true
on:
tags: true
+32
View File
@@ -0,0 +1,32 @@
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
版权所有Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn)
All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
Apache Licence是著名的非盈利开源组织Apache采用的协议。
该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
允许代码修改,再作为开源或商业软件发布。需要满足
的条件:
1. 需要给代码的用户一份Apache Licence
2. 如果你修改了代码,需要在被修改的文件中说明;
3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
带有原来代码中的协议,商标,专利声明和其他原来作者规
定需要包含的说明;
4. 如果再发布的产品中包含一个Notice文件,则在Notice文
件中需要带有本协议内容。你可以在Notice中增加自己的
许可,但不可以表现为对Apache Licence构成更改。
具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
+77
View File
@@ -0,0 +1,77 @@
![](https://www.thinkphp.cn/uploads/images/20230630/300c856765af4d8ae758c503185f8739.png)
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助理服务!
![](https://www.topthink.com/uploads/assistant/20230630/4d1a3f0ad2958b49bb8189b7ef824cb0.png)
ThinkPHP生态服务由[顶想云](https://www.topthink.com)TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。
## 文档
[完全开发手册](https://doc.thinkphp.cn)
## 赞助
全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及GIT仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障ThinkPHP的可持续发展。
[![](https://www.thinkphp.cn/sponsor/special.svg)](https://www.thinkphp.cn/sponsor/special)
[![](https://www.thinkphp.cn/sponsor.svg)](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)
+1
View File
@@ -0,0 +1 @@
deny from all
+22
View File
@@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}
+94
View File
@@ -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);
}
}
+58
View File
@@ -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);
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}
+2
View File
@@ -0,0 +1,2 @@
<?php
// 应用公共文件
+18
View File
@@ -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;
}
}
+197
View File
@@ -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, '更新成功');
}
}
+121
View File
@@ -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 ?? [];
}
}
+173
View File
@@ -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);
}
}
+132
View File
@@ -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,
};
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];
+10
View File
@@ -0,0 +1,10 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];
+22
View File
@@ -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);
}
}
+22
View File
@@ -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);
}
}
+60
View File
@@ -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));
}
}
+13
View File
@@ -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';
}
+23
View File
@@ -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');
}
}
+18
View File
@@ -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',
];
}
+17
View File
@@ -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',
];
}
+13
View File
@@ -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;
}
+13
View File
@@ -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;
}
+18
View File
@@ -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');
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace app\model;
use think\Model;
class UserDailyStat extends Model
{
protected $name = 'user_daily_stats';
protected $autoWriteTimestamp = false;
}
+9
View File
@@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];
+9
View File
@@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];
+61
View File
@@ -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, '-_', '+/'));
}
}
+148
View File
@@ -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]);
}
}
+49
View File
@@ -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"
]
}
}
+30
View File
@@ -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,
];
+29
View File
@@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------
return [
// 默认缓存驱动
'default' => 'file',
// 缓存连接方式配置
'stores' => [
'file' => [
// 驱动方式
'type' => 'File',
// 缓存保存目录
'path' => '',
// 缓存前缀
'prefix' => '',
// 缓存有效期 0表示永久缓存
'expire' => 0,
// 缓存标签前缀
'tag_prefix' => 'tag:',
// 序列化机制 例如 ['serialize', 'unserialize']
'serialize' => [],
],
// 更多的缓存连接
],
];
+9
View File
@@ -0,0 +1,9 @@
<?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
return [
// 指令定义
'commands' => [
],
];
+20
View File
@@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | Cookie设置
// +----------------------------------------------------------------------
return [
// cookie 保存时间
'expire' => 0,
// cookie 保存路径
'path' => '/',
// cookie 有效域名
'domain' => '',
// cookie 启用安全传输
'secure' => false,
// httponly设置
'httponly' => false,
// 是否使用 setcookie
'setcookie' => true,
// samesite 设置,支持 'strict' 'lax'
'samesite' => '',
];
+63
View File
@@ -0,0 +1,63 @@
<?php
return [
// 默认使用的数据库连接配置
'default' => env('DB_DRIVER', 'mysql'),
// 自定义时间查询规则
'time_query_rule' => [],
// 自动写入时间戳字段
// true为自动识别类型 false关闭
// 字符串则明确指定时间字段类型 支持 int timestamp datetime date
'auto_timestamp' => true,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
// 时间字段配置 配置格式:create_time,update_time
'datetime_field' => '',
// 数据库连接配置信息
'connections' => [
'mysql' => [
// 数据库类型
'type' => env('DB_TYPE', 'mysql'),
// 服务器地址
'hostname' => env('DB_HOST', '127.0.0.1'),
// 数据库名
'database' => env('DB_NAME', ''),
// 用户名
'username' => env('DB_USER', 'root'),
// 密码
'password' => env('DB_PASS', ''),
// 端口
'hostport' => env('DB_PORT', '3306'),
// 数据库连接参数
'params' => [],
// 数据库编码
'charset' => env('DB_CHARSET', 'utf8mb4'),
// 数据库表前缀
'prefix' => env('DB_PREFIX', ''),
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 是否严格检查字段是否存在
'fields_strict' => true,
// 是否需要断线重连
'break_reconnect' => false,
// 监听SQL
'trigger_sql' => env('APP_DEBUG', true),
// 开启字段缓存
'fields_cache' => false,
],
// 更多的数据库配置信息
],
];
+24
View File
@@ -0,0 +1,24 @@
<?php
return [
// 默认磁盘
'default' => 'local',
// 磁盘列表
'disks' => [
'local' => [
'type' => 'local',
'root' => app()->getRuntimePath() . 'storage',
],
'public' => [
// 磁盘类型
'type' => 'local',
// 磁盘路径
'root' => app()->getRootPath() . 'public/storage',
// 磁盘路径对应的外部URL路径
'url' => '/storage',
// 可见性
'visibility' => 'public',
],
// 更多的磁盘配置信息
],
];
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'secret' => env('JWT_SECRET', 'change-this-secret-key-in-production'),
'expire' => (int) env('JWT_EXPIRE', 86400 * 7),
];
+29
View File
@@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | 多语言设置
// +----------------------------------------------------------------------
return [
// 默认语言
'default_lang' => env('DEFAULT_LANG', 'zh-cn'),
// 自动侦测浏览器语言
'auto_detect_browser' => true,
// 允许的语言列表
'allow_lang_list' => [],
// 多语言自动侦测变量名
'detect_var' => 'lang',
// 是否使用Cookie记录
'use_cookie' => true,
// 多语言cookie变量
'cookie_var' => 'think_lang',
// 多语言header变量
'header_var' => 'think-lang',
// 扩展语言包
'extend_list' => [],
// Accept-Language转义为对应语言包名称
'accept_language' => [
'zh-hans-cn' => 'zh-cn',
],
// 是否支持语言分组
'allow_group' => false,
];
+45
View File
@@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 日志设置
// +----------------------------------------------------------------------
return [
// 默认日志记录通道
'default' => 'file',
// 日志记录级别
'level' => [],
// 日志类型记录的通道 ['error'=>'email',...]
'type_channel' => [],
// 关闭全局日志写入
'close' => false,
// 全局日志处理 支持闭包
'processor' => null,
// 日志通道列表
'channels' => [
'file' => [
// 日志记录方式
'type' => 'File',
// 日志保存目录
'path' => '',
// 单文件日志写入
'single' => false,
// 独立日志级别
'apart_level' => [],
// 最大日志文件数量
'max_files' => 0,
// 使用JSON格式记录
'json' => false,
// 日志处理
'processor' => null,
// 关闭通道日志写入
'close' => false,
// 日志输出格式化
'format' => '[%s][%s] %s',
// 是否实时写入
'realtime_write' => false,
],
// 其它日志通道配置
],
];
+8
View File
@@ -0,0 +1,8 @@
<?php
// 中间件配置
return [
// 别名或分组
'alias' => [],
// 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
'priority' => [],
];
+55
View File
@@ -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',
];
+19
View File
@@ -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' => '',
];
+10
View File
@@ -0,0 +1,10 @@
<?php
// +----------------------------------------------------------------------
// | Trace设置 开启调试模式后有效
// +----------------------------------------------------------------------
return [
// 内置Html和Console两种方式 支持扩展
'type' => 'Html',
// 读取的日志通道名
'channel' => '',
];
+16
View File
@@ -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',
],
];
+25
View File
@@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | 模板设置
// +----------------------------------------------------------------------
return [
// 模板引擎类型使用Think
'type' => 'Think',
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
'auto_rule' => 1,
// 模板目录名
'view_dir_name' => 'view',
// 模板后缀
'view_suffix' => 'html',
// 模板文件名分隔符
'view_depr' => DIRECTORY_SEPARATOR,
// 模板引擎普通标签开始标记
'tpl_begin' => '{',
// 模板引擎普通标签结束标记
'tpl_end' => '}',
// 标签库标签开始标记
'taglib_begin' => '{',
// 标签库标签结束标记
'taglib_end' => '}',
];
+32
View File
@@ -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);
}
+134
View File
@@ -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