Files
chat/backend/app/controller/api/Admin.php
T
2026-07-22 10:18:59 +08:00

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);
}
}