feat(admin): add user-level direct permission system and enhance activity tracking

Features:
- Add user_permissions table for direct user-to-permission grants (ops:user-ops)
- Merge role_permissions + user_permissions in auth chain (login, middleware, getCurrentUser)
- Add getUserQueryScope support for USER role with ops:user-ops (cross-tenant access)
- Unify cross-tenant operation checks via getUserQueryScope (remove hardcoded SUPER_ADMIN checks)
- Add 3 new API endpoints: GET/PUT /:id/permissions, GET /options/permissions
- Support ops:user-ops as alternative permission on all user/tenant management routes
- Frontend: add user-ops permission toggle on UserFormPage and UserDetailPage
- Enhance DC module activity tracking (StreamAIController, SessionController, QuickActionController)
- Fix DC AIController user ID extraction and feature name consistency
- Add verify-activity-tracking.ts validation script
- Update deployment checklist and admin module documentation

DB Migration: 20260309_add_user_permissions_table

Made-with: Cursor
This commit is contained in:
2026-03-10 09:02:35 +08:00
parent 971e903acf
commit 097e7920ab
19 changed files with 693 additions and 87 deletions

View File

@@ -104,6 +104,29 @@ export async function importUsers(
return response.data.data;
}
/**
* 获取用户直授权限
*/
export async function getUserDirectPermissions(userId: string): Promise<string[]> {
const response = await apiClient.get<{ code: number; data: { permissions: string[] } }>(`${BASE_URL}/${userId}/permissions`);
return response.data.data.permissions;
}
/**
* 更新用户直授权限
*/
export async function updateUserDirectPermissions(userId: string, permissions: string[]): Promise<void> {
await apiClient.put(`${BASE_URL}/${userId}/permissions`, { permissions });
}
/**
* 获取所有可直授权限列表
*/
export async function getPermissionOptions(): Promise<Array<{ code: string; name: string; description: string | null }>> {
const response = await apiClient.get<{ code: number; data: Array<{ code: string; name: string; description: string | null }> }>(`${BASE_URL}/options/permissions`);
return response.data.data;
}
/**
* 获取所有租户列表(用于下拉选择)
*/

View File

@@ -18,6 +18,7 @@ import {
Modal,
Tooltip,
Empty,
Switch,
} from 'antd';
import {
ArrowLeftOutlined,
@@ -50,14 +51,20 @@ const UserDetailPage: React.FC = () => {
const [assignTenantVisible, setAssignTenantVisible] = useState(false);
const [modulePermissionVisible, setModulePermissionVisible] = useState(false);
const [selectedMembership, setSelectedMembership] = useState<TenantMembership | null>(null);
const [directPermissions, setDirectPermissions] = useState<string[]>([]);
const [opsToggling, setOpsToggling] = useState(false);
// 加载用户详情
const loadUser = async () => {
if (!id) return;
setLoading(true);
try {
const data = await userApi.getUserById(id);
const [data, perms] = await Promise.all([
userApi.getUserById(id),
userApi.getUserDirectPermissions(id),
]);
setUser(data);
setDirectPermissions(perms);
} catch (error) {
console.error('加载用户详情失败:', error);
message.error('加载用户详情失败');
@@ -142,6 +149,24 @@ const UserDetailPage: React.FC = () => {
});
};
// 切换用户运营权限
const handleToggleUserOps = async (checked: boolean) => {
if (!user) return;
setOpsToggling(true);
try {
const newPerms = checked
? [...directPermissions.filter(p => p !== 'ops:user-ops'), 'ops:user-ops']
: directPermissions.filter(p => p !== 'ops:user-ops');
await userApi.updateUserDirectPermissions(user.id, newPerms);
setDirectPermissions(newPerms);
message.success(checked ? '已开启用户运营权限' : '已关闭用户运营权限');
} catch {
message.error('操作失败');
} finally {
setOpsToggling(false);
}
};
// 打开模块权限配置
const handleConfigModules = (membership: TenantMembership) => {
setSelectedMembership(membership);
@@ -323,7 +348,17 @@ const UserDetailPage: React.FC = () => {
<Descriptions.Item label="创建时间">
{new Date(user.createdAt).toLocaleString('zh-CN')}
</Descriptions.Item>
<Descriptions.Item label="最后登录" span={3}>
<Descriptions.Item label="用户运营权限">
<Switch
checked={directPermissions.includes('ops:user-ops')}
onChange={handleToggleUserOps}
loading={opsToggling}
checkedChildren="已开启"
unCheckedChildren="未开启"
size="small"
/>
</Descriptions.Item>
<Descriptions.Item label="最后登录" span={2}>
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString('zh-CN') : '从未登录'}
</Descriptions.Item>
</Descriptions>

View File

@@ -18,6 +18,7 @@ import {
Divider,
Checkbox,
Alert,
Switch,
} from 'antd';
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
@@ -48,6 +49,7 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
const [departmentOptions, setDepartmentOptions] = useState<DepartmentOption[]>([]);
const [moduleOptions, setModuleOptions] = useState<ModuleOption[]>([]);
const [selectedTenantId, setSelectedTenantId] = useState<string>();
const [hasUserOps, setHasUserOps] = useState(false);
// 加载租户选项
useEffect(() => {
@@ -74,8 +76,11 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
useEffect(() => {
if (mode === 'edit' && id) {
setLoading(true);
userApi.getUserById(id)
.then((user) => {
Promise.all([
userApi.getUserById(id),
userApi.getUserDirectPermissions(id),
])
.then(([user, directPerms]) => {
form.setFieldsValue({
phone: user.phone,
name: user.name,
@@ -85,6 +90,7 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
departmentId: user.department?.id,
});
setSelectedTenantId(user.defaultTenant.id);
setHasUserOps(directPerms.includes('ops:user-ops'));
})
.catch((error) => {
console.error('加载用户数据失败:', error);
@@ -115,7 +121,11 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
tenantRole: values.tenantRole || values.role,
allowedModules: values.allowedModules?.length > 0 ? values.allowedModules : undefined,
};
await userApi.createUser(data);
const created = await userApi.createUser(data);
// 创建后追加直授权限
if (hasUserOps) {
await userApi.updateUserDirectPermissions(created.id, ['ops:user-ops']);
}
message.success('用户创建成功');
} else {
const data: UpdateUserRequest = {
@@ -125,6 +135,8 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
departmentId: values.departmentId || undefined,
};
await userApi.updateUser(id!, data);
// 更新直授权限
await userApi.updateUserDirectPermissions(id!, hasUserOps ? ['ops:user-ops'] : []);
message.success('用户更新成功');
}
navigate('/admin/users');
@@ -222,6 +234,23 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
</Col>
</Row>
{/* 用户运营权限 */}
<Row gutter={24}>
<Col span={12}>
<Form.Item
label="用户运营权限"
tooltip="开启后该用户可访问运营管理端的租户管理、用户管理、运营日志模块"
>
<Switch
checked={hasUserOps}
onChange={setHasUserOps}
checkedChildren="已开启"
unCheckedChildren="未开启"
/>
</Form.Item>
</Col>
</Row>
{mode === 'create' && (
<Alert
message="默认密码"