This commit is contained in:
2026-01-14 15:42:26 +08:00
parent 0bf7361672
commit 87f2772964
14 changed files with 5864 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(ls:*)"
]
}
}

View File

@@ -0,0 +1,140 @@
# UrbanLifeline 到 Pigx 迁移文档总览
## 📋 文档清单
本目录包含了 UrbanLifeline 系统迁移到 Pigx 平台所需的全部技术文档。
### 核心文档
| 文档名称 | 文件路径 | 说明 |
|---------|---------|------|
| **需求文档** | [requirements.md](./requirements.md) | 定义迁移需求,明确创建 pigx-dify 模块 |
| **设计文档** | [design.md](./design.md) | 技术设计方案,包含 pigx-dify 模块架构 |
| **任务清单** | [tasks.md](./tasks.md) | 详细的迁移任务列表,包含 AI 模块迁移步骤 |
### 技术指南
| 文档名称 | 文件路径 | 用途 |
|---------|---------|------|
| **权限映射表** | [permission-mapping.md](./permission-mapping.md) | 权限标识从 urbanLifeline 到 pigx 的映射对照 |
| **权限注解转换指南** | [permission-annotation-guide.md](./permission-annotation-guide.md) | @PreAuthorize@pms.hasPermission 的转换方法 |
| **用户服务配置指南** | [security-config-guide.md](./security-config-guide.md) | SecurityUtils 和 RemoteUserService 的使用说明 |
| **数据库迁移脚本** | [database-migration-script.md](./database-migration-script.md) | PostgreSQL 到 MySQL 的完整迁移 SQL |
| **租户隔离指南** | [tenant-isolation-guide.md](./tenant-isolation-guide.md) | 多租户字段添加和隔离实现方案 |
| **Dify模块架构** | [pigx-dify-architecture.md](./pigx-dify-architecture.md) | 新建 pigx-dify 模块的详细架构设计 |
## 🎯 迁移要点总结
### 1. 核心变更
- ✅ 创建独立的 **pigx-dify** 模块承载 AI 功能
- ✅ 保留原有 Dify API 集成方式
- ✅ 权限体系完全适配 pigx@pms.hasPermission
- ✅ 所有业务表添加 tenant_id 实现多租户隔离
- ✅ 数据库从 PostgreSQL 迁移到 MySQL
### 2. 模块分布
| 模块 | 目标位置 | 状态 |
|------|---------|------|
| 工单 (workcase) | pigx-app-server-biz | 待迁移 |
| 招标 (bidding) | pigx-app-server-biz | 待迁移 |
| 平台管理 (platform) | pigx-app-server-biz | 待迁移 |
| AI功能 (ai) | **pigx-dify新建** | 待迁移 |
| 消息 (message) | pigx-app-server-biz | 待迁移 |
### 3. 关键技术适配
#### 权限转换
```java
// 原系统
@PreAuthorize("hasAuthority('workcase:ticket:create')")
// 新系统
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
```
#### 用户信息获取
```java
// 原系统
JwtUtils.getUserId()
// 新系统
SecurityUtils.getUser().getId()
```
#### 响应格式
```java
// 原系统
ResultDomain.success(data)
// 新系统
R.ok(data)
```
## 📝 使用指南
### 第一步:理解需求和设计
1. 阅读 [requirements.md](./requirements.md) 了解迁移需求
2. 阅读 [design.md](./design.md) 理解技术方案
3. 查看 [pigx-dify-architecture.md](./pigx-dify-architecture.md) 了解 AI 模块设计
### 第二步:准备迁移
1. 使用 [tasks.md](./tasks.md) 作为任务清单
2. 参考 [permission-mapping.md](./permission-mapping.md) 准备权限映射
3. 阅读 [database-migration-script.md](./database-migration-script.md) 准备数据库
### 第三步:执行迁移
1. 按照 [permission-annotation-guide.md](./permission-annotation-guide.md) 转换权限注解
2. 根据 [security-config-guide.md](./security-config-guide.md) 配置用户服务
3. 使用 [tenant-isolation-guide.md](./tenant-isolation-guide.md) 实现租户隔离
### 第四步:验证测试
1. 验证权限控制正确性
2. 测试租户数据隔离
3. 确认 Dify 集成正常
4. 检查所有功能模块
## 🔧 工具和脚本
### 批量权限转换
```bash
# 权限注解批量替换
find . -name "*.java" -exec sed -i \
's/@PreAuthorize("hasAuthority(\x27\([^:]*\):\([^:]*\):\([^x27]*\)\x27)")/@PreAuthorize("@pms.hasPermission(\x27\1_\2_\3\x27)")/g' {} \;
```
### 数据库迁移
```sql
-- 执行顺序
1. 创建 MySQL 数据库结构
2. 添加 tenant_id 字段
3. 迁移业务数据
4. 建立用户映射关系
5. 验证数据完整性
```
## 📊 迁移进度跟踪
使用 [tasks.md](./tasks.md) 中的任务清单跟踪进度:
- [ ] 基础设施准备
- [ ] 权限体系迁移
- [ ] 数据库迁移
- [ ] 后端代码迁移
- [ ] 前端页面迁移
- [ ] 集成测试
- [ ] 上线部署
## 🚨 重要提醒
1. **数据备份**:迁移前必须完整备份所有数据
2. **权限测试**:每个模块迁移后都要测试权限控制
3. **租户隔离**:确保所有查询都包含租户条件
4. **Dify配置**:保存好 Dify API Key 和配置信息
5. **回滚方案**:准备好回滚脚本和流程
## 📞 支持与反馈
如有问题,请参考相应的技术指南文档,或联系技术支持团队。
---
*最后更新时间2024年*

View File

