# 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**