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