diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..63da4213 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(ls:*)" + ] + } +} diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/README.md b/.kiro/specs/urbanlifeline-to-pigx-migration/README.md new file mode 100644 index 00000000..f5faf925 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/README.md @@ -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年* \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/database-migration-script.md b/.kiro/specs/urbanlifeline-to-pigx-migration/database-migration-script.md new file mode 100644 index 00000000..82467a78 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/database-migration-script.md @@ -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; +``` \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/database-migration.md b/.kiro/specs/urbanlifeline-to-pigx-migration/database-migration.md new file mode 100644 index 00000000..deffb54b --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/database-migration.md @@ -0,0 +1,571 @@ +# 数据库迁移脚本指南(PostgreSQL → MySQL) + +## 1. 概述 + +本文档提供了将 urbanLifeline 数据库从 PostgreSQL 迁移到 MySQL(pigx 平台)的完整脚本和指南。 + +## 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知识库ID(Dataset 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 租户拦截器 +- [ ] 执行应用集成测试 +- [ ] 准备回滚方案 \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/design.md b/.kiro/specs/urbanlifeline-to-pigx-migration/design.md new file mode 100644 index 00000000..73757050 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/design.md @@ -0,0 +1,547 @@ +# Design Document + +## Overview + +本设计文档描述了将 urbanLifelineServ 和 urbanLifelineWeb 的**业务功能**迁移到 pigx-ai 平台的技术方案。 + +### 核心原则 +- **只迁移业务代码**:招标、工单、平台管理、AI、消息等业务功能 +- **复用 pigx 基础设施**:人员、部门、权限、认证完全使用 pigx 原生实现 +- **适配 pigx 规范**:使用 PigxUser、R 响应格式、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 createWorkcase(@RequestBody TbWorkcaseDTO workcase) { + return ResultDomain.success(workcaseService.save(workcase)); +} + +// 目标代码 (pigx-app-server 使用 @HasPermission) +@HasPermission("workcase_ticket_add") +@PostMapping +public R 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 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 getWorkcaseList(@RequestBody TbWorkcaseDTO filter) { + return ResultDomain.success(workcaseService.list(filter)); +} + +// 目标代码 (自动添加租户和部门过滤) +@PostMapping("/list") +@HasPermission("workcase_ticket_view") +public R> getWorkcaseList(@RequestBody TbWorkcaseDTO filter) { + // pigx 会自动根据用户的租户ID和数据权限过滤数据 + return R.ok(workcaseService.list(filter)); +} +``` + +```java +// 源代码 (urbanLifelineServ 使用 ResultDomain) +@GetMapping("/list") +public ResultDomain list() { + // ResultDomain 包含 dataList 字段 + return ResultDomain.success(workcaseService.list()); +} + +// 目标代码 (使用 pigx R) +@GetMapping("/list") +public R> list() { + return R.ok(workcaseService.list()); +} + +// 或者使用分页 (pigx IPage) +@GetMapping("/page") +public R> page(Page page) { + return R.ok(workcaseService.page(page)); +} +``` + +**响应格式映射**: +| 源格式 (ResultDomain) | 目标格式 (R) | +|---------------------|----------------| +| 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 格式 + +**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 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** \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/frontend-migration-plan.md b/.kiro/specs/urbanlifeline-to-pigx-migration/frontend-migration-plan.md new file mode 100644 index 00000000..00dbcbe7 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/frontend-migration-plan.md @@ -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 { + code: number + message: string + success: boolean + data?: T + dataList?: T[] + pageDomain?: PageDomain +} + +// 目标代码 (pigx R) +// 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-next,pigx 使用 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 功能(最后) diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/permission-annotation-guide.md b/.kiro/specs/urbanlifeline-to-pigx-migration/permission-annotation-guide.md new file mode 100644 index 00000000..134264f8 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/permission-annotation-guide.md @@ -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 | R | +| 成功响应 | 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 createWorkcase(@RequestBody TbWorkcaseDTO workcase) { + return workcaseService.createWorkcase(workcase); +} + +// 转换后 +@PreAuthorize("@pms.hasPermission('workcase_ticket_add')") +public R 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> listWorkcase() { + return ResultDomain.success(workcaseService.list()); +} + +// 转换后 +@PreAuthorize("@pms.hasPermission('workcase_ticket_view') or @pms.hasPermission('workcase_ticket_admin')") +public R> 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 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 getById(@PathVariable String id) { + TbWorkcaseDTO workcase = workcaseService.getById(id); + if (workcase == null) { + return ResultDomain.fail("工单不存在"); + } + return ResultDomain.success(workcase); + } + + // 列表返回 + @GetMapping("/list") + public ResultDomain> list() { + List list = workcaseService.list(); + ResultDomain> result = ResultDomain.success(); + result.setDataList(list); // 注意:使用dataList字段 + return result; + } +} + +// 转换后 +@RestController +@RequestMapping("/workcase") +public class WorkcaseController { + + // 单个对象返回 + @GetMapping("/{id}") + public R getById(@PathVariable String id) { + TbWorkcaseDTO workcase = workcaseService.getById(id); + if (workcase == null) { + return R.failed("工单不存在"); + } + return R.ok(workcase); + } + + // 列表返回 + @GetMapping("/list") + public R> list() { + List list = workcaseService.list(); + return R.ok(list); // 直接返回列表,不使用dataList + } + + // 分页返回 + @GetMapping("/page") + public R> page(Page page) { + return R.ok(workcaseService.page(page)); + } +} +``` + +### 步骤4:数据权限适配 + +#### 4.1 添加租户隔离 + +```java +// 转换前 - 无租户隔离 +@Service +public class WorkcaseServiceImpl { + + public List listMyWorkcase() { + Long userId = JwtUtils.getUserId(); + return workcaseMapper.selectByUserId(userId); + } +} + +// 转换后 - 支持租户隔离 +@Service +public class WorkcaseServiceImpl { + + public List listMyWorkcase() { + PigxUser user = SecurityUtils.getUser(); + + QueryWrapper 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 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 格式 +- [ ] 前端能正确解析新的响应格式 +- [ ] 错误信息正确传递 + +### 数据权限测试 + +- [ ] 租户数据隔离正常 +- [ ] 部门数据权限正常 +- [ ] 个人数据权限正常 + +## 常见问题 + +### 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) \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/permission-conversion-guide.md b/.kiro/specs/urbanlifeline-to-pigx-migration/permission-conversion-guide.md new file mode 100644 index 00000000..8ecfca4b --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/permission-conversion-guide.md @@ -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` | `R` | +| 租户支持 | 无 | 有 (tenant_id) | + +## 转换步骤详解 + +### 步骤 1:权限注解转换 + +#### 1.1 基本转换规则 + +```java +// ❌ 旧代码 (urbanLifeline) +@PreAuthorize("hasAuthority('workcase:ticket:create')") +public ResultDomain createWorkcase(@RequestBody TbWorkcaseDTO dto) { + // ... +} + +// ✅ 新代码 (pigx) +@PreAuthorize("@pms.hasPermission('workcase_ticket_add')") +public R 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 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 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(PageParam param) { + Page page = workcaseMapper.selectPage(param); + return ResultDomain.success(page.getRecords(), page.getTotal()); +} + +// ✅ 新代码 +public R> list(Page page, TbWorkcaseDTO query) { + IPage 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 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> page(Page page, TbWorkcaseDTO query) { + // ✅ 查询条件自动添加租户过滤 + PigxUser user = SecurityUtils.getUser(); + query.setTenantId(user.getTenantId()); + + return R.ok(workcaseMapper.selectPageVo(page, query)); + } +} +``` + +#### 4.3 Mapper 层租户隔离 + +```xml + + +``` + +### 步骤 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 selectById(@PathVariable("id") Long id); + + /** + * 根据用户名查询用户信息 + */ + @GetMapping("/user/info") + R selectByUsername(@RequestParam("username") String username); + + /** + * 批量查询用户信息 + */ + @PostMapping("/user/list") + R> selectBatchIds(@RequestBody List 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 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 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. 充分测试验证 + +建议分模块逐步迁移,每完成一个模块就进行测试验证。 \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/permission-mapping.md b/.kiro/specs/urbanlifeline-to-pigx-migration/permission-mapping.md new file mode 100644 index 00000000..04c097b2 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/permission-mapping.md @@ -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 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 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 +``` \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/pigx-dify-architecture.md b/.kiro/specs/urbanlifeline-to-pigx-migration/pigx-dify-architecture.md new file mode 100644 index 00000000..754777ca --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/pigx-dify-architecture.md @@ -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 + + + + 4.0.0 + + com.pig4cloud + pigx + 6.4.0 + + + pigx-dify + pom + Dify AI integration module + + + pigx-dify-api + pigx-dify-biz + + +``` + +### 2.2 pigx-dify-api 结构 + +```xml + + + + + com.pig4cloud + pigx-dify + 6.4.0 + + + pigx-dify-api + Dify API interfaces and entities + + + + com.pig4cloud + pigx-common-core + + + com.baomidou + mybatis-plus-annotation + + + org.springdoc + springdoc-openapi-starter-webmvc-api + + + +``` + +### 2.3 pigx-dify-biz 结构 + +```xml + + + + + com.pig4cloud + pigx-dify + 6.4.0 + + + pigx-dify-biz + Dify business implementation + + + + + com.pig4cloud + pigx-dify-api + + + com.pig4cloud + pigx-common-security + + + com.pig4cloud + pigx-common-log + + + com.pig4cloud + pigx-common-mybatis + + + com.pig4cloud + pigx-common-swagger + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-undertow + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.apache.httpcomponents.client5 + httpclient5 + + + org.springframework + spring-webflux + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +## 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 { + + @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 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 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 + + + + + +``` + +## 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 +- [ ] 配置服务注册和发现 +- [ ] 数据库表结构迁移 +- [ ] 前端页面迁移 +- [ ] 集成测试 +- [ ] 部署配置 \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/requirements.md b/.kiro/specs/urbanlifeline-to-pigx-migration/requirements.md new file mode 100644 index 00000000..89afda5d --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/requirements.md @@ -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**: 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 的租户和部门隔离机制 \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/security-config-guide.md b/.kiro/specs/urbanlifeline-to-pigx-migration/security-config-guide.md new file mode 100644 index 00000000..3cad9fb1 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/security-config-guide.md @@ -0,0 +1,663 @@ +# SecurityUtils 和 RemoteUserService 配置指南 + +## 1. 概述 + +本指南详细说明如何在 pigx 平台中配置和使用 SecurityUtils 和 RemoteUserService,实现用户信息获取和远程用户服务调用。 + +## 2. SecurityUtils 使用指南 + +### 2.1 SecurityUtils 介绍 + +SecurityUtils 是 pigx 平台提供的安全工具类,用于获取当前登录用户信息、权限判断等安全相关操作。 + +### 2.2 Maven 依赖 + +```xml + + com.pig4cloud + pigx-common-security + +``` + +### 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 roles = pigxUser.getRoles(); + + // 获取用户权限列表 + Collection 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 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 + + com.pig4cloud + pigx-upms-api + +``` + +### 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 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 result = remoteUserService.info(username, SecurityConstants.FROM_IN); + + if (result.isSuccess() && result.getData() != null) { + return result.getData().getSysUser(); + } + + throw new BusinessException("用户不存在"); + } + + /** + * 批量获取用户信息 + */ + public List getUsersByIds(List userIds) { + List users = new ArrayList<>(); + + for (Long userId : userIds) { + R 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 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 selectById(Long id, String from) { + log.error("调用用户服务失败", throwable); + return R.failed("用户服务暂时不可用"); + } + + @Override + public R 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 result = remoteDeptService.selectById(deptId, SecurityConstants.FROM_IN); + + if (result.isSuccess() && result.getData() != null) { + return result.getData(); + } + + return null; + } + + /** + * 获取部门树 + */ + public List getDeptTree() { + R> 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 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 getUserMap(List 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 listByTenant() { + PigxUser user = SecurityUtils.getUser(); + + QueryWrapper 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 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 + 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 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 listMyWorkcase() { + PigxUser user = SecurityUtils.getUser(); + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("tenant_id", user.getTenantId()) + .eq("create_by", user.getUsername()) + .orderByDesc("create_time"); + + return this.list(wrapper); + } +} +``` \ No newline at end of file diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/tasks.md b/.kiro/specs/urbanlifeline-to-pigx-migration/tasks.md new file mode 100644 index 00000000..f4fdceca --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/tasks.md @@ -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 表中执行菜单配置 SQL(ID从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、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 + - _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 响应格式 + - 更新 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、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模型列表 +- 通用HTTP:GET、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) diff --git a/.kiro/specs/urbanlifeline-to-pigx-migration/tenant-isolation-guide.md b/.kiro/specs/urbanlifeline-to-pigx-migration/tenant-isolation-guide.md new file mode 100644 index 00000000..87e225c9 --- /dev/null +++ b/.kiro/specs/urbanlifeline-to-pigx-migration/tenant-isolation-guide.md @@ -0,0 +1,505 @@ +# 租户字段添加指南 + +## 概述 +pigx 是一个多租户 SaaS 平台,所有业务数据需要通过租户ID(tenant_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 { + + // ... 其他字段 ... + + /** + * 租户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 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 + implements WorkcaseService { + + @Override + public List listByStatus(String status) { + // 租户拦截器会自动添加 tenant_id 条件 + return this.list(new QueryWrapper() + .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 + 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 listAllTenants() { + // 使用 @InterceptorIgnore 注解忽略租户拦截 + return workcaseMapper.selectAllWithoutTenant(); + } +} + +// Mapper 接口 +@Mapper +public interface WorkcaseMapper extends BaseMapper { + + @InterceptorIgnore(tenantLine = "true") + @Select("SELECT * FROM tb_workcase") + List selectAllWithoutTenant(); +} +``` + +### 6.2 定时任务中的租户处理 +```java +@Component +public class WorkcaseScheduledTask { + + @Autowired + private WorkcaseService workcaseService; + + /** + * 定时任务:为每个租户执行任务 + */ + @Scheduled(cron = "0 0 2 * * ?") + public void processAllTenants() { + // 获取所有租户列表 + List 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 tenant1List = workcaseService.list(); + + // 模拟租户2的用户 + mockTenant(2L); + List 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索引 +- [ ] 测试租户隔离功能 +- [ ] 测试跨租户查询权限 +- [ ] 文档更新和团队培训 \ No newline at end of file