1058 lines
38 KiB
PHP
1058 lines
38 KiB
PHP
<?php
|
|
|
|
namespace app\controller\api;
|
|
|
|
use app\model\AiModel;
|
|
use app\model\Conversation as ConversationModel;
|
|
use app\model\Department;
|
|
use app\model\MembershipLevel;
|
|
use app\model\Message;
|
|
use app\model\Role;
|
|
use app\model\SysPermission;
|
|
use app\model\SystemSetting;
|
|
use app\model\User;
|
|
use app\model\UserDailyStat;
|
|
use app\service\AdminScopeService;
|
|
use app\service\ComfyUIService;
|
|
use app\service\DepartmentService;
|
|
use app\service\DifyService;
|
|
use app\service\OpenAIService;
|
|
use app\service\PermissionCatalog;
|
|
use app\service\SettingsService;
|
|
|
|
class Admin extends BaseApi
|
|
{
|
|
public function permissionTree()
|
|
{
|
|
return $this->success([
|
|
'tree' => PermissionCatalog::tree(),
|
|
'menus' => PermissionCatalog::menuMeta(),
|
|
'list' => PermissionCatalog::flatList(),
|
|
]);
|
|
}
|
|
|
|
public function createPermission()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:perm:create', 'menu:permissions', 'can_manage_permissions']);
|
|
$input = $this->request->post();
|
|
$type = $input['type'] ?? '';
|
|
$name = trim($input['name'] ?? '');
|
|
$code = trim($input['code'] ?? '');
|
|
|
|
if (!in_array($type, ['dir', 'menu', 'btn'], true)) {
|
|
return $this->error('类型必须是 dir / menu / btn');
|
|
}
|
|
if ($name === '') {
|
|
return $this->error('请填写名称');
|
|
}
|
|
if ($code === '') {
|
|
$prefix = $type === 'dir' ? 'dir:' : ($type === 'menu' ? 'menu:' : 'btn:');
|
|
$code = $prefix . preg_replace('/[^a-z0-9_]+/i', '_', strtolower($name)) . '_' . substr(uniqid(), -4);
|
|
}
|
|
if (SysPermission::where('code', $code)->find()) {
|
|
return $this->error('权限标识已存在');
|
|
}
|
|
|
|
$parentId = $input['parent_id'] ?? null;
|
|
if ($parentId !== null && $parentId !== '') {
|
|
$parentId = (int) $parentId;
|
|
$parent = SysPermission::find($parentId);
|
|
if (!$parent) {
|
|
return $this->error('上级节点不存在');
|
|
}
|
|
if ($type === 'menu' && $parent->type !== 'dir') {
|
|
return $this->error('菜单的上级必须是目录');
|
|
}
|
|
if ($type === 'btn' && $parent->type !== 'menu') {
|
|
return $this->error('按钮的上级必须是菜单');
|
|
}
|
|
if ($type === 'dir' && $parentId) {
|
|
return $this->error('目录不能有上级');
|
|
}
|
|
} else {
|
|
$parentId = null;
|
|
if ($type !== 'dir') {
|
|
return $this->error('菜单/按钮必须指定上级');
|
|
}
|
|
}
|
|
|
|
$row = SysPermission::create([
|
|
'type' => $type,
|
|
'code' => $code,
|
|
'name' => $name,
|
|
'parent_id' => $parentId,
|
|
'path' => $type === 'menu' ? trim((string) ($input['path'] ?? '')) : null,
|
|
'icon' => $type === 'menu' ? trim((string) ($input['icon'] ?? '')) : null,
|
|
'sort_order' => (int) ($input['sort_order'] ?? 0),
|
|
'is_system' => 0,
|
|
]);
|
|
|
|
return $this->success(['id' => $row->id], '创建成功');
|
|
}
|
|
|
|
public function updatePermission($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:perm:edit', 'menu:permissions', 'can_manage_permissions']);
|
|
$row = SysPermission::find((int) $id);
|
|
if (!$row) {
|
|
return $this->error('权限不存在', 404);
|
|
}
|
|
|
|
$input = $this->request->put();
|
|
$data = [];
|
|
foreach (['name', 'path', 'icon', 'sort_order'] as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$data[$field] = $input[$field];
|
|
}
|
|
}
|
|
if (isset($input['code']) && trim((string) $input['code']) !== '' && trim((string) $input['code']) !== $row->code) {
|
|
if ((int) $row->is_system === 1) {
|
|
return $this->error('系统内置权限标识不可修改');
|
|
}
|
|
$code = trim((string) $input['code']);
|
|
if (SysPermission::where('code', $code)->where('id', '<>', $id)->find()) {
|
|
return $this->error('权限标识已存在');
|
|
}
|
|
$data['code'] = $code;
|
|
}
|
|
if (empty($data)) {
|
|
return $this->error('无更新内容');
|
|
}
|
|
|
|
$row->save($data);
|
|
return $this->success(null, '更新成功');
|
|
}
|
|
|
|
public function deletePermission($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:perm:delete', 'menu:permissions', 'can_manage_permissions']);
|
|
$row = SysPermission::find((int) $id);
|
|
if (!$row) {
|
|
return $this->error('权限不存在', 404);
|
|
}
|
|
if ((int) $row->is_system === 1) {
|
|
return $this->error('系统内置权限不可删除');
|
|
}
|
|
if (SysPermission::where('parent_id', $id)->count() > 0) {
|
|
return $this->error('请先删除下级权限');
|
|
}
|
|
|
|
SysPermission::destroy((int) $id);
|
|
return $this->success(null, '删除成功');
|
|
}
|
|
|
|
public function users()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['menu:users', 'can_manage_users']);
|
|
|
|
$page = max(1, (int) $this->request->get('page', 1));
|
|
$limit = min(50, max(1, (int) $this->request->get('limit', 20)));
|
|
$departmentId = (int) $this->request->get('department_id', 0);
|
|
|
|
$query = User::alias('u')
|
|
->leftJoin('membership_levels ml', 'u.membership_level_id = ml.id')
|
|
->leftJoin('roles r', 'u.role_id = r.id')
|
|
->leftJoin('departments d', 'u.department_id = d.id')
|
|
->field('u.id,u.username,u.email,u.nickname,u.role,u.role_id,u.department_id,u.status,u.membership_level_id,u.created_at,u.last_login_at,ml.name as membership_name,r.name as role_name,r.slug as role_slug,d.name as department_name')
|
|
->order('u.id', 'desc');
|
|
|
|
AdminScopeService::applyUserScope($query, $this->authUser(), 'u');
|
|
|
|
if ($departmentId > 0) {
|
|
$deptIds = DepartmentService::descendantIds($departmentId);
|
|
$query->whereIn('u.department_id', $deptIds);
|
|
}
|
|
|
|
$total = (clone $query)->count();
|
|
$list = $query->page($page, $limit)->select();
|
|
|
|
return $this->success(['list' => $list, 'total' => $total]);
|
|
}
|
|
|
|
public function createUser()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:user:create', 'can_manage_users']);
|
|
$input = $this->request->post();
|
|
|
|
$username = trim($input['username'] ?? '');
|
|
$email = trim($input['email'] ?? '');
|
|
$password = (string) ($input['password'] ?? '');
|
|
$nickname = trim($input['nickname'] ?? '');
|
|
|
|
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('用户名或邮箱已存在');
|
|
}
|
|
|
|
$roleId = isset($input['role_id']) ? (int) $input['role_id'] : (int) Role::where('slug', 'user')->value('id');
|
|
if ($roleId && !Role::find($roleId)) {
|
|
return $this->error('角色不存在');
|
|
}
|
|
|
|
$departmentId = $input['department_id'] ?? null;
|
|
if ($departmentId !== null && $departmentId !== '') {
|
|
$departmentId = (int) $departmentId;
|
|
if ($departmentId > 0 && !Department::find($departmentId)) {
|
|
return $this->error('部门不存在');
|
|
}
|
|
$departmentId = $departmentId > 0 ? $departmentId : null;
|
|
} else {
|
|
$departmentId = null;
|
|
}
|
|
|
|
$membershipId = (int) ($input['membership_level_id'] ?? 1);
|
|
if (!MembershipLevel::find($membershipId)) {
|
|
return $this->error('会员等级不存在');
|
|
}
|
|
|
|
$user = User::create([
|
|
'username' => $username,
|
|
'email' => $email,
|
|
'password_hash' => password_hash($password, PASSWORD_BCRYPT),
|
|
'nickname' => $nickname !== '' ? $nickname : $username,
|
|
'role_id' => $roleId ?: null,
|
|
'department_id' => $departmentId,
|
|
'membership_level_id' => $membershipId,
|
|
'status' => ($input['status'] ?? 'active') === 'disabled' ? 'disabled' : 'active',
|
|
]);
|
|
|
|
if ($roleId) {
|
|
AdminScopeService::syncLegacyRoleField((int) $user->id, $roleId);
|
|
}
|
|
|
|
return $this->success(['id' => $user->id], '创建成功');
|
|
}
|
|
|
|
public function deleteUser($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:user:delete', 'can_manage_users']);
|
|
$targetId = (int) $id;
|
|
$auth = $this->authUser();
|
|
|
|
if ($targetId === (int) $auth['id']) {
|
|
return $this->error('不能删除当前登录账号');
|
|
}
|
|
if (!AdminScopeService::canViewUser($auth, $targetId)) {
|
|
return $this->error('无权操作该用户', 403);
|
|
}
|
|
|
|
$user = User::find($targetId);
|
|
if (!$user) {
|
|
return $this->error('用户不存在', 404);
|
|
}
|
|
|
|
$roleSlug = Role::where('id', $user->role_id)->value('slug');
|
|
if ($roleSlug === 'super_admin') {
|
|
$superRoleId = (int) Role::where('slug', 'super_admin')->value('id');
|
|
$count = User::where('role_id', $superRoleId)->count();
|
|
if ($count <= 1) {
|
|
return $this->error('至少保留一个超级管理员账号');
|
|
}
|
|
}
|
|
|
|
User::destroy($targetId);
|
|
return $this->success(null, '删除成功');
|
|
}
|
|
|
|
public function updateUser($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:user:edit', 'menu:users', 'can_manage_users']);
|
|
|
|
$targetId = (int) $id;
|
|
if (!AdminScopeService::canViewUser($this->authUser(), $targetId)) {
|
|
return $this->error('无权操作该用户', 403);
|
|
}
|
|
|
|
$input = $this->request->put();
|
|
$allowed = ['nickname', 'role', 'status', 'membership_level_id', 'role_id', 'department_id'];
|
|
$data = [];
|
|
|
|
foreach ($allowed as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$data[$field] = $input[$field];
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('password', $input)) {
|
|
$password = trim((string) $input['password']);
|
|
if ($password !== '') {
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:user:reset_password', 'can_manage_users']);
|
|
if (strlen($password) < 6) {
|
|
return $this->error('密码至少 6 位');
|
|
}
|
|
$data['password_hash'] = password_hash($password, PASSWORD_BCRYPT);
|
|
}
|
|
}
|
|
|
|
if (isset($data['membership_level_id']) && !MembershipLevel::find((int) $data['membership_level_id'])) {
|
|
return $this->error('会员等级不存在');
|
|
}
|
|
|
|
if (isset($data['role_id']) && !Role::find((int) $data['role_id'])) {
|
|
return $this->error('角色不存在');
|
|
}
|
|
|
|
if (array_key_exists('department_id', $data)) {
|
|
$deptId = $data['department_id'];
|
|
if ($deptId === '' || $deptId === null) {
|
|
$data['department_id'] = null;
|
|
} else {
|
|
$deptId = (int) $deptId;
|
|
if ($deptId > 0 && !Department::find($deptId)) {
|
|
return $this->error('部门不存在');
|
|
}
|
|
$data['department_id'] = $deptId > 0 ? $deptId : null;
|
|
}
|
|
}
|
|
|
|
if (empty($data)) {
|
|
return $this->error('无更新内容');
|
|
}
|
|
|
|
$user = User::find($targetId);
|
|
if (!$user) {
|
|
return $this->error('用户不存在', 404);
|
|
}
|
|
|
|
User::where('id', $targetId)->update($data);
|
|
|
|
if (isset($data['role_id'])) {
|
|
AdminScopeService::syncLegacyRoleField($targetId, (int) $data['role_id']);
|
|
}
|
|
|
|
return $this->success(null, '更新成功');
|
|
}
|
|
|
|
public function conversations()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), [
|
|
'menu:conversations',
|
|
'btn:conv:view_all',
|
|
'btn:conv:view_subordinate',
|
|
'can_view_all_conversations',
|
|
'can_view_subordinate_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');
|
|
$keyword = trim((string) $this->request->get('keyword', ''));
|
|
$departmentId = (int) $this->request->get('department_id', 0);
|
|
|
|
$query = ConversationModel::alias('c')
|
|
->join('users u', 'c.user_id = u.id')
|
|
->leftJoin('ai_models m', 'c.model_id = m.id')
|
|
->leftJoin('departments d', 'u.department_id = d.id')
|
|
->whereNull('c.deleted_at')
|
|
->field('c.*,u.username,u.email,d.name as department_name,m.name as model_name')
|
|
->order('c.updated_at', 'desc');
|
|
|
|
AdminScopeService::applyConversationScope($query, $this->authUser(), 'c');
|
|
|
|
if ($userId) {
|
|
$query->where('c.user_id', $userId);
|
|
}
|
|
|
|
if ($departmentId > 0) {
|
|
$deptIds = DepartmentService::descendantIds($departmentId);
|
|
$query->whereIn('u.department_id', $deptIds);
|
|
}
|
|
|
|
if ($keyword !== '') {
|
|
$query->where(function ($q) use ($keyword) {
|
|
$q->whereLike('c.title', "%{$keyword}%")
|
|
->whereOr('u.username', 'like', "%{$keyword}%")
|
|
->whereOr('u.email', 'like', "%{$keyword}%");
|
|
});
|
|
}
|
|
|
|
$total = (clone $query)->count();
|
|
$list = $query->page($page, $limit)->select();
|
|
|
|
return $this->success(['list' => $list, 'total' => $total]);
|
|
}
|
|
|
|
public function conversationDetail($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), [
|
|
'menu:conversations',
|
|
'btn:conv:view_all',
|
|
'btn:conv:view_subordinate',
|
|
'can_view_all_conversations',
|
|
'can_view_subordinate_conversations',
|
|
]);
|
|
|
|
$conversation = ConversationModel::alias('c')
|
|
->join('users u', 'c.user_id = u.id')
|
|
->leftJoin('ai_models m', 'c.model_id = m.id')
|
|
->leftJoin('departments d', 'u.department_id = d.id')
|
|
->where('c.id', $id)
|
|
->whereNull('c.deleted_at')
|
|
->field('c.*,u.username,u.email,d.name as department_name,m.name as model_name')
|
|
->find();
|
|
|
|
if (!$conversation) {
|
|
return $this->error('会话不存在', 404);
|
|
}
|
|
|
|
if (!AdminScopeService::canViewConversation($this->authUser(), (int) $conversation->user_id)) {
|
|
return $this->error('无权查看该会话', 403);
|
|
}
|
|
|
|
return $this->success([
|
|
'conversation' => $conversation->toArray(),
|
|
'messages' => $this->buildMessageList((int) $id),
|
|
]);
|
|
}
|
|
|
|
private function buildMessageList(int $conversationId): array
|
|
{
|
|
Message::where('conversation_id', $conversationId)
|
|
->where('role', 'assistant')
|
|
->where('content', '')
|
|
->delete();
|
|
|
|
$rows = Message::where('conversation_id', $conversationId)
|
|
->field('id,role,content,content_type,attachments,created_at')
|
|
->order('created_at', 'asc')
|
|
->select();
|
|
|
|
$list = [];
|
|
foreach ($rows as $item) {
|
|
if ($item->role === 'assistant' && trim((string) $item->content) === '') {
|
|
$attachments = is_string($item->attachments)
|
|
? json_decode($item->attachments, true)
|
|
: ($item->attachments ?? []);
|
|
if (empty($attachments)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$attachments = $item->attachments;
|
|
if (is_string($attachments)) {
|
|
$attachments = json_decode($attachments, true) ?: [];
|
|
}
|
|
|
|
$list[] = [
|
|
'id' => $item->id,
|
|
'role' => $item->role,
|
|
'content' => $item->content,
|
|
'content_type' => $item->content_type,
|
|
'attachments' => $attachments ?: [],
|
|
'created_at' => $item->created_at,
|
|
];
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
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 roles()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['menu:roles', 'can_manage_roles', 'menu:users', 'can_manage_users']);
|
|
$roles = Role::order('sort_order')->order('id')->select();
|
|
return $this->success($roles);
|
|
}
|
|
|
|
public function createRole()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:role:create', 'can_manage_roles']);
|
|
$input = $this->request->post();
|
|
$name = trim($input['name'] ?? '');
|
|
$slug = trim($input['slug'] ?? '');
|
|
|
|
if ($name === '') {
|
|
return $this->error('请填写角色名称');
|
|
}
|
|
if ($slug === '') {
|
|
$slug = 'role_' . uniqid();
|
|
}
|
|
if (Role::where('slug', $slug)->find()) {
|
|
return $this->error('角色标识已存在');
|
|
}
|
|
|
|
$permissions = PermissionCatalog::normalize($input['permissions'] ?? []);
|
|
$role = Role::create([
|
|
'name' => $name,
|
|
'slug' => $slug,
|
|
'permissions' => $permissions,
|
|
'sort_order' => (int) ($input['sort_order'] ?? 0),
|
|
]);
|
|
|
|
return $this->success(['id' => $role->id], '创建成功');
|
|
}
|
|
|
|
public function updateRole($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:role:edit', 'can_manage_roles']);
|
|
$role = Role::find((int) $id);
|
|
if (!$role) {
|
|
return $this->error('角色不存在', 404);
|
|
}
|
|
|
|
$input = $this->request->put();
|
|
$data = [];
|
|
foreach (['name', 'slug', 'sort_order'] as $field) {
|
|
if (isset($input[$field])) {
|
|
$data[$field] = $input[$field];
|
|
}
|
|
}
|
|
if (isset($input['permissions'])) {
|
|
$data['permissions'] = PermissionCatalog::normalize($input['permissions']);
|
|
}
|
|
if (isset($data['slug']) && Role::where('slug', $data['slug'])->where('id', '<>', $id)->find()) {
|
|
return $this->error('角色标识已存在');
|
|
}
|
|
if (empty($data)) {
|
|
return $this->error('无更新内容');
|
|
}
|
|
|
|
$role->save($data);
|
|
return $this->success(null, '更新成功');
|
|
}
|
|
|
|
public function deleteRole($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:role:delete', 'can_manage_roles']);
|
|
$roleId = (int) $id;
|
|
if (Role::where('slug', 'super_admin')->value('id') == $roleId) {
|
|
return $this->error('超级管理员角色不可删除');
|
|
}
|
|
if (User::where('role_id', $roleId)->count() > 0) {
|
|
return $this->error('该角色下仍有用户,无法删除');
|
|
}
|
|
Role::destroy($roleId);
|
|
return $this->success(null, '删除成功');
|
|
}
|
|
|
|
public function departments()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['menu:departments', 'can_manage_departments']);
|
|
$tree = DepartmentService::treeOptions();
|
|
$flat = Department::order('sort_order')->order('id')->select();
|
|
return $this->success(['tree' => $tree, 'list' => $flat]);
|
|
}
|
|
|
|
public function departmentOptions()
|
|
{
|
|
$options = DepartmentService::treeOptions();
|
|
return $this->success($options);
|
|
}
|
|
|
|
public function createDepartment()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:dept:create', 'can_manage_departments']);
|
|
$input = $this->request->post();
|
|
$name = trim($input['name'] ?? '');
|
|
if ($name === '') {
|
|
return $this->error('请填写部门名称');
|
|
}
|
|
|
|
$parentId = $input['parent_id'] ?? null;
|
|
if ($parentId !== null && $parentId !== '') {
|
|
$parentId = (int) $parentId;
|
|
if (!Department::find($parentId)) {
|
|
return $this->error('上级部门不存在');
|
|
}
|
|
} else {
|
|
$parentId = null;
|
|
}
|
|
|
|
$dept = Department::create([
|
|
'name' => $name,
|
|
'parent_id' => $parentId,
|
|
'sort_order' => (int) ($input['sort_order'] ?? 0),
|
|
]);
|
|
|
|
return $this->success(['id' => $dept->id], '创建成功');
|
|
}
|
|
|
|
public function updateDepartment($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:dept:edit', 'can_manage_departments']);
|
|
$dept = Department::find((int) $id);
|
|
if (!$dept) {
|
|
return $this->error('部门不存在', 404);
|
|
}
|
|
|
|
$input = $this->request->put();
|
|
$data = [];
|
|
foreach (['name', 'sort_order'] as $field) {
|
|
if (isset($input[$field])) {
|
|
$data[$field] = $input[$field];
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('parent_id', $input)) {
|
|
$parentId = $input['parent_id'];
|
|
if ($parentId === '' || $parentId === null) {
|
|
$data['parent_id'] = null;
|
|
} else {
|
|
$parentId = (int) $parentId;
|
|
if ($parentId === (int) $id) {
|
|
return $this->error('上级部门不能是自己');
|
|
}
|
|
if ($parentId > 0) {
|
|
$descendants = DepartmentService::descendantIds((int) $id);
|
|
if (in_array($parentId, $descendants, true)) {
|
|
return $this->error('上级部门不能是当前部门的下级');
|
|
}
|
|
if (!Department::find($parentId)) {
|
|
return $this->error('上级部门不存在');
|
|
}
|
|
}
|
|
$data['parent_id'] = $parentId > 0 ? $parentId : null;
|
|
}
|
|
}
|
|
|
|
if (empty($data)) {
|
|
return $this->error('无更新内容');
|
|
}
|
|
|
|
$dept->save($data);
|
|
return $this->success(null, '更新成功');
|
|
}
|
|
|
|
public function deleteDepartment($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:dept:delete', 'can_manage_departments']);
|
|
$deptId = (int) $id;
|
|
if (Department::where('parent_id', $deptId)->count() > 0) {
|
|
return $this->error('请先删除或移走下级部门');
|
|
}
|
|
if (User::where('department_id', $deptId)->count() > 0) {
|
|
return $this->error('该部门下仍有用户,无法删除');
|
|
}
|
|
Department::destroy($deptId);
|
|
return $this->success(null, '删除成功');
|
|
}
|
|
|
|
public function settings()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['menu:settings', 'can_manage_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()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:settings:save', 'can_manage_settings']);
|
|
$input = $this->request->put();
|
|
foreach ($input as $key => $value) {
|
|
SettingsService::set($key, $value);
|
|
}
|
|
return $this->success(null, '设置已更新');
|
|
}
|
|
|
|
public function models()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['menu:models', 'can_manage_models']);
|
|
$this->ensureAiModelExtraConfigColumn();
|
|
|
|
$rows = AiModel::field('id,name,provider,model_id,api_base_url,api_key,max_tokens,temperature,is_default,enabled,support_context,support_image,frequency_penalty,presence_penalty,extra_config,sort_order')
|
|
->order('sort_order')
|
|
->select();
|
|
|
|
$list = [];
|
|
foreach ($rows as $row) {
|
|
$item = $row->toArray();
|
|
$item['has_api_key'] = !empty($item['api_key']);
|
|
$item['api_key_hint'] = $this->maskApiKey($item['api_key'] ?? '');
|
|
unset($item['api_key']);
|
|
if (isset($item['extra_config']) && is_string($item['extra_config'])) {
|
|
$item['extra_config'] = json_decode($item['extra_config'], true) ?: null;
|
|
}
|
|
$list[] = $item;
|
|
}
|
|
|
|
return $this->success($list);
|
|
}
|
|
|
|
public function testModel($id = null)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:model:test', 'can_manage_models']);
|
|
$input = $this->request->post();
|
|
$config = [];
|
|
$provider = $input['provider'] ?? 'openai';
|
|
|
|
if ($id) {
|
|
$model = AiModel::find((int) $id);
|
|
if (!$model) {
|
|
return $this->error('模型不存在', 404);
|
|
}
|
|
$provider = $input['provider'] ?? $model->provider ?? 'openai';
|
|
$config = [
|
|
'api_base_url' => $input['api_base_url'] ?? $model->api_base_url,
|
|
'model_id' => $input['model_id'] ?? $model->model_id,
|
|
'api_key' => !empty($input['api_key']) ? $input['api_key'] : $model->api_key,
|
|
'temperature' => $input['temperature'] ?? $model->temperature,
|
|
];
|
|
} else {
|
|
$config = [
|
|
'api_base_url' => $input['api_base_url'] ?? '',
|
|
'model_id' => $input['model_id'] ?? '',
|
|
'api_key' => $input['api_key'] ?? '',
|
|
'temperature' => $input['temperature'] ?? 0.7,
|
|
];
|
|
}
|
|
|
|
try {
|
|
if ($provider === 'dify') {
|
|
$result = DifyService::testConnection($config);
|
|
} elseif ($provider === 'comfy') {
|
|
$result = ComfyUIService::testConnection($config);
|
|
} else {
|
|
$result = OpenAIService::testConnection($config);
|
|
}
|
|
return $this->success($result, '测试成功');
|
|
} catch (\InvalidArgumentException $e) {
|
|
return $this->error($e->getMessage());
|
|
} catch (\Throwable $e) {
|
|
return $this->error($e->getMessage(), 502);
|
|
}
|
|
}
|
|
|
|
public function createModel()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:model:create', 'can_manage_models']);
|
|
$this->ensureAiModelExtraConfigColumn();
|
|
$input = $this->request->post();
|
|
|
|
$extraConfig = $this->normalizeExtraConfig($input['extra_config'] ?? null, $input['provider'] ?? 'openai');
|
|
if (is_string($extraConfig)) {
|
|
return $this->error($extraConfig);
|
|
}
|
|
|
|
$model = AiModel::create([
|
|
'name' => $input['name'],
|
|
'provider' => in_array($input['provider'] ?? 'openai', ['openai', 'dify', 'comfy'], true) ? $input['provider'] : 'openai',
|
|
'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),
|
|
'support_context' => (int) ($input['support_context'] ?? 1),
|
|
'support_image' => (int) ($input['support_image'] ?? 1),
|
|
'frequency_penalty' => $input['frequency_penalty'] ?? 0,
|
|
'presence_penalty' => $input['presence_penalty'] ?? 0,
|
|
'extra_config' => $extraConfig,
|
|
'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)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:model:edit', 'can_manage_models']);
|
|
$this->ensureAiModelExtraConfigColumn();
|
|
$input = $this->request->put();
|
|
$allowed = ['name', 'provider', 'model_id', 'api_base_url', 'api_key', 'max_tokens', 'temperature', 'is_default', 'enabled', 'support_context', 'support_image', 'frequency_penalty', 'presence_penalty', 'sort_order'];
|
|
$data = [];
|
|
|
|
foreach ($allowed as $field) {
|
|
if (array_key_exists($field, $input)) {
|
|
$data[$field] = $input[$field];
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('api_key', $data) && trim((string) $data['api_key']) === '') {
|
|
unset($data['api_key']);
|
|
}
|
|
|
|
if (isset($data['provider']) && !in_array($data['provider'], ['openai', 'dify', 'comfy'], true)) {
|
|
$data['provider'] = 'openai';
|
|
}
|
|
|
|
if (array_key_exists('extra_config', $input)) {
|
|
$provider = $data['provider'] ?? (AiModel::where('id', $id)->value('provider') ?: 'openai');
|
|
$extraConfig = $this->normalizeExtraConfig($input['extra_config'], $provider);
|
|
if (is_string($extraConfig)) {
|
|
return $this->error($extraConfig);
|
|
}
|
|
$data['extra_config'] = $extraConfig === null
|
|
? null
|
|
: json_encode($extraConfig, JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
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, '更新成功');
|
|
}
|
|
|
|
/**
|
|
* @return array|null|string 成功返回数组/null;失败返回错误文案
|
|
*/
|
|
private function normalizeExtraConfig(mixed $raw, string $provider): array|null|string
|
|
{
|
|
if ($raw === null || $raw === '' || $raw === []) {
|
|
return null;
|
|
}
|
|
|
|
if (is_string($raw)) {
|
|
$decoded = json_decode($raw, true);
|
|
if (!is_array($decoded)) {
|
|
return 'extra_config 不是有效 JSON';
|
|
}
|
|
$raw = $decoded;
|
|
}
|
|
|
|
if (!is_array($raw)) {
|
|
return 'extra_config 格式无效';
|
|
}
|
|
|
|
$config = [];
|
|
foreach (['workflow', 'img2img_workflow', 'inpaint_workflow', 'outpaint_workflow'] as $workflowKey) {
|
|
if (!isset($raw[$workflowKey])) {
|
|
continue;
|
|
}
|
|
if (is_string($raw[$workflowKey])) {
|
|
$wf = json_decode($raw[$workflowKey], true);
|
|
if (!is_array($wf)) {
|
|
return $workflowKey . ' JSON 无效';
|
|
}
|
|
$raw[$workflowKey] = $wf;
|
|
}
|
|
if (is_array($raw[$workflowKey]) && $raw[$workflowKey] !== []) {
|
|
$config[$workflowKey] = $raw[$workflowKey];
|
|
}
|
|
}
|
|
|
|
foreach ([
|
|
'prompt_node',
|
|
'seed_node',
|
|
'aspect_ratio',
|
|
'system_prompt',
|
|
'edit_system_prompt',
|
|
'img2img_prompt_node',
|
|
'img2img_seed_node',
|
|
'img2img_image_node',
|
|
'inpaint_prompt_node',
|
|
'inpaint_seed_node',
|
|
'inpaint_image_node',
|
|
'inpaint_mask_node',
|
|
] as $key) {
|
|
if (!array_key_exists($key, $raw)) {
|
|
continue;
|
|
}
|
|
$val = is_string($raw[$key]) ? trim($raw[$key]) : $raw[$key];
|
|
if ($val === null || $val === '') {
|
|
continue;
|
|
}
|
|
$config[$key] = (string) $val;
|
|
}
|
|
|
|
foreach (['img2img_denoise', 'inpaint_denoise'] as $key) {
|
|
if (!array_key_exists($key, $raw) || $raw[$key] === '' || $raw[$key] === null) {
|
|
continue;
|
|
}
|
|
if (!is_numeric($raw[$key])) {
|
|
return $key . ' 必须是 0 到 1 之间的数字';
|
|
}
|
|
$config[$key] = max(0.01, min(1.0, (float) $raw[$key]));
|
|
}
|
|
|
|
if (array_key_exists('refine_prompt', $raw)) {
|
|
$config['refine_prompt'] = filter_var($raw['refine_prompt'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($config['refine_prompt'] === null) {
|
|
$config['refine_prompt'] = (bool) $raw['refine_prompt'];
|
|
}
|
|
}
|
|
|
|
if ($provider === 'comfy' && $config !== []) {
|
|
$err = ComfyUIService::validateWorkflowConfig($config);
|
|
if ($err) {
|
|
return $err;
|
|
}
|
|
}
|
|
|
|
return $config === [] ? null : $config;
|
|
}
|
|
|
|
private function ensureAiModelExtraConfigColumn(): void
|
|
{
|
|
static $ensured = false;
|
|
if ($ensured) {
|
|
return;
|
|
}
|
|
$ensured = true;
|
|
|
|
try {
|
|
$cols = \think\facade\Db::query("SHOW COLUMNS FROM `ai_models` LIKE 'extra_config'");
|
|
if (!empty($cols)) {
|
|
return;
|
|
}
|
|
\think\facade\Db::execute(
|
|
"ALTER TABLE `ai_models` ADD COLUMN `extra_config` JSON NULL COMMENT 'ComfyUI工作流等扩展配置' AFTER `presence_penalty`"
|
|
);
|
|
} catch (\Throwable $e) {
|
|
// 并发创建或无权限时忽略,后续读写会按无该字段降级
|
|
}
|
|
}
|
|
|
|
public function deleteModel($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:model:delete', 'can_manage_models']);
|
|
AiModel::destroy($id);
|
|
return $this->success(null, '删除成功');
|
|
}
|
|
|
|
public function memberships()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['menu:memberships', 'can_manage_memberships', 'menu:users', 'can_manage_users']);
|
|
$levels = MembershipLevel::order('sort_order')->select();
|
|
return $this->success($levels);
|
|
}
|
|
|
|
public function createMembership()
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:membership:create', 'btn:membership:edit', 'can_manage_memberships']);
|
|
$input = $this->request->post();
|
|
$name = trim($input['name'] ?? '');
|
|
$slug = trim($input['slug'] ?? '');
|
|
|
|
if ($name === '') {
|
|
return $this->error('请填写名称');
|
|
}
|
|
if ($slug === '') {
|
|
$slug = 'level_' . substr(uniqid(), -6);
|
|
}
|
|
if (MembershipLevel::where('slug', $slug)->find()) {
|
|
return $this->error('标识 slug 已存在');
|
|
}
|
|
|
|
$level = MembershipLevel::create([
|
|
'name' => $name,
|
|
'slug' => $slug,
|
|
'max_conversations' => (int) ($input['max_conversations'] ?? 20),
|
|
'max_messages_per_day' => (int) ($input['max_messages_per_day'] ?? 50),
|
|
'max_upload_size_mb' => (int) ($input['max_upload_size_mb'] ?? 5),
|
|
'permissions' => $input['permissions'] ?? [
|
|
'can_upload_image' => false,
|
|
'can_upload_video' => false,
|
|
'can_upload_file' => false,
|
|
'can_use_voice' => false,
|
|
],
|
|
'sort_order' => (int) ($input['sort_order'] ?? 0),
|
|
]);
|
|
|
|
return $this->success(['id' => $level->id], '创建成功');
|
|
}
|
|
|
|
public function updateMembership($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:membership:edit', 'can_manage_memberships']);
|
|
$level = MembershipLevel::find((int) $id);
|
|
if (!$level) {
|
|
return $this->error('会员等级不存在', 404);
|
|
}
|
|
|
|
$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['slug']) && trim((string) $input['slug']) !== '') {
|
|
$slug = trim((string) $input['slug']);
|
|
if (MembershipLevel::where('slug', $slug)->where('id', '<>', $id)->find()) {
|
|
return $this->error('标识 slug 已存在');
|
|
}
|
|
$data['slug'] = $slug;
|
|
}
|
|
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('无更新内容');
|
|
}
|
|
|
|
$level->save($data);
|
|
return $this->success(null, '更新成功');
|
|
}
|
|
|
|
public function deleteMembership($id)
|
|
{
|
|
AdminScopeService::requireAny($this->authUser(), ['btn:membership:delete', 'can_manage_memberships']);
|
|
$levelId = (int) $id;
|
|
$level = MembershipLevel::find($levelId);
|
|
if (!$level) {
|
|
return $this->error('会员等级不存在', 404);
|
|
}
|
|
|
|
if (in_array($level->slug, ['free', 'admin'], true) || $levelId === 1) {
|
|
return $this->error('系统默认会员等级不可删除');
|
|
}
|
|
|
|
$userCount = User::where('membership_level_id', $levelId)->count();
|
|
if ($userCount > 0) {
|
|
return $this->error("该等级下还有 {$userCount} 个用户,请先调整用户会员后再删除");
|
|
}
|
|
|
|
MembershipLevel::destroy($levelId);
|
|
return $this->success(null, '删除成功');
|
|
}
|
|
|
|
private function maskApiKey(?string $key): ?string
|
|
{
|
|
$key = trim((string) $key);
|
|
if ($key === '') {
|
|
return null;
|
|
}
|
|
|
|
$len = strlen($key);
|
|
if ($len <= 8) {
|
|
return str_repeat('*', $len);
|
|
}
|
|
|
|
return substr($key, 0, 4) . str_repeat('*', min($len - 8, 12)) . substr($key, -4);
|
|
}
|
|
}
|