Refactor project pages and update related docs
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -97,3 +97,7 @@ tongweb_62318.properties
|
|||||||
.superpowers/
|
.superpowers/
|
||||||
|
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
|
.codegraph/
|
||||||
|
|
||||||
|
.claude/
|
||||||
669
CLAUDE.md
669
CLAUDE.md
@@ -1,669 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## 快速参考
|
|
||||||
|
|
||||||
**启动项目:**
|
|
||||||
- 后端: `mvn spring-boot:run` 或运行 `ry.bat`
|
|
||||||
- 前端: `cd ruoyi-ui && npm run dev`
|
|
||||||
|
|
||||||
**访问地址:**
|
|
||||||
- 前端: http://localhost:80
|
|
||||||
- 后端: http://localhost:8080
|
|
||||||
- Swagger: http://localhost:8080/swagger-ui/index.html
|
|
||||||
- Druid 监控: http://localhost:8080/druid/ (ruoyi/123456)
|
|
||||||
|
|
||||||
**测试账号:**
|
|
||||||
- 用户名: `admin`
|
|
||||||
- 密码: `admin123`
|
|
||||||
|
|
||||||
**获取 Token:**
|
|
||||||
```bash
|
|
||||||
POST http://localhost:8080/login/test?username=admin&password=admin123
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
**纪检初核系统** - 基于 **若依管理系统 v3.9.1** 构建的企业级前后端分离管理系统,用于员工异常行为风险识别。
|
|
||||||
|
|
||||||
### 技术栈版本
|
|
||||||
|
|
||||||
| 后端技术 | 版本 | 前端技术 | 版本 |
|
|
||||||
|-----------------------------|--------|------------|---------|
|
|
||||||
| Spring Boot | 3.5.8 | Vue.js | 2.6.12 |
|
|
||||||
| Java | 21 | Element UI | 2.15.14 |
|
|
||||||
| MyBatis Spring Boot Starter | 3.0.5 | Vuex | 3.6.0 |
|
|
||||||
| MySQL Connector | 8.2.0 | Vue Router | 3.4.9 |
|
|
||||||
| SpringDoc OpenAPI | 2.8.14 | Axios | 0.28.1 |
|
|
||||||
| EasyExcel | 3.3.4 | ECharts | 5.4.0 |
|
|
||||||
| Quartz | 2.5.2 | Sass | 1.32.13 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常用命令
|
|
||||||
|
|
||||||
### 后端 (Maven)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 编译项目
|
|
||||||
mvn clean compile
|
|
||||||
|
|
||||||
# 运行应用 (开发环境)
|
|
||||||
mvn spring-boot:run
|
|
||||||
|
|
||||||
# 打包部署
|
|
||||||
mvn clean package
|
|
||||||
|
|
||||||
# Windows 启动
|
|
||||||
ry.bat
|
|
||||||
|
|
||||||
# Linux/Mac 启动
|
|
||||||
./ry.sh start
|
|
||||||
```
|
|
||||||
|
|
||||||
### 前端 (npm)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ruoyi-ui
|
|
||||||
|
|
||||||
# 安装依赖 (推荐使用国内镜像)
|
|
||||||
npm install --registry=https://registry.npmmirror.com
|
|
||||||
|
|
||||||
# 开发服务器 (端口 80)
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# 生产构建
|
|
||||||
npm run build:prod
|
|
||||||
|
|
||||||
# 预览生产构建
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
### 数据库初始化
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 初始化若依框架基础表
|
|
||||||
mysql -u root -p < sql/ry_20250522.sql
|
|
||||||
|
|
||||||
# 初始化定时任务表
|
|
||||||
mysql -u root -p < sql/quartz.sql
|
|
||||||
|
|
||||||
# 导入业务表(根据需要执行)
|
|
||||||
mysql -u root -p ccdi < sql/dpc_employee.sql
|
|
||||||
mysql -u root -p ccdi < sql/dpc_intermediary_blacklist.sql
|
|
||||||
# ... 其他业务表脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意:**
|
|
||||||
- 业务表脚本文件名以 `ccdi_` 或 `dpc_` 开头
|
|
||||||
- 部分脚本包含菜单数据,需要按顺序执行
|
|
||||||
- 数据库需要先创建(数据库名: `ccdi`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 模块架构
|
|
||||||
|
|
||||||
```
|
|
||||||
ccdi/
|
|
||||||
├── ruoyi-admin/ # 主应用入口 (Spring Boot 启动类)
|
|
||||||
├── ruoyi-framework/ # 核心框架 (Security, Config, Filters)
|
|
||||||
├── ruoyi-system/ # 系统管理 (Users, Roles, Menus, Depts)
|
|
||||||
├── ruoyi-common/ # 通用工具 (annotations, utils, constants)
|
|
||||||
├── ruoyi-quartz/ # 定时任务
|
|
||||||
├── ruoyi-generator/ # 代码生成器
|
|
||||||
├── ccdi-info-collection/ # 【核心业务模块】信息采集
|
|
||||||
├── ccdi-project/ # 【核心业务模块】项目管理
|
|
||||||
├── ccdi-lsfx/ # 【核心业务模块】流水分析对接
|
|
||||||
├── lsfx-mock-server/ # 流水分析模拟服务器 (Python)
|
|
||||||
├── ruoyi-ui/ # 前端 Vue 应用
|
|
||||||
├── sql/ # 数据库脚本
|
|
||||||
├── bin/ # 启动脚本
|
|
||||||
└── doc/ # 项目文档
|
|
||||||
```
|
|
||||||
|
|
||||||
### 模块依赖关系
|
|
||||||
|
|
||||||
```
|
|
||||||
ruoyi-admin (启动模块)
|
|
||||||
├── ruoyi-framework (核心安全配置)
|
|
||||||
├── ruoyi-system (系统核心业务)
|
|
||||||
├── ruoyi-common (共享工具)
|
|
||||||
├── ruoyi-quartz (定时任务)
|
|
||||||
├── ruoyi-generator (代码生成)
|
|
||||||
├── ccdi-info-collection (信息采集模块)
|
|
||||||
│ └── 依赖 ruoyi-common
|
|
||||||
├── ccdi-project (项目管理模块)
|
|
||||||
│ └── 依赖 ruoyi-common
|
|
||||||
└── ccdi-lsfx (流水分析对接模块)
|
|
||||||
└── 依赖 ruoyi-common
|
|
||||||
```
|
|
||||||
|
|
||||||
**添加新业务模块:**
|
|
||||||
1. 在根目录 `pom.xml` 的 `<modules>` 中添加新模块
|
|
||||||
2. 在新模块的 `pom.xml` 中添加对 `ruoyi-common` 的依赖
|
|
||||||
3. 在 `ruoyi-admin/pom.xml` 中添加对新模块的依赖
|
|
||||||
4. 在新模块中按照分层规范创建 controller/service/mapper/domain 包
|
|
||||||
|
|
||||||
### ccdi-info-collection 业务模块 (核心)
|
|
||||||
|
|
||||||
自定义业务模块,包含以下核心功能:
|
|
||||||
|
|
||||||
| 功能 | Controller | 实体类 |
|
|
||||||
|----------|---------------------------------------|-----------------------------|
|
|
||||||
| 员工基础信息 | CcdiBaseStaffController | CcdiBaseStaff |
|
|
||||||
| 中介黑名单 | CcdiIntermediaryController | CcdiBizIntermediary |
|
|
||||||
| 员工家庭关系 | CcdiStaffFmyRelationController | CcdiStaffFmyRelation |
|
|
||||||
| 员工企业关系 | CcdiStaffEnterpriseRelationController | CcdiStaffEnterpriseRelation |
|
|
||||||
| 信贷客户家庭关系 | CcdiCustFmyRelationController | CcdiCustFmyRelation |
|
|
||||||
| 信贷客户企业关系 | CcdiCustEnterpriseRelationController | CcdiCustEnterpriseRelation |
|
|
||||||
| 员工调动记录 | CcdiStaffTransferController | CcdiStaffTransfer |
|
|
||||||
| 员工招聘记录 | CcdiStaffRecruitmentController | CcdiStaffRecruitment |
|
|
||||||
| 采购交易 | CcdiPurchaseTransactionController | CcdiPurchaseTransaction |
|
|
||||||
|
|
||||||
**分层结构:**
|
|
||||||
|
|
||||||
- Controller: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/`
|
|
||||||
- Service: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/`
|
|
||||||
- Mapper: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/`
|
|
||||||
- Domain: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/`
|
|
||||||
- dto/: 数据传输对象
|
|
||||||
- vo/: 视图对象
|
|
||||||
- excel/: Excel导入导出实体
|
|
||||||
- XML映射: `ccdi-info-collection/src/main/resources/mapper/info/collection/`
|
|
||||||
|
|
||||||
### ccdi-project 业务模块 (核心)
|
|
||||||
|
|
||||||
项目管理模块,用于管理纪检初核项目的全生命周期:
|
|
||||||
|
|
||||||
**核心功能:**
|
|
||||||
- 项目创建、更新、删除、查询
|
|
||||||
- 项目状态管理 (进行中、已完成、已归档)
|
|
||||||
- 项目统计(按状态统计数量)
|
|
||||||
- 模型参数配置管理
|
|
||||||
|
|
||||||
**主要 Controller:**
|
|
||||||
- CcdiProjectController: 项目管理
|
|
||||||
- CcdiModelParamController: 模型参数配置
|
|
||||||
|
|
||||||
**分层结构:**
|
|
||||||
- Controller: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/`
|
|
||||||
- Service: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/`
|
|
||||||
- Mapper: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/`
|
|
||||||
- Domain: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/`
|
|
||||||
- XML映射: `ccdi-project/src/main/resources/mapper/ccdi/project/`
|
|
||||||
|
|
||||||
### ccdi-lsfx 业务模块 (核心)
|
|
||||||
|
|
||||||
流水分析平台对接模块,用于与外部流水分析系统交互:
|
|
||||||
|
|
||||||
**核心功能:**
|
|
||||||
- 获取访问令牌 (Token)
|
|
||||||
- 上传流水文件并解析
|
|
||||||
- 拉取行内流水数据
|
|
||||||
- 查询解析状态和结果
|
|
||||||
- 获取银行流水明细
|
|
||||||
|
|
||||||
**主要组件:**
|
|
||||||
- LsfxAnalysisClient: 流水分析平台客户端
|
|
||||||
- LsfxTestController: 测试接口
|
|
||||||
|
|
||||||
**配置项 (application-dev.yml):**
|
|
||||||
```yaml
|
|
||||||
lsfx:
|
|
||||||
api:
|
|
||||||
base-url: http://localhost:8000 # 流水分析平台地址
|
|
||||||
app-id: your-app-id
|
|
||||||
app-secret: your-app-secret
|
|
||||||
client-id: your-client-id
|
|
||||||
endpoints:
|
|
||||||
get-token: /api/auth/token
|
|
||||||
upload-file: /api/files/upload
|
|
||||||
fetch-inner-flow: /api/flow/inner
|
|
||||||
```
|
|
||||||
|
|
||||||
**分层结构:**
|
|
||||||
- Client: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/`
|
|
||||||
- Controller: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/`
|
|
||||||
- Domain: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/`
|
|
||||||
- request/: 请求对象
|
|
||||||
- response/: 响应对象
|
|
||||||
- Config: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/`
|
|
||||||
|
|
||||||
### lsfx-mock-server (开发测试工具)
|
|
||||||
|
|
||||||
Python 实现的流水分析平台模拟服务器,用于本地开发和测试:
|
|
||||||
|
|
||||||
**用途:**
|
|
||||||
- 模拟流水分析平台的 API 接口
|
|
||||||
- 提供测试数据和模拟响应
|
|
||||||
- 支持错误场景模拟
|
|
||||||
|
|
||||||
**启动方式:**
|
|
||||||
```bash
|
|
||||||
cd lsfx-mock-server
|
|
||||||
python app.py # 默认监听 http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 后端开发规范
|
|
||||||
|
|
||||||
### 通用规范
|
|
||||||
|
|
||||||
- **新模块命名**: 项目英文名首字母集合 + 主要功能 (如 `ruoyi-info-collection`)
|
|
||||||
- **代码分离**: 新功能代码与若依框架自带代码分离,Controller 放在新模块中
|
|
||||||
- **审计字段**: 实体类不继承 BaseEntity,单独添加审计字段,通过注释实现自动插入
|
|
||||||
|
|
||||||
### Java 代码风格
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 使用 @Data 注解
|
|
||||||
@Data
|
|
||||||
public class CcdiBaseStaff {
|
|
||||||
// 审计字段通过注释实现自动插入
|
|
||||||
/** 创建者 */
|
|
||||||
private String createBy;
|
|
||||||
/** 创建时间 */
|
|
||||||
private Date createTime;
|
|
||||||
/** 更新者 */
|
|
||||||
private String updateBy;
|
|
||||||
/** 更新时间 */
|
|
||||||
private Date updateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务层使用 @Resource 注入
|
|
||||||
@Resource
|
|
||||||
private ICcdiBaseStaffService baseStaffService;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 分层规范
|
|
||||||
|
|
||||||
- **Controller**: 所有接口添加 Swagger 注释,分页使用 MyBatis Plus Page
|
|
||||||
- **Service**: 简单 CRUD 用 MyBatis Plus 方法,复杂操作在 XML 写 SQL
|
|
||||||
- **DTO/VO**: 接口传参使用独立 DTO,返回使用独立 VO,不与 entity 混用
|
|
||||||
- **Mapper**: 简单操作继承 BaseMapper,复杂操作在 XML 中定义
|
|
||||||
|
|
||||||
### 禁止事项
|
|
||||||
|
|
||||||
- **禁止使用全限定类名**: 必须使用 `import` 语句导入类,不要在代码中使用 `java.util.List` 这样的全限定名
|
|
||||||
- **禁止使用 `extends ServiceImpl<>`**: Service 接口和实现类分离定义
|
|
||||||
- **禁止 Entity 混用**: DTO、VO、Excel 类必须独立,不与 Entity 混用
|
|
||||||
- **禁止缺少 `@Resource`**: Service 注入必须使用 `@Resource` 注解
|
|
||||||
|
|
||||||
### API 响应格式
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 成功
|
|
||||||
AjaxResult.success("操作成功", data);
|
|
||||||
|
|
||||||
// 错误
|
|
||||||
AjaxResult.error("操作失败");
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
Page<CcdiBaseStaff> page = new Page<>(pageNum, pageSize);
|
|
||||||
IPage<CcdiBaseStaff> result = baseStaffMapper.selectPage(page, queryWrapper);
|
|
||||||
return AjaxResult.success(result);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 前端开发规范
|
|
||||||
|
|
||||||
### 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
ruoyi-ui/src/
|
|
||||||
├── api/ # API 请求定义 (与后端 Controller 对应)
|
|
||||||
├── views/ # 页面组件 (按功能模块组织)
|
|
||||||
│ ├── ccdiBaseStaff/
|
|
||||||
│ ├── ccdiIntermediary/
|
|
||||||
│ └── ...
|
|
||||||
├── components/ # 可复用组件 (复杂组件需拆分)
|
|
||||||
├── router/ # 路由配置
|
|
||||||
└── store/ # Vuex 状态管理
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 调用示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
export function listStaff(query) {
|
|
||||||
return request({
|
|
||||||
url: '/ccdi/baseStaff/list',
|
|
||||||
method: 'get',
|
|
||||||
params: query
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 菜单联动
|
|
||||||
|
|
||||||
添加页面和组件后,需要同步修改数据库中的菜单表 (`sys_menu`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 特殊功能
|
|
||||||
|
|
||||||
### 异步导入
|
|
||||||
|
|
||||||
支持大数据量异步 Excel 导入,通过 taskId 查询导入状态:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PostMapping("/import")
|
|
||||||
public AjaxResult asyncImport(@RequestParam("file") MultipartFile file) {
|
|
||||||
String taskId = asyncImportService.startImport(file);
|
|
||||||
return AjaxResult.success("导入任务已启动", taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/import/status/{taskId}")
|
|
||||||
public AjaxResult getImportStatus(@PathVariable String taskId) {
|
|
||||||
return AjaxResult.success(asyncImportService.getStatus(taskId));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**导入流程:**
|
|
||||||
1. 前端上传 Excel 文件
|
|
||||||
2. 后端异步处理,返回 taskId
|
|
||||||
3. 前端轮询 `/import/status/{taskId}` 获取导入进度
|
|
||||||
4. 导入完成后,可获取成功/失败数据统计
|
|
||||||
|
|
||||||
**导入结果处理:**
|
|
||||||
- 只返回导入失败的数据(含失败原因)
|
|
||||||
- 成功数据不返回,减少响应体积
|
|
||||||
- 支持批量插入,提高性能
|
|
||||||
|
|
||||||
### EasyExcel 字典下拉框
|
|
||||||
|
|
||||||
导入模板支持字典下拉框配置,提升数据录入准确性。使用 `DictDropdownWriteHandler` 实现。
|
|
||||||
|
|
||||||
### 权限控制
|
|
||||||
|
|
||||||
基于 Spring Security + JWT 的角色菜单权限系统:
|
|
||||||
|
|
||||||
- 权限格式: `system:user:edit`, `ccdi:staff:list`
|
|
||||||
- 数据权限: 支持全部、自定义、部门等范围
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 测试与验证
|
|
||||||
|
|
||||||
### 测试账号
|
|
||||||
|
|
||||||
- **用户名**: `admin`
|
|
||||||
- **密码**: `admin123`
|
|
||||||
|
|
||||||
### 登录获取 Token
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 登录接口
|
|
||||||
POST /login/test?username=admin&password=admin123
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 文档
|
|
||||||
|
|
||||||
- **Swagger UI**: `/swagger-ui/index.html`
|
|
||||||
- **API Docs**: `/v3/api-docs`
|
|
||||||
|
|
||||||
### 测试规范
|
|
||||||
|
|
||||||
- 不在命令行启动后端进行测试
|
|
||||||
- 生成可执行的测试脚本进行验证
|
|
||||||
- 测试完成后保存接口输出并生成测试用例报告
|
|
||||||
|
|
||||||
### 开发调试技巧
|
|
||||||
|
|
||||||
**使用 Swagger 测试接口:**
|
|
||||||
1. 访问 `/swagger-ui/index.html`
|
|
||||||
2. 点击接口展开详情
|
|
||||||
3. 点击 "Try it out" 进行测试
|
|
||||||
4. 填写参数后点击 "Execute" 执行
|
|
||||||
|
|
||||||
**查看 SQL 执行日志:**
|
|
||||||
- 在 `application.yml` 中设置日志级别: `com.ruoyi: debug`
|
|
||||||
- 使用 Druid 监控台查看慢 SQL
|
|
||||||
|
|
||||||
**前端代理配置:**
|
|
||||||
前端开发服务器通过代理转发请求到后端:
|
|
||||||
- 前端地址: `http://localhost:80`
|
|
||||||
- 后端地址: `http://localhost:8080`
|
|
||||||
- 代理配置文件: `ruoyi-ui/vue.config.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
| 配置项 | 值 |
|
|
||||||
|---------|-------------------|
|
|
||||||
| 后端端口 | 8080 |
|
|
||||||
| 前端开发端口 | 80 |
|
|
||||||
| 默认管理员 | admin/admin123 |
|
|
||||||
| JWT 有效期 | 30 分钟 |
|
|
||||||
| 文件上传限制 | 单文件 10MB, 总计 20MB |
|
|
||||||
|
|
||||||
### 配置文件位置
|
|
||||||
|
|
||||||
| 配置 | 路径 |
|
|
||||||
|----------|------------------------------------------------------|
|
|
||||||
| 主配置 | `ruoyi-admin/src/main/resources/application.yml` |
|
|
||||||
| 开发环境 | `ruoyi-admin/src/main/resources/application-dev.yml` |
|
|
||||||
| 数据库连接 | `application-dev.yml` |
|
|
||||||
| Redis 配置 | `application-dev.yml` |
|
|
||||||
|
|
||||||
### 数据源配置
|
|
||||||
|
|
||||||
项目使用 Druid 连接池,支持主从分离(默认关闭从库):
|
|
||||||
|
|
||||||
- **数据库连接**: `jdbc:mysql://host:3306/ccdi`
|
|
||||||
- **初始连接数**: 5
|
|
||||||
- **最小连接数**: 10
|
|
||||||
- **最大连接数**: 20
|
|
||||||
- **慢 SQL 记录**: 超过 1000ms 的 SQL 会被记录
|
|
||||||
|
|
||||||
### Redis 配置
|
|
||||||
|
|
||||||
- **默认端口**: 6379
|
|
||||||
- **数据库索引**: 0
|
|
||||||
- **连接超时**: 10s
|
|
||||||
|
|
||||||
### 流水分析平台配置
|
|
||||||
|
|
||||||
项目集成了外部流水分析平台,配置项位于 `application-dev.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
lsfx:
|
|
||||||
api:
|
|
||||||
base-url: http://localhost:8000 # 流水分析平台基础地址
|
|
||||||
app-id: ccdi-app # 应用ID
|
|
||||||
app-secret: ccdi-secret-2024 # 应用密钥
|
|
||||||
client-id: ccdi-client # 客户端ID
|
|
||||||
endpoints:
|
|
||||||
get-token: /api/auth/token # 获取令牌接口
|
|
||||||
upload-file: /api/files/upload # 文件上传接口
|
|
||||||
fetch-inner-flow: /api/flow/inner # 拉取行内流水接口
|
|
||||||
```
|
|
||||||
|
|
||||||
**开发环境使用 Mock 服务器:**
|
|
||||||
- 本地开发时,将 `base-url` 设置为 `http://localhost:8000`
|
|
||||||
- 启动 `lsfx-mock-server` 提供模拟接口
|
|
||||||
- 生产环境替换为真实的流水分析平台地址
|
|
||||||
|
|
||||||
### MCP 配置
|
|
||||||
|
|
||||||
项目使用 MCP (Model Context Protocol) 连接数据库,配置文件: `.mcp.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"mysql": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@fhuang/mcp-mysql-server"],
|
|
||||||
"env": {
|
|
||||||
"MYSQL_HOST": "116.62.17.81",
|
|
||||||
"MYSQL_PORT": "3306",
|
|
||||||
"MYSQL_USER": "root",
|
|
||||||
"MYSQL_PASSWORD": "Kfcx@1234",
|
|
||||||
"MYSQL_DATABASE": "ccdi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用场景:**
|
|
||||||
- 通过 MCP 工具直接查询和操作数据库
|
|
||||||
- 在开发过程中快速验证数据
|
|
||||||
- 生成测试数据和调试 SQL
|
|
||||||
|
|
||||||
### Druid 监控台
|
|
||||||
|
|
||||||
访问地址: `http://localhost:8080/druid/`
|
|
||||||
- 用户名: `ruoyi`
|
|
||||||
- 密码: `123456`
|
|
||||||
|
|
||||||
用于监控 SQL 执行情况、连接池状态等。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 重要文件路径
|
|
||||||
|
|
||||||
| 用途 | 路径 |
|
|
||||||
|---------------|--------------------------------------------------------------------------------|
|
|
||||||
| 应用入口 | `ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java` |
|
|
||||||
| 安全配置 | `ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java` |
|
|
||||||
| 信息采集 Controller | `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/` |
|
|
||||||
| 信息采集 Mapper XML | `ccdi-info-collection/src/main/resources/mapper/info/collection/` |
|
|
||||||
| 项目管理 Controller | `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/` |
|
|
||||||
| 项目管理 Mapper XML | `ccdi-project/src/main/resources/mapper/ccdi/project/` |
|
|
||||||
| 流水分析 Client | `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java` |
|
|
||||||
| Vue 路由 | `ruoyi-ui/src/router/index.js` |
|
|
||||||
| Vuex Store | `ruoyi-ui/src/store/` |
|
|
||||||
| 前端 API | `ruoyi-ui/src/api/` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 数据库规范
|
|
||||||
|
|
||||||
- **新建表名**: 需要加上项目英文名首字母集合前缀 `ccdi_` (如 `ccdi_base_staff`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 文档管理
|
|
||||||
|
|
||||||
- **文档语言**: 使用简体中文编写 .md 文档
|
|
||||||
- **文档目录**: 所有生成的文档放在 `doc/` 目录下,按类型分类
|
|
||||||
- **需求分析**: 在 `doc/` 目录下新建文件夹,以需求内容命名
|
|
||||||
|
|
||||||
### doc 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
doc/
|
|
||||||
├── api-docs/ # API 文档
|
|
||||||
├── database/ # 数据库相关
|
|
||||||
├── design/ # 设计文档
|
|
||||||
├── implementation/ # 实施文档
|
|
||||||
├── requirements/ # 需求文档
|
|
||||||
└── test-scripts/ # 测试脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenSpec 工作流
|
|
||||||
|
|
||||||
项目使用 OpenSpec 进行规范驱动开发,参考 `openspec/AGENTS.md`。
|
|
||||||
|
|
||||||
### 何时创建 Proposal
|
|
||||||
|
|
||||||
**需要创建:**
|
|
||||||
|
|
||||||
- 新功能或能力
|
|
||||||
- 破坏性变更 (API, 数据库结构)
|
|
||||||
- 架构变更
|
|
||||||
- 改变行为的性能优化
|
|
||||||
|
|
||||||
**无需创建:**
|
|
||||||
|
|
||||||
- Bug 修复 (恢复预期行为)
|
|
||||||
- 拼写错误、格式、注释
|
|
||||||
- 非破坏性依赖更新
|
|
||||||
- 配置变更
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 沟通规范
|
|
||||||
|
|
||||||
- 永远使用简体中文进行思考和对话
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题排查
|
|
||||||
|
|
||||||
### 数据库连接失败
|
|
||||||
|
|
||||||
**检查项:**
|
|
||||||
1. 确认 MySQL 服务已启动
|
|
||||||
2. 检查 `application-dev.yml` 中的数据库连接配置
|
|
||||||
3. 确认数据库用户名和密码正确
|
|
||||||
4. 检查数据库是否已创建(数据库名: `ccdi`)
|
|
||||||
|
|
||||||
### Redis 连接失败
|
|
||||||
|
|
||||||
**检查项:**
|
|
||||||
1. 确认 Redis 服务已启动
|
|
||||||
2. 检查 `application-dev.yml` 中的 Redis 配置
|
|
||||||
3. 如果 Redis 不需要密码,将 `password` 配置注释掉
|
|
||||||
|
|
||||||
### 前端无法访问后端接口
|
|
||||||
|
|
||||||
**检查项:**
|
|
||||||
1. 确认后端已启动(端口 8080)
|
|
||||||
2. 检查前端代理配置(`ruoyi-ui/vue.config.js`)
|
|
||||||
3. 确认后端接口路径正确(查看 Controller 的 `@RequestMapping`)
|
|
||||||
|
|
||||||
### 导入功能无响应
|
|
||||||
|
|
||||||
**检查项:**
|
|
||||||
1. 检查文件大小是否超过限制(默认 10MB)
|
|
||||||
2. 查看后端日志是否有异常
|
|
||||||
3. 确认 Excel 模板格式正确
|
|
||||||
4. 检查必填字段是否为空
|
|
||||||
|
|
||||||
### 流水分析平台连接失败
|
|
||||||
|
|
||||||
**检查项:**
|
|
||||||
1. 确认 `lsfx-mock-server` 已启动(开发环境)
|
|
||||||
2. 检查 `application-dev.yml` 中的 `lsfx.api.base-url` 配置
|
|
||||||
3. 验证 app-id、app-secret、client-id 是否正确
|
|
||||||
4. 检查网络连接和防火墙设置
|
|
||||||
5. 查看后端日志中的 HTTP 请求错误信息
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MyBatis Plus 分页使用
|
|
||||||
|
|
||||||
```java
|
|
||||||
// Controller 层
|
|
||||||
@GetMapping("/list")
|
|
||||||
public TableDataInfo list(QueryDTO queryDTO) {
|
|
||||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
|
||||||
Page<VO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
|
|
||||||
Page<VO> result = service.selectPage(page, queryDTO);
|
|
||||||
return getDataTable(result.getRecords(), result.getTotal());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service 层
|
|
||||||
Page<VO> selectPage(Page<VO> page, QueryDTO queryDTO);
|
|
||||||
|
|
||||||
// Mapper 层 (使用 XML)
|
|
||||||
<select id="selectPage" resultType="VO">
|
|
||||||
SELECT * FROM table_name
|
|
||||||
<where>
|
|
||||||
<if test="queryDTO.name != null">
|
|
||||||
AND name LIKE CONCAT('%', #{queryDTO.name}, '%')
|
|
||||||
</if>
|
|
||||||
</where>
|
|
||||||
</select>
|
|
||||||
```
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
|
||||||
DATE_STAMP=$(date "+%Y%m%d")
|
|
||||||
RELEASE_ZIP="$ROOT_DIR/ccdi_${DATE_STAMP}.zip"
|
|
||||||
STAGE_DIR="$ROOT_DIR/.deploy/ccdi-release-package"
|
|
||||||
WORK_DIR="$STAGE_DIR/files"
|
|
||||||
BACKEND_JAR_SOURCE="$ROOT_DIR/ruoyi-admin/target/ruoyi-admin.jar"
|
|
||||||
FRONTEND_DIR="$ROOT_DIR/ruoyi-ui"
|
|
||||||
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
|
|
||||||
FRONTEND_DIST_ZIP="$WORK_DIR/dist.zip"
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
log_error "缺少命令: $1"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_stage_dir() {
|
|
||||||
rm -rf "$STAGE_DIR"
|
|
||||||
mkdir -p "$WORK_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_backend() {
|
|
||||||
log_info "开始构建后端生产 jar"
|
|
||||||
(
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
mvn -pl ruoyi-admin -am clean package -DskipTests
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ ! -f "$BACKEND_JAR_SOURCE" ]; then
|
|
||||||
log_error "未生成后端 jar: $BACKEND_JAR_SOURCE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build_frontend() {
|
|
||||||
log_info "开始构建前端生产 dist"
|
|
||||||
FRONTEND_DIR="$FRONTEND_DIR" zsh -lic 'cd "$FRONTEND_DIR" && nvm use >/dev/null && npm run build:prod'
|
|
||||||
|
|
||||||
if [ ! -f "$FRONTEND_DIST_DIR/index.html" ]; then
|
|
||||||
log_error "前端生产构建失败,未找到: $FRONTEND_DIST_DIR/index.html"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "$FRONTEND_DIR"
|
|
||||||
zip -qr "$FRONTEND_DIST_ZIP" dist
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ ! -f "$FRONTEND_DIST_ZIP" ]; then
|
|
||||||
log_error "未生成前端压缩包: $FRONTEND_DIST_ZIP"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
package_release() {
|
|
||||||
cp "$BACKEND_JAR_SOURCE" "$WORK_DIR/ruoyi-admin.jar"
|
|
||||||
|
|
||||||
rm -f "$RELEASE_ZIP"
|
|
||||||
(
|
|
||||||
cd "$WORK_DIR"
|
|
||||||
zip -qr "$RELEASE_ZIP" ruoyi-admin.jar dist.zip
|
|
||||||
)
|
|
||||||
|
|
||||||
log_info "上线压缩包已生成: $RELEASE_ZIP"
|
|
||||||
log_info "压缩包根层内容: ruoyi-admin.jar, dist.zip"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
require_command mvn
|
|
||||||
require_command zsh
|
|
||||||
require_command zip
|
|
||||||
|
|
||||||
reset_stage_dir
|
|
||||||
build_backend
|
|
||||||
build_frontend
|
|
||||||
package_release
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
|
|
||||||
<sql id="AccountInfoWhereClause">
|
<sql id="AccountInfoWhereClause">
|
||||||
WHERE 1 = 1
|
WHERE 1 = 1
|
||||||
|
AND ai.owner_type <> 'CREDIT_CUSTOMER'
|
||||||
<if test="query.staffName != null and query.staffName != ''">
|
<if test="query.staffName != null and query.staffName != ''">
|
||||||
AND (
|
AND (
|
||||||
(ai.owner_type = 'EMPLOYEE' AND bs.name LIKE CONCAT('%', #{query.staffName}, '%'))
|
(ai.owner_type = 'EMPLOYEE' AND bs.name LIKE CONCAT('%', #{query.staffName}, '%'))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class CcdiAccountInfoMapperTest {
|
|||||||
assertTrue(sql.contains("ai.is_self_account as isactualcontrol"), sql);
|
assertTrue(sql.contains("ai.is_self_account as isactualcontrol"), sql);
|
||||||
assertTrue(sql.contains("ai.monthly_avg_trans_count as avgmonthtxncount"), sql);
|
assertTrue(sql.contains("ai.monthly_avg_trans_count as avgmonthtxncount"), sql);
|
||||||
assertTrue(sql.contains("ai.trans_risk_level as txnrisklevel"), sql);
|
assertTrue(sql.contains("ai.trans_risk_level as txnrisklevel"), sql);
|
||||||
|
assertTrue(sql.contains("ai.owner_type <> 'credit_customer'"), sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
private MappedStatement loadMappedStatement(String statementId) throws Exception {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public final class CcdiProjectStatusConstants {
|
|||||||
public static final String COMPLETED = "1";
|
public static final String COMPLETED = "1";
|
||||||
public static final String ARCHIVED = "2";
|
public static final String ARCHIVED = "2";
|
||||||
public static final String TAGGING = "3";
|
public static final String TAGGING = "3";
|
||||||
|
public static final String TAG_FAILED = "4";
|
||||||
|
|
||||||
private CcdiProjectStatusConstants() {
|
private CcdiProjectStatusConstants() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class CcdiProject implements Serializable {
|
|||||||
/** 配置方式:default-全局默认,custom-自定义 */
|
/** 配置方式:default-全局默认,custom-自定义 */
|
||||||
private String configType;
|
private String configType;
|
||||||
|
|
||||||
/** 项目状态:0-进行中,1-已完成,2-已归档,3-打标中 */
|
/** 项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败 */
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
/** 是否归档:0-未归档,1-已归档 */
|
/** 是否归档:0-未归档,1-已归档 */
|
||||||
|
|||||||
@@ -23,4 +23,7 @@ public class CcdiProjectStatusCountsVO {
|
|||||||
|
|
||||||
/** 打标中项目数(状态3) */
|
/** 打标中项目数(状态3) */
|
||||||
private Long status3;
|
private Long status3;
|
||||||
|
|
||||||
|
/** 打标失败项目数(状态4) */
|
||||||
|
private Long status4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ public class CcdiProjectVO {
|
|||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
|
/** 最近一次打标失败原因 */
|
||||||
|
private String latestTagTaskErrorMessage;
|
||||||
|
|
||||||
|
/** 最近一次打标失败结束时间 */
|
||||||
|
private Date latestTagTaskEndTime;
|
||||||
|
|
||||||
/** 创建者(用户名) */
|
/** 创建者(用户名) */
|
||||||
private String createBy;
|
private String createBy;
|
||||||
|
|
||||||
|
|||||||
@@ -32,4 +32,12 @@ public interface CcdiBankTagTaskMapper extends BaseMapper<CcdiBankTagTask> {
|
|||||||
* @return 任务实体
|
* @return 任务实体
|
||||||
*/
|
*/
|
||||||
CcdiBankTagTask selectRunningTaskByProjectId(@Param("projectId") Long projectId);
|
CcdiBankTagTask selectRunningTaskByProjectId(@Param("projectId") Long projectId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询项目最近一次失败任务
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @return 任务实体
|
||||||
|
*/
|
||||||
|
CcdiBankTagTask selectLatestFailedTaskByProjectId(@Param("projectId") Long projectId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
|
|||||||
task.setUpdateBy(operator);
|
task.setUpdateBy(operator);
|
||||||
task.setUpdateTime(new Date());
|
task.setUpdateTime(new Date());
|
||||||
updateFailedTaskSafely(task, ex);
|
updateFailedTaskSafely(task, ex);
|
||||||
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.PROCESSING, operator);
|
projectService.updateProjectStatus(projectId, CcdiProjectStatusConstants.TAG_FAILED, operator);
|
||||||
log.error("【流水标签】任务执行失败: taskId={}, projectId={}, modelCode={}, triggerType={}, error={}",
|
log.error("【流水标签】任务执行失败: taskId={}, projectId={}, modelCode={}, triggerType={}, error={}",
|
||||||
task.getId(), projectId, modelCode, triggerType, ex.getMessage(), ex);
|
task.getId(), projectId, modelCode, triggerType, ex.getMessage(), ex);
|
||||||
throw ex;
|
throw ex;
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import com.ruoyi.ccdi.project.domain.CcdiProject;
|
|||||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
||||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask;
|
||||||
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
|
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||||
|
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
|
||||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
@@ -43,6 +45,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
|||||||
@Resource
|
@Resource
|
||||||
private CcdiProjectMapper projectMapper;
|
private CcdiProjectMapper projectMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiBankTagTaskMapper bankTagTaskMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private LsfxAnalysisClient lsfxAnalysisClient;
|
private LsfxAnalysisClient lsfxAnalysisClient;
|
||||||
|
|
||||||
@@ -77,6 +82,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
|||||||
// 5. 返回VO
|
// 5. 返回VO
|
||||||
CcdiProjectVO vo = new CcdiProjectVO();
|
CcdiProjectVO vo = new CcdiProjectVO();
|
||||||
BeanUtils.copyProperties(project, vo);
|
BeanUtils.copyProperties(project, vo);
|
||||||
|
fillLatestTagFailure(project, vo);
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +122,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
|||||||
}
|
}
|
||||||
CcdiProjectVO vo = new CcdiProjectVO();
|
CcdiProjectVO vo = new CcdiProjectVO();
|
||||||
BeanUtils.copyProperties(project, vo);
|
BeanUtils.copyProperties(project, vo);
|
||||||
|
fillLatestTagFailure(project, vo);
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +190,12 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
|||||||
);
|
);
|
||||||
vo.setStatus3(status3Count);
|
vo.setStatus3(status3Count);
|
||||||
|
|
||||||
|
Long status4Count = projectMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<CcdiProject>()
|
||||||
|
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAG_FAILED)
|
||||||
|
);
|
||||||
|
vo.setStatus4(status4Count);
|
||||||
|
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,10 +276,23 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
|||||||
case CcdiProjectStatusConstants.COMPLETED -> "已完成";
|
case CcdiProjectStatusConstants.COMPLETED -> "已完成";
|
||||||
case CcdiProjectStatusConstants.ARCHIVED -> "已归档";
|
case CcdiProjectStatusConstants.ARCHIVED -> "已归档";
|
||||||
case CcdiProjectStatusConstants.TAGGING -> "打标中";
|
case CcdiProjectStatusConstants.TAGGING -> "打标中";
|
||||||
|
case CcdiProjectStatusConstants.TAG_FAILED -> "打标失败";
|
||||||
default -> "未知";
|
default -> "未知";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fillLatestTagFailure(CcdiProject project, CcdiProjectVO vo) {
|
||||||
|
if (!CcdiProjectStatusConstants.TAG_FAILED.equals(project.getStatus())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CcdiBankTagTask latestFailedTask = bankTagTaskMapper.selectLatestFailedTaskByProjectId(project.getProjectId());
|
||||||
|
if (latestFailedTask == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vo.setLatestTagTaskErrorMessage(latestFailedTask.getErrorMessage());
|
||||||
|
vo.setLatestTagTaskEndTime(latestFailedTask.getEndTime());
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveOperator(String operator) {
|
private String resolveOperator(String operator) {
|
||||||
return StringUtils.hasText(operator) ? operator : "system";
|
return StringUtils.hasText(operator) ? operator : "system";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
limit 1
|
limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectLatestFailedTaskByProjectId" resultMap="CcdiBankTagTaskResultMap">
|
||||||
|
select id, project_id, trigger_type, model_code, status, need_rerun, success_rule_count,
|
||||||
|
failed_rule_count, hit_count, error_message, start_time, end_time,
|
||||||
|
create_by, create_time, update_by, update_time
|
||||||
|
from ccdi_bank_tag_task
|
||||||
|
where project_id = #{projectId}
|
||||||
|
and status = 'FAILED'
|
||||||
|
order by id desc
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ class CcdiBankTagServiceImplTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRollbackProjectStatusToProcessingWhenRebuildFails() {
|
void shouldMarkProjectTagFailedWhenRebuildFails() {
|
||||||
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
|
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
|
||||||
|
|
||||||
CcdiBankTagRule rule = buildRule("LARGE_TRANSACTION", "大额交易",
|
CcdiBankTagRule rule = buildRule("LARGE_TRANSACTION", "大额交易",
|
||||||
@@ -329,7 +329,7 @@ class CcdiBankTagServiceImplTest {
|
|||||||
assertThrows(RuntimeException.class,
|
assertThrows(RuntimeException.class,
|
||||||
() -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL));
|
() -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL));
|
||||||
|
|
||||||
verify(projectService).updateProjectStatus(40L, "0", "tester");
|
verify(projectService).updateProjectStatus(40L, "4", "tester");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class CcdiBankTagServiceRiskCountRefreshTest {
|
|||||||
|
|
||||||
verify(taskMapper).updateTask(argThat(task -> "FAILED".equals(task.getStatus())
|
verify(taskMapper).updateTask(argThat(task -> "FAILED".equals(task.getStatus())
|
||||||
&& "refresh failed".equals(task.getErrorMessage())));
|
&& "refresh failed".equals(task.getErrorMessage())));
|
||||||
verify(projectService).updateProjectStatus(40L, "0", "tester");
|
verify(projectService).updateProjectStatus(40L, "4", "tester");
|
||||||
}
|
}
|
||||||
|
|
||||||
private CcdiBankTagRule buildRule() {
|
private CcdiBankTagRule buildRule() {
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import com.ruoyi.ccdi.project.domain.CcdiProject;
|
|||||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
|
||||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask;
|
||||||
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
|
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||||
|
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
|
||||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||||
@@ -25,11 +27,14 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -47,6 +52,9 @@ class CcdiProjectServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private CcdiProjectMapper projectMapper;
|
private CcdiProjectMapper projectMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiBankTagTaskMapper bankTagTaskMapper;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LsfxAnalysisClient lsfxAnalysisClient;
|
private LsfxAnalysisClient lsfxAnalysisClient;
|
||||||
|
|
||||||
@@ -55,13 +63,55 @@ class CcdiProjectServiceImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCountTaggingProjectsSeparately() {
|
void shouldCountTaggingProjectsSeparately() {
|
||||||
when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L);
|
when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 0L);
|
||||||
|
|
||||||
CcdiProjectStatusCountsVO counts = service.getStatusCounts();
|
CcdiProjectStatusCountsVO counts = service.getStatusCounts();
|
||||||
|
|
||||||
assertEquals(1L, counts.getStatus3());
|
assertEquals(1L, counts.getStatus3());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCountTagFailedProjectsSeparately() {
|
||||||
|
when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 5L);
|
||||||
|
|
||||||
|
CcdiProjectStatusCountsVO counts = service.getStatusCounts();
|
||||||
|
|
||||||
|
assertEquals(5L, counts.getStatus4());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnLatestFailedTagTaskOnFailedProjectDetail() {
|
||||||
|
Date endTime = new Date();
|
||||||
|
CcdiProject project = new CcdiProject();
|
||||||
|
project.setProjectId(40L);
|
||||||
|
project.setStatus("4");
|
||||||
|
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||||
|
|
||||||
|
CcdiBankTagTask failedTask = new CcdiBankTagTask();
|
||||||
|
failedTask.setErrorMessage("threshold missing");
|
||||||
|
failedTask.setEndTime(endTime);
|
||||||
|
when(bankTagTaskMapper.selectLatestFailedTaskByProjectId(40L)).thenReturn(failedTask);
|
||||||
|
|
||||||
|
CcdiProjectVO result = service.getProjectById(40L);
|
||||||
|
|
||||||
|
assertEquals("threshold missing", result.getLatestTagTaskErrorMessage());
|
||||||
|
assertEquals(endTime, result.getLatestTagTaskEndTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotReturnLatestFailedTagTaskWhenProjectIsNotFailed() {
|
||||||
|
CcdiProject project = new CcdiProject();
|
||||||
|
project.setProjectId(40L);
|
||||||
|
project.setStatus("0");
|
||||||
|
when(projectMapper.selectById(40L)).thenReturn(project);
|
||||||
|
|
||||||
|
CcdiProjectVO result = service.getProjectById(40L);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertNull(result.getLatestTagTaskErrorMessage());
|
||||||
|
verify(bankTagTaskMapper, never()).selectLatestFailedTaskByProjectId(any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectUpdatingArchivedProjectToTagging() {
|
void shouldRejectUpdatingArchivedProjectToTagging() {
|
||||||
CcdiProject archived = new CcdiProject();
|
CcdiProject archived = new CcdiProject();
|
||||||
@@ -84,6 +134,16 @@ class CcdiProjectServiceImplTest {
|
|||||||
() -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数"));
|
() -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowWritingWhenProjectTagFailed() {
|
||||||
|
CcdiProject tagFailed = new CcdiProject();
|
||||||
|
tagFailed.setProjectId(40L);
|
||||||
|
tagFailed.setStatus("4");
|
||||||
|
when(projectMapper.selectById(40L)).thenReturn(tagFailed);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldArchiveCompletedProject() {
|
void shouldArchiveCompletedProject() {
|
||||||
CcdiProject project = new CcdiProject();
|
CcdiProject project = new CcdiProject();
|
||||||
@@ -110,6 +170,16 @@ class CcdiProjectServiceImplTest {
|
|||||||
assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester"));
|
assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectArchivingProjectWhenStatusIsTagFailed() {
|
||||||
|
CcdiProject project = new CcdiProject();
|
||||||
|
project.setProjectId(41L);
|
||||||
|
project.setStatus("4");
|
||||||
|
when(projectMapper.selectById(41L)).thenReturn(project);
|
||||||
|
|
||||||
|
assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectWritingWhenProjectIsArchived() {
|
void shouldRejectWritingWhenProjectIsArchived() {
|
||||||
CcdiProject archived = new CcdiProject();
|
CcdiProject archived = new CcdiProject();
|
||||||
|
|||||||
@@ -14,12 +14,25 @@ class CcdiProjectStatusSqlTest {
|
|||||||
void shouldContainTaggingStatusInInitAndMigrationSql() throws IOException {
|
void shouldContainTaggingStatusInInitAndMigrationSql() throws IOException {
|
||||||
Path repoRoot = Path.of("..");
|
Path repoRoot = Path.of("..");
|
||||||
String initSql = Files.readString(repoRoot.resolve("sql/ccdi_project.sql"));
|
String initSql = Files.readString(repoRoot.resolve("sql/ccdi_project.sql"));
|
||||||
|
String prodInitSql = Files.readString(repoRoot.resolve("sql/ccdi_prod_init.sql"));
|
||||||
String migrationSql = Files.readString(repoRoot.resolve("sql/migration/2026-03-18-add-project-tagging-status.sql"));
|
String migrationSql = Files.readString(repoRoot.resolve("sql/migration/2026-03-18-add-project-tagging-status.sql"));
|
||||||
|
String tagFailedMigrationSql =
|
||||||
|
Files.readString(repoRoot.resolve("sql/migration/2026-05-27-add-project-tag-failed-status.sql"));
|
||||||
|
|
||||||
assertTrue(initSql.contains("打标中"));
|
assertTrue(initSql.contains("打标中"));
|
||||||
assertTrue(initSql.contains("'3'"));
|
assertTrue(initSql.contains("'3'"));
|
||||||
assertTrue(migrationSql.contains("ccdi_project_status"));
|
assertTrue(migrationSql.contains("ccdi_project_status"));
|
||||||
assertTrue(migrationSql.contains("打标中"));
|
assertTrue(migrationSql.contains("打标中"));
|
||||||
assertTrue(migrationSql.contains("'3'"));
|
assertTrue(migrationSql.contains("'3'"));
|
||||||
|
|
||||||
|
assertTrue(initSql.contains("打标失败"));
|
||||||
|
assertTrue(initSql.contains("'4'"));
|
||||||
|
assertTrue(prodInitSql.contains("打标失败"));
|
||||||
|
assertTrue(prodInitSql.contains("'4','ccdi_project_status'"));
|
||||||
|
assertTrue(tagFailedMigrationSql.contains("ccdi_project_status"));
|
||||||
|
assertTrue(tagFailedMigrationSql.contains("打标失败"));
|
||||||
|
assertTrue(tagFailedMigrationSql.contains("'4'"));
|
||||||
|
assertTrue(tagFailedMigrationSql.contains("latest_task.status = 'FAILED'"));
|
||||||
|
assertTrue(tagFailedMigrationSql.contains("project.status IN ('0', '3')"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 账号库列表排除信贷客户后端实施计划
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
账号库管理列表不展示 `ccdi_account_info.owner_type = 'CREDIT_CUSTOMER'` 的信贷客户账号,避免信贷客户账号批量导入后进入页面列表并影响查询性能。
|
||||||
|
|
||||||
|
## 2. 实施范围
|
||||||
|
|
||||||
|
- 后端账号库列表查询 SQL
|
||||||
|
- 账号库导出查询复用同一筛选条件
|
||||||
|
- 本次不调整前端筛选项、接口参数、返回结构、新增编辑导入校验
|
||||||
|
|
||||||
|
## 3. 实施步骤
|
||||||
|
|
||||||
|
1. 在 `CcdiAccountInfoMapper.xml` 的 `AccountInfoWhereClause` 增加固定条件:
|
||||||
|
`AND ai.owner_type <> 'CREDIT_CUSTOMER'`
|
||||||
|
2. 保持现有 `ownerType` 动态筛选逻辑不变,使 `ownerType=CREDIT_CUSTOMER` 查询自然返回空结果。
|
||||||
|
3. 不新增前端“信贷客户”筛选项,不扩展账号库维护端归属类型。
|
||||||
|
|
||||||
|
## 4. 验证要点
|
||||||
|
|
||||||
|
- 无筛选条件时列表不返回 `CREDIT_CUSTOMER` 数据。
|
||||||
|
- `ownerType=EMPLOYEE`、`RELATION`、`INTERMEDIARY`、`EXTERNAL` 时仍按原逻辑查询。
|
||||||
|
- `ownerType=CREDIT_CUSTOMER` 时返回空结果。
|
||||||
|
- 账号库导出与列表使用同一排除口径。
|
||||||
|
|
||||||
|
## 5. 前提
|
||||||
|
|
||||||
|
信贷客户账号导入 `ccdi_account_info` 时,`owner_type` 必须固定写入 `CREDIT_CUSTOMER`。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 项目打标失败状态后端实施计划
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 后端计划:`docs/plans/backend/2026-05-27-project-tag-failed-status-backend-implementation.md`
|
||||||
|
- 实施记录:`docs/reports/implementation/2026-05-27-project-tag-failed-status-implementation.md`
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
新增正式项目状态 `4-打标失败`,打标任务失败后项目状态停留在失败态;项目详情接口在失败态下返回最近失败任务错误信息,列表接口不返回完整错误。
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. 扩展项目状态常量、实体注释、状态文案和状态统计 VO,新增 `TAG_FAILED = "4"` 与 `status4`。
|
||||||
|
2. 修改打标失败流转:`CcdiBankTagServiceImpl.rebuildProject` 捕获异常后保留任务失败信息,并将项目状态更新为 `4`。
|
||||||
|
3. 新增 `CcdiBankTagTaskMapper.selectLatestFailedTaskByProjectId`,按 `id desc limit 1` 查询项目最近失败任务。
|
||||||
|
4. 扩展 `CcdiProjectVO`,只新增 `latestTagTaskErrorMessage`、`latestTagTaskEndTime`;`getProjectById` 仅在状态为 `4` 时组装失败任务信息。
|
||||||
|
5. 补充 SQL 初始化与迁移脚本,新增 `ccdi_project_status` 字典值 `4-打标失败`,并回填未归档且最新打标任务失败的 `0/3` 项目。
|
||||||
|
6. 补充后端单测覆盖失败状态流转、详情失败信息、`status4` 统计、`4` 状态可写和 SQL/Mapper 契约。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 执行本次相关后端测试类。
|
||||||
|
- 执行 `mvn -pl ccdi-project -am test` 观察全量状态并记录非本次问题。
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 项目打标失败状态前端实施计划
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 前端计划:`docs/plans/frontend/2026-05-27-project-tag-failed-status-frontend-implementation.md`
|
||||||
|
- 实施记录:`docs/reports/implementation/2026-05-27-project-tag-failed-status-implementation.md`
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
前端支持 `4-打标失败` 状态展示;项目列表只展示失败状态和进入项目入口;项目详情页展示失败提示,并通过详情接口字段查看完整错误。
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. 在项目列表、项目详情、历史导入状态映射中增加 `4-打标失败`,使用失败红色样式。
|
||||||
|
2. 在 `SearchBar` 和项目首页状态统计中增加 `4` 筛选与 `status4` 计数。
|
||||||
|
3. 在项目详情页头部下方增加打标失败提示,仅当 `projectInfo.projectStatus === "4"` 且存在 `latestTagTaskErrorMessage` 时展示。
|
||||||
|
4. 详情失败提示提供完整错误弹窗,内容只使用详情接口返回的 `latestTagTaskErrorMessage` 与 `latestTagTaskEndTime`。
|
||||||
|
5. 项目状态轮询在状态脱离 `3-打标中` 后停止,因此遇到 `4` 自动停止并展示失败信息。
|
||||||
|
6. `UploadData.vue` 将 `4` 按 `0-进行中` 处理:允许上传、拉取、征信导入,禁用查看报告入口。
|
||||||
|
7. `ParamConfig.vue` 维持只锁定 `3-打标中` 和 `2-已归档`,因此 `4` 状态允许保存参数并触发重新打标。
|
||||||
|
8. 补充静态单测覆盖状态映射、详情失败提示、列表不展示完整错误、筛选计数和失败态操作口径。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 前端命令执行前先通过 `nvm use` 切换到项目 Node 版本。
|
||||||
|
- 执行相关静态单测。
|
||||||
|
- 执行 `npm run build:prod`。
|
||||||
|
- 在真实业务页面路由中验证列表和详情页显示效果,不打开 prototype 页面。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 2026-05-22 生产安全组网络访问清单
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 目标目录:`docs/reports/implementation/`
|
||||||
|
- 文档用途:根据 `ruoyi-admin/src/main/resources/application-pro.yml` 生成生产运行所需网络 IP 与端口清单,供服务器安全组配置使用。
|
||||||
|
- 路径检查结果:符合仓库实施记录归档规范。
|
||||||
|
|
||||||
|
## 配置来源
|
||||||
|
|
||||||
|
- 后端监听端口:`server.port=62318`
|
||||||
|
- 生产文件公开访问基址:`credit-parse.api.file-public-base-url=http://64.116.19.153`
|
||||||
|
- 生产 MySQL:`64.116.19.156:3306`
|
||||||
|
- 生产 Redis:`64.116.19.155:6379`
|
||||||
|
- 流水分析平台:`http://64.202.32.176/c4c3`
|
||||||
|
- 征信解析平台:`http://64.202.32.40:8083`
|
||||||
|
|
||||||
|
说明:本文只记录网络地址和端口,不记录生产账号、密码、密钥等敏感配置。
|
||||||
|
|
||||||
|
## 生产运行安全组放行清单
|
||||||
|
|
||||||
|
### 入站规则
|
||||||
|
|
||||||
|
| 目标服务器 | 来源 | 协议/端口 | 用途 | 配置依据 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `64.116.19.153/32` | 业务访问源 IP 段或前置代理/SLB | TCP `62318` | 访问后端服务 | `server.port=62318` |
|
||||||
|
| `64.116.19.153/32` | `64.202.32.40/32` | TCP `80` | 征信解析平台读取已上传 HTML 文件 | `file-public-base-url=http://64.116.19.153`,HTTP 默认端口为 `80` |
|
||||||
|
|
||||||
|
### 出站规则
|
||||||
|
|
||||||
|
| 源服务器 | 目标 | 协议/端口 | 用途 | 配置依据 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `64.116.19.153/32` | `64.116.19.156/32` | TCP `3306` | 后端连接生产 MySQL | `spring.datasource.druid.master.url` |
|
||||||
|
| `64.116.19.153/32` | `64.116.19.155/32` | TCP `6379` | 后端连接生产 Redis | `spring.data.redis.host` / `port` |
|
||||||
|
| `64.116.19.153/32` | `64.202.32.176/32` | TCP `80` | 后端调用流水分析平台接口 | `lsfx.api.base-url=http://64.202.32.176/c4c3` |
|
||||||
|
| `64.116.19.153/32` | `64.202.32.40/32` | TCP `8083` | 后端调用征信解析发起与结果查询接口 | `credit-parse.api.url` / `result-url` |
|
||||||
|
|
||||||
|
## 配置校验点
|
||||||
|
|
||||||
|
1. 生产服务若按 `pro` 配置运行,启动参数需要使用 `--spring.profiles.active=pro`。
|
||||||
|
2. `file-public-base-url` 当前未带端口,因此征信解析平台回读文件时访问的是 `64.116.19.153:80`;如果服务器只开放 `62318`,外部平台无法按当前 `pro` 配置读取 `/profile/credit-html/**` 文件。
|
||||||
|
3. `/profile/**` 在后端资源映射和安全配置中允许匿名 GET 访问,因此安全组应确保征信解析平台到文件公开入口的网络链路可达。
|
||||||
|
|
||||||
|
## 本次验证
|
||||||
|
|
||||||
|
- 已检查 `ruoyi-admin/src/main/resources/application-pro.yml` 中生产端口、MySQL、Redis、流水分析平台、征信解析平台和文件公开访问基址。
|
||||||
|
- 已检查 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditHtmlStorageService.java`,确认远程文件地址由 `file-public-base-url` 拼接 `/profile/credit-html/**` 生成。
|
||||||
|
- 已检查 `ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java` 与 `SecurityConfig.java`,确认 `/profile/**` 对应本地上传目录并允许匿名 GET 访问。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 账号库列表排除信贷客户实施记录
|
||||||
|
|
||||||
|
## 1. 本次实施内容
|
||||||
|
|
||||||
|
- 在 `CcdiAccountInfoMapper.xml` 的公共查询条件 `AccountInfoWhereClause` 中增加 `AND ai.owner_type <> 'CREDIT_CUSTOMER'`。
|
||||||
|
- 账号库列表分页与导出查询共用该条件,因此两处均不再返回信贷客户账号。
|
||||||
|
- 在 `CcdiAccountInfoMapperTest` 中补充 SQL 渲染断言,覆盖信贷客户排除条件。
|
||||||
|
- 前端页面、筛选项、接口参数和账号库新增编辑导入校验未调整。
|
||||||
|
|
||||||
|
## 2. 影响范围
|
||||||
|
|
||||||
|
- 影响接口:`/ccdi/accountInfo/list`
|
||||||
|
- 影响查询:`selectAccountInfoPage`、`selectAccountInfoListForExport`
|
||||||
|
- 不影响账号详情、新增、编辑、删除、导入模板和导入处理。
|
||||||
|
|
||||||
|
## 3. 验证记录
|
||||||
|
|
||||||
|
- 已确认 Mapper XML 包含固定排除条件。
|
||||||
|
- 已确认 `ownerType=CREDIT_CUSTOMER` 会与固定排除条件组合为空结果。
|
||||||
|
- 已执行 `git diff --check -- ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAccountInfoMapper.xml docs/plans/backend/2026-05-27-account-info-exclude-credit-customer-backend-implementation.md docs/reports/implementation/2026-05-27-account-info-exclude-credit-customer-implementation.md`,无空白问题。
|
||||||
|
- 已执行 `mvn -pl ccdi-info-collection -am -DskipTests compile`,结果 `BUILD SUCCESS`。
|
||||||
|
- 已执行 `mvn -pl ccdi-info-collection -am test`,结果 `BUILD SUCCESS`,共运行 171 个测试,失败 0、错误 0。
|
||||||
|
|
||||||
|
## 4. 前提说明
|
||||||
|
|
||||||
|
信贷客户账号需要以 `owner_type = 'CREDIT_CUSTOMER'` 写入 `ccdi_account_info`,否则本次列表排除条件无法识别。
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 生产打包技能命名优化实施记录
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
|
||||||
|
- 实施日期:2026-05-27
|
||||||
|
- 实施对象:`/Users/wkc/.codex/skills/fullstack-prod-package`
|
||||||
|
- 实施内容:优化生产打包技能,使最终发布压缩包文件名包含项目英文代码
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
1. 更新打包脚本 `scripts/package_fullstack_prod.py`:
|
||||||
|
- 新增 `--project-code` 参数,用作最终 zip 文件名前缀
|
||||||
|
- 未传入 `--project-code` 时,默认使用后端项目目录名作为项目英文代码
|
||||||
|
- 对项目英文代码进行规范化处理,仅保留英文、数字、点、下划线和连字符
|
||||||
|
- 最终压缩包命名从 `YYYYMMDD-HHMMSS.zip` 调整为 `projectcode-YYYYMMDD-HHMMSS.zip`
|
||||||
|
- 打包完成输出增加 `PROJECT_CODE`
|
||||||
|
|
||||||
|
2. 更新技能说明 `SKILL.md`:
|
||||||
|
- 调整技能描述,保持触发条件清晰
|
||||||
|
- 标准命令增加 `--project-code projectcode`
|
||||||
|
- 说明默认推断规则和验证要求
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 后续使用 `fullstack-prod-package` 生成生产包时,最终 zip 文件名会包含项目英文代码。
|
||||||
|
- 生产包内部内容不变,仍仅包含:
|
||||||
|
- `dist.zip`
|
||||||
|
- 后端运行 Jar
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- `python3 -m py_compile` 通过
|
||||||
|
- `--help` 输出已包含 `--project-code`
|
||||||
|
- 使用现有 `ruoyi-ui/dist` 与 `ruoyi-admin.jar` 在临时目录执行轻量打包验证成功
|
||||||
|
- 验证生成文件名:`ccdi-20260527-152829.zip`
|
||||||
|
- 验证 zip 内容仍仅包含:
|
||||||
|
- `dist.zip`
|
||||||
|
- `ruoyi-admin.jar`
|
||||||
|
- 临时验证目录已删除
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 全栈生产包生成实施记录
|
||||||
|
|
||||||
|
## 基本信息
|
||||||
|
|
||||||
|
- 实施日期:2026-05-27
|
||||||
|
- 实施内容:生成前端 `dist.zip` 与后端可运行 Jar 的生产发布压缩包
|
||||||
|
- 最终产物:`/Users/wkc/Downloads/20260527-150234.zip`
|
||||||
|
|
||||||
|
## 执行内容
|
||||||
|
|
||||||
|
1. 核对项目构建配置:
|
||||||
|
- 前端目录:`ruoyi-ui`
|
||||||
|
- 前端 Node 版本:通过 `.nvmrc` 使用 `v14.21.3`
|
||||||
|
- 前端生产构建命令:`npm run build:prod`
|
||||||
|
- 后端构建命令:`mvn clean package -DskipTests`
|
||||||
|
- 后端运行 Jar:`ruoyi-admin/target/ruoyi-admin.jar`
|
||||||
|
|
||||||
|
2. 执行后端生产构建:
|
||||||
|
- 在仓库根目录执行 `mvn clean package -DskipTests`
|
||||||
|
- Maven Reactor 全模块构建成功
|
||||||
|
- 生成后端运行包 `ruoyi-admin.jar`
|
||||||
|
|
||||||
|
3. 执行前端生产构建与发布包生成:
|
||||||
|
- 通过 `nvm use` 切换至 Node `v14.21.3`
|
||||||
|
- 执行 `npm run build:prod`
|
||||||
|
- 生成 `ruoyi-ui/dist`
|
||||||
|
- 将 `dist` 压缩为 `dist.zip`
|
||||||
|
- 将 `dist.zip` 与 `ruoyi-admin.jar` 合并为最终发布包
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 更新构建产物目录:
|
||||||
|
- `ruoyi-ui/dist`
|
||||||
|
- 各后端模块 `target`
|
||||||
|
- 新增本地发布产物:
|
||||||
|
- `/Users/wkc/Downloads/20260527-150234.zip`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- Node 版本已确认:`v14.21.3`
|
||||||
|
- Java 版本已确认:`21.0.9`
|
||||||
|
- Maven 版本已确认:`3.9.14`
|
||||||
|
- 后端构建结果:成功
|
||||||
|
- 前端构建结果:成功,存在前端资源体积 warning,不影响构建完成
|
||||||
|
- `ruoyi-ui/dist` 文件数:377
|
||||||
|
- 后端 Jar:`ruoyi-admin.jar`,约 100 MB
|
||||||
|
- 最终压缩包:`20260527-150234.zip`,约 94 MB
|
||||||
|
- 最终压缩包内容已通过 `unzip -l` 验证,仅包含:
|
||||||
|
- `dist.zip`
|
||||||
|
- `ruoyi-admin.jar`
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 项目打标失败状态实施记录
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
- 后端新增项目状态 `4-打标失败`,扩展状态统计 `status4` 和状态文案。
|
||||||
|
- 打标失败后项目状态由原先回退 `0-进行中` 改为写入 `4-打标失败`,任务表仍记录 `FAILED/error_message`。
|
||||||
|
- 项目详情接口在失败态下返回最近失败任务的 `latestTagTaskErrorMessage` 与 `latestTagTaskEndTime`;项目列表不返回完整错误。
|
||||||
|
- SQL 初始化和迁移脚本新增 `ccdi_project_status` 字典值 `4-打标失败`,并提供生产数据回填 SQL。
|
||||||
|
- 前端列表、筛选、计数和详情页支持 `4-打标失败`;详情页提供失败提示和完整错误弹窗。
|
||||||
|
- `UploadData.vue` 与 `ParamConfig.vue` 将 `4` 按进行中口径处理,允许重新上传、拉取、修改参数和重新分析,不开放报告查看/归档入口。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 后端:项目状态模型、打标任务失败流转、项目详情接口、状态统计接口、打标任务 Mapper、SQL 初始化和迁移。
|
||||||
|
- 前端:项目首页列表与筛选、项目详情头部提示、上传数据页报告入口权限、状态映射。
|
||||||
|
- 数据:迁移脚本会将未归档、当前状态为 `0/3`、最新打标任务为 `FAILED` 的项目更新为 `4`。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 通过:`mvn -pl ccdi-project -am -Dtest=CcdiBankTagServiceImplTest,CcdiBankTagServiceRiskCountRefreshTest,CcdiProjectServiceImplTest,CcdiProjectStatusSqlTest,CcdiBankTagTaskMapperXmlTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||||
|
- 全量后端:`mvn -pl ccdi-project -am test` 未通过,剩余失败为既有问题:
|
||||||
|
- 多个测试使用 static mock,但当前 `SubclassByteBuddyMockMaker` 不支持 static mocks。
|
||||||
|
- `CcdiProjectOverviewControllerContractTest.shouldExposeOverviewReportExportEndpointContract` 期望摘要为“一键导出结果总览报告”,实际为“导出结果总览报告”。
|
||||||
|
- 通过:`cd ruoyi-ui && nvm use && node tests/unit/project-tag-failed-status.test.js && node tests/unit/project-detail-tagging-polling.test.js && node tests/unit/project-archive-readonly-guard.test.js && node tests/unit/upload-data-disabled-cards.test.js`
|
||||||
|
- 通过:`cd ruoyi-ui && nvm use && npm run build:prod`,仅有既有资源体积 warning。
|
||||||
|
- 真实页面验证:`browser-use` 工具不可用;已使用 Playwright 打开真实业务页面路由,并通过本地 mock 后端构造失败项目数据,验证列表只显示“打标失败”且不泄露完整错误,详情页可查看完整错误,失败状态下上传/拉取入口可见。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 拉取本行信息近一年日期范围实施记录
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
- 将“拉取本行信息”弹窗的时间跨度最早可选日期由固定 `2025-01-01` 调整为动态近一年窗口。
|
||||||
|
- 日期可选范围按当前日期滚动计算:
|
||||||
|
- 最晚可选日期:昨天。
|
||||||
|
- 最早可选日期:最晚可选日期往前一年。
|
||||||
|
- 提交前校验同步使用同一套最早、最晚日期边界。
|
||||||
|
- 校验提示调整为“时间跨度仅支持近一年内日期,且最晚只能选择到昨天”。
|
||||||
|
- 补充前端单测断言,防止日期范围再次退回固定日期。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 前端页面:
|
||||||
|
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||||
|
- 前端测试:
|
||||||
|
- `ruoyi-ui/tests/unit/upload-data-pull-bank-info-date-limit.test.js`
|
||||||
|
- 不涉及后端接口、不改数据库。
|
||||||
|
|
||||||
|
## 验证情况
|
||||||
|
|
||||||
|
- 已执行 `source ~/.nvm/nvm.sh && nvm use && node tests/unit/upload-data-pull-bank-info-date-limit.test.js`,通过。
|
||||||
|
- 已执行 `source ~/.nvm/nvm.sh && nvm use && npm run build:prod`,通过;仅存在项目既有资源体积告警。
|
||||||
|
- 已在真实项目详情页验证“拉取本行信息”弹窗:
|
||||||
|
- 验证页面:`http://localhost:8090/ccdiProject/detail/90336`。
|
||||||
|
- 当前系统日期:`2026-05-27`。
|
||||||
|
- 默认开始日期:`2025-05-26`。
|
||||||
|
- 默认结束日期:`2026-05-26`。
|
||||||
|
- 日期面板中 `2025-05-25` 禁用,`2025-05-26` 可选。
|
||||||
|
- 日期面板中 `2026-05-26` 可选,`2026-05-27` 及之后日期禁用。
|
||||||
|
- 浏览器验证截图已保存到 `output/browser-use/2026-05-27-pull-bank-info-date-range.png`,该文件为本地验证产物,不提交 Git。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 银行流水打标 UNION 排序规则冲突修复记录
|
||||||
|
|
||||||
|
## 问题现象
|
||||||
|
|
||||||
|
- 生产执行银行流水打标规则 `ABNORMAL_CUSTOMER_TRANSACTION` 时失败。
|
||||||
|
- 异常为 `Illegal mix of collations for operation 'UNION'`。
|
||||||
|
- 报错位置为 `CcdiBankTagAnalysisMapper.xml` 中 `selectAbnormalCustomerTransactionStatements` 查询。
|
||||||
|
|
||||||
|
## 根因
|
||||||
|
|
||||||
|
- 该规则会将 `ccdi_base_staff` 与 `ccdi_staff_fmy_relation` 组装为同一个人员主体派生表,并继续与银行流水、账户库、中介库、企业库做多分支 `UNION ALL`。
|
||||||
|
- 生产表字段排序规则确认无异常后,问题收敛到应用数据库连接会话。
|
||||||
|
- 生产 JDBC URL 只设置了 `characterEncoding=UTF-8`,未固定 `connectionCollation`。现场查询显示生产 MySQL 8.0.36 的 `@@character_set_connection` 为 `utf8mb3`,`@@collation_connection` 与 `@@collation_server` 均为 `utf8mb3_general_ci`;SQL 字符串字面量与 `CAST(... AS CHAR)` 也解析为 `utf8mb3_general_ci`,与项目字段统一使用的 `utf8mb4_general_ci` 不一致,容易在 `UNION` 合并字符串列时触发排序规则冲突。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
- 生产数据源 JDBC URL 增加 `connectionCollation=utf8mb4_general_ci`,固定应用连接会话排序规则。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 数据库结构:无变更。
|
||||||
|
- 后端配置:仅影响生产 profile 的 MySQL 连接会话排序规则。
|
||||||
|
- 后端代码:未修改 Java 和 MyBatis SQL 业务逻辑。
|
||||||
|
- 前端:无影响。
|
||||||
|
|
||||||
|
## 验证情况
|
||||||
|
|
||||||
|
- 已确认异常 SQL 对应规则为 `ABNORMAL_CUSTOMER_TRANSACTION`。
|
||||||
|
- 已确认生产 JDBC URL 未固定 `connectionCollation`。
|
||||||
|
- 现场查询 `@@character_set_connection` 返回 `utf8mb3`,`@@collation_connection` 与 `@@collation_server` 返回 `utf8mb3_general_ci`。
|
||||||
|
- 现场查询 `COLLATION('本人')` 与 `COLLATION(CAST(1 AS CHAR))` 返回 `utf8mb3_general_ci`。
|
||||||
|
- 未连接生产数据库执行验证;生产发布并重启后需确认应用连接中的 `@@collation_connection` 为 `utf8mb4_general_ci`,再重新触发失败项目的银行流水打标任务验证。
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 2026-05-28 全栈生产包实施记录
|
||||||
|
|
||||||
|
## 保存路径确认
|
||||||
|
|
||||||
|
- 文档目录:`docs/reports/implementation/`
|
||||||
|
- 本文档:`docs/reports/implementation/2026-05-28-fullstack-prod-package-095235.md`
|
||||||
|
|
||||||
|
## 实施内容
|
||||||
|
|
||||||
|
- 按 `fullstack-prod-package` 技能执行全栈生产打包。
|
||||||
|
- 前端目录:`ruoyi-ui`
|
||||||
|
- 后端目录:`ruoyi-admin`,构建命令在仓库根目录执行。
|
||||||
|
- 项目英文编码:`ccdi`
|
||||||
|
- 最终生产包:`/Users/wkc/Downloads/ccdi-20260528-095235.zip`
|
||||||
|
|
||||||
|
## 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 /Users/wkc/.codex/skills/fullstack-prod-package/scripts/package_fullstack_prod.py \
|
||||||
|
--frontend-dir /Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui \
|
||||||
|
--backend-dir /Users/wkc/Desktop/ccdi/ccdi/ruoyi-admin \
|
||||||
|
--backend-build-command 'cd /Users/wkc/Desktop/ccdi/ccdi && mvn clean package -DskipTests' \
|
||||||
|
--project-code ccdi
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
- 前端 `npm run build:prod` 执行成功,使用 `.nvmrc` 指定的 Node `14.21.3`。
|
||||||
|
- 前端 `ruoyi-ui/dist` 已生成,目录大小约 `8.7M`。
|
||||||
|
- 后端 `mvn clean package -DskipTests` 执行成功,生成 `ruoyi-admin/target/ruoyi-admin.jar`,大小约 `100M`。
|
||||||
|
- 最终压缩包已生成,大小约 `94M`。
|
||||||
|
- `unzip -l /Users/wkc/Downloads/ccdi-20260528-095235.zip` 验证通过,根目录仅包含:
|
||||||
|
- `dist.zip`
|
||||||
|
- `ruoyi-admin.jar`
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 前端构建存在 Vue CLI 默认资源体积告警,未导致构建失败。
|
||||||
|
- Maven 构建跳过测试,符合生产打包命令参数 `-DskipTests`。
|
||||||
|
- 本次打包基于执行时当前工作区内容,执行前工作区已存在未提交变更。
|
||||||
@@ -34,7 +34,7 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://116.62.17.81:3307/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
url: jdbc:mysql://116.62.17.81:3307/ccdi?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_general_ci&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||||
username: root
|
username: root
|
||||||
password: Kfcx@1234
|
password: Kfcx@1234
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ spring:
|
|||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://64.116.19.156:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
url: jdbc:mysql://64.116.19.156:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_general_ci&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||||
username: lx_ai
|
username: lx_ai
|
||||||
password: lx-ai@9520
|
password: lx-ai@9520
|
||||||
# 从库数据源
|
# 从库数据源
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ export default {
|
|||||||
'0': 'primary',
|
'0': 'primary',
|
||||||
'1': 'success',
|
'1': 'success',
|
||||||
'2': 'info',
|
'2': 'info',
|
||||||
'3': 'warning'
|
'3': 'warning',
|
||||||
|
'4': 'danger'
|
||||||
}
|
}
|
||||||
return statusMap[status] || 'info'
|
return statusMap[status] || 'info'
|
||||||
},
|
},
|
||||||
@@ -185,7 +186,8 @@ export default {
|
|||||||
'0': '进行中',
|
'0': '进行中',
|
||||||
'1': '已完成',
|
'1': '已完成',
|
||||||
'2': '已归档',
|
'2': '已归档',
|
||||||
'3': '打标中'
|
'3': '打标中',
|
||||||
|
'4': '打标失败'
|
||||||
}
|
}
|
||||||
return statusMap[status] || '未知'
|
return statusMap[status] || '未知'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
>
|
>
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="scope.row.status === '0' || scope.row.status === '3'"
|
v-if="['0', '3', '4'].includes(scope.row.status)"
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
icon="el-icon-right"
|
icon="el-icon-right"
|
||||||
@@ -199,6 +199,7 @@ export default {
|
|||||||
1: "#52c41a",
|
1: "#52c41a",
|
||||||
2: "#8c8c8c",
|
2: "#8c8c8c",
|
||||||
3: "#fa8c16",
|
3: "#fa8c16",
|
||||||
|
4: "#f56c6c",
|
||||||
};
|
};
|
||||||
return colorMap[status] || "#8c8c8c";
|
return colorMap[status] || "#8c8c8c";
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export default {
|
|||||||
'0': 0,
|
'0': 0,
|
||||||
'1': 0,
|
'1': 0,
|
||||||
'2': 0,
|
'2': 0,
|
||||||
'3': 0
|
'3': 0,
|
||||||
|
'4': 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -53,7 +54,8 @@ export default {
|
|||||||
{ label: '进行中', value: '0', count: 0 },
|
{ label: '进行中', value: '0', count: 0 },
|
||||||
{ label: '已完成', value: '1', count: 0 },
|
{ label: '已完成', value: '1', count: 0 },
|
||||||
{ label: '已归档', value: '2', count: 0 },
|
{ label: '已归档', value: '2', count: 0 },
|
||||||
{ label: '打标中', value: '3', count: 0 }
|
{ label: '打标中', value: '3', count: 0 },
|
||||||
|
{ label: '打标失败', value: '4', count: 0 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ export default {
|
|||||||
return String(this.projectInfo.projectStatus) === "2";
|
return String(this.projectInfo.projectStatus) === "2";
|
||||||
},
|
},
|
||||||
isReportDisabled() {
|
isReportDisabled() {
|
||||||
return ["0", "3"].includes(String(this.projectInfo.projectStatus));
|
return ["0", "3", "4"].includes(String(this.projectInfo.projectStatus));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -505,7 +505,9 @@ export default {
|
|||||||
return today;
|
return today;
|
||||||
},
|
},
|
||||||
getPullBankInfoMinSelectableDate() {
|
getPullBankInfoMinSelectableDate() {
|
||||||
return new Date(2025, 0, 1);
|
const minSelectableDate = this.getPullBankInfoMaxSelectableDate();
|
||||||
|
minSelectableDate.setFullYear(minSelectableDate.getFullYear() - 1);
|
||||||
|
return minSelectableDate;
|
||||||
},
|
},
|
||||||
getPullBankInfoMaxSelectableDate() {
|
getPullBankInfoMaxSelectableDate() {
|
||||||
const yesterday = this.getPullBankInfoTodayStart();
|
const yesterday = this.getPullBankInfoTodayStart();
|
||||||
@@ -546,10 +548,10 @@ export default {
|
|||||||
},
|
},
|
||||||
isPullBankInfoDateDisabled(time) {
|
isPullBankInfoDateDisabled(time) {
|
||||||
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
|
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
|
||||||
const todayStart = this.getPullBankInfoTodayStart();
|
const maxSelectableDate = this.getPullBankInfoMaxSelectableDate();
|
||||||
return (
|
return (
|
||||||
time.getTime() < minSelectableDate.getTime() ||
|
time.getTime() < minSelectableDate.getTime() ||
|
||||||
time.getTime() >= todayStart.getTime()
|
time.getTime() > maxSelectableDate.getTime()
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
hasInvalidPullBankInfoDateRange(dateRange) {
|
hasInvalidPullBankInfoDateRange(dateRange) {
|
||||||
@@ -590,7 +592,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasInvalidPullBankInfoDateRange([startDate, endDate])) {
|
if (this.hasInvalidPullBankInfoDateRange([startDate, endDate])) {
|
||||||
this.$message.warning("时间跨度仅支持 2025-01-01 至昨天");
|
this.$message.warning("时间跨度仅支持近一年内日期,且最晚只能选择到昨天");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,6 +664,7 @@ export default {
|
|||||||
1: "success",
|
1: "success",
|
||||||
2: "archived",
|
2: "archived",
|
||||||
3: "tagging",
|
3: "tagging",
|
||||||
|
4: "failed",
|
||||||
};
|
};
|
||||||
return statusMap[status] || "processing";
|
return statusMap[status] || "processing";
|
||||||
},
|
},
|
||||||
@@ -673,6 +676,7 @@ export default {
|
|||||||
1: "已完成",
|
1: "已完成",
|
||||||
2: "已归档",
|
2: "已归档",
|
||||||
3: "打标中",
|
3: "打标中",
|
||||||
|
4: "打标失败",
|
||||||
};
|
};
|
||||||
return statusMap[status] || "未知";
|
return statusMap[status] || "未知";
|
||||||
},
|
},
|
||||||
@@ -1050,6 +1054,11 @@ export default {
|
|||||||
color: #909399;
|
color: #909399;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-failed {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-time {
|
.update-time {
|
||||||
|
|||||||
@@ -57,6 +57,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isProjectTagFailed && projectInfo.latestTagTaskErrorMessage"
|
||||||
|
class="tag-failure-alert"
|
||||||
|
>
|
||||||
|
<div class="tag-failure-alert__content">
|
||||||
|
<i class="el-icon-warning-outline"></i>
|
||||||
|
<div>
|
||||||
|
<div class="tag-failure-alert__title">项目打标失败</div>
|
||||||
|
<div class="tag-failure-alert__message">
|
||||||
|
{{ tagFailureSummary }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="text" size="mini" @click="openTagFailureDialog">
|
||||||
|
查看完整错误
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 动态组件渲染区域 -->
|
<!-- 动态组件渲染区域 -->
|
||||||
<component
|
<component
|
||||||
:is="currentComponent"
|
:is="currentComponent"
|
||||||
@@ -81,6 +99,20 @@
|
|||||||
:visible.sync="evidenceDrawerVisible"
|
:visible.sync="evidenceDrawerVisible"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
/>
|
/>
|
||||||
|
<el-dialog
|
||||||
|
title="打标失败详情"
|
||||||
|
:visible.sync="tagFailureDialogVisible"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="projectInfo.latestTagTaskEndTime"
|
||||||
|
class="tag-failure-dialog__time"
|
||||||
|
>
|
||||||
|
失败时间:{{ formatUpdateTime(projectInfo.latestTagTaskEndTime) }}
|
||||||
|
</div>
|
||||||
|
<pre class="tag-failure-dialog__message">{{ projectInfo.latestTagTaskErrorMessage }}</pre>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -126,10 +158,13 @@ export default {
|
|||||||
warningCount: 0,
|
warningCount: 0,
|
||||||
warningThreshold: 60,
|
warningThreshold: 60,
|
||||||
projectStatus: "0",
|
projectStatus: "0",
|
||||||
|
latestTagTaskErrorMessage: "",
|
||||||
|
latestTagTaskEndTime: "",
|
||||||
},
|
},
|
||||||
evidenceConfirmVisible: false,
|
evidenceConfirmVisible: false,
|
||||||
evidenceDrawerVisible: false,
|
evidenceDrawerVisible: false,
|
||||||
evidencePayload: {},
|
evidencePayload: {},
|
||||||
|
tagFailureDialogVisible: false,
|
||||||
projectStatusPollingTimer: null,
|
projectStatusPollingTimer: null,
|
||||||
projectStatusPollingInterval: 1000,
|
projectStatusPollingInterval: 1000,
|
||||||
projectStatusPollingLoading: false,
|
projectStatusPollingLoading: false,
|
||||||
@@ -139,6 +174,13 @@ export default {
|
|||||||
isProjectArchived() {
|
isProjectArchived() {
|
||||||
return String(this.projectInfo.projectStatus) === "2";
|
return String(this.projectInfo.projectStatus) === "2";
|
||||||
},
|
},
|
||||||
|
isProjectTagFailed() {
|
||||||
|
return String(this.projectInfo.projectStatus) === "4";
|
||||||
|
},
|
||||||
|
tagFailureSummary() {
|
||||||
|
const message = this.projectInfo.latestTagTaskErrorMessage || "";
|
||||||
|
return message.length > 120 ? `${message.slice(0, 120)}...` : message;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$route.params.projectId"(newId) {
|
"$route.params.projectId"(newId) {
|
||||||
@@ -346,6 +388,7 @@ export default {
|
|||||||
1: "success", // 已完成
|
1: "success", // 已完成
|
||||||
2: "info", // 已归档
|
2: "info", // 已归档
|
||||||
3: "warning", // 打标中
|
3: "warning", // 打标中
|
||||||
|
4: "danger", // 打标失败
|
||||||
};
|
};
|
||||||
return statusMap[status] || "info";
|
return statusMap[status] || "info";
|
||||||
},
|
},
|
||||||
@@ -356,9 +399,13 @@ export default {
|
|||||||
1: "已完成",
|
1: "已完成",
|
||||||
2: "已归档",
|
2: "已归档",
|
||||||
3: "打标中",
|
3: "打标中",
|
||||||
|
4: "打标失败",
|
||||||
};
|
};
|
||||||
return statusMap[status] || "未知";
|
return statusMap[status] || "未知";
|
||||||
},
|
},
|
||||||
|
openTagFailureDialog() {
|
||||||
|
this.tagFailureDialogVisible = true;
|
||||||
|
},
|
||||||
/** 获取配置类型标签文字 */
|
/** 获取配置类型标签文字 */
|
||||||
getConfigTypeLabel(configType) {
|
getConfigTypeLabel(configType) {
|
||||||
const configTypeMap = {
|
const configTypeMap = {
|
||||||
@@ -606,6 +653,67 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-failure-alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #9f3a38;
|
||||||
|
background: #fff2f0;
|
||||||
|
border: 1px solid #ffd6d1;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-failure-alert__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-failure-alert__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-failure-alert__message {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-failure-dialog__time {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-failure-dialog__message {
|
||||||
|
max-height: 420px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
color: #303133;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ export default {
|
|||||||
'0': 0,
|
'0': 0,
|
||||||
'1': 0,
|
'1': 0,
|
||||||
'2': 0,
|
'2': 0,
|
||||||
'3': 0
|
'3': 0,
|
||||||
|
'4': 0
|
||||||
},
|
},
|
||||||
// 新增/编辑弹窗
|
// 新增/编辑弹窗
|
||||||
addDialogVisible: false,
|
addDialogVisible: false,
|
||||||
@@ -142,7 +143,8 @@ export default {
|
|||||||
'0': counts.status0 || 0,
|
'0': counts.status0 || 0,
|
||||||
'1': counts.status1 || 0,
|
'1': counts.status1 || 0,
|
||||||
'2': counts.status2 || 0,
|
'2': counts.status2 || 0,
|
||||||
'3': counts.status3 || 0
|
'3': counts.status3 || 0,
|
||||||
|
'4': counts.status4 || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
|||||||
@@ -23,4 +23,14 @@ assert(
|
|||||||
"组件脚本中应提供“最晚可选日期”为昨天的统一 helper"
|
"组件脚本中应提供“最晚可选日期”为昨天的统一 helper"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/getPullBankInfoMinSelectableDate\(\)[\s\S]*?getPullBankInfoMaxSelectableDate\(\)[\s\S]*?setFullYear\([^)]*getFullYear\(\) - 1\)/.test(source),
|
||||||
|
"拉取本行信息最早可选日期应跟随最晚可选日期滚动到近一年内"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
!/return new Date\(2025, 0, 1\)/.test(source),
|
||||||
|
"拉取本行信息日期范围不应再固定从 2025-01-01 开始"
|
||||||
|
);
|
||||||
|
|
||||||
console.log("upload-data-pull-bank-info-date-limit test passed");
|
console.log("upload-data-pull-bank-info-date-limit test passed");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@ CREATE TABLE `ccdi_project` (
|
|||||||
`project_name` VARCHAR(200) NOT NULL COMMENT '项目名称',
|
`project_name` VARCHAR(200) NOT NULL COMMENT '项目名称',
|
||||||
`description` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
|
`description` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
|
||||||
`config_type` VARCHAR(20) NOT NULL DEFAULT 'default' COMMENT '配置方式:default-全局默认,custom-自定义',
|
`config_type` VARCHAR(20) NOT NULL DEFAULT 'default' COMMENT '配置方式:default-全局默认,custom-自定义',
|
||||||
`status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态:0-进行中,1-已完成,2-已归档,3-打标中',
|
`status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败',
|
||||||
`is_archived` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否归档:0-未归档,1-已归档',
|
`is_archived` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否归档:0-未归档,1-已归档',
|
||||||
`target_count` INT NOT NULL DEFAULT 0 COMMENT '目标人数',
|
`target_count` INT NOT NULL DEFAULT 0 COMMENT '目标人数',
|
||||||
`high_risk_count` INT NOT NULL DEFAULT 0 COMMENT '高风险人数',
|
`high_risk_count` INT NOT NULL DEFAULT 0 COMMENT '高风险人数',
|
||||||
@@ -42,7 +42,8 @@ VALUES
|
|||||||
(1, '进行中', '0', 'ccdi_project_status', '', 'primary', 'Y', '0', 'admin', NOW()),
|
(1, '进行中', '0', 'ccdi_project_status', '', 'primary', 'Y', '0', 'admin', NOW()),
|
||||||
(2, '已完成', '1', 'ccdi_project_status', '', 'success', 'N', '0', 'admin', NOW()),
|
(2, '已完成', '1', 'ccdi_project_status', '', 'success', 'N', '0', 'admin', NOW()),
|
||||||
(3, '已归档', '2', 'ccdi_project_status', '', 'info', 'N', '0', 'admin', NOW()),
|
(3, '已归档', '2', 'ccdi_project_status', '', 'info', 'N', '0', 'admin', NOW()),
|
||||||
(4, '打标中', '3', 'ccdi_project_status', '', 'warning', 'N', '0', 'admin', NOW());
|
(4, '打标中', '3', 'ccdi_project_status', '', 'warning', 'N', '0', 'admin', NOW()),
|
||||||
|
(5, '打标失败', '4', 'ccdi_project_status', '', 'danger', 'N', '0', 'admin', NOW());
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 4. 插入配置方式字典
|
-- 4. 插入配置方式字典
|
||||||
|
|||||||
53
sql/migration/2026-05-27-add-project-tag-failed-status.sql
Normal file
53
sql/migration/2026-05-27-add-project-tag-failed-status.sql
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
ALTER TABLE ccdi_project
|
||||||
|
MODIFY COLUMN status CHAR(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0'
|
||||||
|
COMMENT '项目状态:0-进行中,1-已完成,2-已归档,3-打标中,4-打标失败';
|
||||||
|
|
||||||
|
INSERT INTO sys_dict_data (
|
||||||
|
dict_sort,
|
||||||
|
dict_label,
|
||||||
|
dict_value,
|
||||||
|
dict_type,
|
||||||
|
css_class,
|
||||||
|
list_class,
|
||||||
|
is_default,
|
||||||
|
status,
|
||||||
|
create_by,
|
||||||
|
create_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
5,
|
||||||
|
'打标失败',
|
||||||
|
'4',
|
||||||
|
'ccdi_project_status',
|
||||||
|
'',
|
||||||
|
'danger',
|
||||||
|
'N',
|
||||||
|
'0',
|
||||||
|
'admin',
|
||||||
|
NOW()
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_dict_data
|
||||||
|
WHERE dict_type = 'ccdi_project_status'
|
||||||
|
AND dict_value = '4'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE ccdi_project project
|
||||||
|
JOIN (
|
||||||
|
SELECT latest.project_id, latest.status
|
||||||
|
FROM ccdi_bank_tag_task latest
|
||||||
|
JOIN (
|
||||||
|
SELECT project_id, MAX(id) AS latest_id
|
||||||
|
FROM ccdi_bank_tag_task
|
||||||
|
GROUP BY project_id
|
||||||
|
) latest_id
|
||||||
|
ON latest.id = latest_id.latest_id
|
||||||
|
) latest_task
|
||||||
|
ON latest_task.project_id = project.project_id
|
||||||
|
SET project.status = '4',
|
||||||
|
project.update_by = 'system',
|
||||||
|
project.update_time = NOW()
|
||||||
|
WHERE latest_task.status = 'FAILED'
|
||||||
|
AND project.status IN ('0', '3')
|
||||||
|
AND (project.is_archived IS NULL OR project.is_archived = 0)
|
||||||
|
AND project.del_flag = '0';
|
||||||
Reference in New Issue
Block a user