gateway tomcat去除
This commit is contained in:
@@ -63,7 +63,7 @@ CREATE TABLE workcase.tb_chat_room_member(
|
||||
member_id VARCHAR(50) NOT NULL, -- 成员记录ID
|
||||
room_id VARCHAR(50) NOT NULL, -- 聊天室ID
|
||||
user_id VARCHAR(50) NOT NULL, -- 用户ID(来客ID或员工ID)
|
||||
user_type VARCHAR(20) NOT NULL, -- 用户类型:guest-来客 stuff-客服 ai-AI助手
|
||||
user_type VARCHAR(20) NOT NULL, -- 用户类型:guest-来客 staff-客服 ai-AI助手
|
||||
user_name VARCHAR(100) NOT NULL, -- 用户名称
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 状态:active-活跃 left-已离开 removed-被移除
|
||||
unread_count INTEGER NOT NULL DEFAULT 0, -- 该成员的未读消息数
|
||||
|
||||
@@ -30,8 +30,8 @@ INSERT INTO sys.tb_sys_role (
|
||||
('ROLE-0003', 'role_user', '普通用户', '系统普通用户角色',
|
||||
'global', NULL, true, 'system', NULL, now(), false),
|
||||
|
||||
-- 访客(全局)
|
||||
('ROLE-0004', 'role_guest', '访客', '系统访客角色,仅限查看基础信息',
|
||||
-- 访客(全局)- 注册用户默认角色,具备客服聊天和工单的所有接口权限
|
||||
('ROLE-0004', 'role_guest', '访客', '访客角色,具备客服聊天和工单的所有接口权限',
|
||||
'global', NULL, true, 'system', NULL, now(), false);
|
||||
|
||||
-- =============================
|
||||
@@ -138,7 +138,30 @@ INSERT INTO sys.tb_sys_permission (
|
||||
('PERM-0624', 'perm_workcase_tickets', '工单管理', 'workcase:tickets:view', '访问工单管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0625', 'perm_workcase_conversation', '对话数据', 'workcase:conversation:view', '访问对话数据管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0626', 'perm_workcase_agent', '智能体管理', 'workcase:agent:view', '访问智能体管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0627', 'perm_workcase_log', '日志管理', 'workcase:log:view', '访问日志管理', 'module_workcase', true, 'system', NULL, now(), false);
|
||||
('PERM-0627', 'perm_workcase_log', '日志管理', 'workcase:log:view', '访问日志管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0628', 'perm_workcase_chatroom', '聊天室控制台', 'workcase:chatroom:view', '访问聊天室控制台', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
|
||||
-- Workcase 接口权限(访客用户可用)
|
||||
-- AI对话接口权限
|
||||
('PERM-0701', 'perm_workcase_chat_create', 'AI对话创建', 'workcase:chat:create', '创建对话', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0702', 'perm_workcase_chat_update', 'AI对话更新', 'workcase:chat:update', '更新对话', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0703', 'perm_workcase_chat_list', 'AI对话查询', 'workcase:chat:list', '查询对话列表', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0704', 'perm_workcase_chat_message', 'AI对话消息', 'workcase:chat:message', '获取对话消息列表', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0705', 'perm_workcase_chat_stream', 'AI流式对话', 'workcase:chat:stream', '流式对话接口', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0706', 'perm_workcase_chat_analyze', 'AI对话分析', 'workcase:chat:analyze', '分析对话生成工单信息', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
-- 聊天室接口权限
|
||||
('PERM-0711', 'perm_workcase_room_create', '聊天室创建', 'workcase:room:create', '创建聊天室', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0712', 'perm_workcase_room_update', '聊天室更新', 'workcase:room:update', '更新聊天室', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0713', 'perm_workcase_room_close', '聊天室关闭', 'workcase:room:close', '关闭聊天室', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0714', 'perm_workcase_room_view', '聊天室查看', 'workcase:room:view', '查看聊天室', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0715', 'perm_workcase_room_member', '聊天室成员', 'workcase:room:member', '管理聊天室成员', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0716', 'perm_workcase_room_message', '聊天室消息', 'workcase:room:message', '发送和查看聊天室消息', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
-- 工单接口权限
|
||||
('PERM-0721', 'perm_workcase_ticket_create', '工单创建', 'workcase:ticket:create', '创建工单', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0722', 'perm_workcase_ticket_update', '工单更新', 'workcase:ticket:update', '更新工单', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0723', 'perm_workcase_ticket_view', '工单查看', 'workcase:ticket:view', '查看工单详情和列表', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0724', 'perm_workcase_ticket_process', '工单处理', 'workcase:ticket:process', '工单处理过程管理', 'module_workcase', true, 'system', NULL, now(), false),
|
||||
('PERM-0725', 'perm_workcase_ticket_device', '工单设备', 'workcase:ticket:device', '工单设备管理', 'module_workcase', true, 'system', NULL, now(), false);
|
||||
-- =============================
|
||||
-- 5. 初始化视图(菜单)
|
||||
-- =============================
|
||||
@@ -224,6 +247,9 @@ INSERT INTO sys.tb_sys_view (
|
||||
('VIEW-W001', 'view_workcase_home', '智能客服', NULL, '/aichat', 'public/AIChat/AIChatView.vue', 'Home', 1,
|
||||
'route', NULL, 'workcase', 'SubSidebarLayout', 10, '智能客服首页', 'system', now(), false),
|
||||
|
||||
('VIEW-W002', 'view_workcase_chatroom', '聊天室控制台', NULL, '/chatroom', 'public/ChatRoom/ChatRoomView.vue', 'MessageSquare', 1,
|
||||
'route', NULL, 'workcase', 'SubSidebarLayout', 20, '实时聊天室控制台', 'system', now(), false),
|
||||
|
||||
-- 管理端视图(使用 SubSidebarLayout 布局)
|
||||
('VIEW-W101', 'view_workcase_admin_overview', '数据概览', NULL, '/admin/overview', 'admin/overview/OverviewView.vue', 'BarChart3', 1,
|
||||
'route', NULL, 'workcase', 'SubSidebarLayout', 110, '泰豪小电数据概览', 'system', now(), false),
|
||||
@@ -308,17 +334,39 @@ INSERT INTO sys.tb_sys_role_permission (
|
||||
('RP-U-0015', 'role_user', 'perm_message_view', 'system', NULL, now(), false),
|
||||
('RP-U-0016', 'role_user', 'perm_config_view', 'system', NULL, now(), false);
|
||||
|
||||
-- 访客权限(仅查看 + 基础菜单访问)
|
||||
-- 访客权限(基础菜单 + workcase聊天和工单全部接口权限)
|
||||
INSERT INTO sys.tb_sys_role_permission (
|
||||
optsn, role_id, permission_id, creator, dept_path, create_time, deleted
|
||||
) VALUES
|
||||
-- 平台基础菜单访问权限
|
||||
('RP-G-0001', 'role_guest', 'perm_platform_home', 'system', NULL, now(), false),
|
||||
('RP-G-0002', 'role_guest', 'perm_platform_chat', 'system', NULL, now(), false),
|
||||
('RP-G-0003', 'role_guest', 'perm_platform_workcase', 'system', NULL, now(), false),
|
||||
-- 系统功能权限(仅查看)
|
||||
('RP-G-0011', 'role_guest', 'perm_user_view', 'system', NULL, now(), false),
|
||||
('RP-G-0012', 'role_guest', 'perm_file_view', 'system', NULL, now(), false),
|
||||
('RP-G-0013', 'role_guest', 'perm_message_view', 'system', NULL, now(), false);
|
||||
('RP-G-0013', 'role_guest', 'perm_message_view', 'system', NULL, now(), false),
|
||||
-- Workcase AI对话接口权限
|
||||
('RP-G-0021', 'role_guest', 'perm_workcase_chat_create', 'system', NULL, now(), false),
|
||||
('RP-G-0022', 'role_guest', 'perm_workcase_chat_update', 'system', NULL, now(), false),
|
||||
('RP-G-0023', 'role_guest', 'perm_workcase_chat_list', 'system', NULL, now(), false),
|
||||
('RP-G-0024', 'role_guest', 'perm_workcase_chat_message', 'system', NULL, now(), false),
|
||||
('RP-G-0025', 'role_guest', 'perm_workcase_chat_stream', 'system', NULL, now(), false),
|
||||
('RP-G-0026', 'role_guest', 'perm_workcase_chat_analyze', 'system', NULL, now(), false),
|
||||
-- Workcase 聊天室接口权限
|
||||
('RP-G-0031', 'role_guest', 'perm_workcase_room_create', 'system', NULL, now(), false),
|
||||
('RP-G-0032', 'role_guest', 'perm_workcase_room_update', 'system', NULL, now(), false),
|
||||
('RP-G-0033', 'role_guest', 'perm_workcase_room_close', 'system', NULL, now(), false),
|
||||
('RP-G-0034', 'role_guest', 'perm_workcase_room_view', 'system', NULL, now(), false),
|
||||
('RP-G-0035', 'role_guest', 'perm_workcase_room_member', 'system', NULL, now(), false),
|
||||
('RP-G-0036', 'role_guest', 'perm_workcase_room_message', 'system', NULL, now(), false),
|
||||
('RP-G-0037', 'role_guest', 'perm_workcase_chatroom', 'system', NULL, now(), false),
|
||||
-- Workcase 工单接口权限
|
||||
('RP-G-0041', 'role_guest', 'perm_workcase_ticket_create', 'system', NULL, now(), false),
|
||||
('RP-G-0042', 'role_guest', 'perm_workcase_ticket_update', 'system', NULL, now(), false),
|
||||
('RP-G-0043', 'role_guest', 'perm_workcase_ticket_view', 'system', NULL, now(), false),
|
||||
('RP-G-0044', 'role_guest', 'perm_workcase_ticket_process', 'system', NULL, now(), false),
|
||||
('RP-G-0045', 'role_guest', 'perm_workcase_ticket_device', 'system', NULL, now(), false);
|
||||
|
||||
-- =============================
|
||||
-- 7. 视图权限关联
|
||||
@@ -345,10 +393,9 @@ INSERT INTO sys.tb_sys_view_permission (
|
||||
('VP-P203', 'view_platform_admin_knowledge', 'perm_platform_admin_knowledge', 'system', NULL, now(), false),
|
||||
('VP-P204', 'view_platform_admin_config', 'perm_platform_admin_config', 'system', NULL, now(), false),
|
||||
|
||||
-- Workcase服务用户端视图关联(使用同一个workcase访问权限)
|
||||
-- Workcase服务用户端视图关联
|
||||
('VP-W001', 'view_workcase_home', 'perm_platform_workcase', 'system', NULL, now(), false),
|
||||
('VP-W002', 'view_workcase_list', 'perm_platform_workcase', 'system', NULL, now(), false),
|
||||
('VP-W003', 'view_workcase_detail', 'perm_platform_workcase', 'system', NULL, now(), false),
|
||||
('VP-W002', 'view_workcase_chatroom', 'perm_workcase_chatroom', 'system', NULL, now(), false),
|
||||
|
||||
-- Workcase服务管理端视图关联
|
||||
('VP-W101', 'view_workcase_admin_overview', 'perm_workcase_overview', 'system', NULL, now(), false),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 初始化聊天室人员
|
||||
-- user_admin
|
||||
INSERT INTO workcase.tb_chat_room_member(
|
||||
optsn, member_id, room_id, user_id, user_type, user_name, status, unread_count, last_read_time, last_read_msg_id, join_time, leave_time, creator, create_time, update_time
|
||||
) VALUES
|
||||
('MEM-0001', 'member_admin', 'room_0001', 'user_admin', 'staff', '系统管理员', 'active', 0, null, null, now(), null, 'system', now(), null);
|
||||
@@ -104,6 +104,11 @@
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp-sse</artifactId>
|
||||
</dependency>
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -82,6 +82,35 @@
|
||||
<groupId>org.apache.dubbo</groupId>
|
||||
<artifactId>dubbo</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jakarta Servlet API (用于 HttpServletRequest 等) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Web (用于 MultipartFile 等) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring WebMVC (用于 SseEmitter 等) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger/OpenAPI 注解 (用于 @Schema 等) -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||
<version>2.2.36</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -59,6 +59,24 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -100,14 +100,13 @@ public class GatewayAuthConfig {
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
try {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
logger.info("Authorization header: {}", authHeader != null ? "Bearer ***" : "null");
|
||||
String token = extractToken(request);
|
||||
logger.debug("提取到Token: {}", token != null ? "***" : "null");
|
||||
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER_PREFIX)) {
|
||||
String token = authHeader.substring(BEARER_PREFIX.length());
|
||||
if (StringUtils.hasText(token)) {
|
||||
String cacheKey = LOGIN_TOKEN_PREFIX + token;
|
||||
LoginDomain login = redisService.get(cacheKey, LoginDomain.class);
|
||||
logger.info("Redis key: {}, login: {}", cacheKey, login != null ? "loaded" : "null");
|
||||
logger.debug("Redis key: {}, login: {}", cacheKey, login != null ? "loaded" : "null");
|
||||
|
||||
if (login != null) {
|
||||
if (login.getUserPermissions() != null) {
|
||||
@@ -118,7 +117,7 @@ public class GatewayAuthConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info("加载用户权限: {} 个", authorities.size());
|
||||
logger.debug("加载用户权限: {} 个", authorities.size());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -127,5 +126,25 @@ public class GatewayAuthConfig {
|
||||
|
||||
return authorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头或URL参数提取Token
|
||||
*/
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
// 1. 优先从请求头获取
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER_PREFIX)) {
|
||||
return authHeader.substring(BEARER_PREFIX.length()).trim();
|
||||
}
|
||||
|
||||
// 2. 从URL参数获取(用于WebSocket连接)
|
||||
String tokenParam = request.getParameter("token");
|
||||
if (StringUtils.hasText(tokenParam)) {
|
||||
logger.debug("从URL参数获取Token");
|
||||
return tokenParam.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
// 从Redis加载 LoginDomain,并将权限装配到 Spring Security 上下文
|
||||
if (redisService != null) {
|
||||
Object obj = redisService.get(REDIS_LOGIN_PREFIX + userId);
|
||||
Object obj = redisService.get(REDIS_LOGIN_PREFIX + token);
|
||||
if (obj instanceof LoginDomain loginDomain) {
|
||||
// 组装权限码 authorities(已存在)
|
||||
List<SimpleGrantedAuthority> permAuthorities = null;
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
<artifactId>common-utils</artifactId>
|
||||
<version>${urban-lifeline.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger/OpenAPI 注解 (用于 @Schema 等) -->
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||
<version>2.2.36</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -23,4 +23,10 @@ public class TbSysUserRoleDTO extends BaseDTO {
|
||||
|
||||
@Schema(description = "角色ID")
|
||||
private String roleId;
|
||||
|
||||
@Schema(description = "部门ID")
|
||||
private String deptId;
|
||||
|
||||
@Schema(description = "部门全路径")
|
||||
private String deptPath;
|
||||
}
|
||||
@@ -19,6 +19,25 @@
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Jakarta Servlet API (用于 ServletUtils) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Web (用于 FastJsonConfiguration) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for Excel -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.xyzh.common.utils.config;
|
||||
|
||||
import com.alibaba.fastjson2.support.config.FastJsonConfig;
|
||||
import com.alibaba.fastjson2.support.spring6.http.converter.FastJsonHttpMessageConverter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -20,6 +20,7 @@ import java.util.List;
|
||||
* @since 2025-11-28
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(WebMvcConfigurer.class)
|
||||
public class FastJsonConfiguration implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,4 +79,24 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- Jakarta Servlet API (用于 ServletUtils) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Web (用于 FastJsonConfiguration) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -127,6 +127,12 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -18,6 +18,89 @@
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<!-- Gateway 是 WebFlux 应用,必须排除所有 Servlet/Tomcat 相关依赖 -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- 覆盖父 pom 继承的 spring-boot-starter-web,设为 test scope 使其不参与编译和运行 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- 覆盖父 pom 继承的 spring-boot-starter-security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- 覆盖父 pom 继承的 springdoc -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- 排除 Tomcat -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<version>10.1.48</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-el</artifactId>
|
||||
<version>10.1.48</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-websocket</artifactId>
|
||||
<version>10.1.48</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- 排除 spring-webmvc -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<version>${spring-framework.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Cloud Gateway -->
|
||||
<dependency>
|
||||
@@ -81,24 +164,81 @@
|
||||
<dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-auth</artifactId>
|
||||
<exclusions>
|
||||
<!-- 排除Tomcat,Gateway必须使用Netty -->
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis(用于Token验证、限流等) -->
|
||||
<dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-redis</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<dependency>
|
||||
<!-- <dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-utils</artifactId>
|
||||
</dependency>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency> -->
|
||||
|
||||
<!-- 核心模块 -->
|
||||
<dependency>
|
||||
<groupId>org.xyzh.common</groupId>
|
||||
<artifactId>common-core</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Actuator(健康检查) -->
|
||||
@@ -138,6 +278,30 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Gateway 需要 WebFlux 版本的 Spring Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Servlet API (provided - 仅用于编译 common-auth 中的类,运行时不需要) -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring WebMVC (provided - 仅用于编译 common-auth/common-utils 中的类) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -6,9 +6,6 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.xyzh.common.auth.config.SecurityConfig;
|
||||
import org.xyzh.common.auth.config.WebMvcConfig;
|
||||
import org.xyzh.common.auth.config.GatewayAuthConfig;
|
||||
|
||||
/**
|
||||
* @description Gateway 网关启动类
|
||||
@@ -25,11 +22,12 @@ import org.xyzh.common.auth.config.GatewayAuthConfig;
|
||||
"org.xyzh.common" // 公共模块(包括 common-auth)
|
||||
},
|
||||
excludeFilters = {
|
||||
// 排除 Spring MVC 相关配置,Gateway 使用 WebFlux
|
||||
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {
|
||||
SecurityConfig.class, // Spring MVC Security配置
|
||||
WebMvcConfig.class, // Spring MVC配置
|
||||
GatewayAuthConfig.class // 微服务Gateway模式配置(使用Servlet Filter)
|
||||
// 使用正则表达式排除 Spring MVC 相关配置,避免加载 WebMvcConfigurer 类
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = {
|
||||
"org\\.xyzh\\.common\\.auth\\.config\\.SecurityConfig",
|
||||
"org\\.xyzh\\.common\\.auth\\.config\\.WebMvcConfig",
|
||||
"org\\.xyzh\\.common\\.auth\\.config\\.GatewayAuthConfig",
|
||||
"org\\.xyzh\\.common\\.utils\\.config\\.FastJsonConfiguration"
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -131,27 +131,32 @@ public class AuthGlobalFilter implements GlobalFilter, Ordered {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取 Token
|
||||
* 从请求头或URL参数中提取 Token
|
||||
*/
|
||||
private String extractToken(ServerHttpRequest request) {
|
||||
// 1. 优先从请求头获取
|
||||
List<String> headers = request.getHeaders().get(authProperties.getTokenHeader());
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return null;
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
String header = headers.get(0);
|
||||
if (StringUtils.hasText(header)) {
|
||||
// 支持 Bearer 前缀
|
||||
String prefix = authProperties.getTokenPrefix();
|
||||
if (StringUtils.hasText(prefix) && header.startsWith(prefix)) {
|
||||
return header.substring(prefix.length()).trim();
|
||||
}
|
||||
// 也支持直接传递 Token(不带前缀)
|
||||
return header.trim();
|
||||
}
|
||||
}
|
||||
|
||||
String header = headers.get(0);
|
||||
if (!StringUtils.hasText(header)) {
|
||||
return null;
|
||||
// 2. 从URL参数获取(用于WebSocket连接)
|
||||
String tokenParam = request.getQueryParams().getFirst("token");
|
||||
if (StringUtils.hasText(tokenParam)) {
|
||||
log.debug("从URL参数获取Token");
|
||||
return tokenParam.trim();
|
||||
}
|
||||
|
||||
// 支持 Bearer 前缀
|
||||
String prefix = authProperties.getTokenPrefix();
|
||||
if (StringUtils.hasText(prefix) && header.startsWith(prefix)) {
|
||||
return header.substring(prefix.length()).trim();
|
||||
}
|
||||
|
||||
// 也支持直接传递 Token(不带前缀)
|
||||
return header.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,6 +92,14 @@ spring:
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
# ==================== 工单服务 WebSocket 路由 ====================
|
||||
- id: workcase-websocket
|
||||
uri: lb:ws://workcase-service
|
||||
predicates:
|
||||
- Path=/urban-lifeline/workcase/ws/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
# ==================== 工单服务路由 ====================
|
||||
- id: workcase-service
|
||||
uri: lb://workcase-service
|
||||
|
||||
@@ -101,6 +101,11 @@
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>${mybatis.spring.version}</version>
|
||||
</dependency>
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -59,6 +59,22 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -434,29 +434,6 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- Spring Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Log4j2 日志(统一配置,所有子模块继承) -->
|
||||
<dependency>
|
||||
@@ -464,11 +441,6 @@
|
||||
<artifactId>spring-boot-starter-log4j2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- dubbo依赖管理 -->
|
||||
<dependency>
|
||||
|
||||
@@ -85,6 +85,24 @@
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>${mybatis.spring.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -1,6 +1,11 @@
|
||||
package org.xyzh.system.controller;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -15,15 +20,22 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.xyzh.api.auth.service.AuthService;
|
||||
import org.xyzh.api.system.service.GuestService;
|
||||
import org.xyzh.api.system.service.ModulePermissionService;
|
||||
import org.xyzh.api.system.vo.PermissionVO;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.core.domain.LoginParam;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.dto.sys.TbGuestDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysPermissionDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysUserDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysUserInfoDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysUserRoleDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysViewDTO;
|
||||
import org.xyzh.common.utils.id.IdUtil;
|
||||
import org.xyzh.common.utils.validation.ValidationUtils;
|
||||
import org.xyzh.common.auth.utils.JwtTokenUtil;
|
||||
import org.xyzh.common.redis.service.RedisService;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -48,6 +60,15 @@ public class GuestController {
|
||||
@DubboReference(version = "1.0.0", group = "auth", timeout = 5000, check = false, retries = 0)
|
||||
private AuthService authService;
|
||||
|
||||
@DubboReference(version = "1.0.0", group = "system", timeout = 5000, check = false, retries = 0)
|
||||
private ModulePermissionService modulePermissionService;
|
||||
|
||||
@Autowired
|
||||
private JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
|
||||
@PostMapping
|
||||
public ResultDomain<TbGuestDTO> createGuest(TbGuestDTO guest) {
|
||||
@@ -191,8 +212,53 @@ public class GuestController {
|
||||
userInfoDTO.setUsername(guest.getName());
|
||||
loginDomain.setUserInfo(userInfoDTO);
|
||||
|
||||
// 设置角色信息
|
||||
List<TbSysUserRoleDTO> userRoles = new ArrayList<>();
|
||||
TbSysUserRoleDTO userRole = new TbSysUserRoleDTO();
|
||||
userRole.setUserId(guest.getUserId());
|
||||
userRole.setRoleId("role_guest");
|
||||
userRole.setDeptId("dept_root");
|
||||
userRoles.add(userRole);
|
||||
loginDomain.setUserRoles(userRoles);
|
||||
|
||||
// 获取用户权限信息
|
||||
List<TbSysPermissionDTO> userPermissions = new ArrayList<>();
|
||||
List<TbSysViewDTO> userViews = new ArrayList<>();
|
||||
|
||||
ResultDomain<PermissionVO> permissionsResult = modulePermissionService.getUserPermissions(guest.getUserId());
|
||||
if (permissionsResult.getSuccess() && permissionsResult.getDataList() != null) {
|
||||
for (PermissionVO permission : permissionsResult.getDataList()) {
|
||||
if (permission.getPermissionId() != null) {
|
||||
TbSysPermissionDTO permissionDTO = PermissionVO.toPermissionDTO(permission);
|
||||
if (permissionDTO != null) {
|
||||
userPermissions.add(permissionDTO);
|
||||
}
|
||||
}
|
||||
if (permission.getViewId() != null) {
|
||||
TbSysViewDTO viewDTO = PermissionVO.toViewDTO(permission);
|
||||
if (viewDTO != null) {
|
||||
userViews.add(viewDTO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loginDomain.setUserPermissions(userPermissions);
|
||||
loginDomain.setUserViews(userViews);
|
||||
|
||||
loginDomain.setLoginType("wechat_miniprogram");
|
||||
|
||||
// 生成 JWT Token
|
||||
String token = jwtTokenUtil.generateToken(loginDomain);
|
||||
loginDomain.setToken(token);
|
||||
|
||||
// 将登录信息存储到 Redis(24小时有效期)
|
||||
String loginKey = "login:token:" + token;
|
||||
redisService.set(loginKey, JSON.toJSONString(loginDomain), 24, TimeUnit.HOURS);
|
||||
|
||||
// 存储用户登录状态
|
||||
String userLoginKey = "login:user:" + guest.getUserId();
|
||||
redisService.set(userLoginKey, token, 24, TimeUnit.HOURS);
|
||||
|
||||
return loginDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.dto.sys.TbGuestDTO;
|
||||
import org.xyzh.common.dto.sys.TbSysUserRoleDTO;
|
||||
import org.xyzh.system.mapper.user.TbGuestMapper;
|
||||
import org.xyzh.system.mapper.user.TbSysUserRoleMapper;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
@@ -34,10 +38,22 @@ public class GuestServiceImpl implements GuestService{
|
||||
@Autowired
|
||||
private TbGuestMapper guestMapper;
|
||||
|
||||
@Autowired
|
||||
private TbSysUserRoleMapper userRoleMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<TbGuestDTO> createGuest(TbGuestDTO guest) {
|
||||
guestMapper.insertGuest(guest);
|
||||
|
||||
// 绑定访客角色(role_guest)
|
||||
TbSysUserRoleDTO userRole = new TbSysUserRoleDTO();
|
||||
userRole.setUserId(guest.getUserId());
|
||||
userRole.setRoleId("role_guest");
|
||||
userRole.setDeptId("dept_root");
|
||||
userRole.setCreateTime(new Date());
|
||||
userRoleMapper.insertUserRole(userRole);
|
||||
|
||||
return ResultDomain.success("创建成功",guest);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,35 +4,34 @@ server:
|
||||
servlet:
|
||||
context-path: /urban-lifeline/system
|
||||
# ================== Auth ====================
|
||||
urban-lifeline:
|
||||
auth:
|
||||
enabled: true
|
||||
auth:
|
||||
enabled: true
|
||||
gateway-mode: true
|
||||
# 认证接口:可以按服务自定义
|
||||
login-path: /urban-lifeline/auth/login
|
||||
logout-path: /urban-lifeline/auth/logout
|
||||
captcha-path: /urban-lifeline/auth/captcha
|
||||
refresh-path: /urban-lifeline/auth/refresh
|
||||
|
||||
# 认证接口:可以按服务自定义
|
||||
login-path: /urban-lifeline/auth/login
|
||||
logout-path: /urban-lifeline/auth/logout
|
||||
captcha-path: /urban-lifeline/auth/captcha
|
||||
refresh-path: /urban-lifeline/auth/refresh
|
||||
# 通用白名单(非认证接口)
|
||||
whitelist:
|
||||
# Swagger/OpenAPI 文档相关(建议不带 context-path)
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
|
||||
# 通用白名单(非认证接口)
|
||||
whitelist:
|
||||
# Swagger/OpenAPI 文档相关(建议不带 context-path)
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
# 静态资源
|
||||
- /favicon.ico
|
||||
- /error
|
||||
|
||||
# 静态资源
|
||||
- /favicon.ico
|
||||
- /error
|
||||
# 健康检查
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
|
||||
# 健康检查
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
|
||||
# 其他需要放行的路径
|
||||
# - /public/**
|
||||
# - /api/public/**
|
||||
# 其他需要放行的路径
|
||||
# - /public/**
|
||||
# - /api/public/**
|
||||
|
||||
|
||||
# ================== Security ==================
|
||||
|
||||
@@ -4,35 +4,35 @@ server:
|
||||
# servlet:
|
||||
# context-path: /urban-lifeline/system # 微服务架构下,context-path由Gateway管理
|
||||
# ================== Auth ====================
|
||||
urban-lifeline:
|
||||
auth:
|
||||
enabled: true
|
||||
|
||||
# 认证接口:可以按服务自定义
|
||||
login-path: /urban-lifeline/auth/login
|
||||
logout-path: /urban-lifeline/auth/logout
|
||||
captcha-path: /urban-lifeline/auth/captcha
|
||||
refresh-path: /urban-lifeline/auth/refresh
|
||||
auth:
|
||||
enabled: true
|
||||
gateway-mode: true
|
||||
# 认证接口:可以按服务自定义
|
||||
login-path: /urban-lifeline/auth/login
|
||||
logout-path: /urban-lifeline/auth/logout
|
||||
captcha-path: /urban-lifeline/auth/captcha
|
||||
refresh-path: /urban-lifeline/auth/refresh
|
||||
|
||||
# 通用白名单(非认证接口)
|
||||
whitelist:
|
||||
# Swagger/OpenAPI 文档相关(建议不带 context-path)
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
# 通用白名单(非认证接口)
|
||||
whitelist:
|
||||
# Swagger/OpenAPI 文档相关(建议不带 context-path)
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
|
||||
# 静态资源
|
||||
- /favicon.ico
|
||||
- /error
|
||||
# 静态资源
|
||||
- /favicon.ico
|
||||
- /error
|
||||
|
||||
# 健康检查
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
# 健康检查
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
|
||||
# 其他需要放行的路径
|
||||
# - /public/**
|
||||
# - /api/public/**
|
||||
# 其他需要放行的路径
|
||||
# - /public/**
|
||||
# - /api/public/**
|
||||
|
||||
security:
|
||||
aes:
|
||||
|
||||
@@ -116,6 +116,12 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,110 @@
|
||||
package org.xyzh.workcase.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
import org.xyzh.common.auth.contants.AuthContants;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* WebSocket握手拦截器
|
||||
* 从网关传递的请求头中获取已验证的用户信息
|
||||
*/
|
||||
@Component
|
||||
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WebSocketAuthInterceptor.class);
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
|
||||
log.info("WebSocket握手开始,URI: {}", request.getURI());
|
||||
|
||||
try {
|
||||
// 从网关传递的请求头中获取用户ID(网关已完成token验证)
|
||||
String userId = extractUserIdFromRequest(request);
|
||||
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
log.warn("WebSocket握手失败:请求头中未找到用户ID,网关认证可能失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 可选:也获取token,供后续使用
|
||||
String token = extractTokenFromRequest(request);
|
||||
|
||||
// 将用户信息存储到WebSocket会话属性中,供后续使用
|
||||
attributes.put("userId", userId);
|
||||
if (StringUtils.hasText(token)) {
|
||||
attributes.put("token", token);
|
||||
}
|
||||
|
||||
log.info("WebSocket握手成功,userId: {}", userId);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket握手异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Exception exception) {
|
||||
if (exception != null) {
|
||||
log.error("WebSocket握手后发生异常", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从网关传递的请求头中提取用户ID
|
||||
*/
|
||||
private String extractUserIdFromRequest(ServerHttpRequest request) {
|
||||
if (request instanceof ServletServerHttpRequest) {
|
||||
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
|
||||
|
||||
// 从网关传递的请求头获取(标准微服务架构方式)
|
||||
String userId = servletRequest.getHeaders().getFirst(AuthContants.USER_ID_ATTRIBUTE);
|
||||
if (StringUtils.hasText(userId)) {
|
||||
log.debug("从网关请求头获取用户ID: {}", userId);
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("未能从请求头中获取用户ID");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取token(可选)
|
||||
*/
|
||||
private String extractTokenFromRequest(ServerHttpRequest request) {
|
||||
if (request instanceof ServletServerHttpRequest) {
|
||||
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
|
||||
|
||||
// 从网关传递的请求头获取
|
||||
String token = servletRequest.getHeaders().getFirst(AuthContants.TOKEN_ATTRIBUTE);
|
||||
if (StringUtils.hasText(token)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// 也支持从Authorization头获取
|
||||
String authHeader = servletRequest.getHeaders().getFirst("Authorization");
|
||||
if (StringUtils.hasText(authHeader)) {
|
||||
if (authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
return authHeader;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.xyzh.workcase.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
@@ -14,6 +15,9 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Autowired
|
||||
private WebSocketAuthInterceptor webSocketAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
// 配置消息代理
|
||||
@@ -29,9 +33,15 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// 注册STOMP端点
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*") // 允许跨域
|
||||
.withSockJS(); // 支持SockJS降级方案
|
||||
// 原生WebSocket端点(不使用SockJS)
|
||||
registry.addEndpoint("/workcase/ws/chat")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.addInterceptors(webSocketAuthInterceptor);
|
||||
|
||||
// SockJS端点(用于不支持WebSocket的浏览器降级)
|
||||
registry.addEndpoint("/workcase/ws/chat-sockjs")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.addInterceptors(webSocketAuthInterceptor)
|
||||
.withSockJS();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.xyzh.workcase.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -13,9 +15,18 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.xyzh.api.ai.dto.ChatPrepareData;
|
||||
import org.xyzh.api.ai.dto.TbChat;
|
||||
import org.xyzh.api.ai.dto.TbChatMessage;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMemberDTO;
|
||||
import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
|
||||
import org.xyzh.api.workcase.dto.TbCustomerServiceDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.service.ChatRoomService;
|
||||
import org.xyzh.api.workcase.service.WorkcaseChatService;
|
||||
import org.xyzh.api.workcase.vo.ChatMemberVO;
|
||||
import org.xyzh.api.workcase.vo.ChatRoomMessageVO;
|
||||
import org.xyzh.api.workcase.vo.ChatRoomVO;
|
||||
import org.xyzh.api.workcase.vo.CustomerServiceVO;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.page.PageRequest;
|
||||
import org.xyzh.common.utils.validation.ValidationResult;
|
||||
@@ -28,7 +39,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* @description 工单对话控制器
|
||||
* - AI对话管理
|
||||
* - AI对话管理(智能问答)
|
||||
* - ChatRoom聊天室管理(实时IM)
|
||||
* - 微信客服消息接收
|
||||
* - 词云管理
|
||||
* @filename WorkcaseChatController.java
|
||||
@@ -44,9 +56,13 @@ public class WorkcaseChatContorller {
|
||||
@Autowired
|
||||
private WorkcaseChatService workcaseChatService;
|
||||
|
||||
@Autowired
|
||||
private ChatRoomService chatRoomService;
|
||||
|
||||
// ========================= AI对话管理 =========================
|
||||
|
||||
@Operation(summary = "创建对话")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:create')")
|
||||
@PostMapping
|
||||
public ResultDomain<TbChat> createChat(@RequestBody TbChat chat) {
|
||||
ValidationResult vr = ValidationUtils.validate(chat, Arrays.asList(
|
||||
@@ -59,6 +75,7 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "更新对话")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:update')")
|
||||
@PutMapping
|
||||
public ResultDomain<TbChat> updateChat(@RequestBody TbChat chat) {
|
||||
ValidationResult vr = ValidationUtils.validate(chat, Arrays.asList(
|
||||
@@ -71,12 +88,14 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "查询对话列表")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:list')")
|
||||
@PostMapping("/list")
|
||||
public ResultDomain<TbChat> getChatList(@RequestBody TbChat filter) {
|
||||
return workcaseChatService.getChatList(filter);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取对话消息列表")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:message')")
|
||||
@PostMapping("/message/list")
|
||||
public ResultDomain<TbChatMessage> getChatMessageList(@RequestBody TbChat filter) {
|
||||
ValidationResult vr = ValidationUtils.validate(filter, Arrays.asList(
|
||||
@@ -89,6 +108,7 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "准备对话会话")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:stream')")
|
||||
@PostMapping("/prepare")
|
||||
public ResultDomain<String> prepareChatMessageSession(@RequestBody ChatPrepareData prepareData) {
|
||||
ValidationResult vr = ValidationUtils.validate(prepareData, Arrays.asList(
|
||||
@@ -102,18 +122,21 @@ public class WorkcaseChatContorller {
|
||||
}
|
||||
|
||||
@Operation(summary = "流式对话(SSE)")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:stream')")
|
||||
@GetMapping(value = "/stream/{sessionId}", produces = "text/event-stream")
|
||||
public SseEmitter streamChatMessage(@PathVariable String sessionId) {
|
||||
return workcaseChatService.streamChatMessageWithSse(sessionId);
|
||||
}
|
||||
|
||||
@Operation(summary = "停止对话")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:stream')")
|
||||
@PostMapping("/stop/{taskId}")
|
||||
public ResultDomain<Boolean> stopChat(@RequestBody TbChat filter, @PathVariable String taskId) {
|
||||
return workcaseChatService.stopChatMessageByTaskId(filter, taskId);
|
||||
}
|
||||
|
||||
@Operation(summary = "评论对话消息")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:message')")
|
||||
@PostMapping("/comment")
|
||||
public ResultDomain<Boolean> commentChatMessage(@RequestBody TbChat filter,
|
||||
@RequestParam String messageId, @RequestParam String comment) {
|
||||
@@ -123,43 +146,233 @@ public class WorkcaseChatContorller {
|
||||
// ========================= 对话分析 =========================
|
||||
|
||||
@Operation(summary = "分析对话(AI预填工单信息)")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:analyze')")
|
||||
@GetMapping("/analyze/{chatId}")
|
||||
public ResultDomain<TbWorkcaseDTO> analyzeChat(@PathVariable String chatId) {
|
||||
return workcaseChatService.analyzeChat(chatId);
|
||||
}
|
||||
|
||||
@Operation(summary = "总结对话")
|
||||
@PreAuthorize("hasAuthority('workcase:chat:analyze')")
|
||||
@PostMapping("/summary/{chatId}")
|
||||
public ResultDomain<TbWorkcaseDTO> summaryChat(@PathVariable String chatId) {
|
||||
return workcaseChatService.summaryChat(chatId);
|
||||
}
|
||||
|
||||
// ========================= ChatRoom聊天室管理(实时IM) =========================
|
||||
|
||||
@Operation(summary = "创建聊天室")
|
||||
@PreAuthorize("hasAuthority('workcase:room:create')")
|
||||
@PostMapping("/room")
|
||||
public ResultDomain<TbChatRoomDTO> createChatRoom(@RequestBody TbChatRoomDTO chatRoom) {
|
||||
ValidationResult vr = ValidationUtils.validate(chatRoom, Arrays.asList(
|
||||
ValidationUtils.requiredString("workcaseId", "工单ID"),
|
||||
ValidationUtils.requiredString("guestId", "来客ID")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.createChatRoom(chatRoom);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新聊天室")
|
||||
@PreAuthorize("hasAuthority('workcase:room:update')")
|
||||
@PutMapping("/room")
|
||||
public ResultDomain<TbChatRoomDTO> updateChatRoom(@RequestBody TbChatRoomDTO chatRoom) {
|
||||
ValidationResult vr = ValidationUtils.validate(chatRoom, Arrays.asList(
|
||||
ValidationUtils.requiredString("roomId", "聊天室ID")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.updateChatRoom(chatRoom);
|
||||
}
|
||||
|
||||
@Operation(summary = "关闭聊天室")
|
||||
@PreAuthorize("hasAuthority('workcase:room:close')")
|
||||
@PostMapping("/room/{roomId}/close")
|
||||
public ResultDomain<Boolean> closeChatRoom(@PathVariable String roomId, @RequestParam String closedBy) {
|
||||
return chatRoomService.closeChatRoom(roomId, closedBy);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取聊天室详情")
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@GetMapping("/room/{roomId}")
|
||||
public ResultDomain<TbChatRoomDTO> getChatRoomById(@PathVariable String roomId) {
|
||||
return chatRoomService.getChatRoomById(roomId);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询聊天室")
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@PostMapping("/room/page")
|
||||
public ResultDomain<ChatRoomVO> getChatRoomPage(@RequestBody PageRequest<TbChatRoomDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
|
||||
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.getChatRoomPage(pageRequest);
|
||||
}
|
||||
|
||||
// ========================= ChatRoom成员管理 =========================
|
||||
|
||||
@Operation(summary = "添加聊天室成员")
|
||||
@PreAuthorize("hasAuthority('workcase:room:member')")
|
||||
@PostMapping("/room/member")
|
||||
public ResultDomain<TbChatRoomMemberDTO> addChatRoomMember(@RequestBody TbChatRoomMemberDTO member) {
|
||||
ValidationResult vr = ValidationUtils.validate(member, Arrays.asList(
|
||||
ValidationUtils.requiredString("roomId", "聊天室ID"),
|
||||
ValidationUtils.requiredString("userId", "用户ID")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.addChatRoomMember(member);
|
||||
}
|
||||
|
||||
@Operation(summary = "移除聊天室成员")
|
||||
@PreAuthorize("hasAuthority('workcase:room:member')")
|
||||
@DeleteMapping("/room/member/{memberId}")
|
||||
public ResultDomain<Boolean> removeChatRoomMember(@PathVariable String memberId) {
|
||||
return chatRoomService.removeChatRoomMember(memberId);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取聊天室成员列表")
|
||||
@PreAuthorize("hasAuthority('workcase:room:member')")
|
||||
@GetMapping("/room/{roomId}/members")
|
||||
public ResultDomain<ChatMemberVO> getChatRoomMemberList(@PathVariable String roomId) {
|
||||
return chatRoomService.getChatRoomMemberList(roomId);
|
||||
}
|
||||
|
||||
// ========================= ChatRoom消息管理 =========================
|
||||
|
||||
@Operation(summary = "发送聊天室消息")
|
||||
@PreAuthorize("hasAuthority('workcase:room:message')")
|
||||
@PostMapping("/room/message")
|
||||
public ResultDomain<TbChatRoomMessageDTO> sendMessage(@RequestBody TbChatRoomMessageDTO message) {
|
||||
ValidationResult vr = ValidationUtils.validate(message, Arrays.asList(
|
||||
ValidationUtils.requiredString("roomId", "聊天室ID"),
|
||||
ValidationUtils.requiredString("senderId", "发送者ID"),
|
||||
ValidationUtils.requiredString("content", "消息内容")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.sendMessage(message);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询聊天室消息")
|
||||
@PreAuthorize("hasAuthority('workcase:room:message')")
|
||||
@PostMapping("/room/message/page")
|
||||
public ResultDomain<ChatRoomMessageVO> getChatMessagePage(@RequestBody PageRequest<TbChatRoomMessageDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
|
||||
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.getChatMessagePage(pageRequest);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除聊天室消息")
|
||||
@PreAuthorize("hasAuthority('workcase:room:message')")
|
||||
@DeleteMapping("/room/message/{messageId}")
|
||||
public ResultDomain<Boolean> deleteRoomMessage(@PathVariable String messageId) {
|
||||
return chatRoomService.deleteMessage(messageId);
|
||||
}
|
||||
|
||||
// ========================= 客服人员管理 =========================
|
||||
|
||||
@Operation(summary = "添加客服人员")
|
||||
@PostMapping("/customer-service")
|
||||
public ResultDomain<TbCustomerServiceDTO> addCustomerService(@RequestBody TbCustomerServiceDTO customerService) {
|
||||
ValidationResult vr = ValidationUtils.validate(customerService, Arrays.asList(
|
||||
ValidationUtils.requiredString("userId", "员工ID")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.addCustomerService(customerService);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新客服人员")
|
||||
@PutMapping("/customer-service")
|
||||
public ResultDomain<TbCustomerServiceDTO> updateCustomerService(@RequestBody TbCustomerServiceDTO customerService) {
|
||||
ValidationResult vr = ValidationUtils.validate(customerService, Arrays.asList(
|
||||
ValidationUtils.requiredString("userId", "员工ID")
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.updateCustomerService(customerService);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除客服人员")
|
||||
@DeleteMapping("/customer-service/{userId}")
|
||||
public ResultDomain<Boolean> deleteCustomerService(@PathVariable String userId) {
|
||||
return chatRoomService.deleteCustomerService(userId);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询客服人员")
|
||||
@PostMapping("/customer-service/page")
|
||||
public ResultDomain<CustomerServiceVO> getCustomerServicePage(@RequestBody PageRequest<TbCustomerServiceDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
|
||||
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
|
||||
));
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return chatRoomService.getCustomerServicePage(pageRequest);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新客服在线状态")
|
||||
@PostMapping("/customer-service/{userId}/status")
|
||||
public ResultDomain<Boolean> updateCustomerServiceStatus(@PathVariable String userId, @RequestParam String status) {
|
||||
return chatRoomService.updateCustomerServiceStatus(userId, status);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取可接待客服列表")
|
||||
@GetMapping("/customer-service/available")
|
||||
public ResultDomain<CustomerServiceVO> getAvailableCustomerServices() {
|
||||
return chatRoomService.getAvailableCustomerServices();
|
||||
}
|
||||
|
||||
@Operation(summary = "自动分配客服")
|
||||
@PostMapping("/room/{roomId}/assign")
|
||||
public ResultDomain<CustomerServiceVO> assignCustomerService(@PathVariable String roomId) {
|
||||
return chatRoomService.assignCustomerService(roomId);
|
||||
}
|
||||
|
||||
// ========================= 微信客服消息回调 =========================
|
||||
|
||||
@Operation(summary = "微信客服消息回调验证(GET)")
|
||||
@GetMapping("/kefu/callback")
|
||||
public String kefuCallbackVerify(
|
||||
@RequestParam("msg_signature") String msgSignature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestParam("echostr") String echostr) {
|
||||
// TODO: 验证签名并返回 echostr
|
||||
// 实际应使用微信提供的加解密工具验证
|
||||
return echostr;
|
||||
}
|
||||
// @Operation(summary = "微信客服消息回调验证(GET)")
|
||||
// @GetMapping("/kefu/callback")
|
||||
// public String kefuCallbackVerify(
|
||||
// @RequestParam("msg_signature") String msgSignature,
|
||||
// @RequestParam("timestamp") String timestamp,
|
||||
// @RequestParam("nonce") String nonce,
|
||||
// @RequestParam("echostr") String echostr) {
|
||||
// // TODO: 验证签名并返回 echostr
|
||||
// // 实际应使用微信提供的加解密工具验证
|
||||
// return echostr;
|
||||
// }
|
||||
|
||||
@Operation(summary = "微信客服消息回调(POST)")
|
||||
@PostMapping("/kefu/callback")
|
||||
public String kefuCallback(
|
||||
@RequestParam("msg_signature") String msgSignature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String xmlBody) {
|
||||
// TODO: 解密消息,调用同步接口拉取消息
|
||||
// 收到回调后,应调用 kefuMessageService.syncMessages() 拉取新消息
|
||||
// 然后通过 processMessages() 处理消息
|
||||
return "success";
|
||||
}
|
||||
// @Operation(summary = "微信客服消息回调(POST)")
|
||||
// @PostMapping("/kefu/callback")
|
||||
// public String kefuCallback(
|
||||
// @RequestParam("msg_signature") String msgSignature,
|
||||
// @RequestParam("timestamp") String timestamp,
|
||||
// @RequestParam("nonce") String nonce,
|
||||
// @RequestBody String xmlBody) {
|
||||
// // TODO: 解密消息,调用同步接口拉取消息
|
||||
// // 收到回调后,应调用 kefuMessageService.syncMessages() 拉取新消息
|
||||
// // 然后通过 processMessages() 处理消息
|
||||
// return "success";
|
||||
// }
|
||||
|
||||
// ========================= 词云管理 =========================
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.xyzh.workcase.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -42,6 +43,7 @@ public class WorkcaseController {
|
||||
// ========================= 工单管理 =========================
|
||||
|
||||
@Operation(summary = "创建工单")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:create')")
|
||||
@PostMapping
|
||||
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
|
||||
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
|
||||
@@ -55,6 +57,7 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "更新工单")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:update')")
|
||||
@PutMapping
|
||||
public ResultDomain<TbWorkcaseDTO> updateWorkcase(@RequestBody TbWorkcaseDTO workcase) {
|
||||
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
|
||||
@@ -67,6 +70,7 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "删除工单")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:update')")
|
||||
@DeleteMapping("/{workcaseId}")
|
||||
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(@PathVariable String workcaseId) {
|
||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||
@@ -75,18 +79,21 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "获取工单详情")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
||||
@GetMapping("/{workcaseId}")
|
||||
public ResultDomain<TbWorkcaseDTO> getWorkcaseById(@PathVariable String workcaseId) {
|
||||
return workcaseService.getWorkcaseById(workcaseId);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询工单列表")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
||||
@PostMapping("/list")
|
||||
public ResultDomain<TbWorkcaseDTO> getWorkcaseList(@RequestBody TbWorkcaseDTO filter) {
|
||||
return workcaseService.getWorkcaseList(filter);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询工单")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
||||
@PostMapping("/page")
|
||||
public ResultDomain<TbWorkcaseDTO> getWorkcasePage(@RequestBody PageRequest<TbWorkcaseDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
@@ -123,6 +130,7 @@ public class WorkcaseController {
|
||||
// ========================= 工单处理过程 =========================
|
||||
|
||||
@Operation(summary = "创建工单处理过程")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@PostMapping("/process")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> createWorkcaseProcess(@RequestBody TbWorkcaseProcessDTO process) {
|
||||
ValidationResult vr = ValidationUtils.validate(process, Arrays.asList(
|
||||
@@ -136,6 +144,7 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "更新工单处理过程")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@PutMapping("/process")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> updateWorkcaseProcess(@RequestBody TbWorkcaseProcessDTO process) {
|
||||
ValidationResult vr = ValidationUtils.validate(process, Arrays.asList(
|
||||
@@ -148,6 +157,7 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "删除工单处理过程")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@DeleteMapping("/process/{processId}")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> deleteWorkcaseProcess(@PathVariable String processId) {
|
||||
TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
|
||||
@@ -156,12 +166,14 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "查询工单处理过程列表")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@PostMapping("/process/list")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(@RequestBody TbWorkcaseProcessDTO filter) {
|
||||
return workcaseService.getWorkcaseProcessList(filter);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询工单处理过程")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||
@PostMapping("/process/page")
|
||||
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(@RequestBody PageRequest<TbWorkcaseProcessDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
@@ -177,6 +189,7 @@ public class WorkcaseController {
|
||||
// ========================= 工单设备管理 =========================
|
||||
|
||||
@Operation(summary = "创建工单设备")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
||||
@PostMapping("/device")
|
||||
public ResultDomain<TbWorkcaseDeviceDTO> createWorkcaseDevice(@RequestBody TbWorkcaseDeviceDTO device) {
|
||||
ValidationResult vr = ValidationUtils.validate(device, Arrays.asList(
|
||||
@@ -190,6 +203,7 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "更新工单设备")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
||||
@PutMapping("/device")
|
||||
public ResultDomain<TbWorkcaseDeviceDTO> updateWorkcaseDevice(@RequestBody TbWorkcaseDeviceDTO device) {
|
||||
ValidationResult vr = ValidationUtils.validate(device, Arrays.asList(
|
||||
@@ -203,6 +217,7 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "删除工单设备")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
||||
@DeleteMapping("/device/{workcaseId}/{device}")
|
||||
public ResultDomain<TbWorkcaseDeviceDTO> deleteWorkcaseDevice(@PathVariable String workcaseId, @PathVariable String device) {
|
||||
TbWorkcaseDeviceDTO deviceDTO = new TbWorkcaseDeviceDTO();
|
||||
@@ -212,12 +227,14 @@ public class WorkcaseController {
|
||||
}
|
||||
|
||||
@Operation(summary = "查询工单设备列表")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
||||
@PostMapping("/device/list")
|
||||
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDeviceList(@RequestBody TbWorkcaseDeviceDTO filter) {
|
||||
return workcaseService.getWorkcaseDeviceList(filter);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询工单设备")
|
||||
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
||||
@PostMapping("/device/page")
|
||||
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDevicePage(@RequestBody PageRequest<TbWorkcaseDeviceDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
|
||||
@@ -7,7 +7,7 @@ server:
|
||||
# ================== Auth ====================
|
||||
auth:
|
||||
enabled: true
|
||||
gate-way: true
|
||||
gateway-mode: true
|
||||
whitelist:
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
|
||||
18
urbanLifelineWeb/.vscode/launch.json
vendored
18
urbanLifelineWeb/.vscode/launch.json
vendored
@@ -31,6 +31,21 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "启动 workcase 开发服务器",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run dev",
|
||||
"cwd": "${workspaceFolder}/packages/workcase",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "new",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "启动 Bidding 开发服务器",
|
||||
"type": "node-terminal",
|
||||
@@ -53,7 +68,8 @@
|
||||
"configurations": [
|
||||
"启动 Shared 开发服务器",
|
||||
"启动 Platform 开发服务器",
|
||||
"启动 Bidding 开发服务器"
|
||||
"启动 workcase 开发服务器",
|
||||
// "启动 Bidding 开发服务器"
|
||||
],
|
||||
"stopAll": true,
|
||||
"presentation": {
|
||||
|
||||
@@ -98,8 +98,10 @@ declare module 'shared/api/ai' {
|
||||
|
||||
declare module 'shared/api/workcase' {
|
||||
export const workcaseAPI: any
|
||||
export const workcaseChatAPI: any
|
||||
}
|
||||
|
||||
|
||||
// ============ types模块 ==================
|
||||
declare module 'shared/types' {
|
||||
import type { BaseDTO } from '../../../shared/src/types/base'
|
||||
@@ -143,15 +145,30 @@ declare module 'shared/types' {
|
||||
TbWorkcaseDeviceDTO,
|
||||
// 聊天室相关
|
||||
TbChatRoomDTO,
|
||||
TbChatMessageDTO,
|
||||
TbChatRoomMessageDTO,
|
||||
TbChatRoomMemberDTO,
|
||||
TbVideoMeetingDTO,
|
||||
TbMeetingParticipantDTO,
|
||||
TbMeetingTranscriptionDTO,
|
||||
ChatRoomVO,
|
||||
ChatMessageVO,
|
||||
ChatRoomMessageVO,
|
||||
ChatMemberVO,
|
||||
VideoMeetingVO,
|
||||
MeetingParticipantVO,
|
||||
SendMessageParam,
|
||||
CreateMeetingParam,
|
||||
MarkReadParam
|
||||
MarkReadParam,
|
||||
// 客服相关
|
||||
TbCustomerServiceDTO,
|
||||
CustomerServiceVO,
|
||||
// 词云
|
||||
TbWordCloudDTO,
|
||||
// 来客相关
|
||||
TbGuestDTO,
|
||||
GuestVO,
|
||||
CustomerVO,
|
||||
ConversationVO
|
||||
} from '../../../shared/src/types/workcase'
|
||||
|
||||
// 重新导出 menu
|
||||
|
||||
11
urbanLifelineWeb/packages/platform/pnpm-lock.yaml
generated
11
urbanLifelineWeb/packages/platform/pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ dependencies:
|
||||
element-plus:
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(vue@3.5.25)
|
||||
lucide-vue-next:
|
||||
specifier: ^0.561.0
|
||||
version: 0.561.0(vue@3.5.25)
|
||||
pinia:
|
||||
specifier: ^2.2.8
|
||||
version: 2.3.1(typescript@5.9.3)(vue@3.5.25)
|
||||
@@ -1464,6 +1467,14 @@ packages:
|
||||
yallist: 3.1.1
|
||||
dev: true
|
||||
|
||||
/lucide-vue-next@0.561.0(vue@3.5.25):
|
||||
resolution: {integrity: sha512-c5HUckO0qHklVSOf/0vaSR3pEb8fYImRDCRDLde56uqS9js0D/e3RAvq0/YFWjkmyOBKCb0/IdskdoHZQEkT5g==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
dependencies:
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
dev: false
|
||||
|
||||
/magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
dependencies:
|
||||
|
||||
@@ -98,8 +98,10 @@ declare module 'shared/api/ai' {
|
||||
|
||||
declare module 'shared/api/workcase' {
|
||||
export const workcaseAPI: any
|
||||
export const workcaseChatAPI: any
|
||||
}
|
||||
|
||||
|
||||
// ============ types模块 ==================
|
||||
declare module 'shared/types' {
|
||||
import type { BaseDTO } from '../../../shared/src/types/base'
|
||||
@@ -143,15 +145,30 @@ declare module 'shared/types' {
|
||||
TbWorkcaseDeviceDTO,
|
||||
// 聊天室相关
|
||||
TbChatRoomDTO,
|
||||
TbChatMessageDTO,
|
||||
TbChatRoomMessageDTO,
|
||||
TbChatRoomMemberDTO,
|
||||
TbVideoMeetingDTO,
|
||||
TbMeetingParticipantDTO,
|
||||
TbMeetingTranscriptionDTO,
|
||||
ChatRoomVO,
|
||||
ChatMessageVO,
|
||||
ChatRoomMessageVO,
|
||||
ChatMemberVO,
|
||||
VideoMeetingVO,
|
||||
MeetingParticipantVO,
|
||||
SendMessageParam,
|
||||
CreateMeetingParam,
|
||||
MarkReadParam
|
||||
MarkReadParam,
|
||||
// 客服相关
|
||||
TbCustomerServiceDTO,
|
||||
CustomerServiceVO,
|
||||
// 词云
|
||||
TbWordCloudDTO,
|
||||
// 来客相关
|
||||
TbGuestDTO,
|
||||
GuestVO,
|
||||
CustomerVO,
|
||||
ConversationVO
|
||||
} from '../../../shared/src/types/workcase'
|
||||
|
||||
// 重新导出 menu
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@stomp/stompjs": "^7.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"element-plus": "^2.12.0",
|
||||
"express": "^4.18.2",
|
||||
"lucide-vue-next": "^0.561.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
|
||||
90
urbanLifelineWeb/packages/shared/pnpm-lock.yaml
generated
90
urbanLifelineWeb/packages/shared/pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2(vue@3.5.25)
|
||||
'@stomp/stompjs':
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
@@ -17,9 +20,15 @@ dependencies:
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.22.1
|
||||
lucide-vue-next:
|
||||
specifier: ^0.561.0
|
||||
version: 0.561.0(vue@3.5.25)
|
||||
ofetch:
|
||||
specifier: ^1.4.1
|
||||
version: 1.5.1
|
||||
sockjs-client:
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.25(typescript@5.9.3)
|
||||
@@ -1000,6 +1009,10 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@stomp/stompjs@7.2.1:
|
||||
resolution: {integrity: sha512-DLd/WeicnHS5SsWWSk3x6/pcivqchNaEvg9UEGVqAcfYEBVmS9D6980ckXjTtfpXLjdLDsd96M7IuX4w7nzq5g==}
|
||||
dev: false
|
||||
|
||||
/@sxzz/popperjs-es@2.11.7:
|
||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||
dev: false
|
||||
@@ -1464,6 +1477,17 @@ packages:
|
||||
ms: 2.0.0
|
||||
dev: false
|
||||
|
||||
/debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
||||
/debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1729,6 +1753,11 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/eventsource@2.0.2:
|
||||
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: false
|
||||
|
||||
/express@4.22.1:
|
||||
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@@ -1768,6 +1797,13 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/faye-websocket@0.11.4:
|
||||
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
websocket-driver: 0.7.4
|
||||
dev: false
|
||||
|
||||
/fdir@6.5.0(picomatch@4.0.3):
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1976,6 +2012,10 @@ packages:
|
||||
toidentifier: 1.0.1
|
||||
dev: false
|
||||
|
||||
/http-parser-js@0.5.10:
|
||||
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
|
||||
dev: false
|
||||
|
||||
/iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2262,6 +2302,14 @@ packages:
|
||||
yallist: 3.1.1
|
||||
dev: true
|
||||
|
||||
/lucide-vue-next@0.561.0(vue@3.5.25):
|
||||
resolution: {integrity: sha512-c5HUckO0qHklVSOf/0vaSR3pEb8fYImRDCRDLde56uqS9js0D/e3RAvq0/YFWjkmyOBKCb0/IdskdoHZQEkT5g==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
dependencies:
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
dev: false
|
||||
|
||||
/magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
dependencies:
|
||||
@@ -2533,6 +2581,10 @@ packages:
|
||||
side-channel: 1.1.0
|
||||
dev: false
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: false
|
||||
|
||||
/range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -2588,6 +2640,10 @@ packages:
|
||||
set-function-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: false
|
||||
|
||||
/resolve@1.22.11:
|
||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3054,6 +3110,19 @@ packages:
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
/sockjs-client@1.6.1:
|
||||
resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
eventsource: 2.0.2
|
||||
faye-websocket: 0.11.4
|
||||
inherits: 2.0.4
|
||||
url-parse: 1.5.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3294,6 +3363,13 @@ packages:
|
||||
picocolors: 1.1.1
|
||||
dev: true
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: false
|
||||
|
||||
/utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
@@ -3407,6 +3483,20 @@ packages:
|
||||
'@vue/shared': 3.5.25
|
||||
typescript: 5.9.3
|
||||
|
||||
/websocket-driver@0.7.4:
|
||||
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
http-parser-js: 0.5.10
|
||||
safe-buffer: 5.2.1
|
||||
websocket-extensions: 0.1.4
|
||||
dev: false
|
||||
|
||||
/websocket-extensions@0.1.4:
|
||||
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './workcase'
|
||||
export * from './workcaseChat'
|
||||
@@ -0,0 +1,301 @@
|
||||
import { api } from '@/api/index'
|
||||
import type { ResultDomain, PageRequest } from '@/types'
|
||||
import type { TbChat, TbChatMessage, ChatPrepareData } from '@/types/ai'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase/workcase'
|
||||
import type {
|
||||
TbChatRoomDTO,
|
||||
TbChatRoomMemberDTO,
|
||||
TbChatRoomMessageDTO,
|
||||
TbCustomerServiceDTO,
|
||||
TbWordCloudDTO,
|
||||
ChatRoomVO,
|
||||
ChatMemberVO,
|
||||
ChatRoomMessageVO,
|
||||
CustomerServiceVO
|
||||
} from '@/types/workcase/chatRoom'
|
||||
|
||||
/**
|
||||
* @description 工单对话相关接口
|
||||
* @filename workcaseChat.ts
|
||||
* @author cascade
|
||||
* @copyright xyzh
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
export const workcaseChatAPI = {
|
||||
baseUrl: '/urban-lifeline/workcase/chat',
|
||||
|
||||
// ====================== AI对话管理 ======================
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
*/
|
||||
async createChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
const response = await api.post<TbChat>(`${this.baseUrl}`, chat)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新对话
|
||||
*/
|
||||
async updateChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
const response = await api.put<TbChat>(`${this.baseUrl}`, chat)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询对话列表
|
||||
*/
|
||||
async getChatList(filter: TbChat): Promise<ResultDomain<TbChat>> {
|
||||
const response = await api.post<TbChat>(`${this.baseUrl}/list`, filter)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取对话消息列表
|
||||
*/
|
||||
async getChatMessageList(filter: TbChat): Promise<ResultDomain<TbChatMessage>> {
|
||||
const response = await api.post<TbChatMessage>(`${this.baseUrl}/message/list`, filter)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 准备对话会话
|
||||
*/
|
||||
async prepareChatMessageSession(prepareData: ChatPrepareData): Promise<ResultDomain<string>> {
|
||||
const response = await api.post<string>(`${this.baseUrl}/prepare`, prepareData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 流式对话(SSE)- 返回EventSource URL
|
||||
*/
|
||||
getStreamUrl(sessionId: string): string {
|
||||
return `${this.baseUrl}/stream/${sessionId}`
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止对话
|
||||
*/
|
||||
async stopChat(filter: TbChat, taskId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>(`${this.baseUrl}/stop/${taskId}`, filter)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 评论对话消息
|
||||
*/
|
||||
async commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>(`${this.baseUrl}/comment`, filter, {
|
||||
params: { messageId, comment }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 对话分析 ======================
|
||||
|
||||
/**
|
||||
* 分析对话(AI预填工单信息)
|
||||
*/
|
||||
async analyzeChat(chatId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
|
||||
const response = await api.get<TbWorkcaseDTO>(`${this.baseUrl}/analyze/${chatId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 总结对话
|
||||
*/
|
||||
async summaryChat(chatId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
|
||||
const response = await api.post<TbWorkcaseDTO>(`${this.baseUrl}/summary/${chatId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== ChatRoom聊天室管理 ======================
|
||||
|
||||
/**
|
||||
* 创建聊天室
|
||||
*/
|
||||
async createChatRoom(chatRoom: TbChatRoomDTO): Promise<ResultDomain<TbChatRoomDTO>> {
|
||||
const response = await api.post<TbChatRoomDTO>(`${this.baseUrl}/room`, chatRoom)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新聊天室
|
||||
*/
|
||||
async updateChatRoom(chatRoom: TbChatRoomDTO): Promise<ResultDomain<TbChatRoomDTO>> {
|
||||
const response = await api.put<TbChatRoomDTO>(`${this.baseUrl}/room`, chatRoom)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭聊天室
|
||||
*/
|
||||
async closeChatRoom(roomId: string, closedBy: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>(`${this.baseUrl}/room/${roomId}/close`, null, {
|
||||
params: { closedBy }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取聊天室详情
|
||||
*/
|
||||
async getChatRoomById(roomId: string): Promise<ResultDomain<TbChatRoomDTO>> {
|
||||
const response = await api.get<TbChatRoomDTO>(`${this.baseUrl}/room/${roomId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询聊天室
|
||||
*/
|
||||
async getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>): Promise<ResultDomain<ChatRoomVO>> {
|
||||
const response = await api.post<ChatRoomVO>(`${this.baseUrl}/room/page`, pageRequest)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== ChatRoom成员管理 ======================
|
||||
|
||||
/**
|
||||
* 添加聊天室成员
|
||||
*/
|
||||
async addChatRoomMember(member: TbChatRoomMemberDTO): Promise<ResultDomain<TbChatRoomMemberDTO>> {
|
||||
const response = await api.post<TbChatRoomMemberDTO>(`${this.baseUrl}/room/member`, member)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除聊天室成员
|
||||
*/
|
||||
async removeChatRoomMember(memberId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`${this.baseUrl}/room/member/${memberId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取聊天室成员列表
|
||||
*/
|
||||
async getChatRoomMemberList(roomId: string): Promise<ResultDomain<ChatMemberVO>> {
|
||||
const response = await api.get<ChatMemberVO>(`${this.baseUrl}/room/${roomId}/members`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== ChatRoom消息管理 ======================
|
||||
|
||||
/**
|
||||
* 发送聊天室消息
|
||||
*/
|
||||
async sendMessage(message: TbChatRoomMessageDTO): Promise<ResultDomain<TbChatRoomMessageDTO>> {
|
||||
const response = await api.post<TbChatRoomMessageDTO>(`${this.baseUrl}/room/message`, message)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询聊天室消息
|
||||
*/
|
||||
async getChatMessagePage(pageRequest: PageRequest<TbChatRoomMessageDTO>): Promise<ResultDomain<ChatRoomMessageVO>> {
|
||||
const response = await api.post<ChatRoomMessageVO>(`${this.baseUrl}/room/message/page`, pageRequest)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除聊天室消息
|
||||
*/
|
||||
async deleteMessage(messageId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`${this.baseUrl}/room/message/${messageId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 客服人员管理 ======================
|
||||
|
||||
/**
|
||||
* 添加客服人员
|
||||
*/
|
||||
async addCustomerService(customerService: TbCustomerServiceDTO): Promise<ResultDomain<TbCustomerServiceDTO>> {
|
||||
const response = await api.post<TbCustomerServiceDTO>(`${this.baseUrl}/customer-service`, customerService)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新客服人员
|
||||
*/
|
||||
async updateCustomerService(customerService: TbCustomerServiceDTO): Promise<ResultDomain<TbCustomerServiceDTO>> {
|
||||
const response = await api.put<TbCustomerServiceDTO>(`${this.baseUrl}/customer-service`, customerService)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除客服人员
|
||||
*/
|
||||
async deleteCustomerService(userId: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`${this.baseUrl}/customer-service/${userId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询客服人员
|
||||
*/
|
||||
async getCustomerServicePage(pageRequest: PageRequest<TbCustomerServiceDTO>): Promise<ResultDomain<CustomerServiceVO>> {
|
||||
const response = await api.post<CustomerServiceVO>(`${this.baseUrl}/customer-service/page`, pageRequest)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新客服在线状态
|
||||
*/
|
||||
async updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>(`${this.baseUrl}/customer-service/${userId}/status`, null, {
|
||||
params: { status }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可接待客服列表
|
||||
*/
|
||||
async getAvailableCustomerServices(): Promise<ResultDomain<CustomerServiceVO>> {
|
||||
const response = await api.get<CustomerServiceVO>(`${this.baseUrl}/customer-service/available`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动分配客服
|
||||
*/
|
||||
async assignCustomerService(roomId: string): Promise<ResultDomain<CustomerServiceVO>> {
|
||||
const response = await api.post<CustomerServiceVO>(`${this.baseUrl}/room/${roomId}/assign`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 词云管理 ======================
|
||||
|
||||
/**
|
||||
* 添加词云
|
||||
*/
|
||||
async addWordCloud(wordCloud: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
|
||||
const response = await api.post<TbWordCloudDTO>(`${this.baseUrl}/wordcloud`, wordCloud)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新词云
|
||||
*/
|
||||
async updateWordCloud(wordCloud: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
|
||||
const response = await api.put<TbWordCloudDTO>(`${this.baseUrl}/wordcloud`, wordCloud)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询词云列表
|
||||
*/
|
||||
async getWordCloudList(filter: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
|
||||
const response = await api.post<TbWordCloudDTO>(`${this.baseUrl}/wordcloud/list`, filter)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询词云
|
||||
*/
|
||||
async getWordCloudPage(pageRequest: PageRequest<TbWordCloudDTO>): Promise<ResultDomain<TbWordCloudDTO>> {
|
||||
const response = await api.post<TbWordCloudDTO>(`${this.baseUrl}/wordcloud/page`, pageRequest)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
|
||||
import IframeView from 'shared/components/iframe/IframeView.vue'
|
||||
import IframeView from '@/components/iframe/IframeView.vue'
|
||||
|
||||
interface ChatMessageVO {
|
||||
messageId: string
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as ChatRoom } from './ChatRoom.vue';
|
||||
export { default as ChatRoom } from './chatRoom/ChatRoom.vue';
|
||||
@@ -28,7 +28,7 @@ export interface TbChatRoomDTO extends BaseDTO {
|
||||
/**
|
||||
* 聊天消息DTO
|
||||
*/
|
||||
export interface TbChatMessageDTO extends BaseDTO {
|
||||
export interface TbChatRoomMessageDTO extends BaseDTO {
|
||||
messageId?: string
|
||||
roomId?: string
|
||||
senderId?: string
|
||||
@@ -174,7 +174,7 @@ export interface ChatRoomVO extends BaseVO {
|
||||
* 聊天消息VO
|
||||
* 用于前端展示聊天消息
|
||||
*/
|
||||
export interface ChatMessageVO extends BaseVO {
|
||||
export interface ChatRoomMessageVO extends BaseVO {
|
||||
messageId?: string
|
||||
roomId?: string
|
||||
senderId?: string
|
||||
@@ -292,3 +292,60 @@ export interface MarkReadParam {
|
||||
roomId: string
|
||||
messageIds?: string[]
|
||||
}
|
||||
|
||||
// ==================== 客服相关 ====================
|
||||
|
||||
/**
|
||||
* 客服人员DTO
|
||||
*/
|
||||
export interface TbCustomerServiceDTO extends BaseDTO {
|
||||
userId?: string
|
||||
userName?: string
|
||||
status?: string
|
||||
maxConcurrentChats?: number
|
||||
currentChatCount?: number
|
||||
totalServedCount?: number
|
||||
avgResponseTime?: number
|
||||
avgRating?: number
|
||||
skills?: string[]
|
||||
priority?: number
|
||||
lastOnlineTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服人员VO
|
||||
*/
|
||||
export interface CustomerServiceVO extends BaseVO {
|
||||
userId?: string
|
||||
userName?: string
|
||||
userAvatar?: string
|
||||
status?: string
|
||||
statusName?: string
|
||||
maxConcurrentChats?: number
|
||||
currentChatCount?: number
|
||||
totalServedCount?: number
|
||||
avgResponseTime?: number
|
||||
avgResponseTimeFormatted?: string
|
||||
avgRating?: number
|
||||
skills?: string[]
|
||||
skillNames?: string[]
|
||||
priority?: number
|
||||
lastOnlineTime?: string
|
||||
isAvailable?: boolean
|
||||
}
|
||||
|
||||
// ==================== 词云相关 ====================
|
||||
|
||||
/**
|
||||
* 词云DTO
|
||||
*/
|
||||
export interface TbWordCloudDTO extends BaseDTO {
|
||||
wordCloudId?: string
|
||||
word?: string
|
||||
category?: string
|
||||
weight?: number
|
||||
frequency?: number
|
||||
sentiment?: string
|
||||
source?: string
|
||||
relatedWords?: string[]
|
||||
}
|
||||
|
||||
@@ -10,17 +10,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@stomp/stompjs": "^7.2.1",
|
||||
"@vueuse/core": "^11.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.8.6",
|
||||
"lucide-vue-next": "^0.561.0",
|
||||
"pinia": "^2.2.8",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@module-federation/vite": "^1.9.3",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/sockjs-client": "^1.5.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"typescript": "^5.7.2",
|
||||
|
||||
106
urbanLifelineWeb/packages/workcase/pnpm-lock.yaml
generated
106
urbanLifelineWeb/packages/workcase/pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2(vue@3.5.25)
|
||||
'@stomp/stompjs':
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1
|
||||
'@vueuse/core':
|
||||
specifier: ^11.3.0
|
||||
version: 11.3.0(vue@3.5.25)
|
||||
@@ -17,9 +20,15 @@ dependencies:
|
||||
element-plus:
|
||||
specifier: ^2.8.6
|
||||
version: 2.12.0(vue@3.5.25)
|
||||
lucide-vue-next:
|
||||
specifier: ^0.561.0
|
||||
version: 0.561.0(vue@3.5.25)
|
||||
pinia:
|
||||
specifier: ^2.2.8
|
||||
version: 2.3.1(typescript@5.9.3)(vue@3.5.25)
|
||||
sockjs-client:
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.25(typescript@5.9.3)
|
||||
@@ -34,6 +43,9 @@ devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.1
|
||||
'@types/sockjs-client':
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.4(vite@6.4.1)(vue@3.5.25)
|
||||
@@ -841,6 +853,10 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@stomp/stompjs@7.2.1:
|
||||
resolution: {integrity: sha512-DLd/WeicnHS5SsWWSk3x6/pcivqchNaEvg9UEGVqAcfYEBVmS9D6980ckXjTtfpXLjdLDsd96M7IuX4w7nzq5g==}
|
||||
dev: false
|
||||
|
||||
/@sxzz/popperjs-es@2.11.7:
|
||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||
dev: false
|
||||
@@ -865,6 +881,10 @@ packages:
|
||||
undici-types: 6.21.0
|
||||
dev: true
|
||||
|
||||
/@types/sockjs-client@1.5.4:
|
||||
resolution: {integrity: sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==}
|
||||
dev: true
|
||||
|
||||
/@types/web-bluetooth@0.0.16:
|
||||
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
|
||||
dev: false
|
||||
@@ -1188,6 +1208,17 @@ packages:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
dev: true
|
||||
|
||||
/debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
||||
/debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1319,6 +1350,18 @@ packages:
|
||||
/estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
/eventsource@2.0.2:
|
||||
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: false
|
||||
|
||||
/faye-websocket@0.11.4:
|
||||
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
websocket-driver: 0.7.4
|
||||
dev: false
|
||||
|
||||
/fdir@6.5.0(picomatch@4.0.3):
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -1422,6 +1465,14 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/http-parser-js@0.5.10:
|
||||
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
|
||||
dev: false
|
||||
|
||||
/inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: false
|
||||
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
dev: true
|
||||
@@ -1464,6 +1515,14 @@ packages:
|
||||
yallist: 3.1.1
|
||||
dev: true
|
||||
|
||||
/lucide-vue-next@0.561.0(vue@3.5.25):
|
||||
resolution: {integrity: sha512-c5HUckO0qHklVSOf/0vaSR3pEb8fYImRDCRDLde56uqS9js0D/e3RAvq0/YFWjkmyOBKCb0/IdskdoHZQEkT5g==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
dependencies:
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
dev: false
|
||||
|
||||
/magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
dependencies:
|
||||
@@ -1499,7 +1558,6 @@ packages:
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
@@ -1563,6 +1621,14 @@ packages:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
dev: false
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: false
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: false
|
||||
|
||||
/rollup@4.53.3:
|
||||
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -1595,11 +1661,28 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: false
|
||||
|
||||
/semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/sockjs-client@1.6.1:
|
||||
resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
eventsource: 2.0.2
|
||||
faye-websocket: 0.11.4
|
||||
inherits: 2.0.4
|
||||
url-parse: 1.5.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1632,6 +1715,13 @@ packages:
|
||||
picocolors: 1.1.1
|
||||
dev: true
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: false
|
||||
|
||||
/vite@6.4.1(@types/node@22.19.1):
|
||||
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@@ -1737,6 +1827,20 @@ packages:
|
||||
'@vue/shared': 3.5.25
|
||||
typescript: 5.9.3
|
||||
|
||||
/websocket-driver@0.7.4:
|
||||
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
http-parser-js: 0.5.10
|
||||
safe-buffer: 5.2.1
|
||||
websocket-extensions: 0.1.4
|
||||
dev: false
|
||||
|
||||
/websocket-extensions@0.1.4:
|
||||
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: true
|
||||
|
||||
@@ -98,8 +98,10 @@ declare module 'shared/api/ai' {
|
||||
|
||||
declare module 'shared/api/workcase' {
|
||||
export const workcaseAPI: any
|
||||
export const workcaseChatAPI: any
|
||||
}
|
||||
|
||||
|
||||
// ============ types模块 ==================
|
||||
declare module 'shared/types' {
|
||||
import type { BaseDTO } from '../../../shared/src/types/base'
|
||||
@@ -143,21 +145,30 @@ declare module 'shared/types' {
|
||||
TbWorkcaseDeviceDTO,
|
||||
// 聊天室相关
|
||||
TbChatRoomDTO,
|
||||
TbChatMessageDTO,
|
||||
TbChatRoomMessageDTO,
|
||||
TbChatRoomMemberDTO,
|
||||
TbVideoMeetingDTO,
|
||||
TbMeetingParticipantDTO,
|
||||
TbMeetingTranscriptionDTO,
|
||||
ChatRoomVO,
|
||||
ChatMessageVO,
|
||||
ChatRoomMessageVO,
|
||||
ChatMemberVO,
|
||||
VideoMeetingVO,
|
||||
MeetingParticipantVO,
|
||||
SendMessageParam,
|
||||
CreateMeetingParam,
|
||||
MarkReadParam,
|
||||
// 客服相关
|
||||
TbCustomerServiceDTO,
|
||||
CustomerServiceVO,
|
||||
// 词云
|
||||
TbWordCloudDTO,
|
||||
// 来客相关
|
||||
TbGuestDTO,
|
||||
GuestVO
|
||||
GuestVO,
|
||||
CustomerVO,
|
||||
ConversationVO
|
||||
} from '../../../shared/src/types/workcase'
|
||||
|
||||
// 重新导出 menu
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<div class="room-avatar">
|
||||
{{ room.guestName.substring(0, 1) }}
|
||||
{{ room.guestName?.substring(0, 1) || '?' }}
|
||||
</div>
|
||||
|
||||
<!-- 信息 -->
|
||||
@@ -114,67 +114,50 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElButton, ElInput, ElDialog } from 'element-plus'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
|
||||
import { Search, FileText, MessageSquare } from 'lucide-vue-next'
|
||||
import ChatRoom from 'shared/components/chatRoom/ChatRoom.vue'
|
||||
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
|
||||
import { workcaseChatAPI } from 'shared/api/workcase'
|
||||
import { fileAPI } from 'shared/api/file'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
import type { ChatRoomVO, ChatRoomMessageVO, TbChatRoomMessageDTO } from 'shared/types'
|
||||
import SockJS from 'sockjs-client'
|
||||
import { Client } from '@stomp/stompjs'
|
||||
|
||||
interface ChatRoomVO {
|
||||
roomId: string
|
||||
workcaseId: string
|
||||
roomName: string
|
||||
guestName: string
|
||||
lastMessage: string | null
|
||||
lastMessageTime: string | null
|
||||
unreadCount: number
|
||||
// WebSocket配置 (通过网关代理访问workcase服务)
|
||||
// 原生WebSocket URL (ws://或wss://)
|
||||
const getWsUrl = () => {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
// 直接连接网关,跳过Nginx调试
|
||||
return `ws://localhost:8180/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
|
||||
}
|
||||
|
||||
interface ChatMessageVO {
|
||||
messageId: string
|
||||
senderId: string
|
||||
senderName: string
|
||||
senderAvatar: string
|
||||
content: string
|
||||
files: string[]
|
||||
sendTime: string
|
||||
}
|
||||
// STOMP客户端
|
||||
let stompClient: any = null
|
||||
let roomSubscription: any = null
|
||||
let listSubscription: any = null
|
||||
|
||||
// 当前用户ID
|
||||
const userId = ref('CURRENT_USER_ID')
|
||||
// 当前用户ID(从登录状态获取)
|
||||
const userId = ref(localStorage.getItem('userId') || '')
|
||||
|
||||
// 搜索文本
|
||||
const searchText = ref('')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const messageLoading = ref(false)
|
||||
|
||||
// 聊天室列表
|
||||
const chatRooms = ref<ChatRoomVO[]>([
|
||||
{
|
||||
roomId: 'ROOM001',
|
||||
workcaseId: 'WC001',
|
||||
roomName: '工单#WC001 - 电源故障',
|
||||
guestName: '张三',
|
||||
lastMessage: '好的,谢谢您的帮助',
|
||||
lastMessageTime: new Date().toISOString(),
|
||||
unreadCount: 3
|
||||
},
|
||||
{
|
||||
roomId: 'ROOM002',
|
||||
workcaseId: 'WC002',
|
||||
roomName: '工单#WC002 - 设备维修',
|
||||
guestName: '李四',
|
||||
lastMessage: '请问什么时候能来处理?',
|
||||
lastMessageTime: new Date(Date.now() - 3600000).toISOString(),
|
||||
unreadCount: 0
|
||||
}
|
||||
])
|
||||
const chatRooms = ref<ChatRoomVO[]>([])
|
||||
|
||||
// 当前选中的聊天室ID
|
||||
const currentRoomId = ref<string | null>(null)
|
||||
|
||||
// 当前聊天室
|
||||
const currentRoom = computed(() =>
|
||||
chatRooms.value.find(r => r.roomId === currentRoomId.value)
|
||||
chatRooms.value.find((r: ChatRoomVO) => r.roomId === currentRoomId.value)
|
||||
)
|
||||
|
||||
// 当前工单ID
|
||||
@@ -184,15 +167,15 @@ const currentWorkcaseId = computed(() => currentRoom.value?.workcaseId || '')
|
||||
const filteredRooms = computed(() => {
|
||||
if (!searchText.value) return chatRooms.value
|
||||
const keyword = searchText.value.toLowerCase()
|
||||
return chatRooms.value.filter(room =>
|
||||
room.roomName.toLowerCase().includes(keyword) ||
|
||||
room.guestName.toLowerCase().includes(keyword) ||
|
||||
room.workcaseId.toLowerCase().includes(keyword)
|
||||
return chatRooms.value.filter((room: ChatRoomVO) =>
|
||||
room.roomName?.toLowerCase().includes(keyword) ||
|
||||
room.guestName?.toLowerCase().includes(keyword) ||
|
||||
room.workcaseId?.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 消息列表
|
||||
const messages = ref<ChatMessageVO[]>([])
|
||||
const messages = ref<ChatRoomMessageVO[]>([])
|
||||
|
||||
// 工单详情对话框
|
||||
const showWorkcaseDetail = ref(false)
|
||||
@@ -201,66 +184,93 @@ const showWorkcaseDetail = ref(false)
|
||||
const currentMeetingUrl = ref('')
|
||||
const showMeetingIframe = ref(false)
|
||||
|
||||
// 获取聊天室列表
|
||||
const fetchChatRooms = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await workcaseChatAPI.getChatRoomPage({
|
||||
filter: { status: 'active' },
|
||||
pageParam: { page: 1, pageSize: 100, total: 0 }
|
||||
})
|
||||
if (result.success && result.pageDomain) {
|
||||
chatRooms.value = result.pageDomain.dataList || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取聊天室列表失败:', error)
|
||||
ElMessage.error('获取聊天室列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择聊天室
|
||||
const selectRoom = (roomId: string) => {
|
||||
currentRoomId.value = roomId
|
||||
// TODO: 加载该聊天室的消息
|
||||
loadMessages(roomId)
|
||||
}
|
||||
|
||||
// 加载消息
|
||||
const loadMessages = async (roomId: string) => {
|
||||
// TODO: 调用API加载消息
|
||||
messages.value = [
|
||||
{
|
||||
messageId: 'MSG001',
|
||||
senderId: 'OTHER_USER',
|
||||
senderName: '张三',
|
||||
senderAvatar: 'avatar.jpg',
|
||||
content: '你好,我的设备出现故障了',
|
||||
files: [],
|
||||
sendTime: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
messageId: 'MSG002',
|
||||
senderId: userId.value,
|
||||
senderName: '客服',
|
||||
senderAvatar: 'avatar.jpg',
|
||||
content: '您好,请问是什么故障?',
|
||||
files: [],
|
||||
sendTime: new Date().toISOString()
|
||||
messageLoading.value = true
|
||||
try {
|
||||
const result = await workcaseChatAPI.getChatMessagePage({
|
||||
filter: { roomId },
|
||||
pageParam: { page: 1, pageSize: 100, total: 0 }
|
||||
})
|
||||
if (result.success && result.pageDomain) {
|
||||
messages.value = result.pageDomain.dataList || []
|
||||
}
|
||||
]
|
||||
scrollToBottom()
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
ElMessage.error('加载消息失败')
|
||||
} finally {
|
||||
messageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理发送消息(从ChatRoom组件触发)
|
||||
const handleSendMessage = async (content: string, files: File[]) => {
|
||||
if (!currentRoomId.value) return
|
||||
|
||||
// TODO: 上传文件获取fileIds
|
||||
const fileIds: string[] = []
|
||||
try {
|
||||
// 上传文件获取fileIds
|
||||
let fileIds: string[] = []
|
||||
if (files.length > 0) {
|
||||
const uploadResult = await fileAPI.batchUpload(files, 'chatroom')
|
||||
if (uploadResult.success && uploadResult.dataList) {
|
||||
fileIds = uploadResult.dataList.map((f: any) => f.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
const newMessage: ChatMessageVO = {
|
||||
messageId: 'MSG' + Date.now(),
|
||||
senderId: userId.value,
|
||||
senderName: '客服',
|
||||
senderAvatar: 'avatar.jpg',
|
||||
content,
|
||||
files: fileIds,
|
||||
sendTime: new Date().toISOString()
|
||||
// 构造消息
|
||||
const messageData: TbChatRoomMessageDTO = {
|
||||
roomId: currentRoomId.value,
|
||||
senderId: userId.value,
|
||||
senderType: 'agent',
|
||||
content,
|
||||
files: fileIds,
|
||||
messageType: 'text'
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const result = await workcaseChatAPI.sendMessage(messageData)
|
||||
if (result.success && result.data) {
|
||||
// 添加到消息列表
|
||||
messages.value.push(result.data as ChatRoomMessageVO)
|
||||
scrollToBottom()
|
||||
} else {
|
||||
ElMessage.error(result.message || '发送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
ElMessage.error('发送消息失败')
|
||||
}
|
||||
|
||||
messages.value.push(newMessage)
|
||||
|
||||
// TODO: 通过WebSocket发送到服务器
|
||||
console.log('发送消息:', { content, files })
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (fileId: string) => {
|
||||
// TODO: 下载文件
|
||||
console.log('下载文件:', fileId)
|
||||
window.open(`${FILE_DOWNLOAD_URL}/${fileId}`, '_blank')
|
||||
}
|
||||
|
||||
// 发起会议
|
||||
@@ -268,14 +278,9 @@ const startMeeting = async () => {
|
||||
if (!currentRoomId.value) return
|
||||
|
||||
// TODO: 调用后端API创建Jitsi会议
|
||||
// const meeting = await createMeeting(currentRoomId.value)
|
||||
|
||||
// 模拟会议URL
|
||||
const meetingId = 'meeting-' + Date.now()
|
||||
const meetingId = 'meeting-' + currentRoomId.value + '-' + Date.now()
|
||||
currentMeetingUrl.value = `https://meet.jit.si/${meetingId}`
|
||||
showMeetingIframe.value = true
|
||||
|
||||
console.log('发起会议:', currentMeetingUrl.value)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
@@ -284,7 +289,7 @@ const scrollToBottom = () => {
|
||||
}
|
||||
|
||||
// 格式化时间(用于聊天室列表)
|
||||
const formatTime = (time: string | null) => {
|
||||
const formatTime = (time: string | null | undefined) => {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
@@ -296,6 +301,122 @@ const formatTime = (time: string | null) => {
|
||||
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
||||
}
|
||||
|
||||
// ==================== WebSocket连接管理 ====================
|
||||
|
||||
// 初始化WebSocket连接(使用原生WebSocket,不降级)
|
||||
const initWebSocket = () => {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const wsUrl = getWsUrl()
|
||||
|
||||
console.log('WebSocket连接URL:', wsUrl)
|
||||
|
||||
// 创建STOMP客户端,使用原生WebSocket
|
||||
stompClient = new Client({
|
||||
brokerURL: wsUrl, // 使用原生WebSocket URL
|
||||
connectHeaders: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
reconnectDelay: 5000,
|
||||
heartbeatIncoming: 4000,
|
||||
heartbeatOutgoing: 4000,
|
||||
debug: (str: string) => {
|
||||
console.log('[STOMP]', str)
|
||||
},
|
||||
onConnect: () => {
|
||||
console.log('WebSocket已连接')
|
||||
// 订阅聊天室列表更新
|
||||
subscribeToListUpdate()
|
||||
// 如果当前有选中的聊天室,订阅该聊天室消息
|
||||
if (currentRoomId.value) {
|
||||
subscribeToRoom(currentRoomId.value)
|
||||
}
|
||||
},
|
||||
onDisconnect: () => {
|
||||
console.log('WebSocket已断开')
|
||||
},
|
||||
onStompError: (frame: any) => {
|
||||
console.error('STOMP错误:', frame)
|
||||
},
|
||||
onWebSocketError: (event: any) => {
|
||||
console.error('WebSocket错误:', event)
|
||||
}
|
||||
})
|
||||
|
||||
stompClient.activate()
|
||||
}
|
||||
|
||||
// 订阅聊天室列表更新 (用于更新列表中的lastMessage)
|
||||
const subscribeToListUpdate = () => {
|
||||
if (!stompClient || !stompClient.connected) return
|
||||
|
||||
listSubscription = stompClient.subscribe('/topic/chat/list-update', (message: any) => {
|
||||
const chatMessage = JSON.parse(message.body)
|
||||
// 更新对应聊天室的lastMessage和lastMessageTime
|
||||
const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === chatMessage.roomId)
|
||||
if (roomIndex !== -1) {
|
||||
chatRooms.value[roomIndex] = {
|
||||
...chatRooms.value[roomIndex],
|
||||
lastMessage: chatMessage.content,
|
||||
lastMessageTime: chatMessage.sendTime
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 订阅指定聊天室消息 (用于实时接收消息)
|
||||
const subscribeToRoom = (roomId: string) => {
|
||||
if (!stompClient || !stompClient.connected) return
|
||||
|
||||
// 先取消之前的订阅
|
||||
if (roomSubscription) {
|
||||
roomSubscription.unsubscribe()
|
||||
roomSubscription = null
|
||||
}
|
||||
|
||||
roomSubscription = stompClient.subscribe(`/topic/chat/${roomId}`, (message: any) => {
|
||||
const chatMessage = JSON.parse(message.body) as ChatRoomMessageVO
|
||||
// 避免重复添加自己发送的消息
|
||||
if (chatMessage.senderId !== userId.value) {
|
||||
messages.value.push(chatMessage)
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 断开WebSocket连接
|
||||
const disconnectWebSocket = () => {
|
||||
if (roomSubscription) {
|
||||
roomSubscription.unsubscribe()
|
||||
roomSubscription = null
|
||||
}
|
||||
if (listSubscription) {
|
||||
listSubscription.unsubscribe()
|
||||
listSubscription = null
|
||||
}
|
||||
if (stompClient) {
|
||||
stompClient.deactivate()
|
||||
stompClient = null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听currentRoomId变化,切换聊天室时重新订阅
|
||||
watch(currentRoomId, (newRoomId) => {
|
||||
if (newRoomId && stompClient?.connected) {
|
||||
subscribeToRoom(newRoomId)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchChatRooms()
|
||||
initWebSocket()
|
||||
})
|
||||
|
||||
// 组件卸载时断开连接
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket()
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import url("./ChatRoomView.scss");
|
||||
|
||||
@@ -41,7 +41,8 @@ export default defineConfig(({ mode }) => ({
|
||||
define: {
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: true,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true,
|
||||
global: 'globalThis'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
@@ -64,6 +65,7 @@ export default defineConfig(({ mode }) => ({
|
||||
'/api': {
|
||||
target: 'http://localhost:8180',
|
||||
changeOrigin: true,
|
||||
ws: true, // 启用 WebSocket 代理
|
||||
rewrite: (path: string) => path.replace(/^\/api/, '')
|
||||
},
|
||||
// 代理共享模块请求到 shared 服务
|
||||
|
||||
6
urbanLifelineWeb/packages/workcase_wechat/package.json
Normal file
6
urbanLifelineWeb/packages/workcase_wechat/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"sockjs-client": "^1.6.1",
|
||||
"@stomp/stompjs": "^7.2.1"
|
||||
}
|
||||
}
|
||||
100
urbanLifelineWeb/packages/workcase_wechat/pnpm-lock.yaml
generated
Normal file
100
urbanLifelineWeb/packages/workcase_wechat/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,100 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@stomp/stompjs':
|
||||
specifier: ^7.2.1
|
||||
version: 7.2.1
|
||||
sockjs-client:
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1
|
||||
|
||||
packages:
|
||||
|
||||
/@stomp/stompjs@7.2.1:
|
||||
resolution: {integrity: sha512-DLd/WeicnHS5SsWWSk3x6/pcivqchNaEvg9UEGVqAcfYEBVmS9D6980ckXjTtfpXLjdLDsd96M7IuX4w7nzq5g==}
|
||||
dev: false
|
||||
|
||||
/debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
||||
/eventsource@2.0.2:
|
||||
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dev: false
|
||||
|
||||
/faye-websocket@0.11.4:
|
||||
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
websocket-driver: 0.7.4
|
||||
dev: false
|
||||
|
||||
/http-parser-js@0.5.10:
|
||||
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
|
||||
dev: false
|
||||
|
||||
/inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: false
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: false
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: false
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: false
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: false
|
||||
|
||||
/sockjs-client@1.6.1:
|
||||
resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
eventsource: 2.0.2
|
||||
faye-websocket: 0.11.4
|
||||
inherits: 2.0.4
|
||||
url-parse: 1.5.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: false
|
||||
|
||||
/websocket-driver@0.7.4:
|
||||
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
http-parser-js: 0.5.10
|
||||
safe-buffer: 5.2.1
|
||||
websocket-extensions: 0.1.4
|
||||
dev: false
|
||||
|
||||
/websocket-extensions@0.1.4:
|
||||
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user