fix:数据库迁移
This commit is contained in:
parent
b805f456a6
commit
dba6def3c5
4
.gitignore
vendored
4
.gitignore
vendored
@ -8,6 +8,7 @@ dist/
|
||||
*/dist/
|
||||
build/
|
||||
*/build/
|
||||
**/pnpm-lock.yaml
|
||||
|
||||
# pnpm
|
||||
.pnpm-debug.log*
|
||||
@ -31,7 +32,6 @@ build/
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
/logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
@ -45,4 +45,4 @@ coverage/
|
||||
backend/prisma/migrations/
|
||||
|
||||
tmpclaude-*
|
||||
.claude/worktrees/
|
||||
.claude/worktrees/
|
||||
|
||||
@ -46,34 +46,13 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
log.info("登录请求:username={}, tenantCode={}", username, tenantCode);
|
||||
|
||||
// 1. 根据用户名查询用户
|
||||
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getDeleted, 0));
|
||||
|
||||
if (user == null) {
|
||||
log.warn("用户不存在:{}", username);
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
|
||||
log.info("用户查询成功,ID={}, tenantId={}", user.getId(), user.getTenantId());
|
||||
log.info("数据库密码哈希:{}", user.getPassword());
|
||||
log.info("输入密码:{}", password);
|
||||
|
||||
// 2. 验证密码
|
||||
boolean passwordMatches = passwordEncoder.matches(password, user.getPassword());
|
||||
log.info("密码验证结果:{}", passwordMatches);
|
||||
if (!passwordMatches) {
|
||||
log.warn("密码不匹配,用户名:{}", username);
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 3. 验证租户
|
||||
Long tenantId;
|
||||
// 1. 确定租户(优先使用 tenantCode)
|
||||
Tenant tenant = null;
|
||||
Long tenantId = null;
|
||||
if (tenantCode != null && !tenantCode.isEmpty()) {
|
||||
// 根据租户编码查询租户
|
||||
Tenant tenant = tenantMapper.selectOne(new LambdaQueryWrapper<Tenant>()
|
||||
.eq(Tenant::getCode, tenantCode));
|
||||
tenant = tenantMapper.selectOne(new LambdaQueryWrapper<Tenant>()
|
||||
.eq(Tenant::getCode, tenantCode)
|
||||
.eq(Tenant::getDeleted, 0));
|
||||
if (tenant == null) {
|
||||
throw new BusinessException("租户不存在");
|
||||
}
|
||||
@ -81,16 +60,49 @@ public class AuthServiceImpl implements AuthService {
|
||||
throw new BusinessException("租户已失效");
|
||||
}
|
||||
tenantId = tenant.getId();
|
||||
} else {
|
||||
tenantId = user.getTenantId();
|
||||
}
|
||||
if (tenantId == null) {
|
||||
throw new BusinessException("请指定租户编码");
|
||||
}
|
||||
|
||||
// 4. 验证租户是否有效
|
||||
Tenant tenant = tenantMapper.selectById(tenantId);
|
||||
// 2. 根据租户 + 用户名查询用户(避免跨租户同名导致 selectOne 返回多条)
|
||||
User user;
|
||||
if (tenantId != null) {
|
||||
user = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getDeleted, 0));
|
||||
} else {
|
||||
// 未指定租户时,先查出所有同名用户
|
||||
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getDeleted, 0));
|
||||
if (users == null || users.isEmpty()) {
|
||||
log.warn("用户不存在:{}", username);
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
if (users.size() > 1) {
|
||||
throw new BusinessException("存在同名用户,请指定租户编码");
|
||||
}
|
||||
user = users.get(0);
|
||||
tenantId = user.getTenantId();
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
log.warn("用户不存在:{}", username);
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
|
||||
log.info("用户查询成功,ID={}, tenantId={}", user.getId(), user.getTenantId());
|
||||
|
||||
// 3. 验证密码
|
||||
if (!passwordEncoder.matches(password, user.getPassword())) {
|
||||
log.warn("密码不匹配,用户名:{}", username);
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 4. 校验租户有效性(tenantCode 未传时也要校验一次)
|
||||
if (tenant == null) {
|
||||
tenant = tenantMapper.selectById(tenantId);
|
||||
}
|
||||
if (tenant == null || tenant.getDeleted() != 0) {
|
||||
throw new BusinessException("租户不存在");
|
||||
}
|
||||
if (tenant.getValidState() != 1) {
|
||||
|
||||
@ -215,7 +215,14 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
*/
|
||||
private List<MenuTreeVO> buildTree(List<Menu> menus, Long parentId) {
|
||||
return menus.stream()
|
||||
.filter(menu -> menu.getParentId().equals(parentId))
|
||||
.filter(menu -> {
|
||||
Long pid = menu.getParentId();
|
||||
// 兼容历史数据:parent_id 为空视为根节点(0)
|
||||
if (pid == null) {
|
||||
pid = 0L;
|
||||
}
|
||||
return pid.equals(parentId);
|
||||
})
|
||||
.map(this::convertToTreeVO)
|
||||
.peek(vo -> vo.setChildren(buildTree(menus, vo.getId())))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@ -55,16 +55,24 @@ public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
log.debug("通过用户 ID 找到用户:{}, username: {}", userId, user.getUsername());
|
||||
} else {
|
||||
// 如果通过 ID 没找到,尝试通过用户名查询
|
||||
user = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getDeleted, 0));
|
||||
if (users.size() > 1) {
|
||||
throw new UsernameNotFoundException("存在同名用户,请使用用户ID进行认证");
|
||||
}
|
||||
user = users.isEmpty() ? null : users.get(0);
|
||||
log.debug("通过用户名查询用户:{}, 结果:{}", username, user != null ? "找到" : "未找到");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 不是数字,通过用户名查询
|
||||
user = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getDeleted, 0));
|
||||
if (users.size() > 1) {
|
||||
throw new UsernameNotFoundException("存在同名用户,请使用用户ID进行认证");
|
||||
}
|
||||
user = users.isEmpty() ? null : users.get(0);
|
||||
log.debug("通过用户名查询用户:{}, 结果:{}", username, user != null ? "找到" : "未找到");
|
||||
}
|
||||
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
-- ============================================
|
||||
-- 添加缺失的权限(菜单、活动、评审等)
|
||||
-- ============================================
|
||||
|
||||
-- 添加菜单管理权限
|
||||
INSERT INTO t_auth_permission (tenant_id, name, code, resource, action, description, valid_state) VALUES
|
||||
(1, '菜单创建', 'menu:create', 'menu', 'create', '创建菜单权限', 1),
|
||||
(1, '菜单查询', 'menu:read', 'menu', 'read', '查询菜单权限', 1),
|
||||
(1, '菜单更新', 'menu:update', 'menu', 'update', '更新菜单权限', 1),
|
||||
(1, '菜单删除', 'menu:delete', 'menu', 'delete', '删除菜单权限', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 添加活动管理权限
|
||||
INSERT INTO t_auth_permission (tenant_id, name, code, resource, action, description, valid_state) VALUES
|
||||
(1, '活动创建', 'contest:create', 'contest', 'create', '创建活动权限', 1),
|
||||
(1, '活动查询', 'contest:read', 'contest', 'read', '查询活动权限', 1),
|
||||
(1, '活动更新', 'contest:update', 'contest', 'update', '更新活动权限', 1),
|
||||
(1, '活动删除', 'contest:delete', 'contest', 'delete', '删除活动权限', 1),
|
||||
(1, '活动报名', 'contest:register', 'contest', 'register', '活动报名权限', 1),
|
||||
(1, '活动评审', 'contest:review', 'contest', 'review', '活动评审权限', 1),
|
||||
(1, '活动作品', 'contest:work', 'contest', 'work', '活动作品权限', 1),
|
||||
(1, '活动评委', 'contest:judge', 'contest', 'judge', '活动评委权限', 1),
|
||||
(1, '活动结果', 'contest:result', 'contest', 'result', '活动结果权限', 1),
|
||||
(1, '活动规则', 'contest:rule', 'contest', 'rule', '活动规则权限', 1),
|
||||
(1, '活动公告', 'contest:notice', 'contest', 'notice', '活动公告权限', 1),
|
||||
(1, '活动评审分配', 'contest:review:assign', 'contest', 'review:assign', '活动评审分配权限', 1),
|
||||
(1, '活动评审评分', 'contest:review:score', 'contest', 'review:score', '活动评审评分权限', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 添加学校管理权限
|
||||
INSERT INTO t_auth_permission (tenant_id, name, code, resource, action, description, valid_state) VALUES
|
||||
(1, '学校查询', 'school:read', 'school', 'read', '查询学校权限', 1),
|
||||
(1, '部门查询', 'department:read', 'department', 'read', '查询部门权限', 1),
|
||||
(1, '年级查询', 'grade:read', 'grade', 'read', '查询年级权限', 1),
|
||||
(1, '班级查询', 'class:read', 'class', 'read', '查询班级权限', 1),
|
||||
(1, '教师查询', 'teacher:read', 'teacher', 'read', '查询教师权限', 1),
|
||||
(1, '学生查询', 'student:read', 'student', 'read', '查询学生权限', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 添加系统管理权限
|
||||
INSERT INTO t_auth_permission (tenant_id, name, code, resource, action, description, valid_state) VALUES
|
||||
(1, '字典查询', 'dict:read', 'dict', 'read', '查询字典权限', 1),
|
||||
(1, '配置查询', 'config:read', 'config', 'read', '查询配置权限', 1),
|
||||
(1, '日志查询', 'log:read', 'log', 'read', '查询日志权限', 1),
|
||||
(1, '权限查询', 'permission:read', 'permission', 'read', '查询权限权限', 1),
|
||||
(1, '权限创建', 'permission:create', 'permission', 'create', '创建权限权限', 1),
|
||||
(1, '权限更新', 'permission:update', 'permission', 'update', '更新权限权限', 1),
|
||||
(1, '权限删除', 'permission:delete', 'permission', 'delete', '删除权限权限', 1)
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- 获取刚插入的权限 ID 并关联到 super_admin 角色 (id=1)
|
||||
-- 菜单管理权限
|
||||
INSERT INTO t_auth_role_permission (role_id, permission_id)
|
||||
SELECT 1, id FROM t_auth_permission WHERE code IN ('menu:create', 'menu:read', 'menu:update', 'menu:delete')
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
-- 活动管理权限
|
||||
INSERT INTO t_auth_role_permission (role_id, permission_id)
|
||||
SELECT 1, id FROM t_auth_permission WHERE code LIKE 'contest%'
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
-- 学校管理权限
|
||||
INSERT INTO t_auth_role_permission (role_id, permission_id)
|
||||
SELECT 1, id FROM t_auth_permission WHERE code IN ('school:read', 'department:read', 'grade:read', 'class:read', 'teacher:read', 'student:read')
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
-- 系统管理权限
|
||||
INSERT INTO t_auth_role_permission (role_id, permission_id)
|
||||
SELECT 1, id FROM t_auth_permission WHERE code IN ('dict:read', 'config:read', 'log:read', 'permission:read', 'permission:create', 'permission:update', 'permission:delete')
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
@ -1,2 +1,2 @@
|
||||
# 开发环境
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_BASE_URL=
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# 生产环境
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_BASE_URL=
|
||||
# 如果后端部署在不同域名,可以改成完整地址:
|
||||
# VITE_API_BASE_URL=https://api.your-domain.com
|
||||
|
||||
|
||||
@ -75,6 +75,11 @@ const componentMap: Record<string, () => Promise<any>> = {
|
||||
import("@/views/system/public-users/Index.vue"),
|
||||
}
|
||||
|
||||
// 用于兜底加载未在 componentMap 中声明的视图组件(避免 Vite 动态 import 分析警告)
|
||||
const viewComponentModules = import.meta.glob(
|
||||
"@/views/**/*.vue"
|
||||
) as Record<string, () => Promise<any>>
|
||||
|
||||
/**
|
||||
* 获取图标组件
|
||||
*/
|
||||
@ -89,7 +94,7 @@ export function getIconComponent(iconName: string | null | undefined) {
|
||||
|
||||
/**
|
||||
* 从菜单路径生成路由名称(与 convertMenusToRoutes 中的逻辑一致)
|
||||
* 如果路径相同,使用菜单ID来区分,避免路由名称冲突
|
||||
* 使用菜单ID来区分,避免路由名称冲突(同一路径可能出现在不同菜单树分支)
|
||||
*/
|
||||
function getRouteNameFromPath(
|
||||
path: string | null | undefined,
|
||||
@ -101,8 +106,8 @@ function getRouteNameFromPath(
|
||||
.split("/")
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join("")
|
||||
// 如果是子菜单,添加后缀以避免与父菜单路由名称冲突
|
||||
return isChild ? `${baseName}${menuId}` : baseName
|
||||
// 统一添加 menuId,避免顶级菜单同名(例如多个“/contests”菜单)导致路由被覆盖
|
||||
return `${baseName}${menuId}`
|
||||
}
|
||||
return `Menu${menuId}`
|
||||
}
|
||||
@ -255,13 +260,21 @@ export function convertMenusToRoutes(
|
||||
console.warn(
|
||||
`组件路径 "${componentPath}" (key: "${componentKey}") 未在 componentMap 中定义,请添加到 menu.ts 的 componentMap 中`
|
||||
)
|
||||
// 如果找不到映射,尝试直接导入(可能会失败,但至少不会阻塞)
|
||||
componentLoader = () =>
|
||||
import(
|
||||
/* @vite-ignore */ componentPath.startsWith("@/")
|
||||
? componentPath
|
||||
: `@/views/${componentPath}.vue`
|
||||
)
|
||||
// 如果找不到映射,使用 import.meta.glob 的模块映射进行兜底加载(可被 Vite 分析)
|
||||
let normalizedPath = componentPath.startsWith("@/")
|
||||
? componentPath
|
||||
: `@/views/${componentPath}`
|
||||
if (!normalizedPath.endsWith(".vue")) {
|
||||
normalizedPath = `${normalizedPath}.vue`
|
||||
}
|
||||
|
||||
const fallbackLoader = viewComponentModules[normalizedPath]
|
||||
if (fallbackLoader) {
|
||||
componentLoader = fallbackLoader
|
||||
} else {
|
||||
console.warn(`组件路径 "${normalizedPath}" 未找到对应的视图文件,已跳过渲染该菜单`)
|
||||
componentLoader = undefined
|
||||
}
|
||||
}
|
||||
} else if (menu.children && menu.children.length > 0) {
|
||||
// 如果没有 component 但有子菜单,使用空布局组件来渲染子路由
|
||||
|
||||
@ -2,35 +2,47 @@ import axios, {
|
||||
InternalAxiosRequestConfig,
|
||||
type AxiosInstance,
|
||||
type AxiosResponse,
|
||||
} from "axios"
|
||||
import { message } from "ant-design-vue"
|
||||
import { getToken, removeToken, getTenantCode } from "./auth"
|
||||
import { useAuthStore } from "@/stores/auth"
|
||||
import router from "@/router"
|
||||
} from "axios";
|
||||
import { message } from "ant-design-vue";
|
||||
import { getToken, removeToken, getTenantCode } from "./auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import router from "@/router";
|
||||
|
||||
// 扩展 AxiosInstance 类型,添加 postForm 和 putForm 方法
|
||||
interface ExtendedAxiosInstance extends AxiosInstance {
|
||||
postForm<T = any, R = T>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<R>
|
||||
putForm<T = any, R = T>(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise<R>
|
||||
postForm<T = any, R = T>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
): Promise<R>;
|
||||
putForm<T = any, R = T>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
): Promise<R>;
|
||||
}
|
||||
|
||||
const service: ExtendedAxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || "",
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// 添加表单提交的便捷方法
|
||||
service.postForm = function (url: string, data?: any, config?: InternalAxiosRequestConfig) {
|
||||
const formData = new URLSearchParams()
|
||||
service.postForm = function (
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) {
|
||||
const formData = new URLSearchParams();
|
||||
if (data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value != null) {
|
||||
formData.append(key, String(value))
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
return this.post(url, formData.toString(), {
|
||||
...config,
|
||||
@ -38,17 +50,21 @@ service.postForm = function (url: string, data?: any, config?: InternalAxiosRequ
|
||||
...config?.headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
service.putForm = function (url: string, data?: any, config?: InternalAxiosRequestConfig) {
|
||||
const formData = new URLSearchParams()
|
||||
service.putForm = function (
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) {
|
||||
const formData = new URLSearchParams();
|
||||
if (data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value != null) {
|
||||
formData.append(key, String(value))
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
return this.put(url, formData.toString(), {
|
||||
...config,
|
||||
@ -56,97 +72,97 @@ service.putForm = function (url: string, data?: any, config?: InternalAxiosReque
|
||||
...config?.headers,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Request interceptor
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const token = getToken()
|
||||
const token = getToken();
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 添加租户信息到请求头
|
||||
// 租户编码从 URL 获取,租户ID 从用户信息获取
|
||||
const tenantCode = getTenantCode()
|
||||
const authStore = useAuthStore()
|
||||
const tenantId = authStore.user?.tenantId
|
||||
const tenantCode = getTenantCode();
|
||||
const authStore = useAuthStore();
|
||||
const tenantId = authStore.user?.tenantId;
|
||||
|
||||
if (config.headers) {
|
||||
if (tenantCode) {
|
||||
config.headers["X-Tenant-Code"] = tenantCode
|
||||
config.headers["X-Tenant-Code"] = tenantCode;
|
||||
}
|
||||
if (tenantId) {
|
||||
config.headers["X-Tenant-Id"] = String(tenantId)
|
||||
config.headers["X-Tenant-Id"] = String(tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const res = response.data
|
||||
const res = response.data;
|
||||
|
||||
// 如果响应已经是统一格式 { code, message, data }
|
||||
if (res && typeof res === "object" && "code" in res) {
|
||||
if (res.code !== 200 && res.code !== 0) {
|
||||
message.error(res.message || "请求失败")
|
||||
message.error(res.message || "请求失败");
|
||||
|
||||
if (res.code === 401) {
|
||||
removeToken()
|
||||
removeToken();
|
||||
// 从 URL 获取租户编码,跳转到对应的登录页
|
||||
const path = window.location.pathname
|
||||
const match = path.match(/^\/([^/]+)/)
|
||||
const tenantCode = match ? match[1] : null
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/^\/([^/]+)/);
|
||||
const tenantCode = match ? match[1] : null;
|
||||
if (tenantCode && tenantCode !== "login") {
|
||||
router.push(`/${tenantCode}/login`)
|
||||
router.push(`/${tenantCode}/login`);
|
||||
} else {
|
||||
router.push("/login")
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(res.message || "请求失败"))
|
||||
return Promise.reject(new Error(res.message || "请求失败"));
|
||||
}
|
||||
|
||||
return res.data !== undefined ? res.data : res
|
||||
return res.data !== undefined ? res.data : res;
|
||||
}
|
||||
|
||||
// 如果响应是直接的数据,直接返回
|
||||
return res
|
||||
return res;
|
||||
},
|
||||
(error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message || error.message || "网络错误"
|
||||
|
||||
error.response?.data?.message || error.message || "网络错误";
|
||||
|
||||
// 403 权限错误显示更友好的提示
|
||||
if (error.response?.status === 403) {
|
||||
message.error(errorMessage || "您没有权限执行此操作")
|
||||
message.error(errorMessage || "您没有权限执行此操作");
|
||||
} else {
|
||||
message.error(errorMessage)
|
||||
message.error(errorMessage);
|
||||
}
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
removeToken()
|
||||
removeToken();
|
||||
// 从 URL 获取租户编码,跳转到对应的登录页
|
||||
const path = window.location.pathname
|
||||
const match = path.match(/^\/([^/]+)/)
|
||||
const tenantCode = match ? match[1] : null
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/^\/([^/]+)/);
|
||||
const tenantCode = match ? match[1] : null;
|
||||
if (tenantCode && tenantCode !== "login") {
|
||||
router.push(`/${tenantCode}/login`)
|
||||
router.push(`/${tenantCode}/login`);
|
||||
} else {
|
||||
router.push("/login")
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export default service
|
||||
export default service;
|
||||
|
||||
@ -17,31 +17,16 @@
|
||||
|
||||
<!-- 开发环境快捷切换 -->
|
||||
<div v-if="isDev" class="tenant-tabs">
|
||||
<div
|
||||
v-for="tab in tenantTabs"
|
||||
:key="tab.code"
|
||||
:class="['tenant-tab', { active: activeTab === tab.code }]"
|
||||
@click="switchTab(tab.code)"
|
||||
>
|
||||
<div v-for="tab in tenantTabs" :key="tab.code" :class="['tenant-tab', { active: activeTab === tab.code }]"
|
||||
@click="switchTab(tab.code)">
|
||||
<component :is="tab.icon" />
|
||||
<span>{{ tab.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@finish="handleSubmit"
|
||||
class="login-form"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form :model="form" :rules="rules" @finish="handleSubmit" class="login-form" layout="vertical">
|
||||
<a-form-item name="username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="form.username"
|
||||
size="large"
|
||||
placeholder="请输入用户名"
|
||||
class="custom-input"
|
||||
>
|
||||
<a-input v-model:value="form.username" size="large" placeholder="请输入用户名" class="custom-input">
|
||||
<template #prefix>
|
||||
<UserOutlined class="input-icon" />
|
||||
</template>
|
||||
@ -49,12 +34,7 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password" label="密码">
|
||||
<a-input-password
|
||||
v-model:value="form.password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
class="custom-input"
|
||||
>
|
||||
<a-input-password v-model:value="form.password" size="large" placeholder="请输入密码" class="custom-input">
|
||||
<template #prefix>
|
||||
<LockOutlined class="input-icon" />
|
||||
</template>
|
||||
@ -66,14 +46,7 @@
|
||||
</div>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
block
|
||||
class="login-btn"
|
||||
>
|
||||
<a-button type="primary" html-type="submit" size="large" :loading="loading" block class="login-btn">
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
@ -118,7 +91,7 @@ const isDev = import.meta.env.DEV
|
||||
|
||||
// 开发环境快捷切换 — 按新架构设计
|
||||
const tenantTabs = [
|
||||
{ code: "super", name: "平台超管", icon: SafetyOutlined, username: "admin", password: "admin123" },
|
||||
{ code: "super", name: "平台超管", icon: SafetyOutlined, username: "admin", password: "admin@super" },
|
||||
{ code: "gdlib", name: "广东省图", icon: BankOutlined, username: "admin", password: "admin@gdlib" },
|
||||
{ code: "judge", name: "评委端", icon: TrophyOutlined, username: "admin", password: "admin@judge" },
|
||||
]
|
||||
@ -302,15 +275,20 @@ $teal: #14b8a6;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translate(25px, -25px) scale(1.04);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-15px, 15px) scale(0.96);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translate(-25px, -15px) scale(1.02);
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { resolve } from "path";
|
||||
// 根据环境设置 base 路径
|
||||
var getBase = function (mode) {
|
||||
switch (mode) {
|
||||
case "test":
|
||||
return "/web-test/";
|
||||
case "production":
|
||||
return "/web/";
|
||||
default:
|
||||
return "/";
|
||||
}
|
||||
};
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(function (_a) {
|
||||
var mode = _a.mode;
|
||||
return {
|
||||
base: getBase(mode),
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8580",
|
||||
changeOrigin: true,
|
||||
rewrite: function (path) { return path.replace(/^\/api/, ''); },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -1,18 +1,18 @@
|
||||
import { defineConfig } from "vite"
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import { resolve } from "path"
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { resolve } from "path";
|
||||
|
||||
// 根据环境设置 base 路径
|
||||
const getBase = (mode: string) => {
|
||||
switch (mode) {
|
||||
case "test":
|
||||
return "/web-test/"
|
||||
return "/web-test/";
|
||||
case "production":
|
||||
return "/web/"
|
||||
return "/web/";
|
||||
default:
|
||||
return "/"
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
@ -28,11 +28,11 @@ export default defineConfig(({ mode }) => {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3234",
|
||||
target: "http://localhost:8580",
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
117
pnpm-lock.yaml
generated
117
pnpm-lock.yaml
generated
@ -235,88 +235,6 @@ importers:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(typescript@5.9.3)
|
||||
|
||||
java-frontend:
|
||||
dependencies:
|
||||
'@ant-design/icons-vue':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(vue@3.5.24(typescript@5.9.3))
|
||||
'@vee-validate/zod':
|
||||
specifier: ^4.12.4
|
||||
version: 4.15.1(vue@3.5.24(typescript@5.9.3))(zod@3.25.76)
|
||||
'@wangeditor/editor':
|
||||
specifier: ^5.1.23
|
||||
version: 5.1.23
|
||||
'@wangeditor/editor-for-vue':
|
||||
specifier: ^5.1.12
|
||||
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.24(typescript@5.9.3))
|
||||
ant-design-vue:
|
||||
specifier: ^4.1.1
|
||||
version: 4.2.6(vue@3.5.24(typescript@5.9.3))
|
||||
axios:
|
||||
specifier: ^1.6.7
|
||||
version: 1.13.2
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.19
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.3.1(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
|
||||
three:
|
||||
specifier: ^0.182.0
|
||||
version: 0.182.0
|
||||
vee-validate:
|
||||
specifier: ^4.12.4
|
||||
version: 4.15.1(vue@3.5.24(typescript@5.9.3))
|
||||
vue:
|
||||
specifier: ^3.4.21
|
||||
version: 3.5.24(typescript@5.9.3)
|
||||
vue-router:
|
||||
specifier: ^4.3.0
|
||||
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
|
||||
zod:
|
||||
specifier: ^3.22.4
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.58.2
|
||||
version: 1.58.2
|
||||
'@types/multer':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.0.4
|
||||
version: 5.2.4(vite@5.4.21(@types/node@20.19.25)(sass@1.94.1)(terser@5.44.1))(vue@3.5.24(typescript@5.9.3))
|
||||
'@vue/eslint-config-typescript':
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0(eslint-plugin-vue@9.33.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.18
|
||||
version: 10.4.22(postcss@8.5.6)
|
||||
eslint:
|
||||
specifier: ^8.57.0
|
||||
version: 8.57.1
|
||||
eslint-plugin-vue:
|
||||
specifier: ^9.22.0
|
||||
version: 9.33.0(eslint@8.57.1)
|
||||
postcss:
|
||||
specifier: ^8.4.35
|
||||
version: 8.5.6
|
||||
sass:
|
||||
specifier: ^1.71.1
|
||||
version: 1.94.1
|
||||
tailwindcss:
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.18
|
||||
typescript:
|
||||
specifier: ^5.4.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^5.1.6
|
||||
version: 5.4.21(@types/node@20.19.25)(sass@1.94.1)(terser@5.44.1)
|
||||
vue-tsc:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
@ -1029,11 +947,6 @@ packages:
|
||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/test@1.58.2':
|
||||
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@prisma/client@6.19.0':
|
||||
resolution: {integrity: sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==}
|
||||
engines: {node: '>=18.18'}
|
||||
@ -2706,11 +2619,6 @@ packages:
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@ -3721,16 +3629,6 @@ packages:
|
||||
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
playwright-core@1.58.2:
|
||||
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.58.2:
|
||||
resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
@ -5435,10 +5333,6 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.2.9': {}
|
||||
|
||||
'@playwright/test@1.58.2':
|
||||
dependencies:
|
||||
playwright: 1.58.2
|
||||
|
||||
'@prisma/client@6.19.0(prisma@6.19.0(typescript@5.9.3))(typescript@5.9.3)':
|
||||
optionalDependencies:
|
||||
prisma: 6.19.0(typescript@5.9.3)
|
||||
@ -7374,9 +7268,6 @@ snapshots:
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@ -8511,14 +8402,6 @@ snapshots:
|
||||
dependencies:
|
||||
find-up: 3.0.0
|
||||
|
||||
playwright-core@1.58.2: {}
|
||||
|
||||
playwright@1.58.2:
|
||||
dependencies:
|
||||
playwright-core: 1.58.2
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.5.6):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user