diff --git a/app/routes/api.v3.rbac.roles.$roleId.tsx b/app/routes/api.v3.rbac.roles.$roleId.tsx new file mode 100644 index 0000000..81fcf22 --- /dev/null +++ b/app/routes/api.v3.rbac.roles.$roleId.tsx @@ -0,0 +1,86 @@ +/** + * RBAC API 代理 - 单个角色操作 + * GET /api/v3/rbac/roles/:roleId - 获取角色详情 + * PUT /api/v3/rbac/roles/:roleId - 更新角色 + * DELETE /api/v3/rbac/roles/:roleId - 删除角色 + */ + +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { mockRoles, updateRole as updateMockRole, deleteRole as deleteMockRole } from "~/services/rbac-mock-data.server"; + +// GET - 获取角色详情 +export async function loader({ params }: LoaderFunctionArgs) { + const roleId = parseInt(params.roleId || '0'); + console.log('📡 [API Route] GET /api/v3/rbac/roles/' + roleId); + + const role = mockRoles.find(r => r.id === roleId); + + if (!role) { + return json({ + detail: '角色不存在' + }, { status: 404 }); + } + + return json({ + code: 200, + message: 'success', + data: role + }); +} + +// PUT/DELETE +export async function action({ request, params }: LoaderFunctionArgs) { + const roleId = parseInt(params.roleId || '0'); + const method = request.method; + + console.log('📡 [API Route]', method, '/api/v3/rbac/roles/' + roleId); + + const role = mockRoles.find(r => r.id === roleId); + + if (!role) { + return json({ + detail: '角色不存在' + }, { status: 404 }); + } + + if (method === 'PUT') { + // 更新角色 + const body = await request.json(); + console.log('📋 [API Route] 更新数据:', body); + + // 系统角色保护 + if (role.is_system && body.role_key) { + return json({ + detail: '系统角色的role_key不可修改' + }, { status: 400 }); + } + + // 使用共享Mock数据更新 + updateMockRole(roleId, body); + + return json({ + code: 200, + message: '角色更新成功', + data: role + }); + } + + if (method === 'DELETE') { + // 删除角色 + if (role.is_system) { + return json({ + detail: '系统角色不能删除' + }, { status: 400 }); + } + + // 使用共享Mock数据删除 + deleteMockRole(roleId); + + return json({ + code: 200, + message: '角色删除成功' + }); + } + + return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 }); +} diff --git a/app/routes/api.v3.rbac.roles.$roleId.users.tsx b/app/routes/api.v3.rbac.roles.$roleId.users.tsx new file mode 100644 index 0000000..0d36e3f --- /dev/null +++ b/app/routes/api.v3.rbac.roles.$roleId.users.tsx @@ -0,0 +1,65 @@ +/** + * RBAC API 代理 - 角色用户管理 + * GET /api/v3/rbac/roles/:roleId/users - 获取角色的用户列表 + */ + +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { mockUsers, mockUserRoles, getRoleUsers } from "~/services/rbac-mock-data.server"; + +// GET - 获取角色的用户列表 +export async function loader({ params, request }: LoaderFunctionArgs) { + const roleId = parseInt(params.roleId || '0'); + console.log('📡 [API Route] GET /api/v3/rbac/roles/' + roleId + '/users'); + + // 解析查询参数 + const url = new URL(request.url); + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '20'); + const area = url.searchParams.get('area'); + const username = url.searchParams.get('username'); + + // 使用共享Mock数据获取角色用户 + let users = getRoleUsers(roleId); + + // 过滤 + if (area) { + users = users.filter(u => u.area.includes(area)); + } + + if (username) { + users = users.filter(u => + u.username.includes(username) || u.nick_name.includes(username) + ); + } + + // 添加assigned_at时间戳 + const usersWithTime = users.map(user => { + const userRole = mockUserRoles.find( + ur => ur.user_id === (user.user_id || user.id) && ur.role_id === roleId + ); + return { + ...user, + user_id: user.user_id || user.id, + assigned_at: userRole?.assigned_at || new Date().toISOString() + }; + }); + + // 分页 + const total = usersWithTime.length; + const start = (page - 1) * pageSize; + const end = start + pageSize; + const items = usersWithTime.slice(start, end); + + console.log('✅ [API Route] 返回用户数据:', { roleId, total, itemsCount: items.length }); + + return json({ + code: 200, + message: 'success', + data: { + total, + page, + page_size: pageSize, + items + } + }); +} diff --git a/app/routes/api.v3.rbac.roles._index.tsx b/app/routes/api.v3.rbac.roles._index.tsx new file mode 100644 index 0000000..4d6a81c --- /dev/null +++ b/app/routes/api.v3.rbac.roles._index.tsx @@ -0,0 +1,99 @@ +/** + * RBAC API 代理 - 获取角色列表 + * GET /api/v3/rbac/roles + */ + +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { mockRoles, mockUserRoles, addRole } from "~/services/rbac-mock-data.server"; + +// GET - 获取角色列表 +export async function loader({ request }: LoaderFunctionArgs) { + console.log('📡 [API Route] GET /api/v3/rbac/roles 被调用'); + + // 解析查询参数 + const url = new URL(request.url); + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '20'); + const roleKey = url.searchParams.get('role_key'); + const roleName = url.searchParams.get('role_name'); + + console.log('📋 [API Route] 查询参数:', { page, pageSize, roleKey, roleName }); + + // 过滤数据 + let filteredRoles = [...mockRoles]; + + if (roleKey) { + filteredRoles = filteredRoles.filter(r => r.role_key.includes(roleKey)); + } + + if (roleName) { + filteredRoles = filteredRoles.filter(r => r.role_name.includes(roleName)); + } + + // 添加用户数统计 + const rolesWithCount = filteredRoles.map(role => ({ + ...role, + user_count: mockUserRoles.filter(ur => ur.role_id === role.id).length, + permission_count: 0 // 暂时写死 + })); + + // 分页 + const total = rolesWithCount.length; + const start = (page - 1) * pageSize; + const end = start + pageSize; + const items = rolesWithCount.slice(start, end); + + // 返回标准格式 + const response = { + code: 200, + message: 'success', + data: { + total, + page, + page_size: pageSize, + items + } + }; + + console.log('✅ [API Route] 返回数据:', { total, itemsCount: items.length }); + + return json(response, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization' + } + }); +} + +// POST - 创建角色 +export async function action({ request }: LoaderFunctionArgs) { + const method = request.method; + + if (method === 'POST') { + console.log('📡 [API Route] POST /api/v3/rbac/roles 被调用'); + + const body = await request.json(); + console.log('📋 [API Route] 请求体:', body); + + // 使用共享Mock数据 + const newRole = addRole({ + role_key: body.role_key, + role_name: body.role_name, + description: body.description || '', + data_scope: body.data_scope || 'SELF', + priority: body.priority || 10, + is_system: false + }); + + console.log('✅ [API Route] 角色创建成功:', newRole); + + return json({ + code: 200, + message: '角色创建成功', + data: newRole + }); + } + + return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 }); +} diff --git a/app/routes/api.v3.rbac.users.$userId.roles.$roleId.tsx b/app/routes/api.v3.rbac.users.$userId.roles.$roleId.tsx new file mode 100644 index 0000000..a760797 --- /dev/null +++ b/app/routes/api.v3.rbac.users.$userId.roles.$roleId.tsx @@ -0,0 +1,30 @@ +/** + * RBAC API 代理 - 移除用户角色 + * DELETE /api/v3/rbac/users/:userId/roles/:roleId + */ + +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { removeUserRole } from "~/services/rbac-mock-data.server"; + +// DELETE - 移除用户角色 +export async function action({ params }: LoaderFunctionArgs) { + const userId = parseInt(params.userId || '0'); + const roleId = parseInt(params.roleId || '0'); + + console.log('📡 [API Route] DELETE /api/v3/rbac/users/' + userId + '/roles/' + roleId); + + // 使用共享Mock数据移除角色 + const success = removeUserRole(userId, roleId); + + if (success) { + + return json({ + code: 200, + message: '用户角色移除成功' + }); + } + + return json({ + detail: '用户角色关联不存在' + }, { status: 404 }); +} diff --git a/app/routes/api.v3.rbac.users.$userId.roles.tsx b/app/routes/api.v3.rbac.users.$userId.roles.tsx new file mode 100644 index 0000000..729faf7 --- /dev/null +++ b/app/routes/api.v3.rbac.users.$userId.roles.tsx @@ -0,0 +1,38 @@ +/** + * RBAC API 代理 - 用户角色管理 + * POST /api/v3/rbac/users/:userId/roles - 为用户分配角色 + */ + +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { assignUserRole } from "~/services/rbac-mock-data.server"; + +// POST - 为用户分配角色 +export async function action({ request, params }: LoaderFunctionArgs) { + const userId = parseInt(params.userId || '0'); + const method = request.method; + + console.log('📡 [API Route]', method, '/api/v3/rbac/users/' + userId + '/roles'); + + if (method === 'POST') { + const body = await request.json(); + const roleIds = body.role_ids || []; + + console.log('📋 [API Route] 分配角色:', { userId, roleIds }); + + // 使用共享Mock数据分配角色 + roleIds.forEach((roleId: number) => { + assignUserRole(userId, roleId); + }); + + return json({ + code: 200, + message: '角色分配成功', + data: { + user_id: userId, + roles: roleIds.map((id: number) => ({ role_id: id })) + } + }); + } + + return json({ code: 405, message: 'Method Not Allowed' }, { status: 405 }); +} diff --git a/app/services/rbac-mock-data.server.ts b/app/services/rbac-mock-data.server.ts new file mode 100644 index 0000000..93f4b66 --- /dev/null +++ b/app/services/rbac-mock-data.server.ts @@ -0,0 +1,226 @@ +/** + * RBAC Mock数据 - 共享存储 + * 所有Remix API路由共享这个数据源 + */ + +// ==================== 角色数据(与数据库实际数据一致)==================== + +export const mockRoles = [ + { + id: 1, + role_key: 'admin', + role_name: '市级管理员', + description: '负责本地区的所有业务管理,不包括系统设置和角色权限管理', + data_scope: 'DEPT', + is_system: false, + priority: 0, + created_at: '2025-07-18T10:35:39+08:00', + updated_at: '2025-07-18T10:35:39+08:00' + }, + { + id: 2, + role_key: 'common', + role_name: '普通员工', + description: '仅能操作自己的数据', + data_scope: 'SELF', + is_system: false, + priority: 0, + created_at: '2025-07-18T10:35:39+08:00', + updated_at: '2025-07-18T10:35:39+08:00' + }, + { + id: 52, + role_key: 'provincial_admin', + role_name: '省级管理员', + description: '拥有全部权限,可以管理所有地区的评查点规则、提示词、动态按钮、评查组', + data_scope: 'ALL', + is_system: true, + priority: 1, + created_at: '2025-11-19T17:25:45+08:00', + updated_at: '2025-11-19T17:25:45+08:00' + } +]; + +// ==================== 用户数据 ==================== + +export const mockUsers = [ + { + id: 1, + user_id: 1, + username: 'admin', + nick_name: '系统管理员', + phone_number: '13800138000', + email: 'admin@example.com', + ou_name: '广东省烟草专卖局', + area: '广东省', + status: 1, + is_leader: true + }, + { + id: 2, + user_id: 2, + username: 'zhangsan', + nick_name: '张三', + phone_number: '13800138001', + email: 'zhangsan@example.com', + ou_name: '梅州市烟草专卖局', + area: '梅州', + status: 1, + is_leader: true + }, + { + id: 3, + user_id: 3, + username: 'lisi', + nick_name: '李四', + phone_number: '13800138002', + email: 'lisi@example.com', + ou_name: '云浮市烟草专卖局', + area: '云浮', + status: 1, + is_leader: false + }, + { + id: 4, + user_id: 4, + username: 'wangwu', + nick_name: '王五', + phone_number: '13800138003', + email: 'wangwu@example.com', + ou_name: '揭阳市烟草专卖局', + area: '揭阳', + status: 1, + is_leader: false + }, + { + id: 5, + user_id: 5, + username: 'zhaoliu', + nick_name: '赵六', + phone_number: '13800138004', + email: 'zhaoliu@example.com', + ou_name: '潮州市烟草专卖局', + area: '潮州', + status: 1, + is_leader: false + }, + { + id: 6, + user_id: 6, + username: 'sunqi', + nick_name: '孙七', + phone_number: '13800138005', + email: 'sunqi@example.com', + ou_name: '汕头市烟草专卖局', + area: '汕头', + status: 1, + is_leader: true + } +]; + +// ==================== 用户-角色关联数据 ==================== + +export const mockUserRoles: Array<{ user_id: number; role_id: number; assigned_at: string }> = [ + { user_id: 1, role_id: 52, assigned_at: '2025-01-20T10:00:00' }, // admin - provincial_admin (id=52) + { user_id: 2, role_id: 1, assigned_at: '2025-01-21T10:00:00' }, // zhangsan - 市级管理员 (id=1) + { user_id: 3, role_id: 1, assigned_at: '2025-01-21T11:00:00' }, // lisi - 市级管理员 (id=1) + { user_id: 4, role_id: 2, assigned_at: '2025-01-21T12:00:00' }, // wangwu - 普通员工 (id=2) +]; + +// ==================== 辅助函数 ==================== + +/** + * 获取角色的用户列表 + */ +export function getRoleUsers(roleId: number) { + const userIds = mockUserRoles + .filter(ur => ur.role_id === roleId) + .map(ur => ur.user_id); + + return mockUsers.filter(u => userIds.includes(u.id || u.user_id)); +} + +/** + * 为用户分配角色 + */ +export function assignUserRole(userId: number, roleId: number) { + // 检查是否已存在 + const exists = mockUserRoles.some( + ur => ur.user_id === userId && ur.role_id === roleId + ); + + if (!exists) { + mockUserRoles.push({ + user_id: userId, + role_id: roleId, + assigned_at: new Date().toISOString() + }); + console.log('✅ [Mock Data] 用户角色分配成功:', { userId, roleId }); + console.log('📊 [Mock Data] 当前用户-角色关联:', mockUserRoles); + } else { + console.log('⚠️ [Mock Data] 用户角色关联已存在:', { userId, roleId }); + } +} + +/** + * 移除用户角色 + */ +export function removeUserRole(userId: number, roleId: number) { + const index = mockUserRoles.findIndex( + ur => ur.user_id === userId && ur.role_id === roleId + ); + + if (index > -1) { + mockUserRoles.splice(index, 1); + console.log('✅ [Mock Data] 用户角色移除成功:', { userId, roleId }); + console.log('📊 [Mock Data] 当前用户-角色关联:', mockUserRoles); + return true; + } + + console.log('⚠️ [Mock Data] 用户角色关联不存在:', { userId, roleId }); + return false; +} + +/** + * 添加新角色 + */ +export function addRole(roleData: any) { + const newRole = { + id: Math.max(...mockRoles.map(r => r.id), 0) + 1, + ...roleData, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + mockRoles.push(newRole); + console.log('✅ [Mock Data] 角色创建成功:', newRole); + return newRole; +} + +/** + * 更新角色 + */ +export function updateRole(roleId: number, updates: any) { + const role = mockRoles.find(r => r.id === roleId); + if (role) { + Object.assign(role, updates, { + updated_at: new Date().toISOString() + }); + console.log('✅ [Mock Data] 角色更新成功:', role); + return role; + } + return null; +} + +/** + * 删除角色 + */ +export function deleteRole(roleId: number) { + const index = mockRoles.findIndex(r => r.id === roleId); + if (index > -1) { + const deleted = mockRoles.splice(index, 1)[0]; + console.log('✅ [Mock Data] 角色删除成功:', deleted); + return true; + } + return false; +}