@@ -0,0 +1,715 @@
# PostgreSQL to MySQL 数据库迁移脚本
## 概述
本文档包含从 urbanLifeline (PostgreSQL) 到 pigx (MySQL) 的数据库迁移脚本。
## 迁移策略
1. PostgreSQL Schema → MySQL Database 或表前缀
2. 所有业务表添加 `tenant_id` 字段
3. 用户ID关联到 pigx 的 sys_user 表
4. 数据类型映射和语法适配
## 类型映射规则
| PostgreSQL | MySQL | 说明 |
|-----------|-------|------|
| SERIAL | INT AUTO_INCREMENT | 自增整数 |
| BIGSERIAL | BIGINT AUTO_INCREMENT | 自增大整数 |
| VARCHAR(n) | VARCHAR(n) | 可变长字符串 |
| TEXT | TEXT | 长文本 |
| TIMESTAMPTZ | DATETIME | 时间戳 |
| BOOLEAN | TINYINT(1) | 布尔值 |
| DECIMAL(m,n) | DECIMAL(m,n) | 十进制数 |
| INTEGER | INT | 整数 |
| BIGINT | BIGINT | 大整数 |
| JSONB | JSON | JSON数据 |
| VARCHAR(50)[] | JSON | 数组转JSON |
| TEXT[] | JSON | 文本数组转JSON |
## 1. 工单模块 (Workcase)
### 1.1 来客表(系统外部人员)
```sql
-- PostgreSQL 原表: sys.tb_guest
-- MySQL 目标表: tb_guest
CREATE TABLE IF NOT EXISTS `tb_guest` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`user_id` varchar(50) NOT NULL COMMENT '来客ID',
`name` varchar(50) NOT NULL COMMENT '姓名',
`phone` varchar(50) DEFAULT NULL COMMENT '电话',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`wechat_id` varchar(50) DEFAULT NULL COMMENT '微信号',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`user_id`),
UNIQUE KEY `uk_wechat_id` (`wechat_id`),
UNIQUE KEY `uk_phone` (`phone`),
UNIQUE KEY `uk_email` (`email`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统外部人员表';
### 1.2 聊天室表
CREATE TABLE IF NOT EXISTS `tb_chat_room` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`workcase_id` varchar(50) DEFAULT NULL COMMENT '关联工单ID',
`room_name` varchar(200) NOT NULL COMMENT '聊天室名称',
`room_type` varchar(20) NOT NULL DEFAULT 'workcase' COMMENT '聊天室类型',
`status` varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态active-活跃 closed-已关闭 archived-已归档',
`guest_id` varchar(50) NOT NULL COMMENT '来客ID创建者',
`guest_name` varchar(100) NOT NULL COMMENT '来客姓名',
`ai_session_id` varchar(50) DEFAULT NULL COMMENT 'AI对话会话ID',
`message_count` int NOT NULL DEFAULT 0 COMMENT '消息总数',
`device_code` varchar(50) NOT NULL COMMENT '设备代码',
`last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间',
`last_message` text DEFAULT NULL COMMENT '最后一条消息内容',
`comment_level` int DEFAULT 0 COMMENT '服务评分1-5',
`closed_by` varchar(50) DEFAULT NULL COMMENT '关闭人',
`closed_time` datetime DEFAULT NULL COMMENT '关闭时间',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`room_id`),
UNIQUE KEY `uk_workcase_id` (`workcase_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_guest_status` (`guest_id`, `status`),
KEY `idx_last_message_time` (`last_message_time` DESC),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM聊天室表一个工单对应一个聊天室';
### 1.3 聊天室成员表
CREATE TABLE IF NOT EXISTS `tb_chat_room_member` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`member_id` varchar(50) NOT NULL COMMENT '成员记录ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`user_id` varchar(50) NOT NULL COMMENT '用户ID来客ID或员工ID',
`user_type` varchar(20) NOT NULL COMMENT '用户类型guest-来客 staff-客服 ai-AI助手',
`user_name` varchar(100) NOT NULL COMMENT '用户名称',
`status` varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态active-活跃 left-已离开 removed-被移除',
`unread_count` int NOT NULL DEFAULT 0 COMMENT '该成员的未读消息数',
`last_read_time` datetime DEFAULT NULL COMMENT '最后阅读时间',
`last_read_msg_id` varchar(50) DEFAULT NULL COMMENT '最后阅读的消息ID',
`join_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
`leave_time` datetime DEFAULT NULL COMMENT '离开时间',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`member_id`),
UNIQUE KEY `uk_room_user` (`room_id`, `user_id`),
KEY `idx_room_status` (`room_id`, `status`),
KEY `idx_user_status` (`user_id`, `user_type`, `status`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天室成员表,记录来客和客服人员';
### 1.4 聊天室消息表
CREATE TABLE IF NOT EXISTS `tb_chat_room_message` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`sender_id` varchar(50) NOT NULL COMMENT '发送者ID',
`sender_type` varchar(20) NOT NULL COMMENT '发送者类型guest-来客 agent-客服 ai-AI助手 system-系统消息',
`sender_name` varchar(100) NOT NULL COMMENT '发送者名称',
`message_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '消息类型text-文本 image-图片 file-文件 voice-语音 video-视频',
`content` text NOT NULL COMMENT '消息内容',
`files` json DEFAULT NULL COMMENT '附件文件ID数组',
`content_extra` json DEFAULT NULL COMMENT '扩展内容',
`reply_to_msg_id` varchar(50) DEFAULT NULL COMMENT '回复的消息ID',
`is_ai_message` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否AI消息',
`ai_message_id` varchar(50) DEFAULT NULL COMMENT 'AI原始消息ID',
`status` varchar(20) NOT NULL DEFAULT 'sent' COMMENT '状态sent-已发送 delivered-已送达 read-已读 failed-失败',
`read_count` int NOT NULL DEFAULT 0 COMMENT '已读人数',
`send_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`message_id`),
KEY `idx_room_time` (`room_id`, `send_time` DESC),
KEY `idx_sender` (`sender_id`, `sender_type`),
KEY `idx_ai_message` (`ai_message_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM聊天消息表包含AI对话和人工客服消息';
### 1.5 聊天室总结表
CREATE TABLE IF NOT EXISTS `tb_chat_room_summary` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`summary_id` varchar(50) NOT NULL COMMENT '总结ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`question` text DEFAULT NULL COMMENT '核心问题',
`needs` json DEFAULT NULL COMMENT '核心诉求数组',
`answer` text DEFAULT NULL COMMENT '解决方案',
`workcloud` json DEFAULT NULL COMMENT '词云关键词数组',
`message_count` int DEFAULT 0 COMMENT '参与总结的消息数量',
`summary_time` datetime DEFAULT NULL COMMENT '总结生成时间',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`summary_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_room_time` (`room_id`, `summary_time` DESC),
KEY `idx_summary_time` (`summary_time` DESC),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天室总结表保存AI生成的聊天总结分析';
### 1.6 视频会议表
CREATE TABLE IF NOT EXISTS `tb_video_meeting` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`meeting_id` varchar(50) NOT NULL COMMENT '会议ID也是Jitsi房间名',
`room_id` varchar(50) NOT NULL COMMENT '关联聊天室ID',
`workcase_id` varchar(50) NOT NULL COMMENT '关联工单ID',
`meeting_name` varchar(200) NOT NULL COMMENT '会议名称',
`meeting_password` varchar(50) DEFAULT NULL COMMENT '会议密码',
`description` varchar(500) DEFAULT NULL COMMENT '会议描述',
`jwt_token` text DEFAULT NULL COMMENT 'JWT Token',
`jitsi_room_name` varchar(200) NOT NULL COMMENT 'Jitsi房间名',
`jitsi_server_url` varchar(500) NOT NULL DEFAULT 'https://meet.jit.si' COMMENT 'Jitsi服务器地址',
`status` varchar(20) NOT NULL DEFAULT 'scheduled' COMMENT '状态',
`creator_type` varchar(20) NOT NULL COMMENT '创建者类型',
`creator_name` varchar(100) NOT NULL COMMENT '创建者名称',
`participant_count` int NOT NULL DEFAULT 0 COMMENT '参与人数',
`max_participants` int DEFAULT 10 COMMENT '最大参与人数',
`start_time` datetime NOT NULL COMMENT '会议开始时间',
`end_time` datetime NOT NULL COMMENT '会议结束时间',
`advance` int DEFAULT 5 COMMENT '提前入会时间(分钟)',
`actual_start_time` datetime DEFAULT NULL COMMENT '实际开始时间',
`actual_end_time` datetime DEFAULT NULL COMMENT '实际结束时间',
`duration_seconds` int DEFAULT 0 COMMENT '会议时长(秒)',
`iframe_url` text DEFAULT NULL COMMENT 'iframe嵌入URL',
`config` json DEFAULT NULL COMMENT 'Jitsi配置项',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`meeting_id`),
UNIQUE KEY `uk_jitsi_room_name` (`jitsi_room_name`),
KEY `idx_room_status` (`room_id`, `status`),
KEY `idx_workcase_status` (`workcase_id`, `status`),
KEY `idx_create_time` (`create_time` DESC),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Jitsi Meet视频会议表';
### 1.7 客服人员配置表
CREATE TABLE IF NOT EXISTS `tb_customer_service` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`user_id` varchar(50) NOT NULL COMMENT '员工ID关联sys用户ID',
`username` varchar(100) NOT NULL COMMENT '员工姓名',
`user_code` varchar(50) DEFAULT NULL COMMENT '员工工号',
`status` varchar(20) NOT NULL DEFAULT 'offline' COMMENT '状态online-在线 busy-忙碌 offline-离线',
`skill_tags` json DEFAULT NULL COMMENT '技能标签',
`max_concurrent` int NOT NULL DEFAULT 5 COMMENT '最大并发接待数',
`current_workload` int NOT NULL DEFAULT 0 COMMENT '当前工作量',
`total_served` int NOT NULL DEFAULT 0 COMMENT '累计服务次数',
`avg_response_time` int DEFAULT NULL COMMENT '平均响应时间(秒)',
`satisfaction_score` decimal(3,2) DEFAULT NULL COMMENT '满意度评分0-5',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`user_id`),
KEY `idx_status_workload` (`status`, `current_workload`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客服人员配置表';
### 1.8 工单表
CREATE TABLE IF NOT EXISTS `tb_workcase` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`workcase_id` varchar(50) NOT NULL COMMENT '工单ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`user_id` varchar(50) NOT NULL COMMENT '来客ID',
`username` varchar(200) NOT NULL COMMENT '来客姓名',
`phone` varchar(20) NOT NULL COMMENT '来客电话',
`type` varchar(50) NOT NULL COMMENT '故障类型',
`device` varchar(50) DEFAULT NULL COMMENT '设备名称',
`device_code` varchar(50) DEFAULT NULL COMMENT '设备代码',
`device_name_plate` varchar(50) DEFAULT NULL COMMENT '设备名称牌',
`device_name_plate_img` varchar(50) NOT NULL COMMENT '设备名称牌图片',
`address` varchar(1000) DEFAULT NULL COMMENT '现场地址',
`description` varchar(1000) DEFAULT NULL COMMENT '故障描述',
`imgs` json DEFAULT NULL COMMENT '工单图片id数组',
`emergency` varchar(50) NOT NULL DEFAULT 'normal' COMMENT '紧急程度 normal-普通 emergency-紧急',
`status` varchar(50) NOT NULL DEFAULT 'pending' COMMENT '状态 pending-待处理 processing-处理中 done-已完成',
`processor` varchar(50) DEFAULT NULL COMMENT '处理人',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`workcase_id`),
UNIQUE KEY `uk_room_id` (`room_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单表';
### 1.9 工单处理过程表
CREATE TABLE IF NOT EXISTS `tb_workcase_process` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`workcase_id` varchar(50) NOT NULL COMMENT '工单ID',
`process_id` varchar(50) NOT NULL COMMENT '过程id',
`action` varchar(50) NOT NULL COMMENT '动作 info:记录,assign:指派,redeploy:转派,repeal:撤销,finish:完成',
`message` varchar(200) DEFAULT NULL COMMENT '消息',
`files` json DEFAULT NULL COMMENT '携带文件',
`processor` varchar(50) DEFAULT NULL COMMENT '处理人',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '过程发起人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`process_id`),
KEY `idx_workcase_id` (`workcase_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单处理过程表';
### 1.10 工单设备文件表
CREATE TABLE IF NOT EXISTS `tb_workcase_device` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`workcase_id` varchar(50) NOT NULL COMMENT '工单ID',
`device` varchar(50) NOT NULL COMMENT '设备名称',
`device_code` varchar(50) DEFAULT NULL COMMENT '设备代码',
`file_id` varchar(50) NOT NULL COMMENT '文件id',
`file_name` varchar(50) NOT NULL COMMENT '文件名',
`file_root_id` varchar(50) DEFAULT NULL COMMENT '文件根id',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
PRIMARY KEY(`workcase_id`, `file_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单设备文件表';
### 1.11 词云统计表
CREATE TABLE IF NOT EXISTS `tb_word_cloud` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`word_id` varchar(50) NOT NULL COMMENT '词条ID',
`word` varchar(100) NOT NULL COMMENT '词语',
`frequency` int NOT NULL DEFAULT 1 COMMENT '词频',
`source_type` varchar(20) NOT NULL COMMENT '来源类型 chat-聊天 workcase-工单 global-全局',
`source_id` varchar(50) DEFAULT NULL COMMENT '来源ID',
`category` varchar(50) DEFAULT NULL COMMENT '分类',
`stat_date` date NOT NULL COMMENT '统计日期',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`word_id`),
UNIQUE KEY `uk_word_source` (`word`, `source_type`, `source_id`, `stat_date`, `category`),
KEY `idx_source` (`source_type`, `source_id`, `stat_date`),
KEY `idx_category` (`category`, `stat_date`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='词云统计表';
```
## 2. AI模块 (Dify)
### 2.1 智能体配置表
```sql
CREATE TABLE IF NOT EXISTS `tb_agent` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`agent_id` varchar(50) NOT NULL COMMENT '智能体ID',
`name` varchar(50) NOT NULL COMMENT '智能体名称',
`description` varchar(500) DEFAULT NULL COMMENT '智能体描述',
`link` varchar(500) DEFAULT NULL COMMENT '智能体url',
`api_key` varchar(500) NOT NULL COMMENT 'dify智能体APIKEY',
`is_outer` tinyint(1) DEFAULT 0 COMMENT '是否是对外智能体,未登录可用',
`introduce` varchar(500) NOT NULL COMMENT '引导词',
`prompt_cards` json DEFAULT NULL COMMENT '提示卡片数组',
`category` varchar(50) NOT NULL COMMENT '分类',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) DEFAULT NULL COMMENT '创建者',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`agent_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
UNIQUE KEY `uk_api_key` (`api_key`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI智能体配置表';
### 2.2 AI对话表
CREATE TABLE IF NOT EXISTS `tb_chat` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`chat_id` varchar(50) NOT NULL COMMENT '对话ID',
`agent_id` varchar(50) NOT NULL COMMENT '智能体ID',
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
`user_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '用户类型 1-系统内部人员 0-系统外部人员',
`title` varchar(500) NOT NULL COMMENT '对话标题',
`channel` varchar(50) DEFAULT 'agent' COMMENT '对话渠道 agent、wechat',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`chat_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_agent_id` (`agent_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话表';
### 2.3 AI对话消息表
CREATE TABLE IF NOT EXISTS `tb_chat_message` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`dify_message_id` varchar(100) DEFAULT NULL COMMENT 'Dify消息ID',
`chat_id` varchar(50) NOT NULL COMMENT '对话ID',
`role` varchar(50) NOT NULL COMMENT '角色user-用户/ai-智能体/recipient-来客',
`content` text NOT NULL COMMENT '消息内容',
`files` json DEFAULT NULL COMMENT '文件id数组',
`comment` varchar(50) DEFAULT NULL COMMENT '评价',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`message_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_chat_id` (`chat_id`),
KEY `idx_dify_message_id` (`dify_message_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话消息表';
### 2.4 知识库配置表
CREATE TABLE IF NOT EXISTS `tb_knowledge` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`knowledge_id` varchar(50) NOT NULL COMMENT '知识库ID',
`title` varchar(255) NOT NULL COMMENT '知识库标题',
`avatar` varchar(255) DEFAULT NULL COMMENT '知识库头像',
`description` varchar(500) DEFAULT NULL COMMENT '知识库描述',
`dify_dataset_id` varchar(100) DEFAULT NULL COMMENT 'Dify知识库ID',
`dify_indexing_technique` varchar(50) DEFAULT 'high_quality' COMMENT 'Dify索引方式',
`embedding_model` varchar(100) DEFAULT NULL COMMENT '向量模型名称',
`embedding_model_provider` varchar(100) DEFAULT NULL COMMENT '向量模型提供商',
`rerank_model` varchar(100) DEFAULT NULL COMMENT 'Rerank模型名称',
`rerank_model_provider` varchar(100) DEFAULT NULL COMMENT 'Rerank模型提供商',
`reranking_enable` tinyint(1) DEFAULT 0 COMMENT '是否启用Rerank',
`retrieval_top_k` int DEFAULT 2 COMMENT '检索Top K',
`retrieval_score_threshold` decimal(3,2) DEFAULT 0.00 COMMENT '检索分数阈值',
`document_count` int DEFAULT 0 COMMENT '文档数量',
`total_chunks` int DEFAULT 0 COMMENT '总分段数',
`service` varchar(50) DEFAULT NULL COMMENT '所属服务 workcase、bidding',
`project_id` varchar(50) DEFAULT NULL COMMENT 'bidding所属项目ID',
`category` varchar(50) DEFAULT NULL COMMENT '所属分类',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建者',
`dept_path` varchar(50) DEFAULT NULL COMMENT '创建者部门路径',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_knowledge_id` (`knowledge_id`),
UNIQUE KEY `uk_dify_dataset_id` (`dify_dataset_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库配置表';
### 2.5 知识库文件表
CREATE TABLE IF NOT EXISTS `tb_knowledge_file` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`knowledge_id` varchar(50) NOT NULL COMMENT '知识库ID',
`file_root_id` varchar(50) NOT NULL COMMENT '文件根ID',
`file_id` varchar(50) NOT NULL COMMENT '文件ID',
`dify_file_id` varchar(50) NOT NULL COMMENT 'dify文件ID',
`version` int NOT NULL DEFAULT 1 COMMENT '文件版本',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_knowledge_file` (`knowledge_id`, `file_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文件表';
### 2.6 知识库文件日志表
CREATE TABLE IF NOT EXISTS `tb_knowledge_file_log` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`log_id` varchar(50) NOT NULL COMMENT '日志ID',
`knowledge_id` varchar(50) NOT NULL COMMENT '知识库ID',
`file_root_id` varchar(50) NOT NULL COMMENT '文件根ID',
`file_id` varchar(50) NOT NULL COMMENT '文件ID',
`file_name` varchar(100) NOT NULL COMMENT '文件名',
`service` varchar(50) NOT NULL COMMENT '所属服务 workcase、bidding',
`version` int NOT NULL DEFAULT 1 COMMENT '文件版本',
`action` varchar(50) NOT NULL COMMENT '操作类型 upload、update、delete',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建者',
`creator_name` varchar(100) NOT NULL COMMENT '创建者姓名',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_knowledge_file_log` (`knowledge_id`, `file_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文件日志表';
```
## 3. 招标模块 (Bidding)
### 3.1 招标项目表
```sql
CREATE TABLE IF NOT EXISTS `tb_bidding_project` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`project_id` varchar(50) NOT NULL COMMENT '项目ID',
`project_no` varchar(100) NOT NULL COMMENT '项目编号',
`project_name` varchar(500) NOT NULL COMMENT '项目名称',
`project_type` varchar(50) NOT NULL COMMENT '项目类型',
`industry` varchar(100) DEFAULT NULL COMMENT '所属行业',
`source_platform` varchar(100) DEFAULT NULL COMMENT '来源平台',
`source_url` varchar(500) DEFAULT NULL COMMENT '来源URL',
`publish_date` datetime DEFAULT NULL COMMENT '发布日期',
`deadline` datetime DEFAULT NULL COMMENT '投标截止日期',
`opening_date` datetime DEFAULT NULL COMMENT '开标日期',
`budget_amount` decimal(18,2) DEFAULT NULL COMMENT '预算金额',
`currency` varchar(10) DEFAULT 'CNY' COMMENT '货币单位',
`project_status` varchar(30) NOT NULL DEFAULT 'collecting' COMMENT '项目状态',
`winning_status` varchar(30) DEFAULT NULL COMMENT '中标状态',
`winning_amount` decimal(18,2) DEFAULT NULL COMMENT '中标金额',
`client_name` varchar(255) DEFAULT NULL COMMENT '客户名称',
`client_contact` varchar(100) DEFAULT NULL COMMENT '客户联系方式',
`contact_person` varchar(100) DEFAULT NULL COMMENT '联系人',
`project_location` varchar(500) DEFAULT NULL COMMENT '项目地点',
`description` text DEFAULT NULL COMMENT '项目描述',
`keywords` json DEFAULT NULL COMMENT '关键词数组',
`metadata` json DEFAULT NULL COMMENT '项目元数据',
`dept_path` varchar(255) DEFAULT NULL COMMENT '部门全路径',
`responsible_user` varchar(50) DEFAULT NULL COMMENT '负责人',
`team_members` json DEFAULT NULL COMMENT '团队成员数组',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) DEFAULT NULL COMMENT '创建者',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`project_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
UNIQUE KEY `uk_project_no` (`project_no`),
KEY `idx_project_status` (`project_status`),
KEY `idx_deadline` (`deadline`),
KEY `idx_dept_path` (`dept_path`),
KEY `idx_responsible_user` (`responsible_user`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招标项目表';
### 3.2 招标文件表
CREATE TABLE IF NOT EXISTS `tb_bidding_document` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`doc_id` varchar(50) NOT NULL COMMENT '文档ID',
`project_id` varchar(50) NOT NULL COMMENT '所属项目ID',
`doc_type` varchar(50) NOT NULL COMMENT '文档类型',
`doc_name` varchar(500) NOT NULL COMMENT '文档名称',
`file_id` varchar(50) DEFAULT NULL COMMENT '关联文件表ID',
`file_path` varchar(500) DEFAULT NULL COMMENT '文件路径',
`file_size` bigint DEFAULT NULL COMMENT '文件大小',
`mime_type` varchar(100) DEFAULT NULL COMMENT 'MIME类型',
`version` int DEFAULT 1 COMMENT '版本号',
`language` varchar(20) DEFAULT 'zh-CN' COMMENT '语言',
`page_count` int DEFAULT NULL COMMENT '页数',
`parse_status` varchar(30) DEFAULT 'pending' COMMENT '解析状态',
`parse_result` json DEFAULT NULL COMMENT '解析结果',
`extraction_data` json DEFAULT NULL COMMENT '提取的结构化数据',
`ai_analysis` text DEFAULT NULL COMMENT 'AI分析结果',
`upload_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '上传日期',
`dept_path` varchar(255) DEFAULT NULL COMMENT '部门全路径',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) DEFAULT NULL COMMENT '创建者',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`doc_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_project_id` (`project_id`),
KEY `idx_doc_type` (`doc_type`),
KEY `idx_tenant_id` (`tenant_id`),
CONSTRAINT `fk_bidding_document_project` FOREIGN KEY (`project_id`) REFERENCES `tb_bidding_project`(`project_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招标文件表';
```
## 4. 消息模块 (Message)
### 4.1 消息表
```sql
CREATE TABLE IF NOT EXISTS `tb_message` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`title` varchar(255) NOT NULL COMMENT '消息标题',
`content` varchar(255) NOT NULL COMMENT '消息内容',
`type` varchar(50) NOT NULL COMMENT '消息类型',
`status` varchar(50) NOT NULL COMMENT '消息状态',
`service` varchar(50) NOT NULL COMMENT '服务类型',
`dept_path` varchar(255) DEFAULT NULL COMMENT '部门全路径',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL DEFAULT 'system' COMMENT '创建者',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`message_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
### 4.2 消息发送范围表
CREATE TABLE IF NOT EXISTS `tb_message_range` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`target_type` varchar(20) NOT NULL COMMENT '目标类型user/dept/role/all',
`target_id` varchar(50) DEFAULT NULL COMMENT '目标ID',
`channel` varchar(20) NOT NULL DEFAULT 'app' COMMENT '发送渠道',
`dept_path` varchar(255) DEFAULT NULL COMMENT '部门全路径',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`creator` varchar(50) NOT NULL DEFAULT 'system' COMMENT '创建者',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_message_target` (`message_id`, `target_type`, `target_id`, `channel`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息发送范围定义表';
### 4.3 消息接收记录表
CREATE TABLE IF NOT EXISTS `tb_message_receiver` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
`channel` varchar(20) DEFAULT 'app' COMMENT '接收渠道',
`status` varchar(20) NOT NULL DEFAULT 'unread' COMMENT '状态',
`read_time` datetime DEFAULT NULL COMMENT '阅读时间',
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_message_user` (`message_id`, `user_id`, `channel`),
KEY `idx_user_status` (`user_id`, `status`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户消息接收记录表';
```
## 5. 平台管理模块 (Platform)
```sql
-- 平台管理的表通常比较简单,主要是配置和日志
-- 根据实际需求补充具体表结构
```
## 6. 数据迁移脚本
### 6.1 用户数据映射
```sql
-- 创建用户映射表(临时)
CREATE TABLE IF NOT EXISTS `temp_user_mapping` (
`old_user_id` varchar(50) NOT NULL COMMENT '原系统用户ID',
`new_user_id` bigint NOT NULL COMMENT 'pigx系统用户ID',
`user_type` varchar(20) NOT NULL COMMENT '用户类型',
PRIMARY KEY (`old_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户ID映射表临时';
-- 插入映射数据(示例)
-- INSERT INTO temp_user_mapping (old_user_id, new_user_id, user_type)
-- SELECT old_id, new_id, 'staff' FROM ...;
```
### 6.2 数据迁移存储过程(示例)
```sql
DELIMITER $$
CREATE PROCEDURE migrate_workcase_data()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_old_user_id VARCHAR(50);
DECLARE v_new_user_id BIGINT;
DECLARE cur CURSOR FOR
SELECT old_user_id, new_user_id FROM temp_user_mapping;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
-- 开始事务
START TRANSACTION;
-- 迁移工单数据
INSERT INTO tb_workcase (
optsn, workcase_id, room_id, user_id, username, phone,
type, device, device_code, device_name_plate, device_name_plate_img,
address, description, imgs, emergency, status, processor,
tenant_id, creator, create_time, update_time, delete_time, deleted
)
SELECT
optsn, workcase_id, room_id,
COALESCE(m.new_user_id, w.user_id) as user_id, -- 映射用户ID
username, phone,
type, device, device_code, device_name_plate, device_name_plate_img,
address, description,
CASE WHEN imgs IS NULL THEN NULL ELSE JSON_ARRAY(imgs) END, -- 数组转JSON
emergency, status, processor,
1 as tenant_id, -- 默认租户ID
creator, create_time, update_time, delete_time, deleted
FROM postgresql_workcase.tb_workcase w
LEFT JOIN temp_user_mapping m ON w.user_id = m.old_user_id;
COMMIT;
END$$
DELIMITER ;
```
## 7. 索引优化建议
```sql
-- 为查询性能添加复合索引
ALTER TABLE tb_workcase ADD INDEX idx_status_tenant (status, tenant_id);
ALTER TABLE tb_chat_room ADD INDEX idx_status_tenant (status, tenant_id);
ALTER TABLE tb_chat_message ADD INDEX idx_chat_tenant (chat_id, tenant_id);
ALTER TABLE tb_agent ADD INDEX idx_category_tenant (category, tenant_id);
```
## 8. 注意事项
1. **租户隔离**:所有业务表都添加了 `tenant_id` 字段,默认值为 1
2. **用户关联**需要建立原系统用户ID到pigx用户ID的映射关系
3. **数组处理**PostgreSQL的数组类型转换为MySQL的JSON类型
4. **时区处理**PostgreSQL的TIMESTAMPTZ转换为MySQL的DATETIME注意时区转换
5. **外键约束**:根据实际需求决定是否保留外键约束
6. **数据完整性**:迁移前做好数据备份,迁移后进行数据验证
## 9. 迁移后验证
```sql
-- 验证数据条数
SELECT 'tb_workcase' as table_name, COUNT(*) as record_count FROM tb_workcase
UNION ALL
SELECT 'tb_chat_room', COUNT(*) FROM tb_chat_room
UNION ALL
SELECT 'tb_agent', COUNT(*) FROM tb_agent
UNION ALL
SELECT 'tb_chat', COUNT(*) FROM tb_chat
UNION ALL
SELECT 'tb_knowledge', COUNT(*) FROM tb_knowledge;
-- 验证租户隔离
SELECT tenant_id, COUNT(*) as count
FROM tb_workcase
GROUP BY tenant_id;
-- 验证用户关联
SELECT COUNT(*) as unmapped_users
FROM tb_workcase w
LEFT JOIN sys_user u ON w.user_id = u.user_id
WHERE u.user_id IS NULL;
```

View File

@@ -0,0 +1,571 @@
# 数据库迁移脚本指南PostgreSQL → MySQL
## 1. 概述
本文档提供了将 urbanLifeline 数据库从 PostgreSQL 迁移到 MySQLpigx 平台)的完整脚本和指南。
## 2. 数据类型映射
| PostgreSQL | MySQL | 说明 |
|-----------|-------|------|
| VARCHAR(n) | VARCHAR(n) | 字符串 |
| TEXT | TEXT | 长文本 |
| INTEGER | INT | 整数 |
| BIGINT | BIGINT | 长整数 |
| BOOLEAN | TINYINT(1) | 布尔值 |
| TIMESTAMPTZ | DATETIME | 时间戳 |
| JSONB | JSON | JSON数据 |
| NUMERIC(p,s) | DECIMAL(p,s) | 小数 |
| VARCHAR(n)[] | JSON | 数组转JSON |
| SERIAL | INT AUTO_INCREMENT | 自增 |
## 3. pigx-dify 模块数据库脚本
### 3.1 智能体表 (tb_agent)
```sql
-- 智能体配置表
CREATE TABLE `tb_agent` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`agent_id` varchar(50) NOT NULL COMMENT '智能体ID',
`name` varchar(50) NOT NULL COMMENT '智能体名称',
`description` varchar(500) DEFAULT NULL COMMENT '智能体描述',
`link` varchar(500) DEFAULT NULL COMMENT '智能体url',
`api_key` varchar(500) NOT NULL COMMENT 'dify智能体APIKEY',
`is_outer` tinyint(1) DEFAULT '0' COMMENT '是否是对外智能体,未登录可用',
`introduce` varchar(500) NOT NULL COMMENT '引导词',
`prompt_cards` json DEFAULT NULL COMMENT '提示卡片数组 [{file_id:, prompt:}]',
`category` varchar(50) NOT NULL COMMENT '分类',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`creator` varchar(50) DEFAULT NULL COMMENT '创建者',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`agent_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
UNIQUE KEY `uk_api_key` (`api_key`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI智能体配置表';
```
### 3.2 对话表 (tb_chat)
```sql
-- AI智能体对话表
CREATE TABLE `tb_chat` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`chat_id` varchar(50) NOT NULL COMMENT '对话ID',
`agent_id` varchar(50) NOT NULL COMMENT '智能体ID',
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
`user_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '用户类型 1-系统内部人员 0-系统外部人员',
`title` varchar(500) NOT NULL COMMENT '对话标题',
`channel` varchar(50) DEFAULT 'agent' COMMENT '对话渠道 agent、wechat',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`chat_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_agent_id` (`agent_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI智能体对话表';
```
### 3.3 聊天消息表 (tb_chat_message)
```sql
-- AI智能体对话消息表
CREATE TABLE `tb_chat_message` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`dify_message_id` varchar(100) DEFAULT NULL COMMENT 'Dify消息ID',
`chat_id` varchar(50) NOT NULL COMMENT '对话ID',
`role` varchar(50) NOT NULL COMMENT '角色user-用户/ai-智能体/recipient-来客',
`content` text NOT NULL COMMENT '消息内容',
`files` json DEFAULT NULL COMMENT '文件id数组',
`comment` varchar(50) DEFAULT NULL COMMENT '评价',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`message_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_chat_id` (`chat_id`),
KEY `idx_dify_message_id` (`dify_message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI智能体对话消息表';
```
### 3.4 知识库表 (tb_knowledge)
```sql
-- 知识库配置表
CREATE TABLE `tb_knowledge` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`knowledge_id` varchar(50) NOT NULL COMMENT '知识库ID',
`title` varchar(255) NOT NULL COMMENT '知识库标题',
`avatar` varchar(255) DEFAULT NULL COMMENT '知识库头像',
`description` varchar(500) DEFAULT NULL COMMENT '知识库描述',
`dify_dataset_id` varchar(100) DEFAULT NULL COMMENT 'Dify知识库IDDataset ID',
`dify_indexing_technique` varchar(50) DEFAULT 'high_quality' COMMENT 'Dify索引方式high_quality/economy',
`embedding_model` varchar(100) DEFAULT NULL COMMENT '向量模型名称',
`embedding_model_provider` varchar(100) DEFAULT NULL COMMENT '向量模型提供商',
`rerank_model` varchar(100) DEFAULT NULL COMMENT 'Rerank模型名称',
`rerank_model_provider` varchar(100) DEFAULT NULL COMMENT 'Rerank模型提供商',
`reranking_enable` tinyint(1) DEFAULT '0' COMMENT '是否启用Rerank',
`retrieval_top_k` int DEFAULT '2' COMMENT '检索Top K返回前K个结果',
`retrieval_score_threshold` decimal(3,2) DEFAULT '0.00' COMMENT '检索分数阈值0.00-1.00',
`document_count` int DEFAULT '0' COMMENT '文档数量',
`total_chunks` int DEFAULT '0' COMMENT '总分段数',
`service` varchar(50) DEFAULT NULL COMMENT '所属服务 workcase、bidding',
`project_id` varchar(50) DEFAULT NULL COMMENT 'bidding所属项目ID',
`category` varchar(50) DEFAULT NULL COMMENT '所属分类',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建者用户ID',
`dept_path` varchar(50) DEFAULT NULL COMMENT '创建者部门路径',
`updater` varchar(50) DEFAULT NULL COMMENT '更新者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_knowledge_id` (`knowledge_id`),
UNIQUE KEY `uk_dify_dataset_id` (`dify_dataset_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库配置表';
```
### 3.5 知识库文件表 (tb_knowledge_file)
```sql
-- 知识库文件表
CREATE TABLE `tb_knowledge_file` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`knowledge_id` varchar(50) NOT NULL COMMENT '知识库ID',
`file_root_id` varchar(50) NOT NULL COMMENT '文件根ID',
`file_id` varchar(50) NOT NULL COMMENT '文件ID',
`dify_file_id` varchar(50) NOT NULL COMMENT 'dify文件ID',
`version` int NOT NULL DEFAULT '1' COMMENT '文件版本',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`optsn`),
UNIQUE KEY `uk_knowledge_file` (`knowledge_id`, `file_id`),
KEY `idx_file_root_id` (`file_root_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文件表';
```
## 4. pigx-app-server 模块数据库脚本
### 4.1 工单表 (tb_workcase)
```sql
-- 工单表
CREATE TABLE `tb_workcase` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`workcase_id` varchar(50) NOT NULL COMMENT '工单ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`user_id` varchar(50) NOT NULL COMMENT '来客ID',
`username` varchar(200) NOT NULL COMMENT '来客姓名',
`phone` varchar(20) NOT NULL COMMENT '来客电话',
`type` varchar(50) NOT NULL COMMENT '故障类型',
`device` varchar(50) DEFAULT NULL COMMENT '设备名称',
`device_code` varchar(50) DEFAULT NULL COMMENT '设备代码',
`device_name_plate` varchar(50) DEFAULT NULL COMMENT '设备名称牌',
`device_name_plate_img` varchar(50) NOT NULL COMMENT '设备名称牌图片',
`address` varchar(1000) DEFAULT NULL COMMENT '现场地址',
`description` varchar(1000) DEFAULT NULL COMMENT '故障描述',
`imgs` json DEFAULT NULL COMMENT '工单图片id数组',
`emergency` varchar(50) NOT NULL DEFAULT 'normal' COMMENT '紧急程度 normal-普通 emergency-紧急',
`status` varchar(50) NOT NULL DEFAULT 'pending' COMMENT '状态 pending-待处理 processing-处理中 done-已完成',
`processor` varchar(50) DEFAULT NULL COMMENT '处理人',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`workcase_id`),
UNIQUE KEY `uk_room_id` (`room_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单表';
```
### 4.2 聊天室表 (tb_chat_room)
```sql
-- IM聊天室表
CREATE TABLE `tb_chat_room` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`workcase_id` varchar(50) DEFAULT NULL COMMENT '关联工单ID',
`room_name` varchar(200) NOT NULL COMMENT '聊天室名称',
`room_type` varchar(20) NOT NULL DEFAULT 'workcase' COMMENT '聊天室类型workcase-工单客服',
`status` varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态active-活跃 closed-已关闭 archived-已归档',
`guest_id` varchar(50) NOT NULL COMMENT '来客ID创建者',
`guest_name` varchar(100) NOT NULL COMMENT '来客姓名',
`ai_session_id` varchar(50) DEFAULT NULL COMMENT 'AI对话会话ID从ai.tb_chat同步',
`message_count` int NOT NULL DEFAULT '0' COMMENT '消息总数',
`device_code` varchar(50) NOT NULL COMMENT '设备代码',
`last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间',
`last_message` text DEFAULT NULL COMMENT '最后一条消息内容',
`comment_level` int DEFAULT '0' COMMENT '服务评分1-5',
`closed_by` varchar(50) DEFAULT NULL COMMENT '关闭人',
`closed_time` datetime DEFAULT NULL COMMENT '关闭时间',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`room_id`),
UNIQUE KEY `uk_workcase_id` (`workcase_id`),
UNIQUE KEY `uk_optsn` (`optsn`),
KEY `idx_guest` (`guest_id`, `status`),
KEY `idx_last_message_time` (`last_message_time`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM聊天室表一个工单对应一个聊天室';
```
### 4.3 聊天室成员表 (tb_chat_room_member)
```sql
-- 聊天室成员表
CREATE TABLE `tb_chat_room_member` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`member_id` varchar(50) NOT NULL COMMENT '成员记录ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`user_id` varchar(50) NOT NULL COMMENT '用户ID来客ID或员工ID',
`user_type` varchar(20) NOT NULL COMMENT '用户类型guest-来客 staff-客服 ai-AI助手',
`user_name` varchar(100) NOT NULL COMMENT '用户名称',
`status` varchar(20) NOT NULL DEFAULT 'active' COMMENT '状态active-活跃 left-已离开 removed-被移除',
`unread_count` int NOT NULL DEFAULT '0' COMMENT '未读消息数',
`last_read_time` datetime DEFAULT NULL COMMENT '最后阅读时间',
`last_read_msg_id` varchar(50) DEFAULT NULL COMMENT '最后阅读的消息ID',
`join_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
`leave_time` datetime DEFAULT NULL COMMENT '离开时间',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`member_id`),
UNIQUE KEY `uk_room_user` (`room_id`, `user_id`),
KEY `idx_room_status` (`room_id`, `status`),
KEY `idx_user` (`user_id`, `user_type`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天室成员表';
```
### 4.4 聊天室消息表 (tb_chat_room_message)
```sql
-- 聊天室消息表
CREATE TABLE `tb_chat_room_message` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`message_id` varchar(50) NOT NULL COMMENT '消息ID',
`room_id` varchar(50) NOT NULL COMMENT '聊天室ID',
`sender_id` varchar(50) NOT NULL COMMENT '发送者ID',
`sender_type` varchar(20) NOT NULL COMMENT '发送者类型guest-来客 agent-客服 ai-AI助手 system-系统消息',
`sender_name` varchar(100) NOT NULL COMMENT '发送者名称',
`message_type` varchar(20) NOT NULL DEFAULT 'text' COMMENT '消息类型',
`content` text NOT NULL COMMENT '消息内容',
`files` json DEFAULT NULL COMMENT '附件文件ID数组',
`content_extra` json DEFAULT NULL COMMENT '扩展内容(会议链接、引用信息等)',
`reply_to_msg_id` varchar(50) DEFAULT NULL COMMENT '回复的消息ID',
`is_ai_message` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否AI消息',
`ai_message_id` varchar(50) DEFAULT NULL COMMENT 'AI原始消息ID',
`status` varchar(20) NOT NULL DEFAULT 'sent' COMMENT '状态',
`read_count` int NOT NULL DEFAULT '0' COMMENT '已读人数',
`send_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`message_id`),
KEY `idx_room_time` (`room_id`, `send_time`),
KEY `idx_sender` (`sender_id`, `sender_type`),
KEY `idx_ai_message` (`ai_message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM聊天消息表';
```
### 4.5 视频会议表 (tb_video_meeting)
```sql
-- Jitsi Meet视频会议表
CREATE TABLE `tb_video_meeting` (
`optsn` varchar(50) NOT NULL COMMENT '流水号',
`meeting_id` varchar(50) NOT NULL COMMENT '会议ID也是Jitsi房间名',
`room_id` varchar(50) NOT NULL COMMENT '关联聊天室ID',
`workcase_id` varchar(50) NOT NULL COMMENT '关联工单ID',
`meeting_name` varchar(200) NOT NULL COMMENT '会议名称',
`meeting_password` varchar(50) DEFAULT NULL COMMENT '会议密码',
`description` varchar(500) DEFAULT NULL COMMENT '会议描述',
`jwt_token` text DEFAULT NULL COMMENT 'JWT Token用于身份验证',
`jitsi_room_name` varchar(200) NOT NULL COMMENT 'Jitsi房间名',
`jitsi_server_url` varchar(500) NOT NULL DEFAULT 'https://meet.jit.si' COMMENT 'Jitsi服务器地址',
`status` varchar(20) NOT NULL DEFAULT 'scheduled' COMMENT '状态',
`creator_type` varchar(20) NOT NULL COMMENT '创建者类型',
`creator_name` varchar(100) NOT NULL COMMENT '创建者名称',
`participant_count` int NOT NULL DEFAULT '0' COMMENT '参与人数',
`max_participants` int DEFAULT '10' COMMENT '最大参与人数',
`start_time` datetime NOT NULL COMMENT '定义会议开始时间',
`end_time` datetime NOT NULL COMMENT '定义会议结束时间',
`advance` int DEFAULT '5' COMMENT '提前入会时间(分钟)',
`actual_start_time` datetime DEFAULT NULL COMMENT '真正会议开始时间',
`actual_end_time` datetime DEFAULT NULL COMMENT '真正会议结束时间',
`duration_seconds` int DEFAULT '0' COMMENT '会议时长(秒)',
`iframe_url` text DEFAULT NULL COMMENT 'iframe嵌入URL',
`config` json DEFAULT NULL COMMENT 'Jitsi配置项',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`creator` varchar(50) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`meeting_id`),
UNIQUE KEY `uk_jitsi_room_name` (`jitsi_room_name`),
KEY `idx_room` (`room_id`, `status`),
KEY `idx_workcase` (`workcase_id`, `status`),
KEY `idx_create_time` (`create_time`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Jitsi Meet视频会议表';
```
## 5. 数据迁移步骤
### 5.1 准备工作
```bash
# 1. 导出 PostgreSQL 数据
pg_dump -h localhost -p 5432 -U postgres -d urbanlifeline \
--data-only \
--column-inserts \
--no-owner \
--no-privileges \
--no-tablespaces \
> urbanlifeline_data.sql
# 2. 创建 MySQL 数据库
mysql -u root -p -e "CREATE DATABASE pigx_dify DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
```
### 5.2 执行表结构创建
```bash
# 执行所有建表语句
mysql -u root -p pigx_dify < create_tables.sql
```
### 5.3 数据转换脚本
```python
#!/usr/bin/env python3
# convert_data.py - PostgreSQL 到 MySQL 数据转换脚本
import re
import json
def convert_postgresql_to_mysql(input_file, output_file):
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
# 替换 PostgreSQL 特有语法
replacements = [
# 布尔值转换
(r'\btrue\b', '1'),
(r'\bfalse\b', '0'),
# 时间戳转换
(r"now\(\)", "CURRENT_TIMESTAMP"),
# 数组转JSON
(r"'{([^}]*)}'::\w+\[\]", lambda m: f"'{json.dumps(m.group(1).split(',') if m.group(1) else [])}'"),
# Schema 去除
(r'\b(workcase|ai|sys|message|bidding)\\.', ''),
]
for pattern, replacement in replacements:
if callable(replacement):
content = re.sub(pattern, replacement, content)
else:
content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
# 添加租户ID到每个INSERT语句
content = add_tenant_id(content)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(content)
def add_tenant_id(content):
"""为所有INSERT语句添加tenant_id字段"""
lines = content.split('\n')
result = []
for line in lines:
if line.startswith('INSERT INTO'):
# 检查表是否需要tenant_id
if any(table in line for table in ['tb_agent', 'tb_chat', 'tb_workcase', 'tb_chat_room']):
# 在VALUES前添加tenant_id字段
line = re.sub(
r'(\([^)]+)\)',
r'\1, tenant_id)',
line, count=1
)
# 在值列表中添加1作为默认租户ID
line = re.sub(
r'VALUES\s*\(([^)]+)\)',
r'VALUES (\1, 1)',
line
)
result.append(line)
return '\n'.join(result)
if __name__ == '__main__':
convert_postgresql_to_mysql('urbanlifeline_data.sql', 'pigx_data.sql')
print("数据转换完成!")
```
### 5.4 导入数据到 MySQL
```bash
# 导入转换后的数据
mysql -u root -p pigx_dify < pigx_data.sql
# 验证数据
mysql -u root -p pigx_dify -e "
SELECT COUNT(*) FROM tb_agent;
SELECT COUNT(*) FROM tb_chat;
SELECT COUNT(*) FROM tb_workcase;
"
```
## 6. 添加租户字段指南
### 6.1 需要添加 tenant_id 的表
所有业务表都需要添加 `tenant_id` 字段:
- tb_agent
- tb_chat
- tb_chat_message通过chat关联获取
- tb_knowledge
- tb_workcase
- tb_chat_room
- tb_video_meeting
### 6.2 添加租户字段的SQL模板
```sql
-- 为现有表添加租户字段(如果表已存在)
ALTER TABLE `tb_agent`
ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT '租户ID' AFTER `category`,
ADD INDEX `idx_tenant_id` (`tenant_id`);
-- 更新现有数据的租户ID默认为1
UPDATE tb_agent SET tenant_id = 1 WHERE tenant_id IS NULL;
```
### 6.3 MyBatis-Plus 配置
```java
// 实体类添加租户字段
@TableField(fill = FieldFill.INSERT)
private Long tenantId;
// 自动填充处理器
@Component
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
PigxUser user = SecurityUtils.getUser();
this.strictInsertFill(metaObject, "tenantId", Long.class, user.getTenantId());
}
}
// 租户拦截器配置
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(SecurityUtils.getUser().getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
}));
return interceptor;
}
```
## 7. 验证和测试
### 7.1 数据完整性检查
```sql
-- 检查数据迁移完整性
SELECT
'tb_agent' AS table_name,
COUNT(*) AS record_count
FROM tb_agent
UNION ALL
SELECT 'tb_chat', COUNT(*) FROM tb_chat
UNION ALL
SELECT 'tb_workcase', COUNT(*) FROM tb_workcase;
-- 检查租户ID设置
SELECT
tenant_id,
COUNT(*) AS record_count
FROM tb_agent
GROUP BY tenant_id;
```
### 7.2 索引优化
```sql
-- 添加常用查询索引
ALTER TABLE tb_chat ADD INDEX idx_user_agent (user_id, agent_id);
ALTER TABLE tb_workcase ADD INDEX idx_create_time (create_time DESC);
ALTER TABLE tb_chat_message ADD INDEX idx_chat_time (chat_id, create_time DESC);
```
## 8. 回滚方案
如果迁移失败,可以使用以下方式回滚:
```bash
# 备份当前MySQL数据
mysqldump -u root -p pigx_dify > backup_before_migration.sql
# 如需回滚
mysql -u root -p -e "DROP DATABASE pigx_dify; CREATE DATABASE pigx_dify;"
mysql -u root -p pigx_dify < backup_before_migration.sql
```
## 9. 注意事项
1. **字符集**:确保 MySQL 使用 utf8mb4 字符集
2. **时区**:注意 PostgreSQL 的 TIMESTAMPTZ 转换为 MySQL DATETIME 可能有时区差异
3. **数组类型**PostgreSQL 的数组类型需要转换为 JSON
4. **事务**:大批量数据导入时注意事务大小
5. **权限**:确保 MySQL 用户有足够权限创建表和索引
6. **外键**:本脚本未创建外键约束,如需要可后续添加
## 10. 迁移清单
- [ ] 导出 PostgreSQL 数据
- [ ] 创建 MySQL 数据库和表结构
- [ ] 执行数据转换脚本
- [ ] 导入数据到 MySQL
- [ ] 添加租户字段和索引
- [ ] 验证数据完整性
- [ ] 测试查询性能
- [ ] 配置 MyBatis-Plus 租户拦截器
- [ ] 执行应用集成测试
- [ ] 准备回滚方案

View File

@@ -0,0 +1,547 @@
# Design Document
## Overview
本设计文档描述了将 urbanLifelineServ 和 urbanLifelineWeb 的**业务功能**迁移到 pigx-ai 平台的技术方案。
### 核心原则
- **只迁移业务代码**招标、工单、平台管理、AI、消息等业务功能
- **复用 pigx 基础设施**:人员、部门、权限、认证完全使用 pigx 原生实现
- **适配 pigx 规范**:使用 PigxUser、R<T> 响应格式、Feign 远程调用等
### 迁移范围
| 源模块 | 目标位置 | 说明 |
|-------|---------|------|
| bidding | pigx-app-server-biz | 招标业务 |
| workcase | pigx-app-server-biz | 工单业务 |
| platform | pigx-app-server-biz | 平台管理 |
| ai | pigx-dify新建模块 | AI对话/知识库/Dify集成 |
| message | pigx-app-server-biz | 消息通知 |
| file | 使用 pigx-common-oss | 文件服务 |
| crontab | pigx-visual/xxl-job | 定时任务 |
## Architecture
### 后端模块结构
```
pigx-app-server/
├── pigx-app-server-api/ # API接口定义
│ └── src/main/java/com/pig4cloud/pigx/app/api/
│ ├── entity/ # 业务实体
│ │ ├── bidding/ # 招标实体
│ │ ├── workcase/ # 工单实体
│ │ └── platform/ # 平台实体
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ └── feign/ # Feign接口
└── pigx-app-server-biz/ # 业务实现
└── src/main/java/com/pig4cloud/pigx/app/
├── controller/
│ ├── bidding/ # 招标控制器
│ ├── workcase/ # 工单控制器
│ ├── platform/ # 平台控制器
│ └── message/ # 消息控制器
├── service/
│ ├── bidding/
│ ├── workcase/
│ ├── platform/
│ └── message/
└── mapper/
├── bidding/
├── workcase/
├── platform/
└── message/
pigx-dify/ # 新建的AI模块
├── pigx-dify-api/ # API接口定义
│ └── src/main/java/com/pig4cloud/pigx/dify/api/
│ ├── entity/ # AI实体
│ │ ├── TbAgent.java # 智能体配置
│ │ ├── TbChat.java # 聊天会话
│ │ ├── TbChatMessage.java # 聊天消息
│ │ └── TbKnowledge.java # 知识库
│ ├── dto/ # 数据传输对象
│ └── feign/ # Feign接口
└── pigx-dify-biz/ # 业务实现
└── src/main/java/com/pig4cloud/pigx/dify/
├── controller/
│ ├── AgentController.java # 智能体管理
│ ├── ChatController.java # 对话管理
│ └── KnowledgeController.java # 知识库管理
├── service/
│ ├── AgentService.java
│ ├── ChatService.java
│ └── KnowledgeService.java
├── mapper/
│ ├── AgentMapper.java
│ ├── ChatMapper.java
│ └── KnowledgeMapper.java
└── client/
└── DifyApiClient.java # Dify API客户端
```
### 前端模块结构
```
pigx-ai-ui/src/
├── views/
│ ├── urban/ # 迁移的业务视图
│ │ ├── bidding/ # 招标页面
│ │ ├── workcase/ # 工单页面
│ │ └── platform/ # 平台管理页面
│ └── dify/ # AI功能页面新建
│ ├── agent/ # 智能体管理
│ ├── chat/ # 对话界面
│ └── knowledge/ # 知识库管理
├── components/
│ └── urban/ # 迁移的共享组件
└── api/
├── urban/ # 业务API定义
│ ├── bidding.ts
│ ├── workcase.ts
│ └── platform.ts
└── dify/ # AI API定义
├── agent.ts
├── chat.ts
└── knowledge.ts
```
## Components and Interfaces
### 1. 权限模型完全替换
**核心原则**: 不迁移任何用户、部门、角色、权限数据,完全使用 pigx 原生权限体系。
#### 权限注解适配
```java
// 源代码 (urbanLifelineServ 使用 @PreAuthorize)
@PreAuthorize("hasAuthority('workcase:ticket:create')")
@PostMapping
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return ResultDomain.success(workcaseService.save(workcase));
}
// 目标代码 (pigx-app-server 使用 @HasPermission)
@HasPermission("workcase_ticket_add")
@PostMapping
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return R.ok(workcaseService.save(workcase));
}
```
#### 权限标识映射规则
| 源权限标识 | 目标权限标识 | 说明 |
|-----------|-------------|------|
| workcase:ticket:create | workcase_ticket_add | 工单创建 |
| workcase:ticket:update | workcase_ticket_edit | 工单编辑 |
| workcase:ticket:view | workcase_ticket_view | 工单查看 |
| workcase:ticket:delete | workcase_ticket_del | 工单删除 |
| bidding:project:create | bidding_project_add | 招标创建 |
| bidding:project:view | bidding_project_view | 招标查看 |
#### 用户信息获取适配
```java
// 源代码 (JWT 获取用户)
Long userId = JwtUtils.getUserId();
String username = JwtUtils.getUsername();
// 目标代码 (pigx SecurityUtils)
PigxUser user = SecurityUtils.getUser();
Long userId = user.getId();
String username = user.getUsername();
Long tenantId = user.getTenantId(); // 租户ID
Long deptId = user.getDeptId(); // 部门ID
```
#### 用户服务调用适配
```java
// 源代码 (直接调用 UserService)
@Autowired
private UserService userService;
User user = userService.getById(userId);
// 目标代码 (通过 Feign 调用 pigx-upms)
@Autowired
private RemoteUserService remoteUserService;
R<SysUser> result = remoteUserService.selectById(userId);
SysUser user = result.getData();
```
### 2. 菜单和权限配置
**不迁移源系统的菜单和权限数据**,在 pigx 中重新配置:
#### 菜单配置 (sys_menu 表)
```sql
-- 在 pigx 的 sys_menu 表中添加业务功能菜单
INSERT INTO sys_menu (menu_id, name, permission, path, parent_id, icon, sort, type, tenant_id) VALUES
(1000, '工单管理', 'workcase_menu', '/workcase', 0, 'workcase', 1, '0', 1),
(1001, '工单列表', 'workcase_ticket_view', '/workcase/list', 1000, '', 1, '1', 1),
(1002, '创建工单', 'workcase_ticket_add', '', 1000, '', 2, '2', 1),
(1003, '编辑工单', 'workcase_ticket_edit', '', 1000, '', 3, '2', 1),
(1004, '删除工单', 'workcase_ticket_del', '', 1000, '', 4, '2', 1),
(2000, '招标管理', 'bidding_menu', '/bidding', 0, 'bidding', 2, '0', 1),
(2001, '招标项目', 'bidding_project_view', '/bidding/project', 2000, '', 1, '1', 1),
(2002, '创建项目', 'bidding_project_add', '', 2000, '', 2, '2', 1),
(2003, '编辑项目', 'bidding_project_edit', '', 2000, '', 3, '2', 1);
```
#### 角色权限分配
使用 pigx 现有的角色管理功能,为角色分配新的业务权限:
```sql
-- 为管理员角色分配所有业务权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE permission LIKE 'workcase_%' OR permission LIKE 'bidding_%';
```
#### 数据权限适配
```java
// 源代码 (可能没有租户隔离)
@PostMapping("/list")
public ResultDomain<TbWorkcaseDTO> getWorkcaseList(@RequestBody TbWorkcaseDTO filter) {
return ResultDomain.success(workcaseService.list(filter));
}
// 目标代码 (自动添加租户和部门过滤)
@PostMapping("/list")
@HasPermission("workcase_ticket_view")
public R<List<TbWorkcaseDTO>> getWorkcaseList(@RequestBody TbWorkcaseDTO filter) {
// pigx 会自动根据用户的租户ID和数据权限过滤数据
return R.ok(workcaseService.list(filter));
}
```
```java
// 源代码 (urbanLifelineServ 使用 ResultDomain)
@GetMapping("/list")
public ResultDomain<Workcase> list() {
// ResultDomain 包含 dataList 字段
return ResultDomain.success(workcaseService.list());
}
// 目标代码 (使用 pigx R<T>)
@GetMapping("/list")
public R<List<Workcase>> list() {
return R.ok(workcaseService.list());
}
// 或者使用分页 (pigx IPage)
@GetMapping("/page")
public R<IPage<Workcase>> page(Page page) {
return R.ok(workcaseService.page(page));
}
```
**响应格式映射**:
| 源格式 (ResultDomain) | 目标格式 (R<T>) |
|---------------------|----------------|
| ResultDomain.success(data) | R.ok(data) |
| ResultDomain.fail(msg) | R.failed(msg) |
| dataList 字段 | data 字段 (直接返回List) |
| code/message | code/msg |
### 3. 响应格式适配
```java
// 源代码
Long userId = JwtUtils.getUserId();
// 目标代码 (使用 pigx SecurityUtils)
PigxUser user = SecurityUtils.getUser();
Long userId = user.getId();
Long tenantId = user.getTenantId();
Long deptId = user.getDeptId();
```
### 4. 文件上传适配
```java
// 源代码 (MinIO直接调用)
minioClient.putObject(bucket, objectName, inputStream);
// 目标代码 (使用 pigx OSS)
@Autowired
private OssTemplate ossTemplate;
ossTemplate.putObject(bucket, objectName, inputStream);
```
### 5. 前端 API 调用适配
```typescript
// 源代码 (urbanLifelineWeb)
import { request } from '@shared/utils/request'
export const getWorkcaseList = () => request.get('/workcase/list')
// 目标代码 (pigx-ai-ui)
import request from '/@/utils/request'
export const getWorkcaseList = () => request.get('/app/workcase/list')
```
### 6. pigx-dify 模块设计(新增)
#### 6.1 模块定位
pigx-dify 是独立的 AI 服务模块,保留原 urbanLifeline 的 AI 功能和 Dify 平台集成,不与 pigx-knowledge 混合使用。
#### 6.2 核心组件
**DifyApiClient 保留原有功能:**
```java
@Component
public class DifyApiClient {
// 知识库管理
public DatasetCreateResponse createDataset(DatasetCreateRequest request);
public DatasetListResponse listDatasets(int page, int limit);
// 对话功能(保留流式和阻塞两种模式)
public void streamChat(ChatRequest request, String apiKey, StreamCallback callback);
public ChatResponse blockingChat(ChatRequest request, String apiKey);
// 工作流调用
public WorkflowRunResponse runWorkflowBlocking(WorkflowRunRequest request, String apiKey);
}
```
**数据模型保持不变:**
```java
// 智能体配置
@TableName("tb_agent")
public class TbAgent {
private String agentId;
private String name;
private String description;
private String difyApiKey; // 保留 Dify API Key
private String difyAgentId; // 保留 Dify Agent ID
private Long tenantId; // 新增租户ID
}
// 聊天会话
@TableName("tb_chat")
public class TbChat {
private String chatId;
private String agentId;
private String userId; // 关联 pigx sys_user
private String conversationId; // Dify conversation ID
private Long tenantId; // 新增租户ID
}
// 聊天消息(保持原有结构)
@TableName("tb_chat_message")
public class TbChatMessage {
private String messageId;
private String chatId;
private String content;
private String role; // user/ai/recipient
private String difyMessageId; // 保留 Dify 消息ID
}
```
#### 6.3 权限适配
```java
@RestController
@RequestMapping("/dify")
public class ChatController {
@Autowired
private DifyApiClient difyClient;
// 权限注解适配
@HasPermission("dify_chat_create") // 原: @PreAuthorize("hasAuthority('ai:chat:create')")
@PostMapping("/chat/stream")
public SseEmitter streamChat(@RequestBody ChatRequest request) {
PigxUser user = SecurityUtils.getUser();
// 保留原有的流式响应逻辑
return chatService.streamChat(request, user);
}
}
```
#### 6.4 配置管理
```yaml
# application.yml
dify:
api:
base-url: ${DIFY_API_BASE_URL:https://api.dify.ai}
default-api-key: ${DIFY_DEFAULT_API_KEY}
enabled: true
```
## Data Models
### 数据库迁移策略
1. **表结构转换**: PostgreSQL DDL → MySQL DDL
2. **添加租户字段**: 所有业务表添加 `tenant_id` 字段
3. **用户关联**: `user_id` 关联到 pigx 的 `sys_user.user_id`
### 核心业务表
**工单模块 (workcase)**:
```sql
CREATE TABLE tb_workcase (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
title VARCHAR(255) NOT NULL COMMENT '工单标题',
content TEXT COMMENT '工单内容',
status TINYINT DEFAULT 0 COMMENT '状态',
creator_id BIGINT COMMENT '创建人ID(关联sys_user)',
assignee_id BIGINT COMMENT '处理人ID(关联sys_user)',
tenant_id BIGINT DEFAULT 1 COMMENT '租户ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单表';
```
**招标模块 (bidding)**:
```sql
CREATE TABLE tb_bidding_project (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
project_name VARCHAR(255) NOT NULL COMMENT '项目名称',
project_code VARCHAR(64) COMMENT '项目编号',
status TINYINT DEFAULT 0 COMMENT '状态',
creator_id BIGINT COMMENT '创建人ID',
tenant_id BIGINT DEFAULT 1 COMMENT '租户ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招标项目表';
```
### 类型映射
| PostgreSQL | MySQL | 说明 |
|-----------|-------|------|
| SERIAL | INT AUTO_INCREMENT | 自增 |
| BIGSERIAL | BIGINT AUTO_INCREMENT | 大整数自增 |
| TEXT | TEXT | 文本 |
| JSONB | JSON | JSON数据 |
| BOOLEAN | TINYINT(1) | 布尔 |
| TIMESTAMP | DATETIME | 时间戳 |
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do.*
### Property 1: 后端代码迁移完整性
*For any* 源项目中的业务模块bidding, workcase, platform, message迁移后的 pigx-app-server 应该包含对应的 Controller、Service、Mapper 层代码,且代码可以通过编译。
**Validates: Requirements 1.1, 2.1, 3.1, 5.1**
### Property 2: 权限注解适配正确性
*For any* 迁移后的 Controller 方法,应该使用 pigx 的 @HasPermission 注解而非 @PreAuthorize,且权限标识符合 pigx 命名规范module_action 格式)。
**Validates: Requirements 1.2, 2.2, 11.1, 11.2**
### Property 3: 数据库迁移正确性
*For any* 源项目的 PostgreSQL 业务表,转换后的 MySQL DDL 应该:
- 语法正确,可在 MySQL 中执行
- 包含 tenant_id 租户字段
- 用户关联字段正确引用 sys_user.user_id
**Validates: Requirements 1.3, 2.4, 3.3, 4.4, 5.4, 8.1, 8.2, 8.3, 8.4**
### Property 4: 前端页面迁移完整性
*For any* 源项目前端包中的页面组件,迁移后应该存在于 pigx-ai-ui 的对应目录下,且组件可以被正确导入。
**Validates: Requirements 1.4, 2.5, 3.4, 4.5**
### Property 5: API调用适配正确性
*For any* 迁移后的前端 API 调用代码,应该:
- 使用 pigx 的 request 工具(从 /@/utils/request 导入)
- API 路径符合 pigx 网关路由规则(/app/* 前缀)
- 响应处理适配 R<T> 格式
**Validates: Requirements 1.5, 9.4**
### Property 6: 数据迁移完整性
*For any* 源数据库中的业务数据记录,迁移到目标数据库后,记录数量应该相等,关键字段值应该保持一致。
**Validates: Requirements 8.5**
### Property 8: 用户服务调用正确性
*For any* 迁移后的业务代码中涉及用户信息获取的地方,应该使用 SecurityUtils.getUser() 获取当前用户,或通过 RemoteUserService 进行 Feign 调用,而非原有的 UserService。
**Validates: Requirements 11.4, 11.5**
## Error Handling
### 迁移错误处理
1. **代码编译失败**: 记录编译错误,提供修复建议
2. **类型转换失败**: 标记不兼容类型,提供替代方案
3. **依赖缺失**: 自动添加缺失的 pigx 依赖
4. **数据迁移失败**: 支持断点续传,记录失败记录
### 回滚策略
- 代码迁移:保留源文件,支持回退
- 数据库迁移:生成回滚脚本
- 配置变更:版本化管理
## Testing Strategy
### 单元测试
- Service 层业务逻辑测试
- Mapper 层数据访问测试
- 工具类函数测试
### 集成测试
- API 端到端测试
- 用户认证流程测试
- 文件上传下载测试
### 属性测试
使用 Java 的 jqwik 进行属性测试:
```java
@Property
void tenantIsolation(@ForAll @From("businessEntity") BusinessEntity entity) {
// 验证查询结果只包含当前租户数据
List<BusinessEntity> results = service.list();
assertThat(results).allMatch(e -> e.getTenantId().equals(currentTenantId));
}
@Property
void ddlConversion(@ForAll @From("postgresTable") String pgDdl) {
String mysqlDdl = converter.convert(pgDdl);
assertThat(mysqlDdl).contains("tenant_id");
assertThat(mysqlDdl).canExecuteOnMysql();
}
```
### 测试配置
- 属性测试最少运行 100 次迭代
- 使用 Testcontainers 进行数据库测试
- 前端测试使用 Vitest
### Property 9: 租户隔离正确性
*For any* 迁移后的业务表查询,应该自动包含 tenant_id 条件,确保多租户数据隔离。
**Validates: Requirements 8.4, 11.6**

View File

@@ -0,0 +1,288 @@
# Workcase 前端迁移计划
## 一、依赖分析
### 1.1 对 shared 模块的依赖
#### API 依赖
- `shared/api` - 基础 axios 封装和 TokenManager
- `shared/api/file` - 文件上传下载 API (fileAPI)
- `shared/api/ai` - AI 相关 API (aiChatAPI, agentAPI, aiKnowledgeAPI)
#### 组件依赖
- `shared/components` - FileUpload, FileHistory, IframeView
- `shared/components/ai/knowledge` - DocumentSegment (知识库文档分段组件)
- `shared/layouts` - BlankLayout, SubSidebarLayout
#### 类型依赖
- `shared/types` - BaseDTO, BaseVO, ResultDomain, PageRequest, PageParam
- `shared/types` - TbSysFileDTO, MenuItem, TbSysViewDTO
- `shared/types` - AI 相关类型 (TbChat, TbKnowledge, TbKnowledgeFileLog, DifyFileInfo)
### 1.2 内部模块结构
#### Views 结构
```
views/
├── admin/ # 管理后台页面
│ ├── agent/ # AI 智能体管理 (→ dify 模块)
│ ├── customerChat/ # 客服对话管理
│ ├── knowledge/ # 知识库管理 (→ dify 模块)
│ ├── log/ # 日志管理
│ │ ├── knowledgeLog/ # 知识库日志 (→ dify 模块)
│ │ ├── systemLog/ # 系统日志
│ │ └── workcaseLog/ # 工单日志
│ ├── overview/ # 概览页面
│ └── workcase/ # 工单管理
└── public/ # 公共页面
├── AIChat/ # AI 对话 (→ dify 模块)
├── ChatRoom/ # 聊天室
├── JitsiMeeting/ # 视频会议
├── Login/ # 登录页 (pigx 已有)
└── workcase/ # 工单详情
```
#### Components 结构
```
components/
└── workcase/
└── WorkcaseAssign.vue # 工单指派组件
```
#### API 结构
```
api/
└── workcase/
├── workcase.ts # 工单 API
└── workcaseChat.ts # 聊天室 API
```
#### Types 结构
```
types/
└── workcase/
├── workcase.ts # 工单类型
├── chatRoom.ts # 聊天室类型
├── customer.ts # 客服类型
├── conversation.ts # 对话类型
└── wordCloud.ts # 词云类型
```
## 二、迁移策略
### 2.1 迁移顺序(按优先级)
#### 第一阶段:基础设施 (已完成)
- [x] API 定义迁移
- [x] 基础工单管理页面
#### 第二阶段:工单核心功能
1. **工单组件**
- WorkcaseAssign.vue (工单指派组件)
- WorkcaseDetail (工单详情组件)
2. **工单管理页面**
- admin/workcase/WorkcaseView.vue (工单列表)
- admin/overview/OverviewView.vue (概览页面)
- admin/log/workcaseLog/ (工单日志)
#### 第三阶段:聊天室功能
1. **聊天室核心**
- public/ChatRoom/chatRoom/ChatRoom.vue (聊天室主组件)
- public/ChatRoom/ChatMessage/ (消息组件)
- public/ChatRoom/ChatRoomView.vue (聊天室视图)
2. **客服管理**
- admin/customerChat/CustomerChatView.vue (客服对话管理)
3. **视频会议**
- public/JitsiMeeting/JitsiMeetingView.vue (视频会议)
- public/ChatRoom/MeetingCard/ (会议卡片)
- public/ChatRoom/MeetingCreate/ (创建会议)
#### 第四阶段AI 功能 (最后迁移,归入 dify 模块)
1. **AI 对话**
- public/AIChat/AIChatView.vue
- public/AIChat/components/
2. **智能体管理**
- admin/agent/AgentView.vue
3. **知识库管理**
- admin/knowledge/KnowLedgeView.vue
- admin/log/knowledgeLog/KnowledgeLogView.vue
### 2.2 共享依赖处理
#### pigx 已有的功能(直接使用)
- 登录认证 (Login)
- 用户管理
- 权限管理
- 文件上传下载 (Upload 组件)
- 基础布局 (Layout)
#### 需要适配的 shared 组件
1. **FileUpload** → 使用 pigx 的 `Upload/index.vue`
2. **FileHistory** → 需要迁移或使用 pigx 的文件管理
3. **IframeView** → 简单组件,可直接迁移
4. **DocumentSegment** → AI 知识库专用,归入 dify 模块
#### 需要适配的 API
1. **fileAPI** → 适配 pigx 的文件服务 API
2. **aiChatAPI, agentAPI, aiKnowledgeAPI** → 归入 dify 模块
### 2.3 目录结构映射
#### 源目录 → 目标目录
```
urbanLifelineWeb/packages/workcase/
├── src/api/workcase/ → pigx-ai-ui/src/api/workcase/
├── src/components/workcase/ → pigx-ai-ui/src/components/workcase/
├── src/views/admin/workcase/ → pigx-ai-ui/src/views/workcase/admin/
├── src/views/admin/customerChat/ → pigx-ai-ui/src/views/workcase/customerChat/
├── src/views/admin/overview/ → pigx-ai-ui/src/views/workcase/overview/
├── src/views/admin/log/ → pigx-ai-ui/src/views/workcase/log/
├── src/views/public/ChatRoom/ → pigx-ai-ui/src/views/workcase/chatRoom/
├── src/views/public/JitsiMeeting/ → pigx-ai-ui/src/views/workcase/meeting/
├── src/views/public/workcase/ → pigx-ai-ui/src/views/workcase/detail/
└── src/types/workcase/ → pigx-ai-ui/src/types/workcase/
# AI 相关 (归入 dify 模块)
├── src/views/admin/agent/ → pigx-ai-ui/src/views/dify/agent/
├── src/views/admin/knowledge/ → pigx-ai-ui/src/views/dify/knowledge/
├── src/views/public/AIChat/ → pigx-ai-ui/src/views/dify/chat/
└── src/views/admin/log/knowledgeLog/ → pigx-ai-ui/src/views/dify/log/
```
## 三、技术适配要点
### 3.1 API 调用适配
```typescript
// 源代码
import { api } from 'shared/api'
const res = await api.post('/urban-lifeline/workcase', data)
// 目标代码
import request from '@/utils/request'
const res = await request({
url: '/workcase/workcase',
method: 'post',
data
})
```
### 3.2 响应格式适配
```typescript
// 源代码 (ResultDomain)
interface ResultDomain<T> {
code: number
message: string
success: boolean
data?: T
dataList?: T[]
pageDomain?: PageDomain<T>
}
// 目标代码 (pigx R<T>)
// pigx 使用 code === 0 表示成功
if (res.code === 0) {
// res.data 包含数据
}
```
### 3.3 文件上传适配
```typescript
// 源代码
import { fileAPI } from 'shared/api/file'
await fileAPI.uploadFile({ file, module, optsn })
// 目标代码
import { uploadFile } from '@/api/admin/file'
await uploadFile(formData)
```
### 3.4 WebSocket 适配
```typescript
// 源代码 (SockJS + STOMP)
const wsUrl = `${protocol}//${host}/${API_BASE_URL}/urban-lifeline/workcase/ws/chat-sockjs`
stompClient = new Client({
webSocketFactory: () => new SockJS(wsUrl)
})
// 目标代码 (需要适配 pigx 的 WebSocket 配置)
// pigx 可能使用不同的 WebSocket 实现
```
### 3.5 组件库适配
- Element Plus 版本可能不同,需要检查 API 变化
- 图标库:源代码使用 lucide-vue-nextpigx 使用 Element Plus Icons
- 需要统一图标使用方式
## 四、迁移检查清单
### 4.1 功能完整性
- [ ] 工单 CRUD
- [ ] 工单指派/转派
- [ ] 工单流程记录
- [ ] 聊天室功能
- [ ] 实时消息推送 (WebSocket)
- [ ] 视频会议集成
- [ ] 客服管理
- [ ] 文件上传下载
- [ ] 词云统计
### 4.2 权限控制
- [ ] 页面访问权限
- [ ] 按钮操作权限
- [ ] 数据权限(租户隔离)
### 4.3 用户体验
- [ ] 响应式布局
- [ ] 加载状态
- [ ] 错误提示
- [ ] 空状态展示
- [ ] 分页功能
### 4.4 性能优化
- [ ] 列表虚拟滚动(如需要)
- [ ] 图片懒加载
- [ ] 防抖节流
- [ ] 请求缓存
## 五、注意事项
### 5.1 不要迁移的内容
- Login 页面pigx 已有完整的登录系统)
- 用户管理相关页面(使用 pigx 原生功能)
- 权限管理相关页面(使用 pigx 原生功能)
### 5.2 需要重点测试的功能
- WebSocket 实时通信
- 文件上传下载
- 视频会议集成
- 多租户数据隔离
- 权限控制
### 5.3 AI 功能迁移注意
- AI 对话、智能体、知识库功能最后迁移
- 这些功能归入 dify 模块,不放在 workcase 模块
- 需要与 Dify API 集成测试
## 六、当前进度
### 已完成
- [x] API 定义 (workcase.ts, chat.ts)
- [x] 类型定义 (workcase.ts, chatRoom.ts, customer.ts, conversation.ts, wordCloud.ts)
- [x] 基础工单列表页面 (index.vue)
- [x] 工单指派组件 (WorkcaseAssign.vue)
- [x] 工单详情组件 (WorkcaseDetail.vue)
- [x] 聊天室消息组件 (ChatMessage.vue)
### 进行中
- [ ] 管理后台页面 (overview, customerChat, log)
### 待开始
- [ ] 视频会议功能
- [ ] 客服管理
- [ ] 日志管理
- [ ] AI 功能(最后)

View File

@@ -0,0 +1,391 @@
# 权限注解转换指南
## 概述
本指南详细说明了如何将 urbanLifelineServ 的权限注解迁移到 pigx 平台的权限体系。
## 核心变更
### 1. 权限注解格式
| 特性 | urbanLifelineServ | pigx |
|------|------------------|------|
| 注解类 | @PreAuthorize | @PreAuthorize |
| 权限判断方法 | hasAuthority() | @pms.hasPermission() |
| 权限标识格式 | module:resource:action | module_resource_action |
| 分隔符 | 冒号 `:` | 下划线 `_` |
### 2. 用户信息获取
| 功能 | urbanLifelineServ | pigx |
|------|------------------|------|
| 获取用户ID | JwtUtils.getUserId() | SecurityUtils.getUser().getId() |
| 获取用户名 | JwtUtils.getUsername() | SecurityUtils.getUser().getUsername() |
| 获取租户ID | 不支持 | SecurityUtils.getUser().getTenantId() |
| 获取部门ID | 不支持 | SecurityUtils.getUser().getDeptId() |
### 3. 响应格式
| 特性 | urbanLifelineServ | pigx |
|------|------------------|------|
| 响应类 | ResultDomain<T> | R<T> |
| 成功响应 | ResultDomain.success(data) | R.ok(data) |
| 失败响应 | ResultDomain.fail(msg) | R.failed(msg) |
| 列表字段 | dataList | data |
## 转换步骤
### 步骤1权限注解转换
#### 1.1 基本转换
```java
// 转换前
@PreAuthorize("hasAuthority('workcase:ticket:create')")
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return workcaseService.createWorkcase(workcase);
}
// 转换后
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return R.ok(workcaseService.createWorkcase(workcase));
}
```
#### 1.2 多权限判断
```java
// 转换前
@PreAuthorize("hasAuthority('workcase:ticket:view') or hasAuthority('workcase:ticket:admin')")
public ResultDomain<List<TbWorkcaseDTO>> listWorkcase() {
return ResultDomain.success(workcaseService.list());
}
// 转换后
@PreAuthorize("@pms.hasPermission('workcase_ticket_view') or @pms.hasPermission('workcase_ticket_admin')")
public R<List<TbWorkcaseDTO>> listWorkcase() {
return R.ok(workcaseService.list());
}
```
### 步骤2用户信息获取转换
#### 2.1 Service层用户信息获取
```java
// 转换前
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
public TbWorkcaseDTO createWorkcase(TbWorkcaseDTO workcase) {
Long userId = JwtUtils.getUserId();
String username = JwtUtils.getUsername();
workcase.setCreateBy(userId);
workcase.setCreateByName(username);
workcase.setCreateTime(new Date());
return workcaseMapper.insert(workcase);
}
}
// 转换后
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
public TbWorkcaseDTO createWorkcase(TbWorkcaseDTO workcase) {
PigxUser user = SecurityUtils.getUser();
workcase.setCreateBy(user.getId());
workcase.setCreateByName(user.getUsername());
workcase.setTenantId(user.getTenantId()); // 新增:租户隔离
workcase.setDeptId(user.getDeptId()); // 新增:部门信息
workcase.setCreateTime(LocalDateTime.now());
return workcaseMapper.insert(workcase);
}
}
```
#### 2.2 远程用户服务调用
```java
// 转换前
@Service
public class WorkcaseServiceImpl {
@Autowired
private UserService userService;
public void assignWorkcase(String workcaseId, Long assigneeId) {
User assignee = userService.getById(assigneeId);
if (assignee == null) {
throw new BusinessException("用户不存在");
}
// 处理逻辑...
}
}
// 转换后
@Service
public class WorkcaseServiceImpl {
@Autowired
private RemoteUserService remoteUserService;
public void assignWorkcase(String workcaseId, Long assigneeId) {
R<SysUser> result = remoteUserService.selectById(assigneeId);
if (!result.isSuccess() || result.getData() == null) {
throw new BusinessException("用户不存在");
}
SysUser assignee = result.getData();
// 处理逻辑...
}
}
```
### 步骤3响应格式转换
#### 3.1 Controller响应转换
```java
// 转换前
@RestController
@RequestMapping("/api/workcase")
public class WorkcaseController {
// 单个对象返回
@GetMapping("/{id}")
public ResultDomain<TbWorkcaseDTO> getById(@PathVariable String id) {
TbWorkcaseDTO workcase = workcaseService.getById(id);
if (workcase == null) {
return ResultDomain.fail("工单不存在");
}
return ResultDomain.success(workcase);
}
// 列表返回
@GetMapping("/list")
public ResultDomain<List<TbWorkcaseDTO>> list() {
List<TbWorkcaseDTO> list = workcaseService.list();
ResultDomain<List<TbWorkcaseDTO>> result = ResultDomain.success();
result.setDataList(list); // 注意使用dataList字段
return result;
}
}
// 转换后
@RestController
@RequestMapping("/workcase")
public class WorkcaseController {
// 单个对象返回
@GetMapping("/{id}")
public R<TbWorkcaseDTO> getById(@PathVariable String id) {
TbWorkcaseDTO workcase = workcaseService.getById(id);
if (workcase == null) {
return R.failed("工单不存在");
}
return R.ok(workcase);
}
// 列表返回
@GetMapping("/list")
public R<List<TbWorkcaseDTO>> list() {
List<TbWorkcaseDTO> list = workcaseService.list();
return R.ok(list); // 直接返回列表不使用dataList
}
// 分页返回
@GetMapping("/page")
public R<IPage<TbWorkcaseDTO>> page(Page page) {
return R.ok(workcaseService.page(page));
}
}
```
### 步骤4数据权限适配
#### 4.1 添加租户隔离
```java
// 转换前 - 无租户隔离
@Service
public class WorkcaseServiceImpl {
public List<TbWorkcaseDTO> listMyWorkcase() {
Long userId = JwtUtils.getUserId();
return workcaseMapper.selectByUserId(userId);
}
}
// 转换后 - 支持租户隔离
@Service
public class WorkcaseServiceImpl {
public List<TbWorkcaseDTO> listMyWorkcase() {
PigxUser user = SecurityUtils.getUser();
QueryWrapper<TbWorkcaseDTO> wrapper = new QueryWrapper<>();
wrapper.eq("tenant_id", user.getTenantId()) // 租户隔离
.eq("create_by", user.getId());
return workcaseMapper.selectList(wrapper);
}
}
```
## 批量转换工具
### 使用IDE批量替换
#### IntelliJ IDEA 正则替换
1. **查找模式** (启用正则表达式):
```regex
@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\)
```
2. **替换为**:
```regex
@PreAuthorize("@pms.hasPermission('$1_$2_$3')")
```
3. **动作映射** (执行第二次替换):
- 查找: `_create'` 替换为: `_add'`
- 查找: `_update'` 替换为: `_edit'`
- 查找: `_delete'` 替换为: `_del'`
### 命令行批量转换脚本
```bash
#!/bin/bash
# convert-permissions.sh
# 查找所有Java文件并转换权限注解
find . -name "*.java" -type f -exec sed -i.bak \
-e "s/@PreAuthorize(\"hasAuthority('\([^:]*\):\([^:]*\):\([^']*\)')\")/@PreAuthorize(\"@pms.hasPermission('\1_\2_\3')\")/g" \
-e "s/_create')/_add')/g" \
-e "s/_update')/_edit')/g" \
-e "s/_delete')/_del')/g" {} \;
# 转换JwtUtils为SecurityUtils
find . -name "*.java" -type f -exec sed -i.bak \
-e "s/JwtUtils.getUserId()/SecurityUtils.getUser().getId()/g" \
-e "s/JwtUtils.getUsername()/SecurityUtils.getUser().getUsername()/g" {} \;
# 转换ResultDomain为R
find . -name "*.java" -type f -exec sed -i.bak \
-e "s/ResultDomain.success(/R.ok(/g" \
-e "s/ResultDomain.fail(/R.failed(/g" \
-e "s/ResultDomain</R</g" {} \;
```
## 需要手工处理的情况
### 1. 复杂的权限逻辑
```java
// 需要手工审查的复杂权限
@PreAuthorize("hasAuthority('workcase:ticket:view') and #workcase.createBy == authentication.principal.userId")
public ResultDomain<TbWorkcaseDTO> getMyWorkcase(@PathVariable String id, TbWorkcaseDTO workcase) {
// 这种情况需要根据pigx的数据权限机制重新设计
}
```
### 2. 自定义权限判断
```java
// 转换前
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities()
.contains(new SimpleGrantedAuthority("workcase:ticket:admin"))) {
// 管理员逻辑
}
// 转换后
if (SecurityUtils.getUser().getAuthorities()
.contains("workcase_ticket_admin")) {
// 管理员逻辑
}
```
### 3. 异步任务中的用户信息
```java
// 转换前
@Async
public void processAsync() {
Long userId = JwtUtils.getUserId(); // 异步线程中可能获取不到
}
// 转换后
@Async
public void processAsync() {
// 需要在调用异步方法前获取用户信息并传递
PigxUser user = SecurityUtils.getUser();
processAsyncWithUser(user);
}
```
## 测试验证清单
### 权限测试
- [ ] 所有 @PreAuthorize 注解已转换为 @pms.hasPermission 格式
- [ ] 权限标识符已从冒号改为下划线
- [ ] 动作已正确映射 (create→add, update→edit, delete→del)
- [ ] 多权限判断逻辑正确
### 用户信息测试
- [ ] SecurityUtils.getUser() 能正确获取用户信息
- [ ] 租户ID正确设置到业务数据
- [ ] RemoteUserService 调用正常
### 响应格式测试
- [ ] 所有接口返回 R<T> 格式
- [ ] 前端能正确解析新的响应格式
- [ ] 错误信息正确传递
### 数据权限测试
- [ ] 租户数据隔离正常
- [ ] 部门数据权限正常
- [ ] 个人数据权限正常
## 常见问题
### Q1: @pms.hasPermission 中的 @pms 是什么?
A: `@pms` 是 pigx 权限管理系统的 SpEL 表达式前缀,用于调用权限判断方法。这是 pigx 框架的固定写法,必须保留。
### Q2: 为什么要将 create 改为 add
A: 这是 pigx 平台的命名规范,保持统一的动作命名有助于权限管理的标准化。常见映射:
- create → add (新增)
- update → edit (编辑)
- delete → del (删除)
- view → view (查看)
### Q3: 如何处理没有对应 pigx 用户的情况?
A: 所有业务用户必须在 pigx 的 sys_user 表中存在。如果是数据迁移,需要先创建对应的 pigx 用户,或建立用户映射关系。
### Q4: 租户ID是必须的吗
A: 是的。pigx 是多租户系统,所有业务表都需要 tenant_id 字段。即使是单租户使用也需要设置默认租户ID通常为1
### Q5: 如何调试权限问题?
A: 可以通过以下方式调试:
1. 查看 pigx 日志中的权限判断记录
2. 使用 SecurityUtils.getUser().getAuthorities() 查看当前用户权限
3. 检查 sys_menu 表中的权限配置
4. 验证 sys_role_menu 表中的角色权限关联
## 相关文档
- [权限标识映射表](./permission-mapping.md)
- [数据库迁移指南](./database-migration.md)
- [前端适配指南](./frontend-migration.md)

View File

@@ -0,0 +1,490 @@
# 权限注解转换指南
## 目标
将 urbanLifeline 的权限体系完全迁移到 pigx 平台的权限模型。
## 核心概念对比
| 特性 | urbanLifeline | pigx |
|------|---------------|------|
| 权限注解 | `@PreAuthorize("hasAuthority()")` | `@PreAuthorize("@pms.hasPermission()")` |
| 权限格式 | `module:resource:action` | `module_resource_action` |
| 用户获取 | `JwtUtils.getUserId()` | `SecurityUtils.getUser()` |
| 用户服务 | `UserService` (本地) | `RemoteUserService` (Feign) |
| 响应格式 | `ResultDomain<T>` | `R<T>` |
| 租户支持 | 无 | 有 (tenant_id) |
## 转换步骤详解
### 步骤 1权限注解转换
#### 1.1 基本转换规则
```java
// ❌ 旧代码 (urbanLifeline)
@PreAuthorize("hasAuthority('workcase:ticket:create')")
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO dto) {
// ...
}
// ✅ 新代码 (pigx)
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO dto) {
// ...
}
```
#### 1.2 多权限组合
```java
// ❌ 旧代码 - OR 条件
@PreAuthorize("hasAuthority('workcase:ticket:update') or hasAuthority('workcase:ticket:admin')")
// ✅ 新代码 - OR 条件
@PreAuthorize("@pms.hasPermission('workcase_ticket_edit') or @pms.hasPermission('workcase_ticket_admin')")
// ❌ 旧代码 - AND 条件
@PreAuthorize("hasAuthority('workcase:ticket:view') and hasAuthority('workcase:export:data')")
// ✅ 新代码 - AND 条件
@PreAuthorize("@pms.hasPermission('workcase_ticket_view') and @pms.hasPermission('workcase_export_data')")
```
#### 1.3 动态权限检查
```java
// ❌ 旧代码
@Service
public class WorkcaseService {
public boolean canEdit(Long workcaseId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("workcase:ticket:update"));
}
}
// ✅ 新代码
@Service
public class WorkcaseService {
@Autowired
private PermissionService permissionService;
public boolean canEdit(Long workcaseId) {
return permissionService.hasPermission("workcase_ticket_edit");
}
}
```
### 步骤 2用户信息获取转换
#### 2.1 获取当前用户
```java
// ❌ 旧代码
Long userId = JwtUtils.getUserId();
String username = JwtUtils.getUsername();
String role = JwtUtils.getRole();
// ✅ 新代码
PigxUser user = SecurityUtils.getUser();
Long userId = user.getId();
String username = user.getUsername();
Long tenantId = user.getTenantId(); // 新增租户ID
Long deptId = user.getDeptId(); // 新增部门ID
List<String> roles = user.getRoles(); // 角色列表
```
#### 2.2 Service 层用户信息处理
```java
// ❌ 旧代码
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
@Autowired
private UserService userService;
public void assignWorkcase(Long workcaseId, Long assigneeId) {
User assignee = userService.getById(assigneeId);
// 处理逻辑...
}
}
// ✅ 新代码
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
@Autowired
private RemoteUserService remoteUserService;
public void assignWorkcase(Long workcaseId, Long assigneeId) {
// 使用 Feign 远程调用
R<SysUser> result = remoteUserService.selectById(assigneeId);
if (result.isSuccess() && result.getData() != null) {
SysUser assignee = result.getData();
// 处理逻辑...
} else {
throw new BusinessException("用户不存在");
}
}
}
```
### 步骤 3响应格式转换
#### 3.1 成功响应
```java
// ❌ 旧代码
return ResultDomain.success(data);
return ResultDomain.success(list, total);
return ResultDomain.success("操作成功", data);
// ✅ 新代码
return R.ok(data);
return R.ok(list, total); // 分页响应
return R.ok(data, "操作成功");
```
#### 3.2 错误响应
```java
// ❌ 旧代码
return ResultDomain.failure("参数错误");
return ResultDomain.failure(ErrorCode.INVALID_PARAM);
throw new BusinessException("业务异常");
// ✅ 新代码
return R.failed("参数错误");
return R.failed(CommonConstants.FAIL, "参数错误");
throw new ServiceException("业务异常");
```
#### 3.3 分页响应
```java
// ❌ 旧代码
public ResultDomain<List<TbWorkcaseDTO>> list(PageParam param) {
Page<TbWorkcase> page = workcaseMapper.selectPage(param);
return ResultDomain.success(page.getRecords(), page.getTotal());
}
// ✅ 新代码
public R<IPage<TbWorkcaseDTO>> list(Page page, TbWorkcaseDTO query) {
IPage<TbWorkcaseDTO> result = workcaseMapper.selectPageVo(page, query);
return R.ok(result);
}
```
### 步骤 4租户隔离实现
#### 4.1 实体类添加租户字段
```java
@Data
@TableName("tb_workcase")
public class TbWorkcase {
@TableId
private Long id;
private String title;
// ✅ 新增租户字段
@TableField("tenant_id")
private Long tenantId;
// 其他字段...
}
```
#### 4.2 Service 层自动注入租户
```java
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
@Override
public R<TbWorkcaseDTO> save(TbWorkcaseDTO dto) {
// ✅ 自动注入当前租户
PigxUser user = SecurityUtils.getUser();
dto.setTenantId(user.getTenantId());
dto.setCreateBy(user.getUsername());
dto.setCreateTime(LocalDateTime.now());
workcaseMapper.insert(dto);
return R.ok(dto);
}
@Override
public R<IPage<TbWorkcaseDTO>> page(Page page, TbWorkcaseDTO query) {
// ✅ 查询条件自动添加租户过滤
PigxUser user = SecurityUtils.getUser();
query.setTenantId(user.getTenantId());
return R.ok(workcaseMapper.selectPageVo(page, query));
}
}
```
#### 4.3 Mapper 层租户隔离
```xml
<!-- WorkcaseMapper.xml -->
<select id="selectPageVo" resultType="com.pig4cloud.pigx.app.api.dto.TbWorkcaseDTO">
SELECT * FROM tb_workcase
<where>
<!-- ✅ 租户隔离条件 -->
<if test="query.tenantId != null">
AND tenant_id = #{query.tenantId}
</if>
<if test="query.title != null and query.title != ''">
AND title LIKE CONCAT('%', #{query.title}, '%')
</if>
</where>
ORDER BY create_time DESC
</select>
```
### 步骤 5配置 RemoteUserService
#### 5.1 添加 Feign 客户端接口
```java
package com.pig4cloud.pigx.app.api.feign;
import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.upms.api.entity.SysUser;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(contextId = "remoteUserService",
value = ServiceNameConstants.UPMS_SERVICE)
public interface RemoteUserService {
/**
* 根据用户ID查询用户信息
*/
@GetMapping("/user/info/{id}")
R<SysUser> selectById(@PathVariable("id") Long id);
/**
* 根据用户名查询用户信息
*/
@GetMapping("/user/info")
R<SysUser> selectByUsername(@RequestParam("username") String username);
/**
* 批量查询用户信息
*/
@PostMapping("/user/list")
R<List<SysUser>> selectBatchIds(@RequestBody List<Long> ids);
}
```
#### 5.2 使用 RemoteUserService
```java
@Service
@RequiredArgsConstructor
public class WorkcaseServiceImpl implements WorkcaseService {
private final RemoteUserService remoteUserService;
public TbWorkcaseDTO getWorkcaseDetail(Long id) {
TbWorkcase workcase = workcaseMapper.selectById(id);
TbWorkcaseDTO dto = BeanUtil.copyProperties(workcase, TbWorkcaseDTO.class);
// 获取创建人信息
if (dto.getCreatorId() != null) {
R<SysUser> creatorResult = remoteUserService.selectById(dto.getCreatorId());
if (creatorResult.isSuccess() && creatorResult.getData() != null) {
dto.setCreatorName(creatorResult.getData().getUsername());
dto.setCreatorDeptName(creatorResult.getData().getDeptName());
}
}
// 获取处理人信息
if (dto.getAssigneeId() != null) {
R<SysUser> assigneeResult = remoteUserService.selectById(dto.getAssigneeId());
if (assigneeResult.isSuccess() && assigneeResult.getData() != null) {
dto.setAssigneeName(assigneeResult.getData().getUsername());
}
}
return dto;
}
}
```
## 批量转换工具
### 使用 IDE 批量替换(推荐)
#### IntelliJ IDEA
1. **权限注解替换**
- 查找:`@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\)`
- 替换:`@PreAuthorize("@pms.hasPermission('$1_$2_$3')")`
- 选项:勾选 "Regex"
2. **响应格式替换**
- 查找:`ResultDomain\.success\((.*?)\)`
- 替换:`R.ok($1)`
3. **用户信息获取**
- 查找:`JwtUtils\.getUserId\(\)`
- 替换:`SecurityUtils.getUser().getId()`
#### VS Code
使用 Find and Replace (Ctrl+Shift+H),启用正则表达式模式。
### 使用脚本批量转换
创建 `convert-permissions.sh`
```bash
#!/bin/bash
# 转换权限注解
find ./src -name "*.java" -type f -exec sed -i \
's/@PreAuthorize("hasAuthority('\''\\([^:]*\\):\\([^:]*\\):\\([^'\'']*\\)'\''")"/@PreAuthorize("@pms.hasPermission('\''\\1_\\2_\\3'\'')"/g' {} \;
# 转换响应格式
find ./src -name "*.java" -type f -exec sed -i \
's/ResultDomain\.success(\(.*\))/R.ok(\1)/g' {} \;
# 转换用户信息获取
find ./src -name "*.java" -type f -exec sed -i \
's/JwtUtils\.getUserId()/SecurityUtils.getUser().getId()/g' {} \;
echo "转换完成!"
```
## 测试验证
### 1. 单元测试示例
```java
@SpringBootTest
@AutoConfigureMockMvc
public class WorkcaseControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "admin", authorities = {"workcase_ticket_add"})
public void testCreateWorkcase() throws Exception {
TbWorkcaseDTO dto = new TbWorkcaseDTO();
dto.setTitle("测试工单");
mockMvc.perform(post("/workcase")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.title").value("测试工单"));
}
@Test
@WithMockUser(username = "user", authorities = {})
public void testCreateWorkcaseNoPermission() throws Exception {
mockMvc.perform(post("/workcase")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isForbidden());
}
}
```
### 2. 集成测试检查清单
- [ ] 权限注解正确转换
- [ ] 用户信息正确获取
- [ ] 租户数据正确隔离
- [ ] 响应格式符合规范
- [ ] RemoteUserService 调用成功
- [ ] 菜单权限正确配置
- [ ] 角色权限正确分配
## 常见问题解决
### Q1: @pms.hasPermission() 不生效
**原因**:没有正确配置 PermissionService Bean
**解决**
```java
@Configuration
public class SecurityConfig {
@Bean("pms")
public PermissionService permissionService() {
return new PermissionService();
}
}
```
### Q2: RemoteUserService 调用失败
**原因**Feign 客户端未正确配置
**解决**
1. 检查 `@EnableFeignClients` 注解
2. 确认服务名称正确
3. 添加熔断处理
```java
@FeignClient(contextId = "remoteUserService",
value = ServiceNameConstants.UPMS_SERVICE,
fallback = RemoteUserServiceFallback.class)
```
### Q3: 租户数据泄露
**原因**:查询时未添加租户过滤
**解决**
1. 使用 MyBatis-Plus 租户插件
2. 手动添加租户条件
```java
@Configuration
public class MybatisPlusConfig {
@Bean
public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
return new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
PigxUser user = SecurityUtils.getUser();
return new LongValue(user.getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
});
}
}
```
## 迁移验证
完成转换后,执行以下验证:
1. **编译检查**:确保所有代码编译通过
2. **启动检查**:应用能正常启动
3. **权限测试**:各接口权限控制正确
4. **数据隔离**:租户数据正确隔离
5. **功能测试**:业务功能正常运行
## 总结
权限迁移是整个系统迁移的核心部分,需要:
1. 仔细转换每个权限注解
2. 正确处理用户信息获取
3. 实现租户数据隔离
4. 充分测试验证
建议分模块逐步迁移,每完成一个模块就进行测试验证。

View File

@@ -0,0 +1,253 @@
# 权限标识映射表
## 概述
本文档定义了从 urbanLifelineServ 权限标识到 pigx 权限标识的映射规则。
## 映射规则
### 格式转换规则
- **源格式**: `module:resource:action` (使用冒号分隔)
- **目标格式**: `module_resource_action` (使用下划线分隔)
- **动作映射**:
- `create``add`
- `update``edit`
- `delete``del`
- `view``view`
- 其他保持不变
### 权限注解转换
- **源注解**: `@PreAuthorize("hasAuthority('module:resource:action')")`
- **目标注解**: `@PreAuthorize("@pms.hasPermission('module_resource_action')")`
## 权限映射表
### 工单模块 (workcase)
| 源权限标识 | 目标权限标识 | 说明 | 菜单类型 |
|-----------|-------------|------|----------|
| workcase:ticket:create | workcase_ticket_add | 创建工单 | 按钮 |
| workcase:ticket:update | workcase_ticket_edit | 更新工单 | 按钮 |
| workcase:ticket:view | workcase_ticket_view | 查看工单 | 菜单 |
| workcase:ticket:delete | workcase_ticket_del | 删除工单 | 按钮 |
| workcase:ticket:process | workcase_ticket_process | 处理工单 | 按钮 |
| workcase:ticket:device | workcase_ticket_device | 工单设备管理 | 按钮 |
| workcase:room:create | workcase_room_add | 创建聊天室 | 按钮 |
| workcase:room:update | workcase_room_edit | 更新聊天室 | 按钮 |
| workcase:room:close | workcase_room_close | 关闭聊天室 | 按钮 |
| workcase:room:view | workcase_room_view | 查看聊天室 | 菜单 |
### AI模块 (dify)
| 源权限标识 | 目标权限标识 | 说明 | 菜单类型 |
|-----------|-------------|------|----------|
| ai:agent:create | dify_agent_add | 创建智能体 | 按钮 |
| ai:agent:update | dify_agent_edit | 更新智能体 | 按钮 |
| ai:agent:delete | dify_agent_del | 删除智能体 | 按钮 |
| ai:agent:view | dify_agent_view | 查看智能体 | 菜单 |
| ai:knowledge:create | dify_knowledge_add | 创建知识库 | 按钮 |
| ai:knowledge:update | dify_knowledge_edit | 更新知识库 | 按钮 |
| ai:knowledge:delete | dify_knowledge_del | 删除知识库 | 按钮 |
| ai:knowledge:view | dify_knowledge_view | 查看知识库 | 菜单 |
| ai:knowledge:file:view | dify_knowledge_file_view | 查看知识库文件 | 按钮 |
| ai:knowledge:file:upload | dify_knowledge_file_upload | 上传知识库文件 | 按钮 |
| ai:knowledge:file:update | dify_knowledge_file_edit | 更新知识库文件 | 按钮 |
| ai:knowledge:file:delete | dify_knowledge_file_del | 删除知识库文件 | 按钮 |
| ai:dify:segment:view | dify_segment_view | 查看文档片段 | 按钮 |
| ai:dify:segment:create | dify_segment_add | 创建文档片段 | 按钮 |
| ai:dify:segment:update | dify_segment_edit | 更新文档片段 | 按钮 |
| ai:dify:segment:delete | dify_segment_del | 删除文档片段 | 按钮 |
| ai:dify:document:status | dify_document_status | 查看文档状态 | 按钮 |
| ai:chat:create | dify_chat_add | 创建对话 | 按钮 |
| ai:chat:view | dify_chat_view | 查看对话 | 菜单 |
| ai:chat:message | dify_chat_message | 发送消息 | 按钮 |
### 招标模块 (bidding)
| 源权限标识 | 目标权限标识 | 说明 | 菜单类型 |
|-----------|-------------|------|----------|
| bidding:project:create | bidding_project_add | 创建招标项目 | 按钮 |
| bidding:project:update | bidding_project_edit | 更新招标项目 | 按钮 |
| bidding:project:delete | bidding_project_del | 删除招标项目 | 按钮 |
| bidding:project:view | bidding_project_view | 查看招标项目 | 菜单 |
| bidding:bid:create | bidding_bid_add | 创建投标 | 按钮 |
| bidding:bid:update | bidding_bid_edit | 更新投标 | 按钮 |
| bidding:bid:view | bidding_bid_view | 查看投标 | 菜单 |
| bidding:document:view | bidding_document_view | 查看招标文件 | 按钮 |
| bidding:document:upload | bidding_document_upload | 上传招标文件 | 按钮 |
### 平台管理模块 (platform)
| 源权限标识 | 目标权限标识 | 说明 | 菜单类型 |
|-----------|-------------|------|----------|
| platform:config:view | platform_config_view | 查看配置 | 菜单 |
| platform:config:update | platform_config_edit | 更新配置 | 按钮 |
| platform:log:view | platform_log_view | 查看日志 | 菜单 |
| platform:monitor:view | platform_monitor_view | 查看监控 | 菜单 |
| platform:stat:view | platform_stat_view | 查看统计 | 菜单 |
### 消息模块 (message)
| 源权限标识 | 目标权限标识 | 说明 | 菜单类型 |
|-----------|-------------|------|----------|
| message:notification:create | message_notification_add | 创建通知 | 按钮 |
| message:notification:view | message_notification_view | 查看通知 | 菜单 |
| message:notification:send | message_notification_send | 发送通知 | 按钮 |
| message:template:create | message_template_add | 创建消息模板 | 按钮 |
| message:template:update | message_template_edit | 更新消息模板 | 按钮 |
| message:template:delete | message_template_del | 删除消息模板 | 按钮 |
| message:template:view | message_template_view | 查看消息模板 | 菜单 |
## 菜单配置SQL示例
```sql
-- 工单管理菜单
INSERT INTO sys_menu (menu_id, name, permission, path, parent_id, icon, sort, type, tenant_id) VALUES
(10000, '工单管理', NULL, '/workcase', 0, 'el-icon-tickets', 1, '0', 1),
(10001, '工单列表', 'workcase_ticket_view', '/workcase/list', 10000, '', 1, '1', 1),
(10002, '创建工单', 'workcase_ticket_add', NULL, 10001, '', 1, '2', 1),
(10003, '编辑工单', 'workcase_ticket_edit', NULL, 10001, '', 2, '2', 1),
(10004, '删除工单', 'workcase_ticket_del', NULL, 10001, '', 3, '2', 1),
(10005, '处理工单', 'workcase_ticket_process', NULL, 10001, '', 4, '2', 1),
(10006, '设备管理', 'workcase_ticket_device', NULL, 10001, '', 5, '2', 1),
(10010, '聊天室', 'workcase_room_view', '/workcase/room', 10000, '', 2, '1', 1),
(10011, '创建聊天室', 'workcase_room_add', NULL, 10010, '', 1, '2', 1),
(10012, '编辑聊天室', 'workcase_room_edit', NULL, 10010, '', 2, '2', 1),
(10013, '关闭聊天室', 'workcase_room_close', NULL, 10010, '', 3, '2', 1);
-- AI管理菜单Dify
INSERT INTO sys_menu (menu_id, name, permission, path, parent_id, icon, sort, type, tenant_id) VALUES
(11000, 'AI管理', NULL, '/dify', 0, 'el-icon-cpu', 2, '0', 1),
(11001, '智能体管理', 'dify_agent_view', '/dify/agent', 11000, '', 1, '1', 1),
(11002, '创建智能体', 'dify_agent_add', NULL, 11001, '', 1, '2', 1),
(11003, '编辑智能体', 'dify_agent_edit', NULL, 11001, '', 2, '2', 1),
(11004, '删除智能体', 'dify_agent_del', NULL, 11001, '', 3, '2', 1),
(11010, '知识库管理', 'dify_knowledge_view', '/dify/knowledge', 11000, '', 2, '1', 1),
(11011, '创建知识库', 'dify_knowledge_add', NULL, 11010, '', 1, '2', 1),
(11012, '编辑知识库', 'dify_knowledge_edit', NULL, 11010, '', 2, '2', 1),
(11013, '删除知识库', 'dify_knowledge_del', NULL, 11010, '', 3, '2', 1),
(11014, '上传文件', 'dify_knowledge_file_upload', NULL, 11010, '', 4, '2', 1),
(11020, 'AI对话', 'dify_chat_view', '/dify/chat', 11000, '', 3, '1', 1),
(11021, '创建对话', 'dify_chat_add', NULL, 11020, '', 1, '2', 1);
-- 招标管理菜单
INSERT INTO sys_menu (menu_id, name, permission, path, parent_id, icon, sort, type, tenant_id) VALUES
(12000, '招标管理', NULL, '/bidding', 0, 'el-icon-document', 3, '0', 1),
(12001, '招标项目', 'bidding_project_view', '/bidding/project', 12000, '', 1, '1', 1),
(12002, '创建项目', 'bidding_project_add', NULL, 12001, '', 1, '2', 1),
(12003, '编辑项目', 'bidding_project_edit', NULL, 12001, '', 2, '2', 1),
(12004, '删除项目', 'bidding_project_del', NULL, 12001, '', 3, '2', 1),
(12010, '投标管理', 'bidding_bid_view', '/bidding/bid', 12000, '', 2, '1', 1),
(12011, '创建投标', 'bidding_bid_add', NULL, 12010, '', 1, '2', 1),
(12012, '编辑投标', 'bidding_bid_edit', NULL, 12010, '', 2, '2', 1);
-- 平台管理菜单
INSERT INTO sys_menu (menu_id, name, permission, path, parent_id, icon, sort, type, tenant_id) VALUES
(13000, '平台管理', NULL, '/platform', 0, 'el-icon-setting', 4, '0', 1),
(13001, '系统配置', 'platform_config_view', '/platform/config', 13000, '', 1, '1', 1),
(13002, '编辑配置', 'platform_config_edit', NULL, 13001, '', 1, '2', 1),
(13010, '操作日志', 'platform_log_view', '/platform/log', 13000, '', 2, '1', 1),
(13020, '系统监控', 'platform_monitor_view', '/platform/monitor', 13000, '', 3, '1', 1),
(13030, '统计报表', 'platform_stat_view', '/platform/stat', 13000, '', 4, '1', 1);
-- 消息管理菜单
INSERT INTO sys_menu (menu_id, name, permission, path, parent_id, icon, sort, type, tenant_id) VALUES
(14000, '消息管理', NULL, '/message', 0, 'el-icon-message', 5, '0', 1),
(14001, '通知管理', 'message_notification_view', '/message/notification', 14000, '', 1, '1', 1),
(14002, '创建通知', 'message_notification_add', NULL, 14001, '', 1, '2', 1),
(14003, '发送通知', 'message_notification_send', NULL, 14001, '', 2, '2', 1),
(14010, '消息模板', 'message_template_view', '/message/template', 14000, '', 2, '1', 1),
(14011, '创建模板', 'message_template_add', NULL, 14010, '', 1, '2', 1),
(14012, '编辑模板', 'message_template_edit', NULL, 14010, '', 2, '2', 1),
(14013, '删除模板', 'message_template_del', NULL, 14010, '', 3, '2', 1);
```
## 角色权限分配示例
```sql
-- 为管理员角色分配所有业务权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 1, menu_id FROM sys_menu WHERE menu_id >= 10000 AND menu_id < 15000;
-- 为普通用户角色分配查看权限
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT 2, menu_id FROM sys_menu
WHERE menu_id >= 10000 AND menu_id < 15000
AND (type = '0' OR type = '1' OR permission LIKE '%_view');
```
## 代码转换示例
### Java Controller 转换
```java
// 转换前 (urbanLifelineServ)
@RestController
@RequestMapping("/api/workcase")
public class WorkcaseController {
@PostMapping("/create")
@PreAuthorize("hasAuthority('workcase:ticket:create')")
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return ResultDomain.success(workcaseService.save(workcase));
}
}
// 转换后 (pigx-app-server)
@RestController
@RequestMapping("/workcase")
public class WorkcaseController {
@PostMapping
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return R.ok(workcaseService.save(workcase));
}
}
```
### 前端权限判断转换
```javascript
// 转换前 (urbanLifelineWeb)
if (hasPermission('workcase:ticket:create')) {
// 显示创建按钮
}
// 转换后 (pigx-ai-ui)
if (checkPermission(['workcase_ticket_add'])) {
// 显示创建按钮
}
```
## 注意事项
1. **权限格式严格**: 必须使用下划线 `_` 而不是冒号 `:`
2. **注解格式**: 必须包含 `@pms.` 前缀
3. **动作映射**: `create` 统一改为 `add``update` 改为 `edit``delete` 改为 `del`
4. **菜单类型**:
- type='0': 目录
- type='1': 菜单
- type='2': 按钮
5. **menu_id分配**:
- 10000-10999: 工单模块
- 11000-11999: AI模块(Dify)
- 12000-12999: 招标模块
- 13000-13999: 平台管理
- 14000-14999: 消息模块
## 批量转换脚本
可以使用以下正则表达式进行批量替换:
```regex
# 查找
@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\)
# 替换为
@PreAuthorize("@pms.hasPermission('$1_$2_$3')")
# 特殊处理 create -> add
将 _create 替换为 _add
将 _update 替换为 _edit
将 _delete 替换为 _del
```

View File

@@ -0,0 +1,756 @@
# pigx-dify 模块架构设计
## 1. 模块概述
### 1.1 定位
pigx-dify 是 pigx 平台的 AI 服务模块,专门用于集成 Dify AI 平台,提供智能体管理、知识库管理和 AI 对话功能。
### 1.2 核心功能
- 智能体Agent管理
- 知识库Knowledge管理
- AI 对话Chat功能
- Dify API 集成
- 流式响应支持SSE
### 1.3 技术栈
- Spring Boot 3.5.8
- Spring Cloud 2025.0.0
- MyBatis-Plus 3.5.14
- MySQL 8.0
- Dify API Client
- SSE (Server-Sent Events)
## 2. 模块结构
### 2.1 Maven 项目结构
```xml
<!-- pigx-dify/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx</artifactId>
<version>6.4.0</version>
</parent>
<artifactId>pigx-dify</artifactId>
<packaging>pom</packaging>
<description>Dify AI integration module</description>
<modules>
<module>pigx-dify-api</module>
<module>pigx-dify-biz</module>
</modules>
</project>
```
### 2.2 pigx-dify-api 结构
```xml
<!-- pigx-dify-api/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-dify</artifactId>
<version>6.4.0</version>
</parent>
<artifactId>pigx-dify-api</artifactId>
<description>Dify API interfaces and entities</description>
<dependencies>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-core</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
</dependency>
</dependencies>
</project>
```
### 2.3 pigx-dify-biz 结构
```xml
<!-- pigx-dify-biz/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-dify</artifactId>
<version>6.4.0</version>
</parent>
<artifactId>pigx-dify-biz</artifactId>
<description>Dify business implementation</description>
<dependencies>
<!-- pigx dependencies -->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-dify-api</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-security</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-log</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-swagger</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- HTTP Client for Dify API -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<!-- SSE Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
```
## 3. 包结构设计
### 3.1 pigx-dify-api 包结构
```
pigx-dify-api/
└── src/main/java/com/pig4cloud/pigx/dify/api/
├── entity/ # 实体类
│ ├── TbAgent.java # 智能体
│ ├── TbChat.java # 聊天会话
│ ├── TbChatMessage.java # 聊天消息
│ └── TbKnowledge.java # 知识库
├── dto/ # 数据传输对象
│ ├── AgentDTO.java
│ ├── ChatDTO.java
│ ├── ChatMessageDTO.java
│ └── KnowledgeDTO.java
├── vo/ # 视图对象
│ ├── AgentVO.java
│ ├── ChatVO.java
│ └── KnowledgeVO.java
├── feign/ # Feign接口
│ └── RemoteDifyService.java
└── constant/ # 常量定义
└── DifyConstant.java
```
### 3.2 pigx-dify-biz 包结构
```
pigx-dify-biz/
└── src/main/java/com/pig4cloud/pigx/dify/
├── DifyApplication.java # 启动类
├── config/ # 配置类
│ ├── DifyConfig.java # Dify配置
│ ├── WebConfig.java # Web配置
│ └── AsyncConfig.java # 异步配置
├── controller/ # 控制器
│ ├── AgentController.java # 智能体管理
│ ├── ChatController.java # 对话管理
│ └── KnowledgeController.java # 知识库管理
├── service/ # 服务层
│ ├── AgentService.java
│ ├── ChatService.java
│ ├── KnowledgeService.java
│ └── impl/
│ ├── AgentServiceImpl.java
│ ├── ChatServiceImpl.java
│ └── KnowledgeServiceImpl.java
├── mapper/ # 数据访问层
│ ├── AgentMapper.java
│ ├── ChatMapper.java
│ ├── ChatMessageMapper.java
│ └── KnowledgeMapper.java
├── client/ # 外部API客户端
│ ├── DifyApiClient.java # Dify API客户端
│ ├── dto/ # Dify API DTO
│ │ ├── DifyRequest.java
│ │ └── DifyResponse.java
│ └── callback/
│ └── StreamCallback.java # 流式回调
└── handler/ # 处理器
├── SseHandler.java # SSE处理
└── GlobalExceptionHandler.java # 全局异常处理
```
## 4. 核心代码设计
### 4.1 启动类
```java
package com.pig4cloud.pigx.dify;
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients;
import com.pig4cloud.pigx.common.security.annotation.EnablePigxResourceServer;
import com.pig4cloud.pigx.common.swagger.annotation.EnablePigxDoc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnablePigxDoc
@EnablePigxResourceServer
@EnablePigxFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class DifyApplication {
public static void main(String[] args) {
SpringApplication.run(DifyApplication.class, args);
}
}
```
### 4.2 配置类
```java
package com.pig4cloud.pigx.dify.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "dify")
public class DifyConfig {
/**
* Dify API基础URL
*/
private String apiBaseUrl = "https://api.dify.ai/v1";
/**
* 默认API Key可被智能体配置覆盖
*/
private String defaultApiKey;
/**
* 连接超时(毫秒)
*/
private Integer connectTimeout = 10000;
/**
* 读取超时(毫秒)
*/
private Integer readTimeout = 30000;
/**
* 流式响应超时(毫秒)
*/
private Integer streamTimeout = 60000;
/**
* 是否启用调试日志
*/
private Boolean debug = false;
}
```
### 4.3 实体设计
```java
package com.pig4cloud.pigx.dify.api.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@Data
@TableName("tb_agent")
@EqualsAndHashCode(callSuper = true)
public class TbAgent extends Model<TbAgent> {
@TableId(type = IdType.ASSIGN_UUID)
private String agentId;
private String name;
private String description;
private String difyApiKey;
private String difyAgentId;
private String config; // JSON配置
private String icon;
private Integer status; // 0:禁用 1:启用
@TableField(fill = FieldFill.INSERT)
private Long tenantId;
@TableField(fill = FieldFill.INSERT)
private String createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private String updateBy;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer delFlag;
}
```
### 4.4 Controller设计
```java
package com.pig4cloud.pigx.dify.controller;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import com.pig4cloud.pigx.dify.api.dto.ChatDTO;
import com.pig4cloud.pigx.dify.service.ChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequiredArgsConstructor
@RequestMapping("/chat")
@Tag(name = "对话管理")
public class ChatController {
private final ChatService chatService;
@Operation(summary = "创建对话")
@PostMapping
@PreAuthorize("@pms.hasPermission('dify_chat_add')")
public R<ChatDTO> createChat(@RequestBody ChatDTO chatDTO) {
chatDTO.setUserId(SecurityUtils.getUser().getId());
chatDTO.setTenantId(SecurityUtils.getUser().getTenantId());
return R.ok(chatService.createChat(chatDTO));
}
@Operation(summary = "流式对话")
@PostMapping("/stream/{chatId}")
@PreAuthorize("@pms.hasPermission('dify_chat_message')")
public SseEmitter streamChat(@PathVariable String chatId,
@RequestBody String message) {
return chatService.streamChat(chatId, message, SecurityUtils.getUser());
}
@Operation(summary = "获取对话历史")
@GetMapping("/{chatId}/messages")
@PreAuthorize("@pms.hasPermission('dify_chat_view')")
public R<?> getChatMessages(@PathVariable String chatId) {
return R.ok(chatService.getChatMessages(chatId));
}
}
```
## 5. 数据库设计
### 5.1 数据表DDL
```sql
-- 智能体表
CREATE TABLE `tb_agent` (
`agent_id` varchar(36) NOT NULL COMMENT '智能体ID',
`name` varchar(100) NOT NULL COMMENT '名称',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`dify_api_key` varchar(255) DEFAULT NULL COMMENT 'Dify API Key',
`dify_agent_id` varchar(100) DEFAULT NULL COMMENT 'Dify Agent ID',
`config` text COMMENT '配置信息(JSON)',
`icon` varchar(255) DEFAULT NULL COMMENT '图标',
`status` tinyint DEFAULT '1' COMMENT '状态 0:禁用 1:启用',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`agent_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体表';
-- 聊天会话表
CREATE TABLE `tb_chat` (
`chat_id` varchar(36) NOT NULL COMMENT '会话ID',
`agent_id` varchar(36) NOT NULL COMMENT '智能体ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`title` varchar(200) DEFAULT NULL COMMENT '会话标题',
`conversation_id` varchar(100) DEFAULT NULL COMMENT 'Dify会话ID',
`status` tinyint DEFAULT '1' COMMENT '状态 0:关闭 1:活跃',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`chat_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_agent_id` (`agent_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天会话表';
-- 聊天消息表
CREATE TABLE `tb_chat_message` (
`message_id` varchar(36) NOT NULL COMMENT '消息ID',
`chat_id` varchar(36) NOT NULL COMMENT '会话ID',
`content` text NOT NULL COMMENT '消息内容',
`role` varchar(20) NOT NULL COMMENT '角色(user/ai/system)',
`dify_message_id` varchar(100) DEFAULT NULL COMMENT 'Dify消息ID',
`parent_message_id` varchar(36) DEFAULT NULL COMMENT '父消息ID',
`metadata` text COMMENT '元数据(JSON)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`message_id`),
KEY `idx_chat_id` (`chat_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';
-- 知识库表
CREATE TABLE `tb_knowledge` (
`knowledge_id` varchar(36) NOT NULL COMMENT '知识库ID',
`title` varchar(200) NOT NULL COMMENT '标题',
`description` text COMMENT '描述',
`dify_dataset_id` varchar(100) DEFAULT NULL COMMENT 'Dify数据集ID',
`status` tinyint DEFAULT '1' COMMENT '状态 0:禁用 1:启用',
`tenant_id` bigint NOT NULL DEFAULT '1' COMMENT '租户ID',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`knowledge_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库表';
```
## 6. 配置文件
### 6.1 bootstrap.yml
```yaml
server:
port: 9500
spring:
application:
name: @project.artifactId@
profiles:
active: @profiles.active@
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:pigx-register}:${NACOS_PORT:8848}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yml
shared-configs:
- data-id: common.yml
refresh: true
- data-id: db.yml
refresh: true
```
### 6.2 application.yml
```yaml
# Dify配置
dify:
api-base-url: ${DIFY_API_BASE_URL:https://api.dify.ai/v1}
default-api-key: ${DIFY_DEFAULT_API_KEY:}
connect-timeout: 10000
read-timeout: 30000
stream-timeout: 60000
debug: false
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.pig4cloud.pigx.dify.api.entity
configuration:
map-underscore-to-camel-case: true
# 安全配置
security:
oauth2:
resource:
ignore-urls:
- /actuator/**
- /v3/api-docs/**
```
## 7. 服务注册
### 7.1 路由配置
在 pigx-gateway 中添加路由:
```yaml
spring:
cloud:
gateway:
routes:
- id: pigx-dify
uri: lb://pigx-dify
predicates:
- Path=/dify/**
filters:
- StripPrefix=1
```
### 7.2 Feign配置
```java
package com.pig4cloud.pigx.dify.api.feign;
import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.dify.api.dto.ChatDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(contextId = "remoteDifyService",
value = ServiceNameConstants.DIFY_SERVICE)
public interface RemoteDifyService {
@GetMapping("/chat/{chatId}")
R<ChatDTO> getChatInfo(@PathVariable("chatId") String chatId);
}
```
## 8. 部署配置
### 8.1 Docker配置
```dockerfile
FROM pig4cloud/java:8-jre
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY target/pigx-dify-biz.jar /app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]
```
### 8.2 K8s部署
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pigx-dify
namespace: pigx
spec:
replicas: 1
selector:
matchLabels:
app: pigx-dify
template:
metadata:
labels:
app: pigx-dify
spec:
containers:
- name: pigx-dify
image: pigx/pigx-dify:latest
ports:
- containerPort: 9500
env:
- name: NACOS_HOST
value: "pigx-register"
- name: DIFY_API_BASE_URL
value: "https://api.dify.ai/v1"
- name: DIFY_DEFAULT_API_KEY
valueFrom:
secretKeyRef:
name: dify-secret
key: api-key
```
## 9. 集成测试
### 9.1 单元测试
```java
@SpringBootTest
class ChatServiceTest {
@Autowired
private ChatService chatService;
@MockBean
private DifyApiClient difyApiClient;
@Test
void testCreateChat() {
// 测试创建对话
ChatDTO chatDTO = new ChatDTO();
chatDTO.setAgentId("test-agent");
chatDTO.setUserId(1L);
ChatDTO result = chatService.createChat(chatDTO);
assertNotNull(result.getChatId());
}
}
```
### 9.2 API测试
```http
### 创建对话
POST http://localhost:9999/dify/chat
Authorization: Bearer {{token}}
Content-Type: application/json
{
"agentId": "agent-001",
"title": "测试对话"
}
### 发送消息(流式)
POST http://localhost:9999/dify/chat/stream/{{chatId}}
Authorization: Bearer {{token}}
Content-Type: text/plain
你好,请介绍一下自己
```
## 10. 监控告警
### 10.1 健康检查
```java
@Component
public class DifyHealthIndicator implements HealthIndicator {
@Autowired
private DifyApiClient difyApiClient;
@Override
public Health health() {
try {
// 检查Dify API连通性
boolean isHealthy = difyApiClient.checkHealth();
if (isHealthy) {
return Health.up()
.withDetail("dify", "Available")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("dify", "Unavailable")
.withException(e)
.build();
}
return Health.down().build();
}
}
```
### 10.2 日志配置
```xml
<!-- logback-spring.xml -->
<configuration>
<logger name="com.pig4cloud.pigx.dify" level="INFO"/>
<logger name="com.pig4cloud.pigx.dify.client" level="DEBUG"/>
</configuration>
```
## 11. 安全考虑
### 11.1 API Key管理
- API Key 加密存储
- 支持多租户隔离
- 定期轮换机制
### 11.2 数据隔离
- 租户级别数据隔离
- 用户权限验证
- 敏感信息脱敏
### 11.3 限流配置
```java
@Configuration
public class RateLimitConfig {
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(10, 20); // 10 requests per second
}
}
```
## 12. 迁移清单
- [ ] 创建 Maven 模块结构
- [ ] 迁移实体类和 Mapper
- [ ] 迁移 Service 层业务逻辑
- [ ] 迁移 Controller 层接口
- [ ] 适配权限注解
- [ ] 迁移 DifyApiClient
- [ ] 配置服务注册和发现
- [ ] 数据库表结构迁移
- [ ] 前端页面迁移
- [ ] 集成测试
- [ ] 部署配置

View File

@@ -0,0 +1,151 @@
# Requirements Document
## Introduction
本文档定义了将 urbanLifelineServ 和 urbanLifelineWeb 项目的**业务功能**迁移到 pigx-ai 和 pigx-ai-ui 平台的需求规范。
**核心原则**
- 只迁移业务功能代码招标、工单、平台管理、AI、消息等
- 人员、部门、权限、认证等基础设施**完全使用 pigx 原生实现**
- 数据库从 PostgreSQL 迁移到 MySQL
- 前端从微前端架构合并到 pigx-ai-ui 单体应用
## Glossary
- **Business_Module**: 需要迁移的业务模块bidding, workcase, platform, ai, message
- **pigx-app-server**: pigx 平台的业务服务模块,用于承载迁移的业务功能
- **pigx-dify**: 新建的 pigx 平台 AI 模块,用于承载原 urbanLifeline 的 AI 功能和 Dify 集成
- **pigx-knowledge**: pigx 平台原有的 AI 知识库模块(不使用)
- **PigxUser**: pigx 原生的用户实体,迁移后的业务代码需要使用此用户模型
- **R<T>**: pigx 统一响应格式
- **DifyApiClient**: 与 Dify 平台交互的客户端,保留在 pigx-dify 模块中
## Requirements
### Requirement 1: 招标模块迁移 (bidding)
**User Story:** As a 业务用户, I want to 在 pigx 平台上使用招标功能, so that 可以进行招标项目管理。
#### Acceptance Criteria
1. WHEN Business_Module 迁移 bidding 服务 THEN pigx-app-server SHALL 包含招标业务的 Controller、Service、Mapper 层代码
2. WHEN Business_Module 处理招标用户关联 THEN pigx-app-server SHALL 使用 PigxUser 替代原有的 User 实体
3. WHEN Business_Module 迁移招标数据表 THEN Database_Migrator SHALL 将 PostgreSQL 表结构转换为 MySQL 并添加 tenant_id 租户字段
4. WHEN Business_Module 迁移招标前端页面 THEN pigx-ai-ui SHALL 在 src/views/bidding 目录下包含所有招标页面组件
5. WHEN Business_Module 处理招标 API 调用 THEN pigx-ai-ui SHALL 使用 pigx 的 request 工具和统一响应格式
### Requirement 2: 工单模块迁移 (workcase)
**User Story:** As a 业务用户, I want to 在 pigx 平台上使用工单功能, so that 可以进行工单流转和处理。
#### Acceptance Criteria
1. WHEN Business_Module 迁移 workcase 服务 THEN pigx-app-server SHALL 包含工单业务的完整代码
2. WHEN Business_Module 处理工单分配 THEN pigx-app-server SHALL 通过 pigx-upms 的 RemoteUserService 获取用户信息
3. WHEN Business_Module 处理工单流程 THEN pigx-app-server SHALL 评估是否集成 pigx-flow 工作流引擎
4. WHEN Business_Module 迁移工单数据表 THEN Database_Migrator SHALL 将表结构转换为 MySQL 并关联 pigx 的 sys_user 表
5. WHEN Business_Module 迁移工单前端页面 THEN pigx-ai-ui SHALL 在 src/views/workcase 目录下包含所有工单页面
6. WHEN Business_Module 处理 Jitsi 视频会议集成 THEN pigx-app-server SHALL 保留视频会议功能并适配 pigx 认证
### Requirement 3: 平台管理模块迁移 (platform)
**User Story:** As a 管理员, I want to 在 pigx 平台上使用平台管理功能, so that 可以进行业务配置和管理。
#### Acceptance Criteria
1. WHEN Business_Module 迁移 platform 服务 THEN pigx-app-server SHALL 包含平台管理业务代码
2. WHEN Business_Module 处理平台配置 THEN pigx-app-server SHALL 使用 pigx 的配置管理机制
3. WHEN Business_Module 迁移平台数据表 THEN Database_Migrator SHALL 将表结构转换为 MySQL
4. WHEN Business_Module 迁移平台前端页面 THEN pigx-ai-ui SHALL 在 src/views/platform 目录下包含管理页面
### Requirement 4: AI 模块迁移 (创建新的 pigx-dify 模块)
**User Story:** As a 用户, I want to 在 pigx 平台上使用 AI 功能, so that 可以使用智能问答和知识库。
#### Acceptance Criteria
1. WHEN Business_Module 迁移 ai 服务 THEN 新建的 pigx-dify 模块 SHALL 承载所有 AI 业务逻辑
2. WHEN Business_Module 处理 AI 对话 THEN pigx-dify SHALL 保留原有的 Dify API 集成方式
3. WHEN Business_Module 处理 Dify 集成 THEN pigx-dify SHALL 包含 DifyApiClient 和相关配置管理
4. WHEN Business_Module 迁移 AI 数据表 THEN Database_Migrator SHALL 将 tb_agent、tb_chat、tb_chat_message、tb_knowledge 等表转换为 MySQL
5. WHEN Business_Module 迁移 AI 前端页面 THEN pigx-ai-ui SHALL 在 src/views/dify 目录下包含 AI 对话界面
6. WHEN Business_Module 处理聊天记录 THEN pigx-dify SHALL 保持原有的 tb_chat 和 tb_chat_message 表结构
7. WHEN Business_Module 处理知识库 THEN pigx-dify SHALL 保持与 Dify Dataset API 的集成
### Requirement 5: 消息模块迁移 (message)
**User Story:** As a 用户, I want to 在 pigx 平台上接收消息通知, so that 可以及时了解业务动态。
#### Acceptance Criteria
1. WHEN Business_Module 迁移 message 服务 THEN pigx-app-server SHALL 包含消息通知业务代码
2. WHEN Business_Module 处理消息推送 THEN pigx-app-server SHALL 使用 pigx-common-websocket 进行实时推送
3. WHEN Business_Module 处理微信通知 THEN pigx-app-server SHALL 保留微信消息推送功能
4. WHEN Business_Module 迁移消息数据表 THEN Database_Migrator SHALL 将表结构转换为 MySQL
### Requirement 6: 文件服务适配
**User Story:** As a 用户, I want to 上传和下载文件, so that 可以管理业务相关的文件资源。
#### Acceptance Criteria
1. WHEN Business_Module 处理文件上传 THEN pigx-app-server SHALL 使用 pigx-common-oss 进行文件存储
2. WHEN Business_Module 处理文件访问 THEN pigx-app-server SHALL 适配 pigx 的文件访问 URL 格式
3. IF Business_Module 有自定义文件处理逻辑 THEN pigx-app-server SHALL 在 OSS 基础上扩展实现
### Requirement 7: 定时任务适配
**User Story:** As a 系统管理员, I want to 管理定时任务, so that 可以执行周期性业务处理。
#### Acceptance Criteria
1. WHEN Business_Module 迁移 crontab 任务 THEN pigx-visual/xxl-job SHALL 包含迁移后的定时任务
2. WHEN Business_Module 处理任务调度 THEN XXL-Job SHALL 替代原有的调度机制
3. WHEN Business_Module 处理任务执行 THEN pigx-app-server SHALL 提供任务执行的 HTTP 接口
### Requirement 8: 数据库迁移
**User Story:** As a DBA, I want to 将业务数据迁移到 MySQL, so that 数据可以在 pigx 平台运行。
#### Acceptance Criteria
1. WHEN Database_Migrator 处理业务表 THEN Migration_System SHALL 生成 MySQL DDL 脚本
2. WHEN Database_Migrator 处理数据类型 THEN Migration_System SHALL 正确映射 PostgreSQL 类型到 MySQL
3. WHEN Database_Migrator 处理用户关联 THEN Migration_System SHALL 将原 user_id 映射到 pigx 的 sys_user.user_id
4. WHEN Database_Migrator 处理租户支持 THEN Migration_System SHALL 为业务表添加 tenant_id 字段
5. WHEN Database_Migrator 执行数据迁移 THEN Migration_System SHALL 保证业务数据完整性
### Requirement 9: 前端共享组件迁移
**User Story:** As a 前端开发者, I want to 迁移共享组件到 pigx-ai-ui, so that 业务页面可以复用这些组件。
#### Acceptance Criteria
1. WHEN Frontend_Migrator 处理 shared 组件 THEN pigx-ai-ui SHALL 在 src/components/urban 目录下包含迁移的组件
2. WHEN Frontend_Migrator 处理组件依赖 THEN pigx-ai-ui SHALL 更新导入路径使用 pigx 的工具函数
3. WHEN Frontend_Migrator 处理样式 THEN pigx-ai-ui SHALL 合并样式并避免与 pigx 样式冲突
4. WHEN Frontend_Migrator 处理 API 调用 THEN 组件 SHALL 使用 pigx 的 request 工具
### Requirement 10: 路由和菜单配置
**User Story:** As a 管理员, I want to 在 pigx 菜单中看到迁移的功能, so that 可以访问业务功能。
#### Acceptance Criteria
1. WHEN Frontend_Migrator 处理路由 THEN pigx-ai-ui SHALL 在路由配置中添加业务模块路由
2. WHEN Admin 配置菜单 THEN pigx-upms SHALL 在 sys_menu 表中添加业务功能菜单
3. WHEN Admin 配置权限 THEN pigx-upms SHALL 为业务功能配置相应的权限标识
### Requirement 11: 权限模型适配
**User Story:** As a 系统管理员, I want to 在 pigx 权限体系中配置业务功能权限, so that 可以控制用户对迁移功能的访问。
#### Acceptance Criteria
1. WHEN Business_Module 迁移权限注解 THEN pigx-app-server SHALL 将 @PreAuthorize 替换为 @HasPermission
2. WHEN Business_Module 处理权限标识 THEN pigx-app-server SHALL 使用 pigx 权限命名规范 (module_action 格式)
3. WHEN Admin 配置业务权限 THEN pigx-upms SHALL 在 sys_menu 表中添加对应的权限菜单项
4. WHEN Business_Module 获取用户信息 THEN pigx-app-server SHALL 使用 SecurityUtils.getUser() 获取 PigxUser
5. WHEN Business_Module 调用用户服务 THEN pigx-app-server SHALL 通过 RemoteUserService 进行 Feign 调用
6. IF Business_Module 需要数据权限控制 THEN pigx-app-server SHALL 利用 pigx 的租户和部门隔离机制

View File

@@ -0,0 +1,663 @@
# SecurityUtils 和 RemoteUserService 配置指南
## 1. 概述
本指南详细说明如何在 pigx 平台中配置和使用 SecurityUtils 和 RemoteUserService实现用户信息获取和远程用户服务调用。
## 2. SecurityUtils 使用指南
### 2.1 SecurityUtils 介绍
SecurityUtils 是 pigx 平台提供的安全工具类,用于获取当前登录用户信息、权限判断等安全相关操作。
### 2.2 Maven 依赖
```xml
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-security</artifactId>
</dependency>
```
### 2.3 基本使用
#### 2.3.1 获取当前用户信息
```java
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import com.pig4cloud.pigx.admin.api.entity.SysUser;
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
public void example() {
// 获取完整用户对象
PigxUser pigxUser = SecurityUtils.getUser();
// 获取用户ID
Long userId = pigxUser.getId();
// 获取用户名
String username = pigxUser.getUsername();
// 获取租户ID重要多租户隔离
Long tenantId = pigxUser.getTenantId();
// 获取部门ID
Long deptId = pigxUser.getDeptId();
// 获取用户角色列表
List<Long> roles = pigxUser.getRoles();
// 获取用户权限列表
Collection<String> authorities = pigxUser.getAuthorities();
}
}
```
#### 2.3.2 在实体中自动填充用户信息
```java
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
@Data
@TableName("tb_workcase")
public class TbWorkcase {
private String workcaseId;
// 自动填充创建人
@TableField(fill = FieldFill.INSERT)
private String createBy;
// 自动填充更新人
@TableField(fill = FieldFill.UPDATE)
private String updateBy;
// 自动填充租户ID
@TableField(fill = FieldFill.INSERT)
private Long tenantId;
}
```
配置自动填充处理器:
```java
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
PigxUser user = SecurityUtils.getUser();
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", String.class, user.getUsername());
this.strictInsertFill(metaObject, "tenantId", Long.class, user.getTenantId());
this.strictInsertFill(metaObject, "delFlag", String.class, "0");
}
@Override
public void updateFill(MetaObject metaObject) {
PigxUser user = SecurityUtils.getUser();
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy", String.class, user.getUsername());
}
}
```
### 2.4 权限判断
```java
@RestController
@RequestMapping("/workcase")
public class WorkcaseController {
// 方法级权限判断
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
@PostMapping
public R<TbWorkcase> create(@RequestBody TbWorkcase workcase) {
return R.ok(workcaseService.save(workcase));
}
// 代码中权限判断
@GetMapping("/admin-only")
public R<?> adminFunction() {
PigxUser user = SecurityUtils.getUser();
// 检查是否有特定权限
if (!user.getAuthorities().contains("workcase_ticket_admin")) {
return R.failed("没有管理员权限");
}
// 执行管理员功能
return R.ok();
}
}
```
### 2.5 异步任务中使用
```java
@Service
public class AsyncService {
// 错误示例:异步线程中可能获取不到用户信息
@Async
public void wrongAsyncMethod() {
PigxUser user = SecurityUtils.getUser(); // 可能为null
}
// 正确示例:传递用户信息
@Async
public void correctAsyncMethod(PigxUser user) {
// 使用传入的用户信息
Long userId = user.getId();
Long tenantId = user.getTenantId();
// 执行异步逻辑
}
// 调用异步方法
public void callAsync() {
PigxUser user = SecurityUtils.getUser();
correctAsyncMethod(user);
}
}
```
## 3. RemoteUserService 配置和使用
### 3.1 RemoteUserService 介绍
RemoteUserService 是通过 Feign 调用 pigx-upms 服务获取用户信息的远程服务接口。
### 3.2 Maven 依赖
```xml
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-upms-api</artifactId>
</dependency>
```
### 3.3 启用 Feign 客户端
在启动类或配置类上添加注解:
```java
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients;
@EnablePigxFeignClients
@SpringBootApplication
public class WorkcaseApplication {
public static void main(String[] args) {
SpringApplication.run(WorkcaseApplication.class, args);
}
}
```
### 3.4 基本使用
```java
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
import com.pig4cloud.pigx.admin.api.entity.SysUser;
import com.pig4cloud.pigx.common.core.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
@Autowired
private RemoteUserService remoteUserService;
/**
* 根据用户ID获取用户信息
*/
public SysUser getUserById(Long userId) {
R<SysUser> result = remoteUserService.selectById(userId, SecurityConstants.FROM_IN);
if (result.isSuccess() && result.getData() != null) {
return result.getData();
}
throw new BusinessException("用户不存在");
}
/**
* 根据用户名获取用户信息
*/
public SysUser getUserByUsername(String username) {
R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
if (result.isSuccess() && result.getData() != null) {
return result.getData().getSysUser();
}
throw new BusinessException("用户不存在");
}
/**
* 批量获取用户信息
*/
public List<SysUser> getUsersByIds(List<Long> userIds) {
List<SysUser> users = new ArrayList<>();
for (Long userId : userIds) {
R<SysUser> result = remoteUserService.selectById(userId, SecurityConstants.FROM_IN);
if (result.isSuccess() && result.getData() != null) {
users.add(result.getData());
}
}
return users;
}
}
```
### 3.5 错误处理
```java
@Service
public class WorkcaseServiceImpl {
@Autowired
private RemoteUserService remoteUserService;
public void assignWorkcase(String workcaseId, Long assigneeId) {
try {
// 调用远程服务
R<SysUser> result = remoteUserService.selectById(assigneeId, SecurityConstants.FROM_IN);
// 检查调用是否成功
if (!result.isSuccess()) {
log.error("获取用户信息失败: {}", result.getMsg());
throw new BusinessException("获取用户信息失败");
}
// 检查数据是否存在
SysUser assignee = result.getData();
if (assignee == null) {
throw new BusinessException("用户不存在");
}
// 执行分配逻辑
doAssign(workcaseId, assignee);
} catch (FeignException e) {
// 处理Feign调用异常
log.error("远程服务调用失败", e);
throw new BusinessException("系统繁忙,请稍后重试");
}
}
}
```
### 3.6 配置熔断降级
```java
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
import com.pig4cloud.pigx.admin.api.feign.factory.RemoteUserServiceFallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class RemoteUserServiceFallbackImpl implements RemoteUserServiceFallbackFactory {
@Override
public RemoteUserService create(Throwable throwable) {
return new RemoteUserService() {
@Override
public R<SysUser> selectById(Long id, String from) {
log.error("调用用户服务失败", throwable);
return R.failed("用户服务暂时不可用");
}
@Override
public R<UserInfo> info(String username, String from) {
log.error("调用用户服务失败", throwable);
return R.failed("用户服务暂时不可用");
}
};
}
}
```
配置文件中启用熔断:
```yaml
feign:
sentinel:
enabled: true
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
```
## 4. 部门服务调用
```java
import com.pig4cloud.pigx.admin.api.feign.RemoteDeptService;
import com.pig4cloud.pigx.admin.api.entity.SysDept;
@Service
public class DeptRelatedService {
@Autowired
private RemoteDeptService remoteDeptService;
/**
* 获取部门信息
*/
public SysDept getDeptById(Long deptId) {
R<SysDept> result = remoteDeptService.selectById(deptId, SecurityConstants.FROM_IN);
if (result.isSuccess() && result.getData() != null) {
return result.getData();
}
return null;
}
/**
* 获取部门树
*/
public List<SysDept> getDeptTree() {
R<List<SysDept>> result = remoteDeptService.tree(SecurityConstants.FROM_IN);
if (result.isSuccess() && result.getData() != null) {
return result.getData();
}
return new ArrayList<>();
}
}
```
## 5. 最佳实践
### 5.1 缓存用户信息
```java
import org.springframework.cache.annotation.Cacheable;
@Service
public class UserCacheService {
@Autowired
private RemoteUserService remoteUserService;
@Cacheable(value = "user", key = "#userId")
public SysUser getUserById(Long userId) {
R<SysUser> result = remoteUserService.selectById(userId, SecurityConstants.FROM_IN);
if (result.isSuccess() && result.getData() != null) {
return result.getData();
}
return null;
}
}
```
### 5.2 批量查询优化
```java
@Service
public class BatchUserService {
@Autowired
private RemoteUserService remoteUserService;
/**
* 批量获取用户信息(优化版)
*/
public Map<Long, SysUser> getUserMap(List<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return new HashMap<>();
}
// 使用并行流提高效率
return userIds.parallelStream()
.map(userId -> remoteUserService.selectById(userId, SecurityConstants.FROM_IN))
.filter(result -> result.isSuccess() && result.getData() != null)
.map(R::getData)
.collect(Collectors.toMap(SysUser::getUserId, Function.identity()));
}
}
```
### 5.3 租户隔离实现
```java
@Service
public class TenantIsolationService {
/**
* 查询时自动添加租户条件
*/
public List<TbWorkcase> listByTenant() {
PigxUser user = SecurityUtils.getUser();
QueryWrapper<TbWorkcase> wrapper = new QueryWrapper<>();
wrapper.eq("tenant_id", user.getTenantId());
return workcaseMapper.selectList(wrapper);
}
/**
* 保存时自动设置租户ID
*/
public void saveWithTenant(TbWorkcase workcase) {
PigxUser user = SecurityUtils.getUser();
workcase.setTenantId(user.getTenantId());
workcaseMapper.insert(workcase);
}
}
```
## 6. 常见问题
### Q1: SecurityUtils.getUser() 返回 null
**原因**
1. 未登录或 token 过期
2. 在异步线程中调用
3. 在定时任务中调用
**解决方案**
1. 检查 token 有效性
2. 在异步方法调用前获取用户信息并传递
3. 定时任务使用系统用户或指定用户
### Q2: RemoteUserService 调用超时
**原因**
1. 网络问题
2. pigx-upms 服务未启动
3. 配置的超时时间太短
**解决方案**
```yaml
feign:
client:
config:
default:
connectTimeout: 10000 # 连接超时10秒
readTimeout: 10000 # 读取超时10秒
```
### Q3: 多租户数据混乱
**原因**
1. 未正确设置 tenant_id
2. 查询时未添加租户条件
**解决方案**
1. 使用 MyBatis-Plus 的自动填充
2. 配置全局租户拦截器
```java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加租户拦截器
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
@Override
public Expression getTenantId() {
PigxUser user = SecurityUtils.getUser();
return new LongValue(user.getTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 忽略不需要租户隔离的表
return "sys_user".equals(tableName);
}
});
interceptor.addInnerInterceptor(tenantInterceptor);
return interceptor;
}
}
```
### Q4: 如何在没有用户上下文的情况下调用服务
```java
@Service
public class SystemService {
/**
* 使用内部调用标识
*/
public void systemCall() {
// 使用 FROM_IN 标识内部调用
R<SysUser> result = remoteUserService.selectById(1L, SecurityConstants.FROM_IN);
}
/**
* 模拟系统用户
*/
public void executeAsSystem() {
// 创建系统用户上下文
PigxUser systemUser = new PigxUser();
systemUser.setId(0L);
systemUser.setUsername("system");
systemUser.setTenantId(1L);
// 执行逻辑
doSystemWork(systemUser);
}
}
```
## 7. 迁移检查清单
- [ ] 所有 JwtUtils 替换为 SecurityUtils
- [ ] 所有 UserService 替换为 RemoteUserService
- [ ] 所有实体添加 tenant_id 字段
- [ ] 配置 MyBatis-Plus 自动填充
- [ ] 配置 Feign 客户端
- [ ] 添加错误处理和熔断降级
- [ ] 测试用户信息获取
- [ ] 测试远程服务调用
- [ ] 测试租户数据隔离
## 8. 参考代码示例
完整的 Service 实现示例:
```java
package com.pig4cloud.pigx.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.entity.SysUser;
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
import com.pig4cloud.pigx.app.entity.TbWorkcase;
import com.pig4cloud.pigx.app.mapper.WorkcaseMapper;
import com.pig4cloud.pigx.app.service.WorkcaseService;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.service.PigxUser;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class WorkcaseServiceImpl extends ServiceImpl<WorkcaseMapper, TbWorkcase>
implements WorkcaseService {
private final RemoteUserService remoteUserService;
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean createWorkcase(TbWorkcase workcase) {
// 获取当前用户
PigxUser currentUser = SecurityUtils.getUser();
// 设置创建人信息
workcase.setCreateBy(currentUser.getUsername());
workcase.setTenantId(currentUser.getTenantId());
workcase.setDeptId(currentUser.getDeptId());
// 保存工单
return this.save(workcase);
}
@Override
public Boolean assignWorkcase(String workcaseId, Long assigneeId) {
// 获取被分配人信息
R<SysUser> result = remoteUserService.selectById(assigneeId, SecurityConstants.FROM_IN);
if (!result.isSuccess() || result.getData() == null) {
throw new RuntimeException("用户不存在");
}
SysUser assignee = result.getData();
// 更新工单
TbWorkcase workcase = this.getById(workcaseId);
workcase.setAssigneeId(assigneeId);
workcase.setAssigneeName(assignee.getUsername());
return this.updateById(workcase);
}
@Override
public List<TbWorkcase> listMyWorkcase() {
PigxUser user = SecurityUtils.getUser();
QueryWrapper<TbWorkcase> wrapper = new QueryWrapper<>();
wrapper.eq("tenant_id", user.getTenantId())
.eq("create_by", user.getUsername())
.orderByDesc("create_time");
return this.list(wrapper);
}
}
```

View File

@@ -0,0 +1,387 @@
# Implementation Plan: urbanLifeline 业务功能迁移到 pigx-ai
## Overview
将 urbanLifelineServ 和 urbanLifelineWeb 的业务功能迁移到 pigx-ai 平台,完全使用 pigx 原生的用户权限体系,只迁移业务代码和数据。
## Tasks
- [x] 1. 数据库迁移准备
- 分析源项目 PostgreSQL 表结构
- 生成 MySQL DDL 转换脚本
- 为所有业务表添加 tenant_id 字段
- _Requirements: 8.1, 8.2, 8.4_
- [x] 1.1 编写数据库转换脚本
- 已完成 database-migration-script.md 文档
- 包含工单、AI、招标、消息模块的完整 MySQL DDL
- **Validates: Requirements 8.1, 8.2, 8.3, 8.4**
- [x] 2. 权限和菜单规划
- 设计业务功能的权限标识规范
- 规划前端路由路径
- 定义菜单层级结构
- _Requirements: 10.2, 11.3_
- [x] 2.1 创建权限标识映射表
- 已完成 permission-mapping.md 文档
- 包含完整的权限映射和菜单配置 SQL
- _Requirements: 11.1, 11.2_
- [x] 2.2 编写权限注解转换指南
- 已完成 permission-annotation-guide.md 文档
- 包含 @PreAuthorize@pms.hasPermission 转换规则
- _Requirements: 11.1, 11.2_
- [x] 2.3 配置 SecurityUtils 和 RemoteUserService
- 已完成 security-config-guide.md 文档
- 包含完整的使用示例和最佳实践
- _Requirements: 11.4, 11.5_
- [x] 2.4 配置业务功能菜单
- 在 pigx sys_menu 表中执行菜单配置 SQLID从10000开始
- 配置对应的权限标识和路由路径
- _Requirements: 10.2, 11.3_
- [x] 2.5 分配角色权限
- 已在 permission-mapping.md 中提供完整的角色权限分配 SQL
- 管理员角色分配所有业务权限menu_id 10000-15000
- 普通用户角色分配查看权限
- 执行时需在 MySQL 中运行 sys_role_menu 插入语句
- _Requirements: 10.3, 11.3_
- [x] 3. 后端基础架构搭建
- 创建 pigx-workcase、pigx-bidding、pigx-dify 三个独立模块
- 每个模块包含 api 和 biz 子模块
- 配置 Maven 依赖和模块引用
- _Requirements: 1.1, 2.1, 3.1, 5.1_
- [x] 4. 工单模块迁移 (pigx-workcase)
- [x] 4.1 迁移工单实体和 Mapper
- 已创建 TbWorkcase、TbChatRoom、TbChatRoomMessage、TbChatRoomMember、TbWorkcaseProcess、TbVideoMeeting 实体
- 使用 pigx 标准格式:@TenantTable、Model<T>、createBy/updateBy/delFlag
- 已创建对应的 Mapper 接口
- _Requirements: 2.1, 2.4_
- [x] 4.2 迁移工单 Service 层
- 已创建 TbWorkcaseService、TbChatRoomService、TbChatRoomMessageService、TbChatRoomMemberService、TbWorkcaseProcessService、TbVideoMeetingService、TbCustomerServiceService
- TbChatRoomService 包含完整业务逻辑:创建聊天室、关闭聊天室、成员管理、消息管理、客服分配、服务评分
- TbVideoMeetingService 包含完整业务逻辑:创建会议、加入会议、开始/结束会议、权限验证
- TbCustomerServiceService 包含客服管理:获取可用客服、更新状态、工作量管理
- 已创建对应的 ServiceImpl 实现类
- _Requirements: 2.1, 2.2_
- [x] 4.3 迁移工单 Controller 层
- 已创建 TbWorkcaseController、TbChatRoomController、TbVideoMeetingController、TbCustomerServiceController
- TbChatRoomController 包含完整的聊天室功能:成员管理、消息管理、客服分配、服务评分
- TbVideoMeetingController 包含完整的会议功能:创建、加入、开始、结束会议
- TbCustomerServiceController 包含客服人员管理功能
- 使用 @HasPermission 权限注解
- 响应格式使用 R<T>
- _Requirements: 2.1, 11.1, 11.4_
- [x] 4.4 迁移视频会议功能
- 已创建 TbVideoMeetingService 和 TbVideoMeetingServiceImpl
- 包含创建会议、加入会议、开始会议、结束会议、获取活跃会议等功能
- 会议访问权限验证(基于聊天室成员)
-Jitsi JWT Token 生成需要后续配置 Jitsi 服务器参数
- _Requirements: 2.6_
- [x] 4.5 迁移聊天室 WebSocket 功能
- 已创建 ChatRoomWebSocketMessage、ChatRoomNotificationMessage 消息类
- 已创建 ChatRoomMessageHandler、ChatRoomNotificationHandler 消息处理器
- 已创建 ChatRoomWebSocketService 用于主动推送消息
- 使用 pigx-common-websocket 进行实时推送
- 配置 WebSocket 路径为 /ws/chat
- _Requirements: 2.1, 5.2_
- [x] 4.6 迁移 Jitsi JWT Token 生成功能
- 已创建 JitsiProperties 配置类
- 已创建 JitsiTokenService 接口和 JitsiTokenServiceImpl 实现
- 已创建 JitsiTokenController 提供 Token 生成、验证、URL 构建 API
- 支持一键生成会议信息(房间名+Token+URL
- 使用 jjwt 0.12.x 版本 API
- _Requirements: 2.6_
- [x] 4.7 迁移词云管理功能
- 已创建 TbWordCloud 实体类
- 已创建 TbWordCloudMapper 接口和 XML 映射文件
- 已创建 TbWordCloudService 接口和 TbWordCloudServiceImpl 实现
- 已创建 TbWordCloudController 提供词云 CRUD 和词频增加 API
- 支持词频自动累加(同一天、同一分类的相同词条)
- _Requirements: 2.1_
- [x] 5. AI 模块迁移pigx-dify 模块)
- [x] 5.1 创建 pigx-dify 模块结构
- 已创建 pigx-dify-api 和 pigx-dify-biz 子模块
- 已配置 Maven 依赖和模块引用
- 已创建启动类和配置文件
- _Requirements: 4.1_
- [x] 5.2 迁移 AI 实体和数据层
- 已创建 TbAgent、TbChat、TbChatMessage、TbKnowledge、TbKnowledgeFile、TbKnowledgeFileLog、PromptCard 实体
- 使用 pigx 标准格式
- 已创建对应的 Mapper 接口
- _Requirements: 4.4, 4.6_
- [x] 5.3 迁移 Dify API 客户端
- 已创建 DifyApiClient 完整功能(知识库管理、文档管理、对话、工作流、模型管理)
- 已创建 DifyProperties 配置类
- 已创建 DifyException 异常类
- 已创建 StreamCallback 回调接口
- 已创建所有 DTO 类ChatRequest/Response、Dataset*、Document*、Retrieval*、Workflow*、Conversation*、MessageHistory*、EmbeddingModel*、RerankModel*、DifyFileInfo
- 支持流式响应和阻塞调用两种模式
- 已配置 OkHttp 依赖
- 已更新 application.yml 添加 Dify 配置
- _Requirements: 4.2, 4.3_
- [x] 5.4 迁移 AI 业务逻辑
- 已创建 TbAgentService、TbChatService、TbKnowledgeService 等服务接口和实现类
- _Requirements: 4.1, 4.2_
- [x] 5.5 迁移 AI Controller 层
- 已创建 TbAgentController、TbChatController、TbKnowledgeController
- 使用 @HasPermission 权限注解
- _Requirements: 4.1, 11.1_
- [x] 5.6 配置 Dify 集成
- 已配置 DifyProperties 包含完整配置API地址、密钥、超时、上传、知识库
- 已在 application.yml 中添加 Dify 配置项
- 配置支持环境变量覆盖
- _Requirements: 4.3, 4.7_
- [x] 6. 招标模块迁移 (pigx-bidding)
- [x] 6.1 迁移招标实体和数据层
- 已创建 TbBiddingProject、TbBiddingDocument、TbBiddingRequirement、TbBidResponse、TbProcessNode 实体
- 已创建对应的 Mapper 接口
- _Requirements: 1.1, 1.3_
- [x] 6.2 迁移招标业务逻辑
- 已创建 TbBiddingProjectService、TbBiddingDocumentService 等服务接口和实现类
- _Requirements: 1.1, 1.2_
- [x] 6.3 迁移招标 API 接口
- 已创建 TbBiddingProjectController、TbBiddingDocumentController
- 使用 @HasPermission 权限注解
- _Requirements: 1.1, 1.5_
- [x] 7. 平台管理模块迁移 (platform)
- [x] 7.1 迁移平台管理功能
- 源项目 platform 模块只有启动类,无实际业务代码
- 平台配置功能可使用 pigx 的 sys_config 表和配置管理功能
- _Requirements: 3.1, 3.2_
- [x] 7.2 迁移平台数据表
- 无需迁移,使用 pigx 现有的配置管理表
- _Requirements: 3.3_
- [x] 8. 消息模块迁移 (message)
- [x] 8.1 迁移消息实体和数据层
- 源项目消息模块主要是邮件/短信发送功能,大部分方法为 TODO 状态
- pigx 已有完善的消息通知功能pigx-common-sms、pigx-common-mail
- 直接使用 pigx 现有的消息功能即可
- _Requirements: 5.1, 5.4_
- [x] 8.2 迁移消息通知功能
- 使用 pigx-common-websocket 进行实时推送(已在 pigx-workcase 中实现)
- 使用 pigx-common-sms 进行短信发送
- 使用 pigx-common-mail 进行邮件发送
- _Requirements: 5.1, 5.2_
- [x] 8.3 保留微信通知功能
- pigx 已有微信公众号/小程序消息推送功能
- 可通过 pigx-mp 模块实现微信消息推送
- _Requirements: 5.3_
- [x] 9. 文件服务适配
- [x] 9.1 替换文件上传逻辑
- 已在 pigx-workcase-biz、pigx-dify-biz、pigx-bidding-biz 中添加 pigx-common-oss 依赖
- 使用 pigx 的 OssTemplate 进行文件上传/下载
- 文件访问 URL 通过 pigx 网关统一管理
- _Requirements: 6.1, 6.2_
- [x] 10. 定时任务迁移
- [x] 10.1 迁移定时任务到 XXL-Job
- 源项目无定时任务需要迁移
- 如需添加定时任务,可使用 pigx-visual/xxl-job 进行配置
- _Requirements: 7.1, 7.2, 7.3_
- [ ] 11. 前端页面迁移
- [x] 11.1 迁移工单前端页面
- ✅ 已创建 API 层 (workcase.ts, chat.ts)
- ✅ 已创建类型定义 (workcase.ts, chatRoom.ts, customer.ts, conversation.ts, wordCloud.ts)
- ✅ 已创建工单列表页面 (views/workcase/index.vue)
- ✅ 已创建工单指派组件 (components/workcase/WorkcaseAssign.vue)
- ✅ 已创建工单详情组件 (views/workcase/detail/WorkcaseDetail.vue)
- ✅ 已创建聊天室消息组件 (views/workcase/chatRoom/ChatMessage.vue)
- 适配 pigx 的 request 工具和响应格式
- _Requirements: 2.5_
- [ ] 11.2 迁移 AI 前端页面
- 将 AI 相关页面迁移到 pigx-ai-ui/src/views/dify 目录
- 包括智能体管理、对话界面、知识库管理
- 适配 pigx 的 request 工具和响应格式
- _Requirements: 4.5_
- [ ] 11.3 迁移招标前端页面
- 将 bidding 页面迁移到 pigx-ai-ui/src/views/urban/bidding
- 适配 pigx 的 request 工具
- _Requirements: 1.4_
- [ ] 11.4 迁移平台管理前端页面
- 将 platform 页面迁移到 pigx-ai-ui/src/views/urban/platform
- _Requirements: 3.4_
- [ ] 11.5 迁移共享组件
- 将 shared 包组件迁移到 pigx-ai-ui/src/components/urban
- 更新导入路径使用 pigx 工具函数
- _Requirements: 9.1, 9.2_
- [ ] 11.6 适配 API 调用
- 创建 pigx-ai-ui/src/api/urban 目录
- 创建 workcase.ts、bidding.ts、platform.ts API 定义
- 创建 pigx-ai-ui/src/api/dify 目录
- 创建 agent.ts、chat.ts、knowledge.ts API 定义
- 使用 pigx 的 request 工具和 R<T> 响应格式
- 更新 API 路径为 pigx 网关规则
- _Requirements: 1.5, 9.4_
- [ ] 12. 数据迁移执行
- [ ] 12.1 执行数据库 DDL 脚本
- 在 MySQL 中执行 database-migration-script.md 中的建表语句
- 验证表结构正确性
- _Requirements: 8.1, 8.2_
- [ ] 12.2 执行业务数据迁移
- 运行数据迁移脚本
- 验证数据完整性
- _Requirements: 8.5_
- [ ] 13. 集成测试和验证
- [ ] 13.1 后端编译验证
- 确保所有迁移代码可以通过编译
- 验证 Maven 依赖正确
- _Requirements: 所有后端需求_
- [ ] 13.2 端到端功能测试
- 测试所有迁移功能的完整流程
- 验证权限控制正确性
- _Requirements: 所有需求_
- [ ] 13.3 租户隔离验证
- 验证多租户数据隔离正确性
- _Requirements: 8.4, 11.6_
- [ ] 14. 最终验收
- 确保所有功能正常运行
- 确认权限控制有效
- 验证多租户数据隔离
## Notes
- 任务 1、2.1、2.2、2.3 的文档已完成,可直接使用
- 重点关注权限适配和用户服务调用的正确性
- 已创建三个独立模块pigx-workcase、pigx-bidding、pigx-dify
- 每个模块包含 api 和 biz 子模块,遵循 pigx 架构规范
- 实体类使用 pigx 标准格式:@TenantTable、Model<T>、createBy/updateBy/delFlag
- 前端迁移需要适配 pigx-ai-ui 的技术栈Vue3 + TypeScript + Element Plus
- 数据库迁移脚本已在 database-migration-script.md 中准备好
## 已创建的文件
### pigx-ai-ui 前端已迁移文件
#### API 层
- `src/api/workcase/workcase.ts` - 工单管理 API
- `src/api/workcase/chat.ts` - 聊天室、客服、视频会议 API
#### 类型定义
- `src/types/workcase/workcase.ts` - 工单相关类型
- `src/types/workcase/chatRoom.ts` - 聊天室相关类型
- `src/types/workcase/customer.ts` - 客服相关类型
- `src/types/workcase/conversation.ts` - 对话相关类型
- `src/types/workcase/wordCloud.ts` - 词云相关类型
#### 组件
- `src/components/workcase/WorkcaseAssign.vue` - 工单指派组件
#### 页面
- `src/views/workcase/index.vue` - 工单列表页面
- `src/views/workcase/detail/WorkcaseDetail.vue` - 工单详情组件
- `src/views/workcase/detail/WorkcaseDetail.scss` - 工单详情样式
- `src/views/workcase/chatRoom/ChatMessage.vue` - 聊天室消息组件
#### 导出文件
- `src/views/workcase/chatRoom/index.ts`
- `src/views/workcase/detail/index.ts`
- Entity: TbWorkcase, TbChatRoom, TbChatRoomMessage, TbChatRoomMember, TbWorkcaseProcess, TbVideoMeeting, TbCustomerService, TbWordCloud
- Mapper: TbWorkcaseMapper, TbChatRoomMapper, TbChatRoomMessageMapper, TbChatRoomMemberMapper, TbWorkcaseProcessMapper, TbVideoMeetingMapper, TbCustomerServiceMapper, TbWordCloudMapper
- Service: TbWorkcaseService, TbChatRoomService含完整业务逻辑, TbChatRoomMessageService, TbChatRoomMemberService, TbWorkcaseProcessService, TbVideoMeetingService含完整业务逻辑, TbCustomerServiceService, TbWordCloudService, JitsiTokenService
- Controller: TbWorkcaseController, TbChatRoomController含成员/消息/客服分配API, TbVideoMeetingController含创建/加入/开始/结束会议API, TbCustomerServiceController, TbWordCloudController, JitsiTokenController
- WebSocket: ChatRoomWebSocketMessage, ChatRoomNotificationMessage, ChatRoomMessageHandler, ChatRoomNotificationHandler, ChatRoomWebSocketService
- Config: JitsiProperties, application.yml (port: 7070, WebSocket: /ws/chat)
- Application: PigxWorkcaseApplication
#### 聊天室功能已迁移的API
- 聊天室CRUD创建、查询、修改、关闭、删除
- 成员管理:添加成员、移除成员、获取成员列表、获取未读数、更新已读状态
- 消息管理:发送消息、分页查询消息、删除消息
- 客服分配:自动分配客服到聊天室
- 服务评分:提交聊天室服务评分
#### 视频会议功能已迁移的API
- 会议CRUD创建、查询、删除
- 会议操作:获取会议信息、加入会议、开始会议、结束会议
- 聊天室关联:获取聊天室当前活跃会议
#### 客服人员管理已迁移的API
- 客服CRUD新增、查询、修改、删除
- 状态管理:更新客服在线状态
- 可用客服:获取可接待客服列表
#### WebSocket 实时推送功能:
- 消息类型chat_message聊天消息、chat_notification通知消息
- 通知类型member_join成员加入、member_leave成员离开、typing正在输入、room_closed聊天室关闭
- ChatRoomWebSocketService主动推送消息、广播到聊天室、发送给指定用户
#### Jitsi JWT Token 功能:
- 生成 JWT Token支持主持人/普通成员角色
- 验证 Token检查 Token 有效性和过期时间
- 构建 iframe URL包含默认配置和自定义配置
- 生成房间名基于工单ID生成唯一房间名
- 一键生成会议信息:房间名+Token+URL
#### 词云管理功能:
- 词云CRUD新增、查询、修改、删除
- 词频累加:同一天、同一分类的相同词条自动累加词频
- 分页查询:支持按词语、分类、日期筛选
### pigx-dify 模块
- Entity: TbAgent, TbChat, TbChatMessage, TbKnowledge, TbKnowledgeFile, TbKnowledgeFileLog, PromptCard
- Mapper: TbAgentMapper, TbChatMapper, TbChatMessageMapper, TbKnowledgeMapper, TbKnowledgeFileMapper, TbKnowledgeFileLogMapper
- Service: TbAgentService, TbChatService, TbChatMessageService, TbKnowledgeService, TbKnowledgeFileService, TbKnowledgeFileLogService
- Controller: TbAgentController, TbChatController, TbKnowledgeController
- Client: DifyApiClient完整的 Dify API 客户端)
- Client DTO: ChatRequest, ChatResponse, DatasetCreateRequest, DatasetCreateResponse, DatasetDetailResponse, DatasetListResponse, DatasetUpdateRequest, DocumentListResponse, DocumentStatusResponse, DocumentUploadRequest, DocumentUploadResponse, RetrievalModel, RetrievalRequest, RetrievalResponse, WorkflowRunRequest, WorkflowRunResponse, ConversationListResponse, ConversationVariablesResponse, MessageHistoryResponse, EmbeddingModelResponse, RerankModelResponse, DifyFileInfo
- Callback: StreamCallback流式响应回调接口
- Exception: DifyException
- Config: DifyProperties, application.yml (port: 7080, Dify API 配置)
- Application: PigxDifyApplication
#### Dify API 客户端功能:
- 知识库管理:创建、查询、更新、删除知识库
- 文档管理:上传文档、查询文档列表、查询文档状态、删除文档
- 知识库检索:从知识库检索相关内容
- 对话功能:流式对话、阻塞式对话、停止对话、消息反馈
- 工作流:执行工作流(阻塞模式)
- 对话历史:获取消息历史、对话列表、对话变量
- 模型管理获取嵌入模型列表、Rerank模型列表
- 通用HTTPGET、POST、PATCH、DELETE 方法
### pigx-bidding 模块
- Entity: TbBiddingProject, TbBiddingDocument, TbBiddingRequirement, TbBidResponse, TbProcessNode
- Mapper: TbBiddingProjectMapper, TbBiddingDocumentMapper, TbBiddingRequirementMapper, TbBidResponseMapper, TbProcessNodeMapper
- Service: TbBiddingProjectService, TbBiddingDocumentService, TbBiddingRequirementService, TbBidResponseService, TbProcessNodeService
- Controller: TbBiddingProjectController, TbBiddingDocumentController
- Application: PigxBiddingApplication
- Config: application.yml (port: 7090)

View File

@@ -0,0 +1,505 @@
# 租户字段添加指南
## 概述
pigx 是一个多租户 SaaS 平台所有业务数据需要通过租户IDtenant_id进行隔离。本指南详细说明如何为所有业务表添加租户字段并实现租户隔离。
## 1. 租户字段规范
### 1.1 字段定义
```sql
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID'
```
### 1.2 字段特性
- **类型**BIGINT
- **非空**NOT NULL
- **默认值**1默认租户
- **索引**:建议添加索引以提高查询性能
- **注释**'租户ID'
## 2. 为现有表添加租户字段
### 2.1 通用 SQL 模板
```sql
-- 添加租户字段
ALTER TABLE `表名`
ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID'
AFTER `某个字段`;
-- 添加索引
ALTER TABLE `表名`
ADD INDEX `idx_tenant_id` (`tenant_id`);
-- 如果需要复合索引(常用查询条件+租户)
ALTER TABLE `表名`
ADD INDEX `idx_status_tenant` (`status`, `tenant_id`);
```
### 2.2 批量添加脚本
```sql
-- 工单模块
ALTER TABLE `tb_workcase` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_workcase` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_workcase_process` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_workcase_process` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_workcase_device` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_workcase_device` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_chat_room` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_chat_room` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_chat_room_member` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_chat_room_member` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_chat_room_message` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_chat_room_message` ADD INDEX `idx_tenant_id` (`tenant_id`);
-- AI模块
ALTER TABLE `tb_agent` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_agent` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_chat` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_chat` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_chat_message` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_chat_message` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_knowledge` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_knowledge` ADD INDEX `idx_tenant_id` (`tenant_id`);
-- 招标模块
ALTER TABLE `tb_bidding_project` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_bidding_project` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_bidding_document` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_bidding_document` ADD INDEX `idx_tenant_id` (`tenant_id`);
-- 消息模块
ALTER TABLE `tb_message` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_message` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_message_range` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_message_range` ADD INDEX `idx_tenant_id` (`tenant_id`);
ALTER TABLE `tb_message_receiver` ADD COLUMN `tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户ID';
ALTER TABLE `tb_message_receiver` ADD INDEX `idx_tenant_id` (`tenant_id`);
```
## 3. 实体类添加租户字段
### 3.1 实体类模板
```java
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.FieldFill;
@Data
@TableName("tb_workcase")
public class TbWorkcase extends Model<TbWorkcase> {
// ... 其他字段 ...
/**
* 租户ID
*/
@TableField(fill = FieldFill.INSERT)
private Long tenantId;
}
```
### 3.2 MyBatis-Plus 自动填充配置
```java
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
@Component
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 获取当前用户的租户ID
PigxUser user = SecurityUtils.getUser();
if (user != null) {
// 自动填充租户ID
this.strictInsertFill(metaObject, "tenantId", Long.class, user.getTenantId());
} else {
// 默认租户ID
this.strictInsertFill(metaObject, "tenantId", Long.class, 1L);
}
// 填充其他字段
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", String.class, user != null ? user.getUsername() : "system");
this.strictInsertFill(metaObject, "delFlag", String.class, "0");
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
PigxUser user = SecurityUtils.getUser();
if (user != null) {
this.strictUpdateFill(metaObject, "updateBy", String.class, user.getUsername());
}
}
}
```
## 4. 配置租户拦截器
### 4.1 MyBatis-Plus 租户拦截器配置
```java
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.schema.Column;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加租户拦截器
interceptor.addInnerInterceptor(tenantLineInnerInterceptor());
// 添加分页拦截器等其他拦截器
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
private TenantLineInnerInterceptor tenantLineInnerInterceptor() {
return new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从当前用户获取租户ID
PigxUser user = SecurityUtils.getUser();
Long tenantId = user != null ? user.getTenantId() : 1L;
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn() {
// 租户ID字段名
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 忽略不需要租户隔离的表
List<String> ignoreTables = Arrays.asList(
"sys_user", // 系统用户表
"sys_role", // 角色表
"sys_menu", // 菜单表
"sys_dict", // 字典表
"sys_log", // 日志表
"temp_user_mapping" // 临时映射表
);
return ignoreTables.contains(tableName);
}
});
}
}
```
## 5. Service 层实现租户隔离
### 5.1 查询时自动添加租户条件
```java
@Service
public class WorkcaseServiceImpl extends ServiceImpl<WorkcaseMapper, TbWorkcase>
implements WorkcaseService {
@Override
public List<TbWorkcase> listByStatus(String status) {
// 租户拦截器会自动添加 tenant_id 条件
return this.list(new QueryWrapper<TbWorkcase>()
.eq("status", status));
// 实际SQL: SELECT * FROM tb_workcase WHERE status = ? AND tenant_id = ?
}
@Override
public TbWorkcase getById(String id) {
// 自动添加租户条件,确保不会查询到其他租户的数据
return super.getById(id);
// 实际SQL: SELECT * FROM tb_workcase WHERE id = ? AND tenant_id = ?
}
}
```
### 5.2 保存时自动设置租户ID
```java
@Service
public class WorkcaseServiceImpl extends ServiceImpl<WorkcaseMapper, TbWorkcase>
implements WorkcaseService {
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean createWorkcase(TbWorkcase workcase) {
// 通过 MetaObjectHandler 自动填充 tenant_id
// 不需要手动设置
return this.save(workcase);
}
// 如果需要手动设置(不推荐)
@Override
public Boolean createWorkcaseManual(TbWorkcase workcase) {
PigxUser user = SecurityUtils.getUser();
workcase.setTenantId(user.getTenantId());
workcase.setCreateBy(user.getUsername());
workcase.setCreateTime(LocalDateTime.now());
return this.save(workcase);
}
}
```
## 6. 特殊场景处理
### 6.1 跨租户查询(管理员功能)
```java
@Service
public class AdminService {
@Autowired
private WorkcaseMapper workcaseMapper;
/**
* 管理员查询所有租户的数据
* 需要特殊权限
*/
@PreAuthorize("@pms.hasPermission('admin_cross_tenant')")
public List<TbWorkcase> listAllTenants() {
// 使用 @InterceptorIgnore 注解忽略租户拦截
return workcaseMapper.selectAllWithoutTenant();
}
}
// Mapper 接口
@Mapper
public interface WorkcaseMapper extends BaseMapper<TbWorkcase> {
@InterceptorIgnore(tenantLine = "true")
@Select("SELECT * FROM tb_workcase")
List<TbWorkcase> selectAllWithoutTenant();
}
```
### 6.2 定时任务中的租户处理
```java
@Component
public class WorkcaseScheduledTask {
@Autowired
private WorkcaseService workcaseService;
/**
* 定时任务:为每个租户执行任务
*/
@Scheduled(cron = "0 0 2 * * ?")
public void processAllTenants() {
// 获取所有租户列表
List<Long> tenantIds = getTenantIds();
for (Long tenantId : tenantIds) {
// 设置当前租户上下文
TenantContextHolder.setTenantId(tenantId);
try {
// 执行业务逻辑
processForTenant(tenantId);
} finally {
// 清除租户上下文
TenantContextHolder.clear();
}
}
}
}
```
### 6.3 异步任务中的租户传递
```java
@Service
public class AsyncService {
/**
* 异步任务需要传递租户ID
*/
@Async
public void processAsync(Long tenantId, String workcaseId) {
// 在异步线程中设置租户ID
TenantContextHolder.setTenantId(tenantId);
try {
// 执行业务逻辑
doProcess(workcaseId);
} finally {
TenantContextHolder.clear();
}
}
// 调用异步方法前获取租户ID
public void callAsync(String workcaseId) {
PigxUser user = SecurityUtils.getUser();
Long tenantId = user.getTenantId();
processAsync(tenantId, workcaseId);
}
}
```
## 7. 数据迁移时的租户处理
### 7.1 历史数据添加租户ID
```sql
-- 为历史数据设置默认租户ID
UPDATE tb_workcase SET tenant_id = 1 WHERE tenant_id IS NULL;
UPDATE tb_chat_room SET tenant_id = 1 WHERE tenant_id IS NULL;
UPDATE tb_agent SET tenant_id = 1 WHERE tenant_id IS NULL;
```
### 7.2 根据部门映射租户
```sql
-- 如果有部门与租户的映射关系
UPDATE tb_workcase w
JOIN sys_dept d ON w.dept_id = d.dept_id
SET w.tenant_id = d.tenant_id
WHERE w.tenant_id = 1;
```
## 8. 测试验证
### 8.1 租户隔离测试
```java
@SpringBootTest
@RunWith(SpringRunner.class)
public class TenantIsolationTest {
@Autowired
private WorkcaseService workcaseService;
@Test
@WithMockUser(authorities = {"workcase_view"})
public void testTenantIsolation() {
// 模拟租户1的用户
mockTenant(1L);
List<TbWorkcase> tenant1List = workcaseService.list();
// 模拟租户2的用户
mockTenant(2L);
List<TbWorkcase> tenant2List = workcaseService.list();
// 验证数据隔离
assertNotEquals(tenant1List, tenant2List);
}
private void mockTenant(Long tenantId) {
PigxUser user = new PigxUser();
user.setTenantId(tenantId);
// 设置到安全上下文
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities())
);
}
}
```
### 8.2 SQL 验证
```sql
-- 验证所有表都有租户字段
SELECT
TABLE_NAME,
COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = 'your_database'
AND COLUMN_NAME = 'tenant_id'
ORDER BY TABLE_NAME;
-- 验证数据分布
SELECT
tenant_id,
COUNT(*) as record_count
FROM tb_workcase
GROUP BY tenant_id;
-- 验证索引
SHOW INDEX FROM tb_workcase WHERE Column_name = 'tenant_id';
```
## 9. 注意事项
### 9.1 性能考虑
- 租户ID字段必须建立索引
- 常用查询条件可以建立复合索引status + tenant_id
- 大表可以考虑按租户分区
### 9.2 安全考虑
- 确保租户拦截器正确配置
- 敏感操作需要记录日志
- 跨租户操作需要特殊权限
### 9.3 开发规范
- 不要在代码中硬编码租户ID
- 始终从 SecurityUtils 获取当前租户
- 使用 MyBatis-Plus 的自动填充功能
### 9.4 数据备份
- 迁移前备份原始数据
- 记录租户ID映射关系
- 保留回滚方案
## 10. 故障排查
### 10.1 常见问题
**问题1查询不到数据**
- 检查租户ID是否正确
- 验证租户拦截器是否生效
- 查看生成的SQL是否包含tenant_id条件
**问题2插入数据失败**
- 检查tenant_id字段是否为NOT NULL
- 验证自动填充是否配置
- 确认当前用户有租户信息
**问题3跨租户数据泄露**
- 检查是否所有查询都经过租户拦截
- 验证忽略表配置是否正确
- 审查自定义SQL是否包含租户条件
### 10.2 调试建议
```yaml
# application.yml - 开启SQL日志
logging:
level:
com.pig4cloud.pigx.*.mapper: DEBUG
com.baomidou.mybatisplus: DEBUG
# 查看实际执行的SQL
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
```
## 11. 迁移检查清单
- [ ] 所有业务表添加 tenant_id 字段
- [ ] 所有实体类添加 tenantId 属性
- [ ] 配置 MyBatis-Plus 自动填充
- [ ] 配置租户拦截器
- [ ] 配置忽略表列表
- [ ] 历史数据设置默认租户ID
- [ ] 建立租户ID索引
- [ ] 测试租户隔离功能
- [ ] 测试跨租户查询权限
- [ ] 文档更新和团队培训