From b023bec2616d8e24135534f1709ac65d95a2031d Mon Sep 17 00:00:00 2001
From: wangys <3401275564@qq.com>
Date: Mon, 22 Dec 2025 17:03:37 +0800
Subject: [PATCH] =?UTF-8?q?gateway=20tomcat=E5=8E=BB=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../postgres/sql/createTableWorkcase.sql | 2 +-
.../postgres/sql/initDataPermission.sql | 63 +++-
.../postgres/sql/initDataWorkcase.sql | 6 +
urbanLifelineServ/ai/pom.xml | 5 +
urbanLifelineServ/apis/pom.xml | 29 ++
urbanLifelineServ/auth/pom.xml | 18 +
.../common/auth/config/GatewayAuthConfig.java | 31 +-
.../auth/filter/JwtAuthenticationFilter.java | 2 +-
urbanLifelineServ/common/common-dto/pom.xml | 8 +
.../xyzh/common/dto/sys/TbSysUserRoleDTO.java | 6 +
urbanLifelineServ/common/common-utils/pom.xml | 19 ++
.../utils/config/FastJsonConfiguration.java | 3 +-
urbanLifelineServ/common/pom.xml | 20 ++
urbanLifelineServ/file/pom.xml | 6 +
urbanLifelineServ/gateway/pom.xml | 168 +++++++++-
.../org/xyzh/gateway/GatewayApplication.java | 14 +-
.../xyzh/gateway/filter/AuthGlobalFilter.java | 33 +-
.../src/main/resources/application.yml | 8 +
urbanLifelineServ/log/pom.xml | 5 +
urbanLifelineServ/message/pom.xml | 16 +
urbanLifelineServ/pom.xml | 30 +-
urbanLifelineServ/system/pom.xml | 18 +
.../system/controller/GuestController.java | 66 ++++
.../system/service/impl/GuestServiceImpl.java | 16 +
.../src/main/resources/application-dev.yml | 51 ++-
.../system/src/main/resources/application.yml | 50 +--
urbanLifelineServ/workcase/pom.xml | 6 +
.../config/WebSocketAuthInterceptor.java | 110 +++++++
.../xyzh/workcase/config/WebSocketConfig.java | 18 +-
.../controller/WorkcaseChatContorller.java | 261 +++++++++++++--
.../controller/WorkcaseController.java | 17 +
.../src/main/resources/application.yml | 2 +-
urbanLifelineWeb/.vscode/launch.json | 18 +-
.../packages/bidding/src/types/shared.d.ts | 21 +-
.../packages/platform/pnpm-lock.yaml | 11 +
.../packages/platform/src/types/shared.d.ts | 21 +-
urbanLifelineWeb/packages/shared/package.json | 2 +
.../packages/shared/pnpm-lock.yaml | 90 +++++
.../packages/shared/src/api/workcase/index.ts | 1 +
.../shared/src/api/workcase/workcaseChat.ts | 301 +++++++++++++++++
.../components/chatRoom/chatRoom/ChatRoom.vue | 2 +-
.../shared/src/components/chatRoom/index.ts | 2 +-
.../shared/src/types/workcase/chatRoom.ts | 61 +++-
.../packages/workcase/package.json | 3 +
.../packages/workcase/pnpm-lock.yaml | 106 +++++-
.../packages/workcase/src/types/shared.d.ts | 15 +-
.../views/public/ChatRoom/ChatRoomView.vue | 311 ++++++++++++------
.../packages/workcase/vite.config.ts | 4 +-
.../packages/workcase_wechat/package.json | 6 +
.../packages/workcase_wechat/pnpm-lock.yaml | 100 ++++++
.../58fece2f5498ef30e0e385846df90df38196ba82 | 1 +
... a71acfafa708e3cf031a5dfbb5150ae026174408} | 0
.../c94f6ecc8c2e943a1daefff4333af748c5da5830 | 1 +
.../e7dda72aabb7327ace1257371777ab9cb044277e | 1 -
.../f261594d19a040c129a8fd4ce88589e0f7988433 | 1 -
55 files changed, 1926 insertions(+), 260 deletions(-)
create mode 100644 urbanLifelineServ/.bin/database/postgres/sql/initDataWorkcase.sql
create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketAuthInterceptor.java
create mode 100644 urbanLifelineWeb/packages/shared/src/api/workcase/workcaseChat.ts
create mode 100644 urbanLifelineWeb/packages/workcase_wechat/package.json
create mode 100644 urbanLifelineWeb/packages/workcase_wechat/pnpm-lock.yaml
create mode 100644 urbanLifelineWeb/packages/workcase_wechat/unpackage/cache/.mp-weixin/.uts2js/cache/uts_9b1b54d07a7a4d66ee84f54872fe1ae86df34bb1/code/cache/58fece2f5498ef30e0e385846df90df38196ba82
rename urbanLifelineWeb/packages/workcase_wechat/unpackage/cache/.mp-weixin/.uts2js/cache/uts_9b1b54d07a7a4d66ee84f54872fe1ae86df34bb1/code/cache/{c17110e7e9205cbffe9484ef36c48da353a3c2e6 => a71acfafa708e3cf031a5dfbb5150ae026174408} (100%)
create mode 100644 urbanLifelineWeb/packages/workcase_wechat/unpackage/cache/.mp-weixin/.uts2js/cache/uts_9b1b54d07a7a4d66ee84f54872fe1ae86df34bb1/code/cache/c94f6ecc8c2e943a1daefff4333af748c5da5830
delete mode 100644 urbanLifelineWeb/packages/workcase_wechat/unpackage/cache/.mp-weixin/.uts2js/cache/uts_9b1b54d07a7a4d66ee84f54872fe1ae86df34bb1/code/cache/e7dda72aabb7327ace1257371777ab9cb044277e
delete mode 100644 urbanLifelineWeb/packages/workcase_wechat/unpackage/cache/.mp-weixin/.uts2js/cache/uts_9b1b54d07a7a4d66ee84f54872fe1ae86df34bb1/code/cache/f261594d19a040c129a8fd4ce88589e0f7988433
diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql
index dbd95f1e..127f732f 100644
--- a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql
+++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql
@@ -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, -- 该成员的未读消息数
diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql
index 3c17047f..f2b62e7a 100644
--- a/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql
+++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql
@@ -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),
diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataWorkcase.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataWorkcase.sql
new file mode 100644
index 00000000..cf786a91
--- /dev/null
+++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataWorkcase.sql
@@ -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);
\ No newline at end of file
diff --git a/urbanLifelineServ/ai/pom.xml b/urbanLifelineServ/ai/pom.xml
index d0fa3cc4..9cf8920b 100644
--- a/urbanLifelineServ/ai/pom.xml
+++ b/urbanLifelineServ/ai/pom.xml
@@ -104,6 +104,11 @@
com.squareup.okhttp3
okhttp-sse
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
\ No newline at end of file
diff --git a/urbanLifelineServ/apis/pom.xml b/urbanLifelineServ/apis/pom.xml
index 9dc297d6..26444721 100644
--- a/urbanLifelineServ/apis/pom.xml
+++ b/urbanLifelineServ/apis/pom.xml
@@ -82,6 +82,35 @@
org.apache.dubbo
dubbo
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+ org.springframework
+ spring-web
+ provided
+
+
+
+
+ org.springframework
+ spring-webmvc
+ provided
+
+
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+ 2.2.36
+ provided
+
\ No newline at end of file
diff --git a/urbanLifelineServ/auth/pom.xml b/urbanLifelineServ/auth/pom.xml
index 897eb535..e6cbbb59 100644
--- a/urbanLifelineServ/auth/pom.xml
+++ b/urbanLifelineServ/auth/pom.xml
@@ -59,6 +59,24 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
\ No newline at end of file
diff --git a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/GatewayAuthConfig.java b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/GatewayAuthConfig.java
index eecfe516..d685a2e1 100644
--- a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/GatewayAuthConfig.java
+++ b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/config/GatewayAuthConfig.java
@@ -100,14 +100,13 @@ public class GatewayAuthConfig {
List 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;
+ }
}
}
diff --git a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/filter/JwtAuthenticationFilter.java b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/filter/JwtAuthenticationFilter.java
index 1ae5b9ac..43afd1f6 100644
--- a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/filter/JwtAuthenticationFilter.java
+++ b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/filter/JwtAuthenticationFilter.java
@@ -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 permAuthorities = null;
diff --git a/urbanLifelineServ/common/common-dto/pom.xml b/urbanLifelineServ/common/common-dto/pom.xml
index 4151742b..db5b34a0 100644
--- a/urbanLifelineServ/common/common-dto/pom.xml
+++ b/urbanLifelineServ/common/common-dto/pom.xml
@@ -35,6 +35,14 @@
common-utils
${urban-lifeline.version}
+
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+ 2.2.36
+ provided
+
\ No newline at end of file
diff --git a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbSysUserRoleDTO.java b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbSysUserRoleDTO.java
index 4cd31f4f..4f9bc1d4 100644
--- a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbSysUserRoleDTO.java
+++ b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbSysUserRoleDTO.java
@@ -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;
}
\ No newline at end of file
diff --git a/urbanLifelineServ/common/common-utils/pom.xml b/urbanLifelineServ/common/common-utils/pom.xml
index ad51b7d6..31b6b205 100644
--- a/urbanLifelineServ/common/common-utils/pom.xml
+++ b/urbanLifelineServ/common/common-utils/pom.xml
@@ -19,6 +19,25 @@
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+ org.springframework
+ spring-web
+ provided
+
+
+ org.springframework
+ spring-webmvc
+ provided
+
+
org.apache.poi
diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java
index 7d00be0c..c889bbae 100644
--- a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java
+++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/config/FastJsonConfiguration.java
@@ -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 {
/**
diff --git a/urbanLifelineServ/common/pom.xml b/urbanLifelineServ/common/pom.xml
index e3e5d983..55eafe1b 100644
--- a/urbanLifelineServ/common/pom.xml
+++ b/urbanLifelineServ/common/pom.xml
@@ -79,4 +79,24 @@
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+ org.springframework
+ spring-web
+ provided
+
+
+ org.springframework
+ spring-webmvc
+ provided
+
+
\ No newline at end of file
diff --git a/urbanLifelineServ/file/pom.xml b/urbanLifelineServ/file/pom.xml
index 2e93cd82..2c30c6c9 100644
--- a/urbanLifelineServ/file/pom.xml
+++ b/urbanLifelineServ/file/pom.xml
@@ -127,6 +127,12 @@
org.springframework.boot
spring-boot-starter-data-redis
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
diff --git a/urbanLifelineServ/gateway/pom.xml b/urbanLifelineServ/gateway/pom.xml
index cb587a66..0c8c2ed5 100644
--- a/urbanLifelineServ/gateway/pom.xml
+++ b/urbanLifelineServ/gateway/pom.xml
@@ -18,6 +18,89 @@
21
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ ${spring-boot.version}
+ test
+
+
+ *
+ *
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+ ${spring-boot.version}
+ test
+
+
+ *
+ *
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.version}
+ test
+
+
+ *
+ *
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+ ${spring-boot.version}
+ test
+
+
+ *
+ *
+
+
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+ 10.1.48
+ test
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-el
+ 10.1.48
+ test
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-websocket
+ 10.1.48
+ test
+
+
+
+ org.springframework
+ spring-webmvc
+ ${spring-framework.version}
+ test
+
+
+
+
@@ -81,24 +164,81 @@
org.xyzh.common
common-auth
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+
+
org.xyzh.common
common-redis
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+
+
-
+
org.xyzh.common
common-core
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-core
+
+
@@ -138,6 +278,30 @@
lombok
true
+
+
+
+ org.springframework.security
+ spring-security-config
+
+
+ org.springframework.security
+ spring-security-web
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+
+ org.springframework
+ spring-webmvc
+ provided
+
diff --git a/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/GatewayApplication.java b/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/GatewayApplication.java
index d4e13b72..742221fe 100644
--- a/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/GatewayApplication.java
+++ b/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/GatewayApplication.java
@@ -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"
})
}
)
diff --git a/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/filter/AuthGlobalFilter.java b/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/filter/AuthGlobalFilter.java
index b9f44c54..03606474 100644
--- a/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/filter/AuthGlobalFilter.java
+++ b/urbanLifelineServ/gateway/src/main/java/org/xyzh/gateway/filter/AuthGlobalFilter.java
@@ -131,27 +131,32 @@ public class AuthGlobalFilter implements GlobalFilter, Ordered {
}
/**
- * 从请求头中提取 Token
+ * 从请求头或URL参数中提取 Token
*/
private String extractToken(ServerHttpRequest request) {
+ // 1. 优先从请求头获取
List 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;
}
/**
diff --git a/urbanLifelineServ/gateway/src/main/resources/application.yml b/urbanLifelineServ/gateway/src/main/resources/application.yml
index 4a8d3770..5e25b021 100644
--- a/urbanLifelineServ/gateway/src/main/resources/application.yml
+++ b/urbanLifelineServ/gateway/src/main/resources/application.yml
@@ -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
diff --git a/urbanLifelineServ/log/pom.xml b/urbanLifelineServ/log/pom.xml
index 3015b126..1113cfe6 100644
--- a/urbanLifelineServ/log/pom.xml
+++ b/urbanLifelineServ/log/pom.xml
@@ -101,6 +101,11 @@
mybatis-spring
${mybatis.spring.version}
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
\ No newline at end of file
diff --git a/urbanLifelineServ/message/pom.xml b/urbanLifelineServ/message/pom.xml
index 485ffaa4..c65f28dd 100644
--- a/urbanLifelineServ/message/pom.xml
+++ b/urbanLifelineServ/message/pom.xml
@@ -59,6 +59,22 @@
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
\ No newline at end of file
diff --git a/urbanLifelineServ/pom.xml b/urbanLifelineServ/pom.xml
index 0f68c454..0d85662e 100644
--- a/urbanLifelineServ/pom.xml
+++ b/urbanLifelineServ/pom.xml
@@ -434,41 +434,13 @@
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
- org.springframework.boot
- spring-boot-starter-logging
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-logging
-
-
-
-
+
org.springframework.boot
spring-boot-starter-log4j2
-
-
- org.springdoc
- springdoc-openapi-starter-webmvc-ui
-
diff --git a/urbanLifelineServ/system/pom.xml b/urbanLifelineServ/system/pom.xml
index 62b193ec..af25b55b 100644
--- a/urbanLifelineServ/system/pom.xml
+++ b/urbanLifelineServ/system/pom.xml
@@ -85,6 +85,24 @@
mybatis-spring
${mybatis.spring.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
\ No newline at end of file
diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java
index fb2ea225..aa6456c9 100644
--- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java
+++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java
@@ -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 createGuest(TbGuestDTO guest) {
@@ -191,8 +212,53 @@ public class GuestController {
userInfoDTO.setUsername(guest.getName());
loginDomain.setUserInfo(userInfoDTO);
+ // 设置角色信息
+ List 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 userPermissions = new ArrayList<>();
+ List userViews = new ArrayList<>();
+
+ ResultDomain 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;
}
}
diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java
index 50fcf4f9..853c6979 100644
--- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java
+++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java
@@ -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 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);
}
diff --git a/urbanLifelineServ/system/src/main/resources/application-dev.yml b/urbanLifelineServ/system/src/main/resources/application-dev.yml
index 8daa9881..af62a4ba 100644
--- a/urbanLifelineServ/system/src/main/resources/application-dev.yml
+++ b/urbanLifelineServ/system/src/main/resources/application-dev.yml
@@ -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
-
- # 其他需要放行的路径
- # - /public/**
- # - /api/public/**
+ # 健康检查
+ - /actuator/health
+ - /actuator/info
+
+ # 其他需要放行的路径
+ # - /public/**
+ # - /api/public/**
# ================== Security ==================
diff --git a/urbanLifelineServ/system/src/main/resources/application.yml b/urbanLifelineServ/system/src/main/resources/application.yml
index 71e69873..29142845 100644
--- a/urbanLifelineServ/system/src/main/resources/application.yml
+++ b/urbanLifelineServ/system/src/main/resources/application.yml
@@ -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
-
- # 其他需要放行的路径
- # - /public/**
- # - /api/public/**
+ # 健康检查
+ - /actuator/health
+ - /actuator/info
+
+ # 其他需要放行的路径
+ # - /public/**
+ # - /api/public/**
security:
aes:
diff --git a/urbanLifelineServ/workcase/pom.xml b/urbanLifelineServ/workcase/pom.xml
index a81545d3..4f0f278e 100644
--- a/urbanLifelineServ/workcase/pom.xml
+++ b/urbanLifelineServ/workcase/pom.xml
@@ -116,6 +116,12 @@
org.springframework.boot
spring-boot-starter-websocket
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+
\ No newline at end of file
diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketAuthInterceptor.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketAuthInterceptor.java
new file mode 100644
index 00000000..54de0987
--- /dev/null
+++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketAuthInterceptor.java
@@ -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 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;
+ }
+}
diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketConfig.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketConfig.java
index ba746463..3e61548e 100644
--- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketConfig.java
+++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WebSocketConfig.java
@@ -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();
}
}
diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java
index 5cb92846..e82941cf 100644
--- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java
+++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java
@@ -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 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 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 getChatList(@RequestBody TbChat filter) {
return workcaseChatService.getChatList(filter);
}
@Operation(summary = "获取对话消息列表")
+ @PreAuthorize("hasAuthority('workcase:chat:message')")
@PostMapping("/message/list")
public ResultDomain 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 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 stopChat(@RequestBody TbChat filter, @PathVariable String taskId) {
return workcaseChatService.stopChatMessageByTaskId(filter, taskId);
}
@Operation(summary = "评论对话消息")
+ @PreAuthorize("hasAuthority('workcase:chat:message')")
@PostMapping("/comment")
public ResultDomain 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 analyzeChat(@PathVariable String chatId) {
return workcaseChatService.analyzeChat(chatId);
}
@Operation(summary = "总结对话")
+ @PreAuthorize("hasAuthority('workcase:chat:analyze')")
@PostMapping("/summary/{chatId}")
public ResultDomain summaryChat(@PathVariable String chatId) {
return workcaseChatService.summaryChat(chatId);
}
+ // ========================= ChatRoom聊天室管理(实时IM) =========================
+
+ @Operation(summary = "创建聊天室")
+ @PreAuthorize("hasAuthority('workcase:room:create')")
+ @PostMapping("/room")
+ public ResultDomain 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 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 closeChatRoom(@PathVariable String roomId, @RequestParam String closedBy) {
+ return chatRoomService.closeChatRoom(roomId, closedBy);
+ }
+
+ @Operation(summary = "获取聊天室详情")
+ @PreAuthorize("hasAuthority('workcase:room:view')")
+ @GetMapping("/room/{roomId}")
+ public ResultDomain getChatRoomById(@PathVariable String roomId) {
+ return chatRoomService.getChatRoomById(roomId);
+ }
+
+ @Operation(summary = "分页查询聊天室")
+ @PreAuthorize("hasAuthority('workcase:room:view')")
+ @PostMapping("/room/page")
+ public ResultDomain getChatRoomPage(@RequestBody PageRequest 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 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 removeChatRoomMember(@PathVariable String memberId) {
+ return chatRoomService.removeChatRoomMember(memberId);
+ }
+
+ @Operation(summary = "获取聊天室成员列表")
+ @PreAuthorize("hasAuthority('workcase:room:member')")
+ @GetMapping("/room/{roomId}/members")
+ public ResultDomain getChatRoomMemberList(@PathVariable String roomId) {
+ return chatRoomService.getChatRoomMemberList(roomId);
+ }
+
+ // ========================= ChatRoom消息管理 =========================
+
+ @Operation(summary = "发送聊天室消息")
+ @PreAuthorize("hasAuthority('workcase:room:message')")
+ @PostMapping("/room/message")
+ public ResultDomain 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 getChatMessagePage(@RequestBody PageRequest 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 deleteRoomMessage(@PathVariable String messageId) {
+ return chatRoomService.deleteMessage(messageId);
+ }
+
+ // ========================= 客服人员管理 =========================
+
+ @Operation(summary = "添加客服人员")
+ @PostMapping("/customer-service")
+ public ResultDomain 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 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 deleteCustomerService(@PathVariable String userId) {
+ return chatRoomService.deleteCustomerService(userId);
+ }
+
+ @Operation(summary = "分页查询客服人员")
+ @PostMapping("/customer-service/page")
+ public ResultDomain getCustomerServicePage(@RequestBody PageRequest 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 updateCustomerServiceStatus(@PathVariable String userId, @RequestParam String status) {
+ return chatRoomService.updateCustomerServiceStatus(userId, status);
+ }
+
+ @Operation(summary = "获取可接待客服列表")
+ @GetMapping("/customer-service/available")
+ public ResultDomain getAvailableCustomerServices() {
+ return chatRoomService.getAvailableCustomerServices();
+ }
+
+ @Operation(summary = "自动分配客服")
+ @PostMapping("/room/{roomId}/assign")
+ public ResultDomain 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";
+ // }
// ========================= 词云管理 =========================
diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java
index a3fa01a6..12958bfc 100644
--- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java
+++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java
@@ -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 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 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 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 getWorkcaseById(@PathVariable String workcaseId) {
return workcaseService.getWorkcaseById(workcaseId);
}
@Operation(summary = "查询工单列表")
+ @PreAuthorize("hasAuthority('workcase:ticket:view')")
@PostMapping("/list")
public ResultDomain getWorkcaseList(@RequestBody TbWorkcaseDTO filter) {
return workcaseService.getWorkcaseList(filter);
}
@Operation(summary = "分页查询工单")
+ @PreAuthorize("hasAuthority('workcase:ticket:view')")
@PostMapping("/page")
public ResultDomain getWorkcasePage(@RequestBody PageRequest 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 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 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 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 getWorkcaseProcessList(@RequestBody TbWorkcaseProcessDTO filter) {
return workcaseService.getWorkcaseProcessList(filter);
}
@Operation(summary = "分页查询工单处理过程")
+ @PreAuthorize("hasAuthority('workcase:ticket:process')")
@PostMapping("/process/page")
public ResultDomain getWorkcaseProcessPage(@RequestBody PageRequest 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 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 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 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 getWorkcaseDeviceList(@RequestBody TbWorkcaseDeviceDTO filter) {
return workcaseService.getWorkcaseDeviceList(filter);
}
@Operation(summary = "分页查询工单设备")
+ @PreAuthorize("hasAuthority('workcase:ticket:device')")
@PostMapping("/device/page")
public ResultDomain getWorkcaseDevicePage(@RequestBody PageRequest pageRequest) {
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
diff --git a/urbanLifelineServ/workcase/src/main/resources/application.yml b/urbanLifelineServ/workcase/src/main/resources/application.yml
index 97538871..54306e66 100644
--- a/urbanLifelineServ/workcase/src/main/resources/application.yml
+++ b/urbanLifelineServ/workcase/src/main/resources/application.yml
@@ -7,7 +7,7 @@ server:
# ================== Auth ====================
auth:
enabled: true
- gate-way: true
+ gateway-mode: true
whitelist:
- /swagger-ui/**
- /swagger-ui.html
diff --git a/urbanLifelineWeb/.vscode/launch.json b/urbanLifelineWeb/.vscode/launch.json
index d5072dff..df833abc 100644
--- a/urbanLifelineWeb/.vscode/launch.json
+++ b/urbanLifelineWeb/.vscode/launch.json
@@ -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": {
diff --git a/urbanLifelineWeb/packages/bidding/src/types/shared.d.ts b/urbanLifelineWeb/packages/bidding/src/types/shared.d.ts
index 5559c62b..488ce0f6 100644
--- a/urbanLifelineWeb/packages/bidding/src/types/shared.d.ts
+++ b/urbanLifelineWeb/packages/bidding/src/types/shared.d.ts
@@ -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
diff --git a/urbanLifelineWeb/packages/platform/pnpm-lock.yaml b/urbanLifelineWeb/packages/platform/pnpm-lock.yaml
index 5a1c1253..bbe29ff3 100644
--- a/urbanLifelineWeb/packages/platform/pnpm-lock.yaml
+++ b/urbanLifelineWeb/packages/platform/pnpm-lock.yaml
@@ -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:
diff --git a/urbanLifelineWeb/packages/platform/src/types/shared.d.ts b/urbanLifelineWeb/packages/platform/src/types/shared.d.ts
index 5559c62b..488ce0f6 100644
--- a/urbanLifelineWeb/packages/platform/src/types/shared.d.ts
+++ b/urbanLifelineWeb/packages/platform/src/types/shared.d.ts
@@ -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
diff --git a/urbanLifelineWeb/packages/shared/package.json b/urbanLifelineWeb/packages/shared/package.json
index d91895ec..2558e938 100644
--- a/urbanLifelineWeb/packages/shared/package.json
+++ b/urbanLifelineWeb/packages/shared/package.json
@@ -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"
},
diff --git a/urbanLifelineWeb/packages/shared/pnpm-lock.yaml b/urbanLifelineWeb/packages/shared/pnpm-lock.yaml
index 2dba20e9..20c4b1cd 100644
--- a/urbanLifelineWeb/packages/shared/pnpm-lock.yaml
+++ b/urbanLifelineWeb/packages/shared/pnpm-lock.yaml
@@ -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'}
diff --git a/urbanLifelineWeb/packages/shared/src/api/workcase/index.ts b/urbanLifelineWeb/packages/shared/src/api/workcase/index.ts
index 945f32a8..ff366b1e 100644
--- a/urbanLifelineWeb/packages/shared/src/api/workcase/index.ts
+++ b/urbanLifelineWeb/packages/shared/src/api/workcase/index.ts
@@ -1 +1,2 @@
export * from './workcase'
+export * from './workcaseChat'
\ No newline at end of file
diff --git a/urbanLifelineWeb/packages/shared/src/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/shared/src/api/workcase/workcaseChat.ts
new file mode 100644
index 00000000..f4df96a5
--- /dev/null
+++ b/urbanLifelineWeb/packages/shared/src/api/workcase/workcaseChat.ts
@@ -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> {
+ const response = await api.post(`${this.baseUrl}`, chat)
+ return response.data
+ },
+
+ /**
+ * 更新对话
+ */
+ async updateChat(chat: TbChat): Promise> {
+ const response = await api.put(`${this.baseUrl}`, chat)
+ return response.data
+ },
+
+ /**
+ * 查询对话列表
+ */
+ async getChatList(filter: TbChat): Promise> {
+ const response = await api.post(`${this.baseUrl}/list`, filter)
+ return response.data
+ },
+
+ /**
+ * 获取对话消息列表
+ */
+ async getChatMessageList(filter: TbChat): Promise> {
+ const response = await api.post(`${this.baseUrl}/message/list`, filter)
+ return response.data
+ },
+
+ /**
+ * 准备对话会话
+ */
+ async prepareChatMessageSession(prepareData: ChatPrepareData): Promise> {
+ const response = await api.post(`${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> {
+ const response = await api.post(`${this.baseUrl}/stop/${taskId}`, filter)
+ return response.data
+ },
+
+ /**
+ * 评论对话消息
+ */
+ async commentChatMessage(filter: TbChat, messageId: string, comment: string): Promise> {
+ const response = await api.post(`${this.baseUrl}/comment`, filter, {
+ params: { messageId, comment }
+ })
+ return response.data
+ },
+
+ // ====================== 对话分析 ======================
+
+ /**
+ * 分析对话(AI预填工单信息)
+ */
+ async analyzeChat(chatId: string): Promise> {
+ const response = await api.get(`${this.baseUrl}/analyze/${chatId}`)
+ return response.data
+ },
+
+ /**
+ * 总结对话
+ */
+ async summaryChat(chatId: string): Promise> {
+ const response = await api.post(`${this.baseUrl}/summary/${chatId}`)
+ return response.data
+ },
+
+ // ====================== ChatRoom聊天室管理 ======================
+
+ /**
+ * 创建聊天室
+ */
+ async createChatRoom(chatRoom: TbChatRoomDTO): Promise> {
+ const response = await api.post(`${this.baseUrl}/room`, chatRoom)
+ return response.data
+ },
+
+ /**
+ * 更新聊天室
+ */
+ async updateChatRoom(chatRoom: TbChatRoomDTO): Promise> {
+ const response = await api.put(`${this.baseUrl}/room`, chatRoom)
+ return response.data
+ },
+
+ /**
+ * 关闭聊天室
+ */
+ async closeChatRoom(roomId: string, closedBy: string): Promise> {
+ const response = await api.post(`${this.baseUrl}/room/${roomId}/close`, null, {
+ params: { closedBy }
+ })
+ return response.data
+ },
+
+ /**
+ * 获取聊天室详情
+ */
+ async getChatRoomById(roomId: string): Promise> {
+ const response = await api.get(`${this.baseUrl}/room/${roomId}`)
+ return response.data
+ },
+
+ /**
+ * 分页查询聊天室
+ */
+ async getChatRoomPage(pageRequest: PageRequest): Promise> {
+ const response = await api.post(`${this.baseUrl}/room/page`, pageRequest)
+ return response.data
+ },
+
+ // ====================== ChatRoom成员管理 ======================
+
+ /**
+ * 添加聊天室成员
+ */
+ async addChatRoomMember(member: TbChatRoomMemberDTO): Promise> {
+ const response = await api.post(`${this.baseUrl}/room/member`, member)
+ return response.data
+ },
+
+ /**
+ * 移除聊天室成员
+ */
+ async removeChatRoomMember(memberId: string): Promise> {
+ const response = await api.delete(`${this.baseUrl}/room/member/${memberId}`)
+ return response.data
+ },
+
+ /**
+ * 获取聊天室成员列表
+ */
+ async getChatRoomMemberList(roomId: string): Promise> {
+ const response = await api.get(`${this.baseUrl}/room/${roomId}/members`)
+ return response.data
+ },
+
+ // ====================== ChatRoom消息管理 ======================
+
+ /**
+ * 发送聊天室消息
+ */
+ async sendMessage(message: TbChatRoomMessageDTO): Promise> {
+ const response = await api.post(`${this.baseUrl}/room/message`, message)
+ return response.data
+ },
+
+ /**
+ * 分页查询聊天室消息
+ */
+ async getChatMessagePage(pageRequest: PageRequest): Promise> {
+ const response = await api.post(`${this.baseUrl}/room/message/page`, pageRequest)
+ return response.data
+ },
+
+ /**
+ * 删除聊天室消息
+ */
+ async deleteMessage(messageId: string): Promise> {
+ const response = await api.delete(`${this.baseUrl}/room/message/${messageId}`)
+ return response.data
+ },
+
+ // ====================== 客服人员管理 ======================
+
+ /**
+ * 添加客服人员
+ */
+ async addCustomerService(customerService: TbCustomerServiceDTO): Promise> {
+ const response = await api.post(`${this.baseUrl}/customer-service`, customerService)
+ return response.data
+ },
+
+ /**
+ * 更新客服人员
+ */
+ async updateCustomerService(customerService: TbCustomerServiceDTO): Promise> {
+ const response = await api.put(`${this.baseUrl}/customer-service`, customerService)
+ return response.data
+ },
+
+ /**
+ * 删除客服人员
+ */
+ async deleteCustomerService(userId: string): Promise> {
+ const response = await api.delete(`${this.baseUrl}/customer-service/${userId}`)
+ return response.data
+ },
+
+ /**
+ * 分页查询客服人员
+ */
+ async getCustomerServicePage(pageRequest: PageRequest): Promise> {
+ const response = await api.post(`${this.baseUrl}/customer-service/page`, pageRequest)
+ return response.data
+ },
+
+ /**
+ * 更新客服在线状态
+ */
+ async updateCustomerServiceStatus(userId: string, status: string): Promise> {
+ const response = await api.post(`${this.baseUrl}/customer-service/${userId}/status`, null, {
+ params: { status }
+ })
+ return response.data
+ },
+
+ /**
+ * 获取可接待客服列表
+ */
+ async getAvailableCustomerServices(): Promise> {
+ const response = await api.get(`${this.baseUrl}/customer-service/available`)
+ return response.data
+ },
+
+ /**
+ * 自动分配客服
+ */
+ async assignCustomerService(roomId: string): Promise> {
+ const response = await api.post(`${this.baseUrl}/room/${roomId}/assign`)
+ return response.data
+ },
+
+ // ====================== 词云管理 ======================
+
+ /**
+ * 添加词云
+ */
+ async addWordCloud(wordCloud: TbWordCloudDTO): Promise> {
+ const response = await api.post(`${this.baseUrl}/wordcloud`, wordCloud)
+ return response.data
+ },
+
+ /**
+ * 更新词云
+ */
+ async updateWordCloud(wordCloud: TbWordCloudDTO): Promise> {
+ const response = await api.put(`${this.baseUrl}/wordcloud`, wordCloud)
+ return response.data
+ },
+
+ /**
+ * 查询词云列表
+ */
+ async getWordCloudList(filter: TbWordCloudDTO): Promise> {
+ const response = await api.post(`${this.baseUrl}/wordcloud/list`, filter)
+ return response.data
+ },
+
+ /**
+ * 分页查询词云
+ */
+ async getWordCloudPage(pageRequest: PageRequest): Promise> {
+ const response = await api.post(`${this.baseUrl}/wordcloud/page`, pageRequest)
+ return response.data
+ }
+}
diff --git a/urbanLifelineWeb/packages/shared/src/components/chatRoom/chatRoom/ChatRoom.vue b/urbanLifelineWeb/packages/shared/src/components/chatRoom/chatRoom/ChatRoom.vue
index 7f788e75..44138b7a 100644
--- a/urbanLifelineWeb/packages/shared/src/components/chatRoom/chatRoom/ChatRoom.vue
+++ b/urbanLifelineWeb/packages/shared/src/components/chatRoom/chatRoom/ChatRoom.vue
@@ -116,7 +116,7 @@