更新
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user