Compare commits

...

107 Commits

Author SHA1 Message Date
wkc
a8e15e16d9 收敛后端重启脚本停机范围 2026-03-30 17:28:31 +08:00
wkc
933214a495 补充项目详情风险人员导出能力 2026-03-30 15:59:06 +08:00
wkc
b96161ecf4 补充风险总览人员导出前后端实施计划 2026-03-30 15:29:38 +08:00
wkc
3f424a5b7e 补充风险总览人员列表导出设计文档 2026-03-30 15:27:02 +08:00
wkc
ea6bd5213f 完成风险明细统一导出功能 2026-03-30 14:32:41 +08:00
wkc
28b846134a 补充风险明细统一导出实施计划 2026-03-30 14:24:49 +08:00
wkc
40194c86fb 补充风险明细统一导出设计文档 2026-03-30 14:20:58 +08:00
wkc
c9838024b3 更新NAS环境数据库端口配置 2026-03-30 13:53:00 +08:00
wkc
d582a65978 修复历史项目导入解环与流水查询SQL 2026-03-30 13:46:04 +08:00
wkc
0a3c03dcf9 完成项目详情风险人员分页改造 2026-03-29 18:44:07 +08:00
wkc
dd3aa5bbae 补充风险总览员工列表分页实施计划 2026-03-29 11:47:08 +08:00
wkc
865d8f823b 补充风险总览员工列表分页设计文档 2026-03-29 11:41:49 +08:00
wkc
9df1b956b3 完成历史项目导入前端闭环 2026-03-29 10:01:42 +08:00
wkc
0889ee4533 收敛历史导入文件前端只读展示 2026-03-29 09:59:56 +08:00
wkc
f2cc9e2700 重构历史项目导入弹窗 2026-03-29 09:58:55 +08:00
wkc
a019abb950 接通历史项目导入前端提交链路 2026-03-29 09:56:46 +08:00
wkc
d6457491e8 接通历史项目导入后端闭环 2026-03-29 09:56:01 +08:00
wkc
46d190aa74 实现历史项目流水复制后端逻辑 2026-03-29 09:54:05 +08:00
wkc
b098d4eed1 实现历史项目导入任务提交流程 2026-03-29 09:49:44 +08:00
wkc
eb0d896114 补充历史项目导入接口契约 2026-03-29 09:47:43 +08:00
wkc
36576fab78 补充历史导入文件记录字段 2026-03-29 09:45:54 +08:00
wkc
c1d56cc153 新增历史项目导入实施计划 2026-03-29 09:40:44 +08:00
wkc
26f77bf458 新增历史项目导入设计文档 2026-03-29 09:37:36 +08:00
wkc
65f25a9258 移除顶部导航源码和文档入口 2026-03-28 17:22:18 +08:00
wkc
2cb4481c3b 实现风险明细员工负面征信功能 2026-03-28 16:13:24 +08:00
wkc
559572da8c 新增风险明细员工负面征信实施计划 2026-03-28 16:06:57 +08:00
wkc
c1018fea0c 新增风险明细员工负面征信设计文档 2026-03-28 16:03:17 +08:00
wkc
cf36b5f05a 新增涉疑交易明细查询导出并补充对手方证件信息 2026-03-27 17:31:11 +08:00
wkc
5e968c8716 对齐结果总览涉疑交易明细样式 2026-03-27 17:26:47 +08:00
wkc
ed1d07ad05 新增开发风险明细涉疑交易明细实施计划 2026-03-27 16:33:10 +08:00
wkc
dc578762b3 新增开发风险明细涉疑交易明细设计文档 2026-03-27 16:27:25 +08:00
wkc
e44e632bb6 切换后端与lsfx-mock默认数据库到MySQL8 2026-03-27 15:29:40 +08:00
wkc
3f98d59741 完成结果总览卡片结构合并实现 2026-03-27 15:25:23 +08:00
wkc
e7ad46edaf 优化结果总览标题层级与人员区标题 2026-03-27 15:05:59 +08:00
wkc
966754e8c6 补充结果总览卡片合并前端实施记录 2026-03-27 14:56:10 +08:00
wkc
7cffdc9e2b 统一结果总览模型与明细区块标题 2026-03-27 14:53:40 +08:00
wkc
a76806acfc 合并结果总览顶部风险总览卡片 2026-03-27 14:52:12 +08:00
wkc
55c8f1c29c 锁定结果总览卡片合并结构断言 2026-03-27 14:49:56 +08:00
wkc
d914c93e93 补充结果总览卡片结构合并实施计划 2026-03-27 14:44:10 +08:00
wkc
3ea227141d 补充结果总览卡片结构合并设计文档 2026-03-27 14:37:58 +08:00
wkc
3a2e4d86e3 补充项目列表重新分析前端实施记录 2026-03-27 14:29:15 +08:00
wkc
9e0efe8010 补充项目列表重新分析确认交互 2026-03-27 14:28:56 +08:00
wkc
2c793eaed6 补充项目列表重新分析前后端实施计划 2026-03-27 14:14:49 +08:00
wkc
d931ac185d 补充项目列表重新分析确认刷新设计 2026-03-27 14:09:07 +08:00
wkc
0e593a9202 补充OpenClaw特权模式设计与实施记录 2026-03-26 09:50:50 +08:00
wkc
762af9de90 补充项目分析弹窗前端修复记录 2026-03-26 09:15:59 +08:00
wkc
b5733486fd 收口项目分析弹窗右侧主区节奏 2026-03-26 09:15:13 +08:00
wkc
d4421862a8 修正项目分析弹窗侧栏档案展示 2026-03-26 09:13:30 +08:00
wkc
21b8b7bf41 收紧项目分析弹窗头带与滚动布局 2026-03-26 09:12:07 +08:00
wkc
2b701602ff 补充项目分析弹窗展示修正实施计划 2026-03-26 09:08:35 +08:00
wkc
cda1028c48 补充项目分析弹窗展示优化设计 2026-03-25 19:44:11 +08:00
wkc
60f935da27 重构家庭资产负债详情展示 2026-03-25 19:28:54 +08:00
wkc
17a6c389d1 补充结果总览详情弹窗优化验证记录 2026-03-25 19:13:42 +08:00
wkc
1e3ea8d4c9 统一结果总览详情弹窗主区视觉 2026-03-25 19:12:30 +08:00
wkc
3fb02f1391 改造结果总览详情弹窗侧栏档案面板 2026-03-25 19:10:49 +08:00
wkc
04381dc434 重构结果总览详情弹窗外层骨架 2026-03-25 19:09:16 +08:00
wkc
2866767503 补充结果总览详情弹窗展示优化实施计划 2026-03-25 19:04:43 +08:00
wkc
d1bfeb8e63 补充结果总览详情窗口展示优化设计文档 2026-03-25 19:00:49 +08:00
wkc
255a41c936 修复结果总览标签展示 2026-03-25 18:47:27 +08:00
wkc
ed427f7a42 合并结果总览详情外层卡片 2026-03-25 18:44:57 +08:00
wkc
7fb1543c4c 取消结果总览详情左侧固定 2026-03-25 18:32:53 +08:00
wkc
0746a44b32 调整结果总览详情侧栏固定布局 2026-03-25 17:26:50 +08:00
wkc
d174dc739f 调整结果总览详情弹窗占比 2026-03-25 17:21:34 +08:00
wkc
54cd982603 调整异常对象逐卡展示口径 2026-03-25 17:16:15 +08:00
wkc
e957cdcc81 调整异常对象卡片单列展示 2026-03-25 17:03:14 +08:00
wkc
9442a4116c 补充异常对象原因快照展示 2026-03-25 16:58:55 +08:00
wkc
be3448eb44 调整结果总览详情弹窗分页与模型摘要 2026-03-25 16:02:46 +08:00
wkc
8e0274df88 补充专项核查展开区改版实施计划 2026-03-25 15:34:13 +08:00
wkc
5867cd5057 补充专项核查展开区改版设计文档 2026-03-25 15:29:44 +08:00
wkc
78ae93330c 实现结果总览详情弹窗前端接线 2026-03-25 15:26:03 +08:00
wkc
a52fb35bd3 实现结果总览详情弹窗后端接口 2026-03-25 15:15:07 +08:00
wkc
717f836190 搭建结果总览详情服务骨架 2026-03-25 15:07:43 +08:00
wkc
8df9dbacd8 补充结果总览详情接口契约 2026-03-25 15:03:24 +08:00
wkc
155da36e78 补充结果总览详情弹窗实施计划 2026-03-25 14:57:07 +08:00
wkc
13769da668 补充结果总览详情弹窗真实数据设计 2026-03-25 14:51:32 +08:00
wkc
e521169a7c 调整上传数据页列表工具栏布局 2026-03-25 14:26:58 +08:00
wkc
ad4e115787 补充结果总览项目分析弹窗前端记录 2026-03-25 14:17:35 +08:00
wkc
ed54b01d26 调整上传数据页轻改版前端实现 2026-03-25 14:11:54 +08:00
wkc
85f4e7bc61 实现结果总览项目分析弹窗主视图 2026-03-25 14:09:47 +08:00
wkc
a13c73f9a8 搭建结果总览项目分析弹窗骨架 2026-03-25 14:05:30 +08:00
wkc
137d6630fe 新增上传数据页轻改版实施计划 2026-03-25 14:03:44 +08:00
wkc
b14eef8482 打通结果总览项目分析弹窗入口 2026-03-25 14:02:45 +08:00
wkc
2793cf437c 新增结果总览项目分析弹窗实施计划 2026-03-25 13:59:24 +08:00
wkc
ad8099889c 新增上传数据页轻改版设计文档 2026-03-25 13:58:13 +08:00
wkc
05ac43f26b 新增结果总览项目分析弹窗设计文档 2026-03-25 13:56:21 +08:00
wkc
071c02192d 修复all模式月固定收入规则命中隔离问题 2026-03-25 10:28:08 +08:00
wkc
5eea3c66ff 实现lsfx-mock全命中SQL对齐 2026-03-25 10:05:30 +08:00
wkc
f217d59f09 新增lsfx-mock全命中实施计划 2026-03-25 09:56:59 +08:00
wkc
3e8e44ae30 新增lsfx-mock全命中SQL对齐设计文档 2026-03-25 09:41:57 +08:00
wkc
98430b4c8d 修正专项核查拓展查询项目参数绑定 2026-03-24 23:38:06 +08:00
wkc
1770d304e5 补充专项核查拓展查询前端验证记录 2026-03-24 23:16:16 +08:00
wkc
d745481eeb 补充专项核查拓展查询详情弹窗 2026-03-24 23:15:35 +08:00
wkc
0fc61aa3cb 修正专项核查调动详情主键链路 2026-03-24 23:15:28 +08:00
wkc
04c9cfc42e 实现专项核查拓展查询主题切换 2026-03-24 23:07:28 +08:00
wkc
5ba70789d4 挂载专项核查拓展查询卡片 2026-03-24 23:04:54 +08:00
wkc
0b80c18838 补充专项核查拓展查询前端接口 2026-03-24 23:03:07 +08:00
wkc
3dc639778e 补充专项核查拓展查询后端验证记录 2026-03-24 23:01:58 +08:00
wkc
8a6b844509 完成专项核查拓展查询服务组装 2026-03-24 23:01:21 +08:00
wkc
0dbf5c5ca4 补充专项核查招聘调动拓展查询SQL 2026-03-24 22:58:26 +08:00
wkc
c1a588b3fd 补充专项核查采购拓展查询SQL 2026-03-24 22:55:59 +08:00
wkc
1d013dc6df 定义专项核查拓展查询接口契约 2026-03-24 22:54:36 +08:00
wkc
dd93798cb9 新增专项核查拓展查询前后端实施计划 2026-03-24 22:41:06 +08:00
wkc
eb4988f80e 修正专项核查拓展查询设计文档口径 2026-03-24 22:16:21 +08:00
wkc
5f8c5a9ec5 新增专项核查拓展查询设计文档 2026-03-24 22:14:26 +08:00
wkc
805bef4099 修复归档项目详情页签地址回写 2026-03-24 22:00:42 +08:00
wkc
294164a504 实现项目归档功能 2026-03-24 21:45:55 +08:00
wkc
bb49d78a3a 新增项目归档前后端实施计划 2026-03-24 21:35:59 +08:00
319 changed files with 29765 additions and 1307 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -27,7 +27,7 @@
- 前端开发直接在当前分支进行,不需要额外创建 git worktree - 前端开发直接在当前分支进行,不需要额外创建 git worktree
- 测试结束后,自动关闭测试过程中启动的前后端进程 - 测试结束后,自动关闭测试过程中启动的前后端进程
- 遇到 MCP 数据库操作时,使用项目配置文件中的数据库连接信息 - 遇到 MCP 数据库操作时,使用项目配置文件中的数据库连接信息
- 执行包含中文内容的 MySQL SQL 脚本时,禁止直接手写 `mysql -e` 或普通重定向执行;必须优先使用 `bin/mysql_utf8_exec.sh <sql-file>`,确保会话字符集为 `utf8mb4`,避免写入乱码 - 执行包含中文内容的 MySQL SQL 脚本或数据库导入时,禁止直接手写 `mysql -e` 或普通重定向执行;必须优先使用 `bin/mysql_utf8_exec.sh <sql-file>`,确保会话字符集为 `utf8mb4`,避免导入或写入乱码
- 银行流水打标相关规则与参数编码需要统一使用全大写;新增或修改 `rule_code``indicator_code``param_code` 时,禁止混用大小写风格 - 银行流水打标相关规则与参数编码需要统一使用全大写;新增或修改 `rule_code``indicator_code``param_code` 时,禁止混用大小写风格
--- ---
@@ -164,7 +164,7 @@ return AjaxResult.success(result);
- 非业务字段如 `create_by``create_time` 由后端自动维护 - 非业务字段如 `create_by``create_time` 由后端自动维护
- 前端表单不要暴露通用审计字段 - 前端表单不要暴露通用审计字段
- 新增菜单、字典、初始化数据时,同步补充 SQL 脚本 - 新增菜单、字典、初始化数据时,同步补充 SQL 脚本
- 执行数据库脚本前,需确认客户端会话字符集为 `utf8mb4`;涉及中文插入、更新时默认使用 `bin/mysql_utf8_exec.sh` - 执行数据库脚本或导入数据库前,需确认客户端会话字符集为 `utf8mb4`;涉及中文插入、更新、导入时默认使用 `bin/mysql_utf8_exec.sh`
### 前端规范 ### 前端规范

BIN
assets/员工账户.xlsx Normal file

Binary file not shown.

View File

@@ -11,8 +11,8 @@ TARGET_DIR="$ROOT_DIR/ruoyi-admin/target"
JAR_NAME="ruoyi-admin.jar" JAR_NAME="ruoyi-admin.jar"
SERVER_PORT=62318 SERVER_PORT=62318
STOP_WAIT_SECONDS=30 STOP_WAIT_SECONDS=30
APP_KEYWORD="$JAR_NAME" APP_MARKER="-Dccdi.backend.root=$ROOT_DIR"
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" JAVA_OPTS="$APP_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
timestamp() { timestamp() {
date "+%Y-%m-%d %H:%M:%S" date "+%Y-%m-%d %H:%M:%S"
@@ -42,24 +42,54 @@ ensure_command() {
fi fi
} }
is_managed_backend_pid() {
pid="$1"
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
args=$(ps -o args= -p "$pid" 2>/dev/null || true)
if [ -z "${args:-}" ]; then
return 1
fi
case "$args" in
*"$APP_MARKER"*"$JAR_NAME"*|*"$JAR_NAME"*"$APP_MARKER"*)
return 0
;;
esac
if [ -f "$PID_FILE" ]; then
file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
if [ "${file_pid:-}" = "$pid" ]; then
case "$args" in
*"java"*"-jar"*"$JAR_NAME"*)
return 0
;;
esac
fi
fi
return 1
}
collect_pids() { collect_pids() {
all_pids="" all_pids=""
if [ -f "$PID_FILE" ]; then if [ -f "$PID_FILE" ]; then
file_pid=$(cat "$PID_FILE" 2>/dev/null || true) file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
if [ -n "${file_pid:-}" ] && kill -0 "$file_pid" 2>/dev/null; then if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
all_pids="$all_pids $file_pid" all_pids="$all_pids $file_pid"
fi fi
fi fi
port_pids=$(lsof -tiTCP:"$SERVER_PORT" -sTCP:LISTEN 2>/dev/null || true) marker_pids=$(pgrep -f "$APP_MARKER" 2>/dev/null || true)
if [ -n "${port_pids:-}" ]; then if [ -n "${marker_pids:-}" ]; then
all_pids="$all_pids $port_pids" for pid in $marker_pids; do
fi if is_managed_backend_pid "$pid"; then
all_pids="$all_pids $pid"
app_pids=$(pgrep -f "$APP_KEYWORD" 2>/dev/null || true) fi
if [ -n "${app_pids:-}" ]; then done
all_pids="$all_pids $app_pids"
fi fi
unique_pids="" unique_pids=""
@@ -155,6 +185,12 @@ status_backend() {
pids=$(collect_pids) pids=$(collect_pids)
if [ -n "${pids:-}" ]; then if [ -n "${pids:-}" ]; then
log_info "后端正在运行,进程: $pids" log_info "后端正在运行,进程: $pids"
return 0
fi
port_pids=$(lsof -tiTCP:"$SERVER_PORT" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "${port_pids:-}" ]; then
log_info "未发现脚本托管的后端进程,但端口 $SERVER_PORT 被其他进程占用: $port_pids"
else else
log_info "后端未运行" log_info "后端未运行"
fi fi
@@ -190,6 +226,7 @@ main() {
ensure_command mvn ensure_command mvn
ensure_command lsof ensure_command lsof
ensure_command pgrep ensure_command pgrep
ensure_command ps
ensure_command tail ensure_command tail
action="${1:-restart}" action="${1:-restart}"

View File

@@ -110,6 +110,12 @@ public class GetBankStatementResponse {
/** 对手方备注 */ /** 对手方备注 */
private String customerReference; private String customerReference;
/** 交易对手方证件号 */
private String customerCertNo;
/** 交易对手方统一社会信用代码 */
private String customerSocialCreditCode;
// ===== 摘要和备注 ===== // ===== 摘要和备注 =====
/** 用户交易摘要 */ /** 用户交易摘要 */

View File

@@ -6,18 +6,23 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDomain; import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport; import com.ruoyi.common.core.page.TableSupport;
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.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.service.ICcdiProjectService; import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
/** /**
* 纪检初核项目管理Controller * 纪检初核项目管理Controller
* *
@@ -53,6 +58,17 @@ public class CcdiProjectController extends BaseController {
return AjaxResult.success("项目更新成功", vo); return AjaxResult.success("项目更新成功", vo);
} }
/**
* 归档项目
*/
@PostMapping("/{projectId}/archive")
@Operation(summary = "归档项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult archiveProject(@PathVariable Long projectId) {
projectService.archiveProject(projectId, SecurityUtils.getUsername());
return AjaxResult.success("项目归档成功");
}
/** /**
* 删除项目 * 删除项目
*/ */
@@ -88,6 +104,28 @@ public class CcdiProjectController extends BaseController {
return getDataTable(result.getRecords(), result.getTotal()); return getDataTable(result.getRecords(), result.getTotal());
} }
/**
* 查询历史项目列表
*/
@GetMapping("/history")
@Operation(summary = "查询历史项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult listHistoryProjects(CcdiProjectQueryDTO queryDTO) {
List<CcdiProjectHistoryListItemVO> result = projectService.listHistoryProjects(queryDTO);
return AjaxResult.success(result);
}
/**
* 从历史项目导入
*/
@PostMapping("/import")
@Operation(summary = "导入历史项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
public AjaxResult importFromHistory(@Validated @RequestBody CcdiProjectImportHistoryDTO dto) {
CcdiProjectVO vo = projectService.importFromHistory(dto, SecurityUtils.getUsername());
return AjaxResult.success("项目创建成功", vo);
}
/** /**
* 查询项目状态统计 * 查询项目状态统计
*/ */

View File

@@ -1,22 +1,36 @@
package com.ruoyi.ccdi.project.controller; package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* 结果总览控制器 * 结果总览控制器
*/ */
@@ -45,8 +59,8 @@ public class CcdiProjectOverviewController extends BaseController {
@GetMapping("/risk-people") @GetMapping("/risk-people")
@Operation(summary = "查询风险人员总览") @Operation(summary = "查询风险人员总览")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')") @PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskPeople(Long projectId) { public AjaxResult getRiskPeople(CcdiProjectRiskPeopleQueryDTO queryDTO) {
CcdiProjectRiskPeopleOverviewVO overview = overviewService.getRiskPeopleOverview(projectId); CcdiProjectRiskPeopleOverviewVO overview = overviewService.getRiskPeopleOverview(queryDTO);
return AjaxResult.success(overview); return AjaxResult.success(overview);
} }
@@ -82,4 +96,76 @@ public class CcdiProjectOverviewController extends BaseController {
CcdiProjectRiskModelPeopleVO people = overviewService.getRiskModelPeople(queryDTO); CcdiProjectRiskModelPeopleVO people = overviewService.getRiskModelPeople(queryDTO);
return AjaxResult.success(people); return AjaxResult.success(people);
} }
/**
* 查询项目分析详情
*/
@GetMapping("/person-analysis/detail")
@Operation(summary = "查询项目分析详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getPersonAnalysisDetail(CcdiProjectPersonAnalysisDetailQueryDTO queryDTO) {
CcdiProjectPersonAnalysisDetailVO detail = overviewService.getPersonAnalysisDetail(queryDTO);
return AjaxResult.success(detail);
}
/**
* 查询涉疑交易明细
*/
@GetMapping("/suspicious-transactions")
@Operation(summary = "查询涉疑交易明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getSuspiciousTransactions(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
CcdiProjectSuspiciousTransactionPageVO pageVO = overviewService.getSuspiciousTransactions(queryDTO);
return AjaxResult.success(pageVO);
}
/**
* 查询项目员工负面征信
*/
@GetMapping("/employee-credit-negative")
@Operation(summary = "查询项目员工负面征信")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getEmployeeCreditNegative(CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO) {
CcdiProjectEmployeeCreditNegativePageVO pageVO = overviewService.getEmployeeCreditNegative(queryDTO);
return AjaxResult.success(pageVO);
}
/**
* 导出涉疑交易明细
*/
@PostMapping("/suspicious-transactions/export")
@Operation(summary = "导出涉疑交易明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportSuspiciousTransactions(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
List<CcdiProjectSuspiciousTransactionExcel> rows = overviewService.exportSuspiciousTransactions(queryDTO);
ExcelUtil<CcdiProjectSuspiciousTransactionExcel> util =
new ExcelUtil<>(CcdiProjectSuspiciousTransactionExcel.class);
util.exportExcel(response, rows, "涉疑交易明细");
}
/**
* 导出风险人员总览
*/
@PostMapping("/risk-people/export")
@Operation(summary = "导出风险人员总览")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskPeople(HttpServletResponse response, Long projectId) {
List<CcdiProjectRiskPeopleOverviewExcel> rows = overviewService.exportRiskPeopleOverview(projectId);
ExcelUtil<CcdiProjectRiskPeopleOverviewExcel> util =
new ExcelUtil<>(CcdiProjectRiskPeopleOverviewExcel.class);
util.exportExcel(response, rows, "风险人员总览");
}
/**
* 导出风险明细
*/
@PostMapping("/risk-details/export")
@Operation(summary = "导出风险明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
overviewService.exportRiskDetails(response, projectId);
}
} }

View File

@@ -1,7 +1,19 @@
package com.ruoyi.ccdi.project.controller; package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService; import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
@@ -48,4 +60,70 @@ public class CcdiProjectSpecialCheckController extends BaseController {
CcdiProjectFamilyAssetLiabilityDetailVO result = specialCheckService.getFamilyAssetLiabilityDetail(queryDTO); CcdiProjectFamilyAssetLiabilityDetailVO result = specialCheckService.getFamilyAssetLiabilityDetail(queryDTO);
return AjaxResult.success(result); return AjaxResult.success(result);
} }
/**
* 查询采购拓展列表
*/
@GetMapping("/extended-query/purchase/list")
@Operation(summary = "查询采购拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedPurchaseList(@Validated CcdiProjectExtendedPurchaseQueryDTO queryDTO) {
CcdiProjectExtendedPurchaseListVO result = specialCheckService.getExtendedPurchaseList(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询采购拓展详情
*/
@GetMapping("/extended-query/purchase/detail")
@Operation(summary = "查询采购拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedPurchaseDetail(@Validated CcdiProjectExtendedPurchaseDetailQueryDTO queryDTO) {
CcdiProjectExtendedPurchaseDetailVO result = specialCheckService.getExtendedPurchaseDetail(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询招聘拓展列表
*/
@GetMapping("/extended-query/recruitment/list")
@Operation(summary = "查询招聘拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedRecruitmentList(@Validated CcdiProjectExtendedRecruitmentQueryDTO queryDTO) {
CcdiProjectExtendedRecruitmentListVO result = specialCheckService.getExtendedRecruitmentList(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询招聘拓展详情
*/
@GetMapping("/extended-query/recruitment/detail")
@Operation(summary = "查询招聘拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedRecruitmentDetail(@Validated CcdiProjectExtendedRecruitmentDetailQueryDTO queryDTO) {
CcdiProjectExtendedRecruitmentDetailVO result = specialCheckService.getExtendedRecruitmentDetail(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询调动拓展列表
*/
@GetMapping("/extended-query/transfer/list")
@Operation(summary = "查询调动拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedTransferList(@Validated CcdiProjectExtendedTransferQueryDTO queryDTO) {
CcdiProjectExtendedTransferListVO result = specialCheckService.getExtendedTransferList(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询调动拓展详情
*/
@GetMapping("/extended-query/transfer/detail")
@Operation(summary = "查询调动拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedTransferDetail(@Validated CcdiProjectExtendedTransferDetailQueryDTO queryDTO) {
CcdiProjectExtendedTransferDetailVO result = specialCheckService.getExtendedTransferDetail(queryDTO);
return AjaxResult.success(result);
}
} }

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 项目员工负面征信查询 DTO
*/
@Data
public class CcdiProjectEmployeeCreditNegativeQueryDTO {
private Long projectId;
private Integer pageNum;
private Integer pageSize;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 专项核查采购拓展查询详情入参
*/
@Data
public class CcdiProjectExtendedPurchaseDetailQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 采购事项ID */
@NotBlank(message = "采购事项ID不能为空")
private String purchaseId;
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 专项核查采购拓展查询列表入参
*/
@Data
public class CcdiProjectExtendedPurchaseQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 申请人姓名 */
private String applicantName;
/** 申请日期开始 */
private String applyDateStart;
/** 申请日期结束 */
private String applyDateEnd;
/** 页码 */
private Integer pageNum = 1;
/** 每页条数 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 专项核查招聘拓展查询详情入参
*/
@Data
public class CcdiProjectExtendedRecruitmentDetailQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 招聘项目编号 */
@NotBlank(message = "招聘项目编号不能为空")
private String recruitId;
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 专项核查招聘拓展查询列表入参
*/
@Data
public class CcdiProjectExtendedRecruitmentQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 面试官姓名 */
private String interviewerName;
/** 页码 */
private Integer pageNum = 1;
/** 每页条数 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 专项核查调动拓展查询详情入参
*/
@Data
public class CcdiProjectExtendedTransferDetailQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 主键ID */
@NotNull(message = "主键ID不能为空")
private Long id;
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 专项核查调动拓展查询列表入参
*/
@Data
public class CcdiProjectExtendedTransferQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 员工姓名 */
private String staffName;
/** 调动日期开始 */
private String transferDateStart;
/** 调动日期结束 */
private String transferDateEnd;
/** 页码 */
private Integer pageNum = 1;
/** 每页条数 */
private Integer pageSize = 10;
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.util.List;
/**
* 历史项目导入DTO
*
* @author ruoyi
*/
@Data
public class CcdiProjectImportHistoryDTO {
/** 新项目名称 */
@NotBlank(message = "项目名称不能为空")
@Length(max = 200, message = "项目名称长度不能超过200个字符")
private String projectName;
/** 项目描述 */
@Length(max = 500, message = "项目描述长度不能超过500个字符")
private String description;
/** 来源项目ID列表 */
@NotEmpty(message = "来源项目不能为空")
private List<Long> sourceProjectIds;
/** 流水起始日期 */
private String startDate;
/** 流水结束日期 */
private String endDate;
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 项目分析详情查询DTO
*/
@Data
public class CcdiProjectPersonAnalysisDetailQueryDTO {
/** 项目ID */
private Long projectId;
/** 员工身份证号 */
private String staffIdCard;
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 风险人员总览查询 DTO
*/
@Data
public class CcdiProjectRiskPeopleQueryDTO {
private Long projectId;
private Integer pageNum;
private Integer pageSize;
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 涉疑交易明细查询DTO
*/
@Data
public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 项目ID */
private Long projectId;
/** 涉疑类型 */
private String suspiciousType;
/** 页码 */
private Integer pageNum;
/** 每页数量 */
private Integer pageSize;
}

View File

@@ -112,6 +112,12 @@ public class CcdiBankStatement implements Serializable {
/** 对手方备注 */ /** 对手方备注 */
private String customerReference; private String customerReference;
/** 交易对手方证件号 */
private String customerCertNo;
/** 交易对手方统一社会信用代码 */
private String customerSocialCreditCode;
// ===== 摘要和备注 ===== // ===== 摘要和备注 =====
/** 用户交易摘要 */ /** 用户交易摘要 */
@@ -199,6 +205,8 @@ public class CcdiBankStatement implements Serializable {
entity.setCustomerLeId(item.getCustomerId()); entity.setCustomerLeId(item.getCustomerId());
entity.setCustomerAccountName(item.getCustomerName()); entity.setCustomerAccountName(item.getCustomerName());
entity.setBatchSequence(item.getUploadSequnceNumber()); entity.setBatchSequence(item.getUploadSequnceNumber());
entity.setCustomerCertNo(item.getCustomerCertNo());
entity.setCustomerSocialCreditCode(item.getCustomerSocialCreditCode());
// 5. 特殊字段处理 // 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null entity.setMetaJson(null); // 根据文档要求强制设为 null

View File

@@ -44,6 +44,15 @@ public class CcdiFileUploadRecord implements Serializable {
/** 文件状态uploading-上传中parsing-解析中parsed_success-解析成功parsed_failed-解析失败 */ /** 文件状态uploading-上传中parsing-解析中parsed_success-解析成功parsed_failed-解析失败 */
private String fileStatus; private String fileStatus;
/** 来源类型 */
private String sourceType;
/** 来源项目ID */
private Long sourceProjectId;
/** 来源项目名称 */
private String sourceProjectName;
/** 主体名称(多个用逗号分隔) */ /** 主体名称(多个用逗号分隔) */
private String enterpriseNames; private String enterpriseNames;

View File

@@ -0,0 +1,38 @@
package com.ruoyi.ccdi.project.domain.event;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
/**
* 历史项目导入提交事件
*/
public class CcdiProjectHistoryImportSubmittedEvent {
private final Long targetProjectId;
private final Integer targetLsfxProjectId;
private final CcdiProjectImportHistoryDTO dto;
private final String operator;
public CcdiProjectHistoryImportSubmittedEvent(Long targetProjectId, Integer targetLsfxProjectId,
CcdiProjectImportHistoryDTO dto, String operator) {
this.targetProjectId = targetProjectId;
this.targetLsfxProjectId = targetLsfxProjectId;
this.dto = dto;
this.operator = operator;
}
public Long getTargetProjectId() {
return targetProjectId;
}
public Integer getTargetLsfxProjectId() {
return targetLsfxProjectId;
}
public CcdiProjectImportHistoryDTO getDto() {
return dto;
}
public String getOperator() {
return operator;
}
}

View File

@@ -0,0 +1,40 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
/**
* 项目员工负面征信导出对象
*/
@Data
public class CcdiProjectEmployeeCreditNegativeExcel {
@Excel(name = "员工姓名")
private String personName;
@Excel(name = "身份证号")
private String personId;
@Excel(name = "最近征信查询日期")
private String queryDate;
@Excel(name = "民事案件笔数")
private Integer civilCnt;
@Excel(name = "民事案件金额")
private BigDecimal civilLmt;
@Excel(name = "强制执行笔数")
private Integer enforceCnt;
@Excel(name = "强制执行金额")
private BigDecimal enforceLmt;
@Excel(name = "行政处罚笔数")
private Integer admCnt;
@Excel(name = "行政处罚金额")
private BigDecimal admLmt;
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
/**
* 风险人员总览导出对象
*/
@Data
public class CcdiProjectRiskPeopleOverviewExcel {
@Excel(name = "姓名")
private String name;
@Excel(name = "身份证号")
private String idNo;
@Excel(name = "所属部门")
private String department;
@Excel(name = "疑似违规数")
private Integer riskCount;
@Excel(name = "风险等级")
private String riskLevel;
@Excel(name = "命中模型数")
private Integer modelCount;
@Excel(name = "核心异常点")
private String riskPoint;
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
/**
* 涉疑交易导出对象
*/
@Data
public class CcdiProjectSuspiciousTransactionExcel {
@Excel(name = "交易时间")
private String trxDate;
@Excel(name = "可疑人员")
private String suspiciousPersonName;
@Excel(name = "关联人")
private String relatedPersonName;
@Excel(name = "关联员工")
private String relatedStaffDisplay;
@Excel(name = "关系")
private String relationType;
@Excel(name = "摘要/交易类型")
private String summaryAndCashType;
@Excel(name = "交易金额")
private BigDecimal displayAmount;
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 项目员工负面征信行
*/
@Data
public class CcdiProjectEmployeeCreditNegativeItemVO {
private String personName;
private String personId;
private String queryDate;
private Integer civilCnt;
private BigDecimal civilLmt;
private Integer enforceCnt;
private BigDecimal enforceLmt;
private Integer admCnt;
private BigDecimal admLmt;
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 项目员工负面征信分页结果
*/
@Data
public class CcdiProjectEmployeeCreditNegativePageVO {
private List<CcdiProjectEmployeeCreditNegativeItemVO> rows = new ArrayList<>();
private Long total = 0L;
}

View File

@@ -0,0 +1,83 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 专项核查采购拓展查询详情
*/
@Data
public class CcdiProjectExtendedPurchaseDetailVO {
private String purchaseId;
private String purchaseCategory;
private String projectName;
private String subjectName;
private String subjectDesc;
private BigDecimal purchaseQty;
private BigDecimal budgetAmount;
private BigDecimal bidAmount;
private BigDecimal actualAmount;
private BigDecimal contractAmount;
private BigDecimal settlementAmount;
private String purchaseMethod;
private String supplierName;
private String contactPerson;
private String contactPhone;
private String supplierUscc;
private String supplierBankAccount;
private String applyDate;
private String planApproveDate;
private String announceDate;
private String bidOpenDate;
private String contractSignDate;
private String expectedDeliveryDate;
private String actualDeliveryDate;
private String acceptanceDate;
private String settlementDate;
private String applicantId;
private String applicantName;
private String applyDepartment;
private String purchaseLeaderId;
private String purchaseLeaderName;
private String purchaseDepartment;
private String createdBy;
private String createTime;
private String updatedBy;
private String updateTime;
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 专项核查采购拓展查询列表项
*/
@Data
public class CcdiProjectExtendedPurchaseListItemVO {
/** 采购事项ID */
private String purchaseId;
/** 项目名称 */
private String projectName;
/** 标的物名称 */
private String subjectName;
/** 申请人姓名 */
private String applicantName;
/** 申请日期 */
private String applyDate;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 专项核查采购拓展查询列表结果
*/
@Data
public class CcdiProjectExtendedPurchaseListVO {
/** 列表数据 */
private List<CcdiProjectExtendedPurchaseListItemVO> rows;
/** 总数 */
private Long total;
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.Date;
import lombok.Data;
/**
* 专项核查招聘拓展查询详情
*/
@Data
public class CcdiProjectExtendedRecruitmentDetailVO {
private String recruitId;
private String recruitName;
private String posName;
private String posCategory;
private String posDesc;
private String candName;
private String candEdu;
private String candId;
private String candSchool;
private String candMajor;
private String candGrad;
private String admitStatus;
private String interviewerName1;
private String interviewerId1;
private String interviewerName2;
private String interviewerId2;
private String createdBy;
private Date createTime;
private String updatedBy;
private Date updateTime;
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 专项核查招聘拓展查询列表项
*/
@Data
public class CcdiProjectExtendedRecruitmentListItemVO {
/** 招聘项目编号 */
private String recruitId;
/** 招聘项目名称 */
private String recruitName;
/** 职位名称 */
private String posName;
/** 面试官摘要 */
private String interviewerNameSummary;
/** 录用情况 */
private String admitStatus;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 专项核查招聘拓展查询列表结果
*/
@Data
public class CcdiProjectExtendedRecruitmentListVO {
/** 列表数据 */
private List<CcdiProjectExtendedRecruitmentListItemVO> rows;
/** 总数 */
private Long total;
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.Date;
import lombok.Data;
/**
* 专项核查调动拓展查询详情
*/
@Data
public class CcdiProjectExtendedTransferDetailVO {
private Long id;
private Long staffId;
private String staffName;
private String transferType;
private String transferSubType;
private Long deptIdBefore;
private String deptNameBefore;
private String gradeBefore;
private String positionBefore;
private String salaryLevelBefore;
private Long deptIdAfter;
private String deptNameAfter;
private String gradeAfter;
private String positionAfter;
private String salaryLevelAfter;
private Date transferDate;
private String createdBy;
private Date createTime;
private String updatedBy;
private Date updateTime;
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 专项核查调动拓展查询列表项
*/
@Data
public class CcdiProjectExtendedTransferListItemVO {
/** 主键ID */
private Long id;
/** 员工姓名 */
private String staffName;
/** 调动类型 */
private String transferType;
/** 调动前部门 */
private String deptNameBefore;
/** 调动后部门 */
private String deptNameAfter;
/** 调动日期 */
private String transferDate;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 专项核查调动拓展查询列表结果
*/
@Data
public class CcdiProjectExtendedTransferListVO {
/** 列表数据 */
private List<CcdiProjectExtendedTransferListItemVO> rows;
/** 总数 */
private Long total;
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.Date;
/**
* 历史项目列表项VO
*
* @author ruoyi
*/
@Data
public class CcdiProjectHistoryListItemVO {
/** 项目ID */
private Long projectId;
/** 项目名称 */
private String projectName;
/** 项目描述 */
private String description;
/** 项目状态 */
private String status;
/** 是否归档 */
private Integer isArchived;
/** 创建时间 */
private Date createTime;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 项目分析异常明细
*/
@Data
public class CcdiProjectPersonAnalysisAbnormalDetailVO {
private List<CcdiProjectPersonAnalysisAbnormalGroupVO> groups;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 项目分析异常分组
*/
@Data
public class CcdiProjectPersonAnalysisAbnormalGroupVO {
private String groupCode;
private String groupName;
private String groupType;
private List<?> records;
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 项目分析人员基础信息
*/
@Data
public class CcdiProjectPersonAnalysisBasicInfoVO {
private String name;
private String idNo;
private String staffCode;
private String department;
private String phone;
private String riskLevel;
private String projectName;
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 项目分析详情
*/
@Data
public class CcdiProjectPersonAnalysisDetailVO {
private CcdiProjectPersonAnalysisBasicInfoVO basicInfo;
private CcdiProjectPersonAnalysisAbnormalDetailVO abnormalDetail;
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 项目分析对象型异常补充字段
*/
@Data
public class CcdiProjectPersonAnalysisObjectFieldVO {
private String label;
private String value;
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 项目分析对象型异常记录
*/
@Data
public class CcdiProjectPersonAnalysisObjectRecordVO {
private String title;
private String subtitle;
private List<String> riskTags;
private String reasonDetail;
private String summary;
private List<CcdiProjectPersonAnalysisObjectFieldVO> extraFields = new ArrayList<>();
}

View File

@@ -9,5 +9,11 @@ import lombok.Data;
@Data @Data
public class CcdiProjectRiskPeopleOverviewVO { public class CcdiProjectRiskPeopleOverviewVO {
private List<CcdiProjectRiskPeopleOverviewItemVO> overviewList; private List<CcdiProjectRiskPeopleOverviewItemVO> rows;
private Long total;
private Long pageNum;
private Long pageSize;
} }

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 涉疑交易明细行
*/
@Data
public class CcdiProjectSuspiciousTransactionItemVO {
private Long bankStatementId;
private String trxDate;
private String suspiciousPersonName;
private String relatedPersonName;
private String relatedStaffName;
private String relatedStaffCode;
private String relationType;
private String userMemo;
private String cashType;
private BigDecimal displayAmount;
private Boolean hasModelRuleHit;
private Boolean hasNameListHit;
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 涉疑交易分页结果
*/
@Data
public class CcdiProjectSuspiciousTransactionPageVO {
private List<CcdiProjectSuspiciousTransactionItemVO> rows;
private Long total;
}

View File

@@ -30,6 +30,11 @@ public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {
int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId, int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId,
@Param("batchId") Integer batchId); @Param("batchId") Integer batchId);
List<CcdiBankStatement> selectStatementsForHistoryImport(@Param("projectId") Long projectId,
@Param("batchId") Integer batchId,
@Param("startDate") String startDate,
@Param("endDate") String endDate);
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page, Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
@Param("query") CcdiBankStatementQueryDTO query); @Param("query") CcdiBankStatementQueryDTO query);

View File

@@ -25,6 +25,14 @@ public interface CcdiFileUploadRecordMapper extends BaseMapper<CcdiFileUploadRec
*/ */
int insertBatch(@Param("list") List<CcdiFileUploadRecord> records); int insertBatch(@Param("list") List<CcdiFileUploadRecord> records);
/**
* 查询来源项目中解析成功且具备logId的文件记录
*
* @param projectIds 来源项目ID列表
* @return 文件记录列表
*/
List<CcdiFileUploadRecord> selectSuccessfulRecordsByProjectIds(@Param("projectIds") List<Long> projectIds);
/** /**
* 统计各状态文件数量 * 统计各状态文件数量
* *

View File

@@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List;
/** /**
* 项目Mapper接口 * 项目Mapper接口
* *
@@ -24,6 +27,14 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
*/ */
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO); Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO);
/**
* 查询历史项目列表
*
* @param queryDTO 查询条件
* @return 历史项目列表
*/
List<CcdiProjectHistoryListItemVO> selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO);
/** /**
* 更新项目风险人数 * 更新项目风险人数
* *

View File

@@ -1,12 +1,20 @@
package com.ruoyi.ccdi.project.mapper; package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@@ -29,10 +37,22 @@ public interface CcdiProjectOverviewMapper {
/** /**
* 查询风险人员总览 * 查询风险人员总览
* *
* @param page 分页参数
* @param query 查询条件
* @return 风险人员聚合分页结果
*/
Page<CcdiProjectEmployeeRiskAggregateVO> selectRiskPeopleOverviewPage(
Page<CcdiProjectEmployeeRiskAggregateVO> page,
@Param("query") CcdiProjectRiskPeopleQueryDTO query
);
/**
* 查询风险人员总览导出列表
*
* @param projectId 项目ID * @param projectId 项目ID
* @return 风险人员聚合列表 * @return 风险人员聚合列表
*/ */
List<CcdiProjectEmployeeRiskAggregateVO> selectRiskPeopleOverviewByProjectId(@Param("projectId") Long projectId); List<CcdiProjectEmployeeRiskAggregateVO> selectRiskPeopleOverviewList(@Param("projectId") Long projectId);
/** /**
* 查询中高风险TOP10 * 查询中高风险TOP10
@@ -62,6 +82,48 @@ public interface CcdiProjectOverviewMapper {
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query @Param("query") CcdiProjectRiskModelPeopleQueryDTO query
); );
/**
* 分页查询涉疑交易明细
*
* @param page 分页参数
* @param query 查询条件
* @return 分页结果
*/
Page<CcdiProjectSuspiciousTransactionItemVO> selectSuspiciousTransactionPage(
Page<CcdiProjectSuspiciousTransactionItemVO> page,
@Param("query") CcdiProjectSuspiciousTransactionQueryDTO query
);
/**
* 分页查询项目员工负面征信
*
* @param page 分页参数
* @param query 查询条件
* @return 分页结果
*/
Page<CcdiProjectEmployeeCreditNegativeItemVO> selectEmployeeCreditNegativePage(
Page<CcdiProjectEmployeeCreditNegativeItemVO> page,
@Param("query") CcdiProjectEmployeeCreditNegativeQueryDTO query
);
/**
* 查询项目员工负面征信导出列表
*
* @param projectId 项目ID
* @return 导出列表
*/
List<CcdiProjectEmployeeCreditNegativeItemVO> selectEmployeeCreditNegativeList(@Param("projectId") Long projectId);
/**
* 查询涉疑交易导出列表
*
* @param query 查询条件
* @return 导出列表
*/
List<CcdiProjectSuspiciousTransactionItemVO> selectSuspiciousTransactionList(
@Param("query") CcdiProjectSuspiciousTransactionQueryDTO query
);
/** /**
* 按员工范围查询命中标签 * 按员工范围查询命中标签
* *
@@ -76,6 +138,42 @@ public interface CcdiProjectOverviewMapper {
@Param("selectedModelCodes") String selectedModelCodes @Param("selectedModelCodes") String selectedModelCodes
); );
/**
* 查询项目分析基础信息
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @return 项目分析基础信息
*/
CcdiProjectPersonAnalysisBasicInfoVO selectPersonAnalysisBasicInfo(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard
);
/**
* 查询项目分析流水异常明细
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @return 流水异常明细
*/
List<CcdiBankStatementListVO> selectPersonAnalysisStatementRows(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard
);
/**
* 查询项目分析对象型异常记录
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @return 对象型异常记录
*/
List<CcdiProjectPersonAnalysisObjectRecordVO> selectPersonAnalysisObjectRows(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard
);
/** /**
* 查询项目风险人数汇总 * 查询项目风险人数汇总
* *

View File

@@ -1,5 +1,15 @@
package com.ruoyi.ccdi.project.mapper; package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
@@ -61,4 +71,76 @@ public interface CcdiProjectSpecialCheckMapper {
@Param("staffIdCard") String staffIdCard, @Param("staffIdCard") String staffIdCard,
@Param("spouseIdCard") String spouseIdCard @Param("spouseIdCard") String spouseIdCard
); );
/**
* 查询专项核查采购拓展列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectExtendedPurchaseListItemVO> selectExtendedPurchasePage(
@Param("page") Page<CcdiProjectExtendedPurchaseListItemVO> page,
@Param("query") CcdiProjectExtendedPurchaseQueryDTO queryDTO
);
/**
* 查询专项核查采购拓展详情
*
* @param projectId 项目ID
* @param purchaseId 采购事项ID
* @return 详情
*/
CcdiProjectExtendedPurchaseDetailVO selectExtendedPurchaseDetail(
@Param("projectId") Long projectId,
@Param("purchaseId") String purchaseId
);
/**
* 查询专项核查招聘拓展列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectExtendedRecruitmentListItemVO> selectExtendedRecruitmentPage(
@Param("page") Page<CcdiProjectExtendedRecruitmentListItemVO> page,
@Param("query") CcdiProjectExtendedRecruitmentQueryDTO queryDTO
);
/**
* 查询专项核查招聘拓展详情
*
* @param projectId 项目ID
* @param recruitId 招聘项目编号
* @return 详情
*/
CcdiProjectExtendedRecruitmentDetailVO selectExtendedRecruitmentDetail(
@Param("projectId") Long projectId,
@Param("recruitId") String recruitId
);
/**
* 查询专项核查调动拓展列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectExtendedTransferListItemVO> selectExtendedTransferPage(
@Param("page") Page<CcdiProjectExtendedTransferListItemVO> page,
@Param("query") CcdiProjectExtendedTransferQueryDTO queryDTO
);
/**
* 查询专项核查调动拓展详情
*
* @param projectId 项目ID
* @param id 主键ID
* @return 详情
*/
CcdiProjectExtendedTransferDetailVO selectExtendedTransferDetail(
@Param("projectId") Long projectId,
@Param("id") Long id
);
} }

View File

@@ -0,0 +1,22 @@
package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
/**
* 历史项目导入服务
*
* @author ruoyi
*/
public interface ICcdiProjectHistoryImportService {
/**
* 提交历史项目导入任务
*
* @param targetProjectId 目标项目ID
* @param targetLsfxProjectId 目标流水分析项目ID
* @param dto 导入参数
* @param operator 操作人
*/
void submitImport(Long targetProjectId, Integer targetLsfxProjectId,
CcdiProjectImportHistoryDTO dto, String operator);
}

View File

@@ -1,11 +1,24 @@
package com.ruoyi.ccdi.project.service; package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/** /**
* 结果总览服务接口 * 结果总览服务接口
@@ -23,10 +36,10 @@ public interface ICcdiProjectOverviewService {
/** /**
* 查询风险人员总览 * 查询风险人员总览
* *
* @param projectId 项目ID * @param queryDTO 查询条件
* @return 风险人员总览 * @return 风险人员总览
*/ */
CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(Long projectId); CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(CcdiProjectRiskPeopleQueryDTO queryDTO);
/** /**
* 查询中高风险人员TOP10 * 查询中高风险人员TOP10
@@ -36,6 +49,16 @@ public interface ICcdiProjectOverviewService {
*/ */
CcdiProjectTopRiskPeopleVO getTopRiskPeople(Long projectId); CcdiProjectTopRiskPeopleVO getTopRiskPeople(Long projectId);
/**
* 查询项目分析详情
*
* @param queryDTO 查询条件
* @return 项目分析详情
*/
default CcdiProjectPersonAnalysisDetailVO getPersonAnalysisDetail(CcdiProjectPersonAnalysisDetailQueryDTO queryDTO) {
return new CcdiProjectPersonAnalysisDetailVO();
}
/** /**
* 查询风险模型卡片 * 查询风险模型卡片
* *
@@ -56,6 +79,71 @@ public interface ICcdiProjectOverviewService {
return new CcdiProjectRiskModelPeopleVO(); return new CcdiProjectRiskModelPeopleVO();
} }
/**
* 查询涉疑交易明细
*
* @param queryDTO 查询条件
* @return 分页结果
*/
default CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
return new CcdiProjectSuspiciousTransactionPageVO();
}
/**
* 导出涉疑交易明细
*
* @param queryDTO 查询条件
* @return 导出列表
*/
default List<CcdiProjectSuspiciousTransactionExcel> exportSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
return List.of();
}
/**
* 导出风险人员总览
*
* @param projectId 项目ID
* @return 导出列表
*/
default List<CcdiProjectRiskPeopleOverviewExcel> exportRiskPeopleOverview(Long projectId) {
return List.of();
}
/**
* 统一导出风险明细
*
* @param response 响应流
* @param projectId 项目ID
*/
default void exportRiskDetails(HttpServletResponse response, Long projectId) {
}
/**
* 导出项目员工负面征信
*
* @param projectId 项目ID
* @return 导出列表
*/
default List<CcdiProjectEmployeeCreditNegativeExcel> exportEmployeeCreditNegative(Long projectId) {
return List.of();
}
/**
* 查询项目员工负面征信
*
* @param queryDTO 查询条件
* @return 分页结果
*/
default CcdiProjectEmployeeCreditNegativePageVO getEmployeeCreditNegative(
CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO
) {
return new CcdiProjectEmployeeCreditNegativePageVO();
}
/** /**
* 重算结果总览员工结果并同步项目风险人数 * 重算结果总览员工结果并同步项目风险人数
* *

View File

@@ -1,11 +1,15 @@
package com.ruoyi.ccdi.project.service; package com.ruoyi.ccdi.project.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.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 java.util.List;
/** /**
* 项目Service接口 * 项目Service接口
* *
@@ -53,6 +57,23 @@ public interface ICcdiProjectService {
*/ */
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO); Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO);
/**
* 查询历史项目列表
*
* @param queryDTO 查询条件
* @return 历史项目列表
*/
List<CcdiProjectHistoryListItemVO> listHistoryProjects(CcdiProjectQueryDTO queryDTO);
/**
* 从历史项目导入
*
* @param dto 导入参数
* @param operator 操作人
* @return 新建项目
*/
CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator);
/** /**
* 查询各状态的项目总数(不受搜索条件影响) * 查询各状态的项目总数(不受搜索条件影响)
* *
@@ -60,6 +81,14 @@ public interface ICcdiProjectService {
*/ */
CcdiProjectStatusCountsVO getStatusCounts(); CcdiProjectStatusCountsVO getStatusCounts();
/**
* 归档项目
*
* @param projectId 项目ID
* @param operator 操作人
*/
void archiveProject(Long projectId, String operator);
/** /**
* 更新项目状态 * 更新项目状态
* *
@@ -76,6 +105,14 @@ public interface ICcdiProjectService {
*/ */
void ensureProjectCanStartTagging(Long projectId); void ensureProjectCanStartTagging(Long projectId);
/**
* 校验项目是否未归档
*
* @param projectId 项目ID
* @param message 拒绝文案
*/
void ensureProjectNotArchived(Long projectId, String message);
/** /**
* 校验项目是否允许写入 * 校验项目是否允许写入
* *

View File

@@ -1,7 +1,19 @@
package com.ruoyi.ccdi.project.service; package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
@@ -29,4 +41,54 @@ public interface ICcdiProjectSpecialCheckService {
CcdiProjectFamilyAssetLiabilityDetailVO getFamilyAssetLiabilityDetail( CcdiProjectFamilyAssetLiabilityDetailVO getFamilyAssetLiabilityDetail(
CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO
); );
/**
* 查询采购拓展列表
*
* @param queryDTO 查询条件
* @return 列表结果
*/
CcdiProjectExtendedPurchaseListVO getExtendedPurchaseList(CcdiProjectExtendedPurchaseQueryDTO queryDTO);
/**
* 查询采购拓展详情
*
* @param queryDTO 查询条件
* @return 详情结果
*/
CcdiProjectExtendedPurchaseDetailVO getExtendedPurchaseDetail(CcdiProjectExtendedPurchaseDetailQueryDTO queryDTO);
/**
* 查询招聘拓展列表
*
* @param queryDTO 查询条件
* @return 列表结果
*/
CcdiProjectExtendedRecruitmentListVO getExtendedRecruitmentList(CcdiProjectExtendedRecruitmentQueryDTO queryDTO);
/**
* 查询招聘拓展详情
*
* @param queryDTO 查询条件
* @return 详情结果
*/
CcdiProjectExtendedRecruitmentDetailVO getExtendedRecruitmentDetail(
CcdiProjectExtendedRecruitmentDetailQueryDTO queryDTO
);
/**
* 查询调动拓展列表
*
* @param queryDTO 查询条件
* @return 列表结果
*/
CcdiProjectExtendedTransferListVO getExtendedTransferList(CcdiProjectExtendedTransferQueryDTO queryDTO);
/**
* 查询调动拓展详情
*
* @param queryDTO 查询条件
* @return 详情结果
*/
CcdiProjectExtendedTransferDetailVO getExtendedTransferDetail(CcdiProjectExtendedTransferDetailQueryDTO queryDTO);
} }

View File

@@ -16,6 +16,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService; import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService; import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import com.ruoyi.ccdi.project.service.ICcdiProjectService; import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient; import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.constants.LsfxConstants; import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest; import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
@@ -169,6 +170,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
throw new IllegalArgumentException("开始日期不能晚于结束日期"); throw new IllegalArgumentException("开始日期不能晚于结束日期");
} }
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
CcdiProject project = projectMapper.selectById(projectId); CcdiProject project = projectMapper.selectById(projectId);
@@ -323,6 +325,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}", log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
projectId, files.length, username); projectId, files.length, username);
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
// 1. 生成批次ID // 1. 生成批次ID
@@ -962,6 +965,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
if (record == null) { if (record == null) {
throw new RuntimeException("上传记录不存在"); throw new RuntimeException("上传记录不存在");
} }
if ("HISTORY_IMPORT".equals(record.getSourceType())) {
throw new ServiceException("历史导入文件不支持删除");
}
if (!"parsed_success".equals(record.getFileStatus())) { if (!"parsed_success".equals(record.getFileStatus())) {
if ("deleted".equals(record.getFileStatus())) { if ("deleted".equals(record.getFileStatus())) {
throw new RuntimeException("文件已删除,请勿重复操作"); throw new RuntimeException("文件已删除,请勿重复操作");

View File

@@ -111,6 +111,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
Long projectId = saveDTO.getProjectId(); Long projectId = saveDTO.getProjectId();
if (projectId > 0) { if (projectId > 0) {
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许修改参数");
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数");
switchToCustomConfigIfNeeded(getRequiredProject(projectId)); switchToCustomConfigIfNeeded(getRequiredProject(projectId));
} }
@@ -192,6 +193,7 @@ public class CcdiModelParamServiceImpl implements ICcdiModelParamService {
Long projectId = saveAllDTO.getProjectId(); Long projectId = saveAllDTO.getProjectId();
if (projectId > 0) { if (projectId > 0) {
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许修改参数");
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数"); projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许修改参数");
switchToCustomConfigIfNeeded(getRequiredProject(projectId)); switchToCustomConfigIfNeeded(getRequiredProject(projectId));
} }

View File

@@ -0,0 +1,27 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.event.CcdiProjectHistoryImportSubmittedEvent;
import com.ruoyi.ccdi.project.service.ICcdiProjectHistoryImportService;
import jakarta.annotation.Resource;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 历史项目导入事件监听器
*/
@Component
public class CcdiProjectHistoryImportEventListener {
@Resource
private ICcdiProjectHistoryImportService historyImportService;
@EventListener
public void onSubmitted(CcdiProjectHistoryImportSubmittedEvent event) {
historyImportService.submitImport(
event.getTargetProjectId(),
event.getTargetLsfxProjectId(),
event.getDto(),
event.getOperator()
);
}
}

View File

@@ -0,0 +1,205 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.ccdi.project.service.ICcdiProjectHistoryImportService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 历史项目导入服务实现
*
* @author ruoyi
*/
@Slf4j
@Service
public class CcdiProjectHistoryImportServiceImpl implements ICcdiProjectHistoryImportService {
private static final AtomicInteger HISTORY_IMPORT_BATCH_ID = new AtomicInteger(1_000_000);
@Resource
private CcdiBankStatementMapper bankStatementMapper;
@Resource
private CcdiFileUploadRecordMapper recordMapper;
@Resource
private CcdiProjectMapper projectMapper;
@Resource
@Qualifier("fileUploadExecutor")
private Executor fileUploadExecutor;
@Resource
private ICcdiBankTagService bankTagService;
@Override
public void submitImport(Long targetProjectId, Integer targetLsfxProjectId,
CcdiProjectImportHistoryDTO dto, String operator) {
fileUploadExecutor.execute(() -> executeImport(targetProjectId, targetLsfxProjectId, dto, operator));
}
private void executeImport(Long targetProjectId, Integer targetLsfxProjectId,
CcdiProjectImportHistoryDTO dto, String operator) {
List<CcdiFileUploadRecord> sourceRecords = recordMapper.selectSuccessfulRecordsByProjectIds(dto.getSourceProjectIds());
if (sourceRecords == null || sourceRecords.isEmpty()) {
log.info("【项目历史导入】无可复制的来源批次: projectId={}, sourceProjectIds={}",
targetProjectId, dto.getSourceProjectIds());
return;
}
List<CcdiBankStatement> statementsToInsert = new ArrayList<>();
List<CcdiFileUploadRecord> recordsToInsert = new ArrayList<>();
Set<String> dedupKeys = new HashSet<>();
for (CcdiFileUploadRecord sourceRecord : sourceRecords) {
List<CcdiBankStatement> sourceStatements = bankStatementMapper.selectStatementsForHistoryImport(
sourceRecord.getProjectId(), sourceRecord.getLogId(), dto.getStartDate(), dto.getEndDate()
);
if (sourceStatements == null || sourceStatements.isEmpty()) {
continue;
}
Integer newBatchId = HISTORY_IMPORT_BATCH_ID.incrementAndGet();
int sizeBefore = statementsToInsert.size();
for (CcdiBankStatement sourceStatement : sourceStatements) {
CcdiBankStatement targetStatement = copyStatement(sourceStatement, targetProjectId, targetLsfxProjectId, newBatchId);
if (dedupKeys.add(buildDedupKey(targetStatement))) {
statementsToInsert.add(targetStatement);
}
}
if (statementsToInsert.size() > sizeBefore) {
recordsToInsert.add(buildHistoryImportRecord(
sourceRecord, targetProjectId, targetLsfxProjectId, newBatchId, resolveSourceProjectName(sourceRecord.getProjectId()), operator
));
}
}
if (!statementsToInsert.isEmpty()) {
bankStatementMapper.insertBatch(statementsToInsert);
}
if (!recordsToInsert.isEmpty()) {
recordMapper.insertBatch(recordsToInsert);
}
if (!statementsToInsert.isEmpty()) {
refreshProjectTargetCount(targetProjectId);
bankTagService.submitAutoRebuild(targetProjectId, TriggerType.AUTO_BATCH_UPLOAD);
}
}
private CcdiBankStatement copyStatement(CcdiBankStatement sourceStatement, Long targetProjectId,
Integer targetLsfxProjectId, Integer newBatchId) {
CcdiBankStatement targetStatement = new CcdiBankStatement();
BeanUtils.copyProperties(sourceStatement, targetStatement);
targetStatement.setBankStatementId(null);
targetStatement.setProjectId(targetProjectId);
targetStatement.setGroupId(targetLsfxProjectId);
targetStatement.setBatchId(newBatchId);
normalizeDedupFields(targetStatement);
return targetStatement;
}
private CcdiFileUploadRecord buildHistoryImportRecord(CcdiFileUploadRecord sourceRecord, Long targetProjectId,
Integer targetLsfxProjectId, Integer newBatchId,
String sourceProjectName, String operator) {
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
record.setProjectId(targetProjectId);
record.setLsfxProjectId(targetLsfxProjectId);
record.setLogId(newBatchId);
record.setFileName(sourceRecord.getFileName());
record.setFileSize(sourceRecord.getFileSize());
record.setFileStatus("parsed_success");
record.setEnterpriseNames(sourceRecord.getEnterpriseNames());
record.setAccountNos(sourceRecord.getAccountNos());
record.setUploadTime(new Date());
record.setUploadUser(operator);
record.setSourceType("HISTORY_IMPORT");
record.setSourceProjectId(sourceRecord.getProjectId());
record.setSourceProjectName(sourceProjectName);
return record;
}
private String resolveSourceProjectName(Long sourceProjectId) {
CcdiProject sourceProject = projectMapper.selectById(sourceProjectId);
return sourceProject == null ? null : sourceProject.getProjectName();
}
private void refreshProjectTargetCount(Long targetProjectId) {
CcdiProject targetProject = projectMapper.selectById(targetProjectId);
if (targetProject == null) {
log.warn("【项目历史导入】刷新目标人数时项目不存在: projectId={}", targetProjectId);
return;
}
int targetCount = bankStatementMapper.countMatchedStaffCountByProjectId(targetProjectId);
targetProject.setTargetCount(targetCount);
projectMapper.updateById(targetProject);
}
private void normalizeDedupFields(CcdiBankStatement statement) {
statement.setLeAccountNo(trimToNull(statement.getLeAccountNo()));
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String buildDedupKey(CcdiBankStatement statement) {
return String.join("|",
valueOf(statement.getTrxDate()),
valueOf(statement.getCurrency()),
valueOf(statement.getLeAccountNo()),
valueOf(statement.getLeAccountName()),
valueOf(statement.getAccountId()),
valueOf(statement.getCustomerAccountName()),
valueOf(statement.getCustomerAccountNo()),
valueOf(statement.getCustomerBank()),
valueOf(statement.getCustomerReference()),
valueOf(statement.getCustomerCertNo()),
valueOf(statement.getCustomerSocialCreditCode()),
valueOf(statement.getAmountDr()),
valueOf(statement.getAmountCr()),
valueOf(statement.getAmountBalance()),
valueOf(statement.getCashType()),
valueOf(statement.getUserMemo()),
valueOf(statement.getBankComments()),
valueOf(statement.getBankTrxNumber()),
valueOf(statement.getBank()),
valueOf(statement.getTrxFlag()),
valueOf(statement.getTrxType()),
valueOf(statement.getExceptionType()),
valueOf(statement.getInternalFlag()),
valueOf(statement.getCreateDate()),
valueOf(statement.getPaymentMethod()),
valueOf(statement.getCretNo())
);
}
private String valueOf(Object value) {
return Objects.toString(value, "");
}
}

View File

@@ -2,8 +2,24 @@ package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisAbnormalDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisAbnormalGroupVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
@@ -13,16 +29,25 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -43,9 +68,15 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
@Resource @Resource
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper; private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
@Resource
private CcdiBankTagResultMapper bankTagResultMapper;
@Resource @Resource
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder; private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@Resource
private CcdiProjectRiskDetailWorkbookExporter workbookExporter;
@Override @Override
public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) { public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) {
CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId); CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId);
@@ -73,16 +104,26 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
} }
@Override @Override
public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(Long projectId) { public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(CcdiProjectRiskPeopleQueryDTO queryDTO) {
Long projectId = queryDTO.getProjectId();
ensureProjectExists(projectId); ensureProjectExists(projectId);
List<CcdiProjectRiskPeopleOverviewItemVO> overviewList = overviewMapper.selectRiskPeopleOverviewByProjectId(projectId) Page<CcdiProjectEmployeeRiskAggregateVO> page = new Page<>(
defaultRiskPeoplePageNum(queryDTO.getPageNum()),
defaultRiskPeoplePageSize(queryDTO.getPageSize())
);
Page<CcdiProjectEmployeeRiskAggregateVO> resultPage = overviewMapper.selectRiskPeopleOverviewPage(page, queryDTO);
List<CcdiProjectRiskPeopleOverviewItemVO> rows = defaultList(resultPage == null ? null : resultPage.getRecords())
.stream() .stream()
.map(aggregate -> buildRiskPeopleItem(projectId, aggregate)) .map(aggregate -> buildRiskPeopleItem(projectId, aggregate))
.toList(); .toList();
CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO(); CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO();
overview.setOverviewList(overviewList); overview.setRows(rows);
overview.setTotal(resultPage == null ? 0L : resultPage.getTotal());
overview.setPageNum(page.getCurrent());
overview.setPageSize(page.getSize());
return overview; return overview;
} }
@@ -100,6 +141,31 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return topRiskPeople; return topRiskPeople;
} }
@Override
public CcdiProjectPersonAnalysisDetailVO getPersonAnalysisDetail(CcdiProjectPersonAnalysisDetailQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
CcdiProjectPersonAnalysisBasicInfoVO basicInfo = overviewMapper.selectPersonAnalysisBasicInfo(
queryDTO.getProjectId(),
queryDTO.getStaffIdCard()
);
List<CcdiBankStatementListVO> statementRows = defaultList(overviewMapper.selectPersonAnalysisStatementRows(
queryDTO.getProjectId(),
queryDTO.getStaffIdCard()
));
List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows = defaultList(overviewMapper.selectPersonAnalysisObjectRows(
queryDTO.getProjectId(),
queryDTO.getStaffIdCard()
));
attachStatementHitTags(statementRows, queryDTO.getProjectId());
normalizeObjectRows(objectRows);
CcdiProjectPersonAnalysisDetailVO detail = new CcdiProjectPersonAnalysisDetailVO();
detail.setBasicInfo(basicInfo == null ? new CcdiProjectPersonAnalysisBasicInfoVO() : basicInfo);
detail.setAbnormalDetail(buildAbnormalDetail(statementRows, objectRows));
return detail;
}
@Override @Override
public CcdiProjectRiskModelCardsVO getRiskModelCards(Long projectId) { public CcdiProjectRiskModelCardsVO getRiskModelCards(Long projectId) {
ensureProjectExists(projectId); ensureProjectExists(projectId);
@@ -131,6 +197,91 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return people; return people;
} }
@Override
public CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectSuspiciousTransactionItemVO> resultPage =
overviewMapper.selectSuspiciousTransactionPage(page, queryDTO);
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public List<CcdiProjectSuspiciousTransactionExcel> exportSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
return defaultList(overviewMapper.selectSuspiciousTransactionList(queryDTO)).stream()
.map(this::buildSuspiciousTransactionExcelRow)
.toList();
}
@Override
public List<CcdiProjectRiskPeopleOverviewExcel> exportRiskPeopleOverview(Long projectId) {
ensureProjectExists(projectId);
return defaultList(overviewMapper.selectRiskPeopleOverviewList(projectId)).stream()
.map(aggregate -> buildRiskPeopleItem(projectId, aggregate))
.map(this::buildRiskPeopleExcelRow)
.toList();
}
@Override
public CcdiProjectEmployeeCreditNegativePageVO getEmployeeCreditNegative(
CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
Page<CcdiProjectEmployeeCreditNegativeItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectEmployeeCreditNegativeItemVO> resultPage =
overviewMapper.selectEmployeeCreditNegativePage(page, queryDTO);
CcdiProjectEmployeeCreditNegativePageVO result = new CcdiProjectEmployeeCreditNegativePageVO();
result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setSuspiciousType("ALL");
List<CcdiProjectSuspiciousTransactionExcel> suspiciousRows = exportSuspiciousTransactions(queryDTO);
List<CcdiProjectEmployeeCreditNegativeExcel> creditRows = exportEmployeeCreditNegative(projectId);
try {
workbookExporter.export(response, projectId, suspiciousRows, creditRows);
} catch (IOException e) {
throw new ServiceException("导出风险明细失败");
}
}
@Override
public List<CcdiProjectEmployeeCreditNegativeExcel> exportEmployeeCreditNegative(Long projectId) {
ensureProjectExists(projectId);
return defaultList(overviewMapper.selectEmployeeCreditNegativeList(projectId)).stream()
.map(this::buildEmployeeCreditNegativeExcelRow)
.toList();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void refreshOverviewEmployeeResults(Long projectId, String operator) { public void refreshOverviewEmployeeResults(Long projectId, String operator) {
@@ -197,6 +348,18 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return item; return item;
} }
private CcdiProjectRiskPeopleOverviewExcel buildRiskPeopleExcelRow(CcdiProjectRiskPeopleOverviewItemVO item) {
CcdiProjectRiskPeopleOverviewExcel row = new CcdiProjectRiskPeopleOverviewExcel();
row.setName(item.getName());
row.setIdNo(item.getIdNo());
row.setDepartment(item.getDepartment());
row.setRiskCount(item.getRiskCount());
row.setRiskLevel(item.getRiskLevel());
row.setModelCount(item.getModelCount());
row.setRiskPoint(item.getRiskPoint());
return row;
}
private void ensureProjectExists(Long projectId) { private void ensureProjectExists(Long projectId) {
getRequiredProject(projectId); getRequiredProject(projectId);
} }
@@ -209,6 +372,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase()); queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
} }
private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
queryDTO.setSuspiciousType("ALL");
return;
}
queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase());
}
private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) { private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) {
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO(); CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
stat.setKey(key); stat.setKey(key);
@@ -241,6 +412,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return value == null ? 0 : value; return value == null ? 0 : value;
} }
private long defaultRiskPeoplePageNum(Integer pageNum) {
return pageNum == null || pageNum <= 0 ? 1L : pageNum.longValue();
}
private long defaultRiskPeoplePageSize(Integer pageSize) {
return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue();
}
private long defaultPageNum(Integer pageNum) { private long defaultPageNum(Integer pageNum) {
return pageNum == null || pageNum < 1 ? 1L : pageNum.longValue(); return pageNum == null || pageNum < 1 ? 1L : pageNum.longValue();
} }
@@ -253,6 +432,118 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return value == null ? List.of() : value; return value == null ? List.of() : value;
} }
private CcdiProjectSuspiciousTransactionExcel buildSuspiciousTransactionExcelRow(
CcdiProjectSuspiciousTransactionItemVO item
) {
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
row.setTrxDate(item.getTrxDate());
row.setSuspiciousPersonName(item.getSuspiciousPersonName());
row.setRelatedPersonName(item.getRelatedPersonName());
row.setRelatedStaffDisplay(formatRelatedStaff(item.getRelatedStaffName(), item.getRelatedStaffCode()));
row.setRelationType(item.getRelationType());
row.setSummaryAndCashType(formatSummaryAndCashType(item.getUserMemo(), item.getCashType()));
row.setDisplayAmount(item.getDisplayAmount());
return row;
}
private CcdiProjectEmployeeCreditNegativeExcel buildEmployeeCreditNegativeExcelRow(
CcdiProjectEmployeeCreditNegativeItemVO item
) {
CcdiProjectEmployeeCreditNegativeExcel row = new CcdiProjectEmployeeCreditNegativeExcel();
row.setPersonName(item.getPersonName());
row.setPersonId(item.getPersonId());
row.setQueryDate(item.getQueryDate());
row.setCivilCnt(item.getCivilCnt());
row.setCivilLmt(item.getCivilLmt());
row.setEnforceCnt(item.getEnforceCnt());
row.setEnforceLmt(item.getEnforceLmt());
row.setAdmCnt(item.getAdmCnt());
row.setAdmLmt(item.getAdmLmt());
return row;
}
private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) {
if (relatedStaffName == null || relatedStaffName.isBlank()) {
return null;
}
if (relatedStaffCode == null || relatedStaffCode.isBlank()) {
return relatedStaffName;
}
return relatedStaffName + "(" + relatedStaffCode + ")";
}
private String formatSummaryAndCashType(String userMemo, String cashType) {
String safeMemo = userMemo == null ? "" : userMemo;
String safeCashType = cashType == null ? "" : cashType;
return safeMemo + "/" + safeCashType;
}
private CcdiProjectPersonAnalysisAbnormalDetailVO buildAbnormalDetail(
List<CcdiBankStatementListVO> statementRows,
List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows
) {
List<CcdiProjectPersonAnalysisAbnormalGroupVO> groups = new ArrayList<>();
if (!statementRows.isEmpty()) {
groups.add(buildAbnormalGroup("BANK_STATEMENT", "流水异常明细", "BANK_STATEMENT", statementRows));
}
if (!objectRows.isEmpty()) {
groups.add(buildAbnormalGroup("RELATED_OBJECT", "异常对象摘要", "OBJECT", objectRows));
}
CcdiProjectPersonAnalysisAbnormalDetailVO abnormalDetail = new CcdiProjectPersonAnalysisAbnormalDetailVO();
abnormalDetail.setGroups(groups);
return abnormalDetail;
}
private CcdiProjectPersonAnalysisAbnormalGroupVO buildAbnormalGroup(
String groupCode,
String groupName,
String groupType,
List<?> records
) {
CcdiProjectPersonAnalysisAbnormalGroupVO group = new CcdiProjectPersonAnalysisAbnormalGroupVO();
group.setGroupCode(groupCode);
group.setGroupName(groupName);
group.setGroupType(groupType);
group.setRecords(records);
return group;
}
private void attachStatementHitTags(List<CcdiBankStatementListVO> statementRows, Long projectId) {
if (statementRows.isEmpty() || projectId == null) {
return;
}
List<Long> bankStatementIds = statementRows.stream()
.map(CcdiBankStatementListVO::getBankStatementId)
.filter(item -> item != null)
.distinct()
.collect(Collectors.toList());
if (bankStatementIds.isEmpty()) {
return;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(projectId, bankStatementIds)
).stream().filter(item -> item.getBankStatementId() != null)
.collect(Collectors.groupingBy(
CcdiBankStatementHitTagVO::getBankStatementId,
LinkedHashMap::new,
Collectors.toList()
));
statementRows.forEach(row -> row.setHitTags(new ArrayList<>(
hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
)));
}
private void normalizeObjectRows(List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows) {
objectRows.forEach(row -> {
if (row.getRiskTags() == null) {
row.setRiskTags(new ArrayList<>());
}
if (row.getExtraFields() == null) {
row.setExtraFields(new ArrayList<>());
}
});
}
private CcdiProject getRequiredProject(Long projectId) { private CcdiProject getRequiredProject(Long projectId) {
CcdiProject project = projectMapper.selectById(projectId); CcdiProject project = projectMapper.selectById(projectId);
if (project == null) { if (project == null) {

View File

@@ -0,0 +1,110 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.common.utils.file.FileUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
/**
* 风险明细工作簿导出器
*/
@Component
public class CcdiProjectRiskDetailWorkbookExporter {
private static final String CONTENT_TYPE =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
public void export(
HttpServletResponse response,
Long projectId,
List<CcdiProjectSuspiciousTransactionExcel> suspiciousRows,
List<CcdiProjectEmployeeCreditNegativeExcel> creditRows
) throws IOException {
response.setContentType(CONTENT_TYPE);
FileUtils.setAttachmentResponseHeader(response, "风险明细_" + projectId + ".xlsx");
try (Workbook workbook = new XSSFWorkbook()) {
writeSuspiciousSheet(workbook.createSheet("涉疑交易明细"), suspiciousRows);
writeCreditSheet(workbook.createSheet("员工负面征信信息"), creditRows);
writeAbnormalAccountSheet(workbook.createSheet("异常账户人员信息"));
workbook.write(response.getOutputStream());
}
}
private void writeSuspiciousSheet(Sheet sheet, List<CcdiProjectSuspiciousTransactionExcel> rows) {
Row header = sheet.createRow(0);
String[] headers = { "交易时间", "可疑人员", "关联人", "关联员工", "关系", "摘要/交易类型", "交易金额" };
writeHeader(header, headers);
for (int i = 0; i < rows.size(); i++) {
CcdiProjectSuspiciousTransactionExcel item = rows.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(safeText(item.getTrxDate()));
row.createCell(1).setCellValue(safeText(item.getSuspiciousPersonName()));
row.createCell(2).setCellValue(safeText(item.getRelatedPersonName()));
row.createCell(3).setCellValue(safeText(item.getRelatedStaffDisplay()));
row.createCell(4).setCellValue(safeText(item.getRelationType()));
row.createCell(5).setCellValue(safeText(item.getSummaryAndCashType()));
row.createCell(6).setCellValue(safeNumber(item.getDisplayAmount()));
}
}
private void writeCreditSheet(Sheet sheet, List<CcdiProjectEmployeeCreditNegativeExcel> rows) {
Row header = sheet.createRow(0);
String[] headers = {
"员工姓名",
"身份证号",
"最近征信查询日期",
"民事案件笔数",
"民事案件金额",
"强制执行笔数",
"强制执行金额",
"行政处罚笔数",
"行政处罚金额"
};
writeHeader(header, headers);
for (int i = 0; i < rows.size(); i++) {
CcdiProjectEmployeeCreditNegativeExcel item = rows.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(safeText(item.getPersonName()));
row.createCell(1).setCellValue(safeText(item.getPersonId()));
row.createCell(2).setCellValue(safeText(item.getQueryDate()));
row.createCell(3).setCellValue(item.getCivilCnt() == null ? 0 : item.getCivilCnt());
row.createCell(4).setCellValue(safeNumber(item.getCivilLmt()));
row.createCell(5).setCellValue(item.getEnforceCnt() == null ? 0 : item.getEnforceCnt());
row.createCell(6).setCellValue(safeNumber(item.getEnforceLmt()));
row.createCell(7).setCellValue(item.getAdmCnt() == null ? 0 : item.getAdmCnt());
row.createCell(8).setCellValue(safeNumber(item.getAdmLmt()));
}
}
private void writeAbnormalAccountSheet(Sheet sheet) {
Row header = sheet.createRow(0);
String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态" };
writeHeader(header, headers);
}
private void writeHeader(Row row, String[] headers) {
for (int i = 0; i < headers.length; i++) {
row.createCell(i).setCellValue(headers[i]);
}
}
private String safeText(String value) {
return value == null ? "" : value;
}
private double safeNumber(BigDecimal value) {
return value == null ? 0D : value.doubleValue();
}
}

View File

@@ -4,8 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants; import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
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.event.CcdiProjectHistoryImportSubmittedEvent;
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.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
@@ -17,11 +20,15 @@ import com.ruoyi.lsfx.domain.response.GetTokenResponse;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
@@ -39,6 +46,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource @Resource
private LsfxAnalysisClient lsfxAnalysisClient; private LsfxAnalysisClient lsfxAnalysisClient;
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) { public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
@@ -114,6 +124,30 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
return projectMapper.selectProjectPage(page, queryDTO); return projectMapper.selectProjectPage(page, queryDTO);
} }
@Override
public List<CcdiProjectHistoryListItemVO> listHistoryProjects(CcdiProjectQueryDTO queryDTO) {
return projectMapper.selectHistoryProjects(queryDTO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator) {
CcdiProjectSaveDTO saveDTO = new CcdiProjectSaveDTO();
saveDTO.setProjectName(dto.getProjectName());
saveDTO.setDescription(dto.getDescription());
saveDTO.setConfigType("default");
CcdiProjectVO project = createProject(saveDTO);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
applicationEventPublisher.publishEvent(
new CcdiProjectHistoryImportSubmittedEvent(project.getProjectId(), project.getLsfxProjectId(), dto, operator)
);
}
});
return project;
}
@Override @Override
public CcdiProjectStatusCountsVO getStatusCounts() { public CcdiProjectStatusCountsVO getStatusCounts() {
CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO(); CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO();
@@ -152,6 +186,26 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
return vo; return vo;
} }
@Override
public void archiveProject(Long projectId, String operator) {
CcdiProject project = getRequiredProject(projectId);
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
throw new ServiceException("项目已归档,无需重复操作");
}
if (!CcdiProjectStatusConstants.COMPLETED.equals(project.getStatus())) {
throw new ServiceException("仅已完成项目允许归档");
}
project.setStatus(CcdiProjectStatusConstants.ARCHIVED);
project.setIsArchived(1);
project.setUpdateBy(operator);
project.setUpdateTime(new Date());
projectMapper.updateById(project);
log.info("【项目】项目状态变更: projectId={}, projectName={}, oldStatus={}, oldStatusLabel={}, newStatus={}, newStatusLabel={}, operator={}",
project.getProjectId(), project.getProjectName(), CcdiProjectStatusConstants.COMPLETED,
resolveStatusLabel(CcdiProjectStatusConstants.COMPLETED), CcdiProjectStatusConstants.ARCHIVED,
resolveStatusLabel(CcdiProjectStatusConstants.ARCHIVED), resolveOperator(operator));
}
@Override @Override
public void updateProjectStatus(Long projectId, String status, String operator) { public void updateProjectStatus(Long projectId, String status, String operator) {
CcdiProject project = getRequiredProject(projectId); CcdiProject project = getRequiredProject(projectId);
@@ -179,6 +233,14 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
} }
} }
@Override
public void ensureProjectNotArchived(Long projectId, String message) {
CcdiProject project = getRequiredProject(projectId);
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
throw new ServiceException(message);
}
}
@Override @Override
public void ensureProjectWritable(Long projectId, String message) { public void ensureProjectWritable(Long projectId, String message) {
CcdiProject project = getRequiredProject(projectId); CcdiProject project = getRequiredProject(projectId);

View File

@@ -1,8 +1,24 @@
package com.ruoyi.ccdi.project.service.impl; package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
@@ -58,6 +74,107 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
return detail; return detail;
} }
@Override
public CcdiProjectExtendedPurchaseListVO getExtendedPurchaseList(CcdiProjectExtendedPurchaseQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
Page<CcdiProjectExtendedPurchaseListItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectExtendedPurchaseListItemVO> resultPage = specialCheckMapper.selectExtendedPurchasePage(page, queryDTO);
CcdiProjectExtendedPurchaseListVO result = new CcdiProjectExtendedPurchaseListVO();
result.setRows(resultPage == null ? List.of() : defaultList(resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public CcdiProjectExtendedPurchaseDetailVO getExtendedPurchaseDetail(
CcdiProjectExtendedPurchaseDetailQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
CcdiProjectExtendedPurchaseDetailVO detail = specialCheckMapper.selectExtendedPurchaseDetail(
queryDTO.getProjectId(),
queryDTO.getPurchaseId()
);
if (detail == null) {
throw new ServiceException("当前记录不属于该项目专项核查范围");
}
return detail;
}
@Override
public CcdiProjectExtendedRecruitmentListVO getExtendedRecruitmentList(
CcdiProjectExtendedRecruitmentQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
Page<CcdiProjectExtendedRecruitmentListItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectExtendedRecruitmentListItemVO> resultPage = specialCheckMapper.selectExtendedRecruitmentPage(
page,
queryDTO
);
CcdiProjectExtendedRecruitmentListVO result = new CcdiProjectExtendedRecruitmentListVO();
result.setRows(resultPage == null ? List.of() : defaultList(resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public CcdiProjectExtendedRecruitmentDetailVO getExtendedRecruitmentDetail(
CcdiProjectExtendedRecruitmentDetailQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
CcdiProjectExtendedRecruitmentDetailVO detail = specialCheckMapper.selectExtendedRecruitmentDetail(
queryDTO.getProjectId(),
queryDTO.getRecruitId()
);
if (detail == null) {
throw new ServiceException("当前记录不属于该项目专项核查范围");
}
return detail;
}
@Override
public CcdiProjectExtendedTransferListVO getExtendedTransferList(CcdiProjectExtendedTransferQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
Page<CcdiProjectExtendedTransferListItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectExtendedTransferListItemVO> resultPage = specialCheckMapper.selectExtendedTransferPage(page, queryDTO);
CcdiProjectExtendedTransferListVO result = new CcdiProjectExtendedTransferListVO();
result.setRows(resultPage == null ? List.of() : defaultList(resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public CcdiProjectExtendedTransferDetailVO getExtendedTransferDetail(
CcdiProjectExtendedTransferDetailQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
CcdiProjectExtendedTransferDetailVO detail = specialCheckMapper.selectExtendedTransferDetail(
queryDTO.getProjectId(),
queryDTO.getId()
);
if (detail == null) {
throw new ServiceException("当前记录不属于该项目专项核查范围");
}
return detail;
}
private void ensureProjectExists(Long projectId) { private void ensureProjectExists(Long projectId) {
CcdiProject project = projectMapper.selectById(projectId); CcdiProject project = projectMapper.selectById(projectId);
if (project == null) { if (project == null) {
@@ -112,4 +229,12 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
private <T> List<T> defaultList(List<T> list) { private <T> List<T> defaultList(List<T> list) {
return list == null ? List.of() : list; return list == null ? List.of() : list;
} }
private long defaultPageNum(Integer pageNum) {
return pageNum == null || pageNum < 1 ? 1L : pageNum.longValue();
}
private long defaultPageSize(Integer pageSize) {
return pageSize == null || pageSize < 1 ? 10L : pageSize.longValue();
}
} }

View File

@@ -25,6 +25,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="customerAccountNo" column="CUSTOMER_ACCOUNT_NO" /> <result property="customerAccountNo" column="CUSTOMER_ACCOUNT_NO" />
<result property="customerBank" column="customer_bank" /> <result property="customerBank" column="customer_bank" />
<result property="customerReference" column="customer_reference" /> <result property="customerReference" column="customer_reference" />
<result property="customerCertNo" column="customer_cert_no" />
<result property="customerSocialCreditCode" column="customer_social_credit_code" />
<result property="userMemo" column="USER_MEMO" /> <result property="userMemo" column="USER_MEMO" />
<result property="bankComments" column="BANK_COMMENTS" /> <result property="bankComments" column="BANK_COMMENTS" />
<result property="bankTrxNumber" column="BANK_TRX_NUMBER" /> <result property="bankTrxNumber" column="BANK_TRX_NUMBER" />
@@ -47,16 +49,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectCcdiBankStatementVo"> <sql id="selectCcdiBankStatementVo">
select bank_statement_id, project_id, LE_ID, ACCOUNT_ID, group_id, bank_statement_id, project_id, LE_ID, ACCOUNT_ID, group_id,
LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE, LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE, TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO, CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS, customer_bank, customer_reference, customer_cert_no, customer_social_credit_code, USER_MEMO, BANK_COMMENTS,
BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE, BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by, internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
meta_json, no_balance, begin_balance, end_balance, meta_json, no_balance, begin_balance, end_balance,
override_bs_id, payment_method, cret_no override_bs_id, payment_method, cret_no
from ccdi_bank_statement
</sql> </sql>
<resultMap id="CcdiBankStatementListVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO"> <resultMap id="CcdiBankStatementListVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO">
@@ -297,6 +298,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="statementOrderBy"/> <include refid="statementOrderBy"/>
</select> </select>
<select id="selectStatementsForHistoryImport" resultMap="CcdiBankStatementResult">
SELECT
<include refid="selectCcdiBankStatementVo"/>
FROM ccdi_bank_statement bs
<where>
(bs.project_id = #{projectId})
AND (bs.batch_id = #{batchId})
<if test="startDate != null and startDate != ''">
AND (<include refid="parsedTrxDateExpr"/>) <![CDATA[ >= ]]>
CASE
WHEN LENGTH(TRIM(#{startDate})) = 10
THEN STR_TO_DATE(CONCAT(TRIM(#{startDate}), ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
ELSE STR_TO_DATE(TRIM(#{startDate}), '%Y-%m-%d %H:%i:%s')
END
</if>
<if test="endDate != null and endDate != ''">
AND (<include refid="parsedTrxDateExpr"/>) <![CDATA[ <= ]]>
CASE
WHEN LENGTH(TRIM(#{endDate})) = 10
THEN STR_TO_DATE(CONCAT(TRIM(#{endDate}), ' 23:59:59'), '%Y-%m-%d %H:%i:%s')
ELSE STR_TO_DATE(TRIM(#{endDate}), '%Y-%m-%d %H:%i:%s')
END
</if>
</where>
ORDER BY bs.batch_sequence ASC, bs.bank_statement_id ASC
</select>
<select id="selectStatementDetailById" resultMap="CcdiBankStatementDetailVOResultMap"> <select id="selectStatementDetailById" resultMap="CcdiBankStatementDetailVOResultMap">
SELECT SELECT
bs.bank_statement_id AS bankStatementId, bs.bank_statement_id AS bankStatementId,
@@ -326,10 +354,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
bs.cret_no AS cretNo, bs.cret_no AS cretNo,
bs.CREATE_DATE AS createDate, bs.CREATE_DATE AS createDate,
fur.file_name AS originalFileName, fur.file_name AS originalFileName,
fur.upload_time AS uploadTime fur.upload_time AS uploadTime,
fur.source_project_name AS sourceProjectName
FROM ccdi_bank_statement bs FROM ccdi_bank_statement bs
LEFT JOIN ( LEFT JOIN (
SELECT latest_record.project_id, latest_record.log_id, latest_record.file_name, latest_record.upload_time SELECT latest_record.project_id, latest_record.log_id, latest_record.file_name, latest_record.upload_time,
latest_record.source_project_name
FROM ccdi_file_upload_record latest_record FROM ccdi_file_upload_record latest_record
INNER JOIN ( INNER JOIN (
SELECT project_id, log_id, MAX(id) AS max_id SELECT project_id, log_id, MAX(id) AS max_id
@@ -383,7 +413,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE, LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE, TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO, CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS, customer_bank, customer_reference, customer_cert_no, customer_social_credit_code, USER_MEMO, BANK_COMMENTS,
BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE, BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by, internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
meta_json, no_balance, begin_balance, end_balance, meta_json, no_balance, begin_balance, end_balance,
@@ -395,7 +425,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{item.leAccountName}, #{item.leAccountNo}, #{item.accountingDateId}, #{item.accountingDate}, #{item.leAccountName}, #{item.leAccountNo}, #{item.accountingDateId}, #{item.accountingDate},
#{item.trxDate}, #{item.currency}, #{item.amountDr}, #{item.amountCr}, #{item.amountBalance}, #{item.trxDate}, #{item.currency}, #{item.amountDr}, #{item.amountCr}, #{item.amountBalance},
#{item.cashType}, #{item.customerLeId}, #{item.customerAccountName}, #{item.customerAccountNo}, #{item.cashType}, #{item.customerLeId}, #{item.customerAccountName}, #{item.customerAccountNo},
#{item.customerBank}, #{item.customerReference}, #{item.userMemo}, #{item.bankComments}, #{item.customerBank}, #{item.customerReference}, #{item.customerCertNo}, #{item.customerSocialCreditCode}, #{item.userMemo}, #{item.bankComments},
#{item.bankTrxNumber}, #{item.bank}, #{item.trxFlag}, #{item.trxType}, #{item.exceptionType}, #{item.bankTrxNumber}, #{item.bank}, #{item.trxFlag}, #{item.trxType}, #{item.exceptionType},
#{item.internalFlag}, #{item.batchId}, #{item.batchSequence}, #{item.createDate}, #{item.createdBy}, #{item.internalFlag}, #{item.batchId}, #{item.batchSequence}, #{item.createDate}, #{item.createdBy},
#{item.metaJson}, #{item.noBalance}, #{item.beginBalance}, #{item.endBalance}, #{item.metaJson}, #{item.noBalance}, #{item.beginBalance}, #{item.endBalance},

View File

@@ -12,6 +12,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="fileName" column="file_name" /> <result property="fileName" column="file_name" />
<result property="fileSize" column="file_size" /> <result property="fileSize" column="file_size" />
<result property="fileStatus" column="file_status" /> <result property="fileStatus" column="file_status" />
<result property="sourceType" column="source_type" />
<result property="sourceProjectId" column="source_project_id" />
<result property="sourceProjectName" column="source_project_name" />
<result property="enterpriseNames" column="enterprise_names" /> <result property="enterpriseNames" column="enterprise_names" />
<result property="accountNos" column="account_nos" /> <result property="accountNos" column="account_nos" />
<result property="errorMessage" column="error_message" /> <result property="errorMessage" column="error_message" />
@@ -21,26 +24,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectCcdiFileUploadRecordVo"> <sql id="selectCcdiFileUploadRecordVo">
select id, project_id, lsfx_project_id, log_id, file_name, file_size, select id, project_id, lsfx_project_id, log_id, file_name, file_size,
file_status, enterprise_names, account_nos, error_message, file_status, source_type, source_project_id, source_project_name,
upload_time, upload_user enterprise_names, account_nos, error_message, upload_time, upload_user
from ccdi_file_upload_record from ccdi_file_upload_record
</sql> </sql>
<!-- 批量插入 --> <!-- 批量插入 -->
<insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id"> <insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">
insert into ccdi_file_upload_record ( insert into ccdi_file_upload_record (
project_id, lsfx_project_id, file_name, file_size, file_status, project_id, lsfx_project_id, log_id, file_name, file_size, file_status,
source_type, source_project_id, source_project_name,
enterprise_names, account_nos, upload_time, upload_user enterprise_names, account_nos, upload_time, upload_user
) values ) values
<foreach collection="list" item="item" separator=","> <foreach collection="list" item="item" separator=",">
( (
#{item.projectId}, #{item.lsfxProjectId}, #{item.fileName}, #{item.projectId}, #{item.lsfxProjectId}, #{item.logId}, #{item.fileName},
#{item.fileSize}, #{item.fileStatus}, #{item.enterpriseNames}, #{item.fileSize}, #{item.fileStatus}, #{item.sourceType},
#{item.accountNos}, #{item.uploadTime}, #{item.uploadUser} #{item.sourceProjectId}, #{item.sourceProjectName},
#{item.enterpriseNames}, #{item.accountNos}, #{item.uploadTime}, #{item.uploadUser}
) )
</foreach> </foreach>
</insert> </insert>
<select id="selectSuccessfulRecordsByProjectIds" resultMap="CcdiFileUploadRecordResult">
<include refid="selectCcdiFileUploadRecordVo"/>
where project_id in
<foreach collection="projectIds" item="projectId" open="(" separator="," close=")">
#{projectId}
</foreach>
and file_status = 'parsed_success'
and log_id is not null
order by project_id asc, log_id asc, id asc
</select>
<!-- 统计各状态文件数量 --> <!-- 统计各状态文件数量 -->
<select id="countByStatus" resultType="java.util.Map"> <select id="countByStatus" resultType="java.util.Map">
select file_status as `status`, count(*) as count select file_status as `status`, count(*) as count

View File

@@ -19,6 +19,15 @@
<result property="createByName" column="create_by_name"/> <result property="createByName" column="create_by_name"/>
</resultMap> </resultMap>
<resultMap id="ProjectHistoryListItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO">
<id property="projectId" column="project_id"/>
<result property="projectName" column="project_name"/>
<result property="description" column="description"/>
<result property="status" column="status"/>
<result property="isArchived" column="is_archived"/>
<result property="createTime" column="create_time"/>
</resultMap>
<!-- 分页查询项目列表 --> <!-- 分页查询项目列表 -->
<select id="selectProjectPage" resultMap="ProjectVOResultMap"> <select id="selectProjectPage" resultMap="ProjectVOResultMap">
SELECT SELECT
@@ -41,6 +50,24 @@
ORDER BY p.update_time DESC ORDER BY p.update_time DESC
</select> </select>
<select id="selectHistoryProjects" resultMap="ProjectHistoryListItemResultMap">
SELECT
p.project_id,
p.project_name,
p.description,
p.status,
p.is_archived,
p.create_time
FROM ccdi_project p
<where>
p.status in ('1', '2')
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
AND p.project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
</if>
</where>
ORDER BY p.update_time DESC
</select>
<update id="updateRiskCountsByProjectId"> <update id="updateRiskCountsByProjectId">
update ccdi_project update ccdi_project
set high_risk_count = #{highRiskCount}, set high_risk_count = #{highRiskCount},

View File

@@ -33,6 +33,21 @@
select="selectRiskHitTagsByScope"/> select="selectRiskHitTagsByScope"/>
</resultMap> </resultMap>
<resultMap id="SuspiciousTransactionItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
<id property="bankStatementId" column="bankStatementId"/>
<result property="trxDate" column="trxDate"/>
<result property="suspiciousPersonName" column="suspiciousPersonName"/>
<result property="relatedPersonName" column="relatedPersonName"/>
<result property="relatedStaffName" column="relatedStaffName"/>
<result property="relatedStaffCode" column="relatedStaffCode"/>
<result property="relationType" column="relationType"/>
<result property="userMemo" column="userMemo"/>
<result property="cashType" column="cashType"/>
<result property="displayAmount" column="displayAmount"/>
<result property="hasModelRuleHit" column="hasModelRuleHit"/>
<result property="hasNameListHit" column="hasNameListHit"/>
</resultMap>
<sql id="digitTableSql"> <sql id="digitTableSql">
select 0 as digit select 0 as digit
union all select 1 union all select 1
@@ -208,32 +223,48 @@
and del_flag = '0' and del_flag = '0'
</select> </select>
<select id="selectRiskPeopleOverviewByProjectId" resultMap="EmployeeRiskAggregateResultMap"> <sql id="riskPeopleOverviewSelectColumns">
result.staff_id_card,
result.staff_name,
result.dept_id,
result.dept_name,
result.rule_count,
result.model_count,
result.hit_count,
null as top_rule_code,
null as top_rule_name,
result.risk_point,
result.risk_level_code,
case
when result.risk_level_code = 'HIGH' then '高风险'
when result.risk_level_code = 'MEDIUM' then '中风险'
else '低风险'
end as risk_level_name,
case
when result.risk_level_code = 'HIGH' then 1
when result.risk_level_code = 'MEDIUM' then 2
else 3
end as risk_level_sort
</sql>
<sql id="riskPeopleOverviewOrderBy">
order by risk_level_sort asc, result.model_count desc, result.rule_count desc, result.staff_id_card asc
</sql>
<select id="selectRiskPeopleOverviewPage" resultMap="EmployeeRiskAggregateResultMap">
select select
result.staff_id_card, <include refid="riskPeopleOverviewSelectColumns"/>
result.staff_name, from ccdi_project_overview_employee_result result
result.dept_id, where result.project_id = #{query.projectId}
result.dept_name, <include refid="riskPeopleOverviewOrderBy"/>
result.rule_count, </select>
result.model_count,
result.hit_count, <select id="selectRiskPeopleOverviewList" resultMap="EmployeeRiskAggregateResultMap">
null as top_rule_code, select
null as top_rule_name, <include refid="riskPeopleOverviewSelectColumns"/>
result.risk_point,
result.risk_level_code,
case
when result.risk_level_code = 'HIGH' then '高风险'
when result.risk_level_code = 'MEDIUM' then '中风险'
else '低风险'
end as risk_level_name,
case
when result.risk_level_code = 'HIGH' then 1
when result.risk_level_code = 'MEDIUM' then 2
else 3
end as risk_level_sort
from ccdi_project_overview_employee_result result from ccdi_project_overview_employee_result result
where result.project_id = #{projectId} where result.project_id = #{projectId}
order by risk_level_sort asc, result.model_count desc, result.rule_count desc, result.staff_id_card asc <include refid="riskPeopleOverviewOrderBy"/>
</select> </select>
<select id="selectTopRiskPeopleByProjectId" resultMap="EmployeeRiskAggregateResultMap"> <select id="selectTopRiskPeopleByProjectId" resultMap="EmployeeRiskAggregateResultMap">
@@ -338,6 +369,281 @@
order by result.staff_name asc, result.staff_id_card asc order by result.staff_name asc, result.staff_id_card asc
</select> </select>
<sql id="suspiciousTransactionBaseSql">
select
bs.bank_statement_id as bankStatementId,
bs.TRX_DATE as trxDate,
bs.USER_MEMO as userMemo,
bs.CASH_TYPE as cashType,
case
when ifnull(bs.AMOUNT_CR, 0) > 0 then bs.AMOUNT_CR
when ifnull(bs.AMOUNT_DR, 0) > 0 then -bs.AMOUNT_DR
else 0
end as displayAmount,
coalesce(relation.relation_name, direct_staff.name, bs.CUSTOMER_ACCOUNT_NAME) as relatedPersonName,
coalesce(family_staff.name, direct_staff.name) as relatedStaffName,
cast(coalesce(family_staff.staff_id, direct_staff.staff_id) as char) as relatedStaffCode,
case
when direct_staff.id_card is not null then '本人'
when relation.relation_type is not null and trim(relation.relation_type) != '' then relation.relation_type
else '关联人'
end as relationType
from ccdi_bank_statement bs
left join ccdi_base_staff direct_staff
on bs.cret_no = direct_staff.id_card
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and relation.relation_cert_no = bs.cret_no
left join ccdi_base_staff family_staff
on relation.person_id = family_staff.id_card
where bs.project_id = #{query.projectId}
and (direct_staff.id_card is not null or relation.person_id is not null)
</sql>
<sql id="suspiciousTransactionModelHitSql">
select distinct
tr.bank_statement_id as bankStatementId
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null
and tr.rule_name like '%可疑%'
</sql>
<sql id="suspiciousTransactionNameHitSql">
select
hits.bankStatementId,
hits.suspiciousPersonName,
hits.matchPriority
from (
select
bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName,
1 as matchPriority
from ccdi_bank_statement bs
inner join ccdi_biz_intermediary intermediary
on trim(bs.customer_cert_no) != ''
and intermediary.person_id = bs.customer_cert_no
where bs.project_id = #{query.projectId}
union all
select
bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName,
2 as matchPriority
from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise
on trim(bs.customer_social_credit_code) != ''
and enterprise.social_credit_code = bs.customer_social_credit_code
and enterprise.risk_level = '1'
and enterprise.ent_source = 'INTERMEDIARY'
where bs.project_id = #{query.projectId}
union all
select
bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName,
3 as matchPriority
from ccdi_bank_statement bs
inner join ccdi_biz_intermediary intermediary
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME
where bs.project_id = #{query.projectId}
union all
select
bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName,
3 as matchPriority
from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and enterprise.enterprise_name = bs.CUSTOMER_ACCOUNT_NAME
and enterprise.risk_level = '1'
and enterprise.ent_source = 'INTERMEDIARY'
where bs.project_id = #{query.projectId}
) hits
</sql>
<sql id="suspiciousTransactionMergedSql">
select
base.bankStatementId,
base.trxDate,
base.relatedPersonName,
base.relatedStaffName,
base.relatedStaffCode,
base.relationType,
base.userMemo,
base.cashType,
base.displayAmount,
1 as hasModelRuleHit,
0 as hasNameListHit,
null as suspiciousPersonName,
null as matchPriority
from (
<include refid="suspiciousTransactionBaseSql"/>
) base
inner join (
<include refid="suspiciousTransactionModelHitSql"/>
) model_hits on model_hits.bankStatementId = base.bankStatementId
union all
select
base.bankStatementId,
base.trxDate,
base.relatedPersonName,
base.relatedStaffName,
base.relatedStaffCode,
base.relationType,
base.userMemo,
base.cashType,
base.displayAmount,
0 as hasModelRuleHit,
1 as hasNameListHit,
name_hits.suspiciousPersonName,
name_hits.matchPriority
from (
<include refid="suspiciousTransactionBaseSql"/>
) base
inner join (
<include refid="suspiciousTransactionNameHitSql"/>
) name_hits on name_hits.bankStatementId = base.bankStatementId
</sql>
<sql id="suspiciousTransactionAggregatedSql">
select
merged.bankStatementId,
max(merged.trxDate) as trxDate,
coalesce(
substring_index(
min(
case
when merged.suspiciousPersonName is not null and merged.suspiciousPersonName != ''
then concat(lpad(merged.matchPriority, 2, '0'), '|', merged.suspiciousPersonName)
else null
end
),
'|',
-1
),
max(merged.relatedPersonName)
) as suspiciousPersonName,
max(merged.relatedPersonName) as relatedPersonName,
max(merged.relatedStaffName) as relatedStaffName,
max(merged.relatedStaffCode) as relatedStaffCode,
max(merged.relationType) as relationType,
max(merged.userMemo) as userMemo,
max(merged.cashType) as cashType,
max(merged.displayAmount) as displayAmount,
max(merged.hasModelRuleHit) as hasModelRuleHit,
max(merged.hasNameListHit) as hasNameListHit
from (
<include refid="suspiciousTransactionMergedSql"/>
) merged
group by merged.bankStatementId
</sql>
<sql id="suspiciousTransactionFilterSql">
<choose>
<when test="query.suspiciousType == 'NAME_LIST'">
where final_result.hasNameListHit = 1
</when>
<when test="query.suspiciousType == 'MODEL_RULE'">
where final_result.hasModelRuleHit = 1
</when>
<otherwise>
where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1
</otherwise>
</choose>
</sql>
<select id="selectSuspiciousTransactionPage" resultMap="SuspiciousTransactionItemResultMap">
<!-- rule_name like '%可疑%' -->
<!-- ccdi_biz_intermediary -->
<!-- ccdi_enterprise_base_info -->
<!-- group by merged.bankStatementId -->
select
final_result.bankStatementId,
final_result.trxDate,
final_result.suspiciousPersonName,
final_result.relatedPersonName,
final_result.relatedStaffName,
final_result.relatedStaffCode,
final_result.relationType,
final_result.userMemo,
final_result.cashType,
final_result.displayAmount,
final_result.hasModelRuleHit,
final_result.hasNameListHit
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
<include refid="suspiciousTransactionFilterSql"/>
order by final_result.trxDate desc, final_result.bankStatementId desc
</select>
<select id="selectSuspiciousTransactionList" resultMap="SuspiciousTransactionItemResultMap">
select
final_result.bankStatementId,
final_result.trxDate,
final_result.suspiciousPersonName,
final_result.relatedPersonName,
final_result.relatedStaffName,
final_result.relatedStaffCode,
final_result.relationType,
final_result.userMemo,
final_result.cashType,
final_result.displayAmount,
final_result.hasModelRuleHit,
final_result.hasNameListHit
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
<include refid="suspiciousTransactionFilterSql"/>
order by final_result.trxDate desc, final_result.bankStatementId desc
</select>
<select id="selectEmployeeCreditNegativePage"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO">
select
coalesce(neg.person_name, result.staff_name) as personName,
neg.person_id as personId,
date_format(neg.query_date, '%Y-%m-%d') as queryDate,
ifnull(neg.civil_cnt, 0) as civilCnt,
ifnull(neg.civil_lmt, 0) as civilLmt,
ifnull(neg.enforce_cnt, 0) as enforceCnt,
ifnull(neg.enforce_lmt, 0) as enforceLmt,
ifnull(neg.adm_cnt, 0) as admCnt,
ifnull(neg.adm_lmt, 0) as admLmt
from ccdi_project_overview_employee_result result
inner join ccdi_credit_negative_info neg
on neg.person_id = result.staff_id_card
where result.project_id = #{query.projectId}
order by neg.query_date desc, neg.person_id asc
</select>
<select id="selectEmployeeCreditNegativeList"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO">
select
coalesce(neg.person_name, result.staff_name) as personName,
neg.person_id as personId,
date_format(neg.query_date, '%Y-%m-%d') as queryDate,
ifnull(neg.civil_cnt, 0) as civilCnt,
ifnull(neg.civil_lmt, 0) as civilLmt,
ifnull(neg.enforce_cnt, 0) as enforceCnt,
ifnull(neg.enforce_lmt, 0) as enforceLmt,
ifnull(neg.adm_cnt, 0) as admCnt,
ifnull(neg.adm_lmt, 0) as admLmt
from ccdi_project_overview_employee_result result
inner join ccdi_credit_negative_info neg
on neg.person_id = result.staff_id_card
where result.project_id = #{projectId}
order by neg.query_date desc, neg.person_id asc
</select>
<select id="selectRiskModelNamesByScope" resultType="java.lang.String"> <select id="selectRiskModelNamesByScope" resultType="java.lang.String">
select select
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelName'))) as model_name json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelName'))) as model_name
@@ -377,6 +683,94 @@
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) asc json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) asc
</select> </select>
<select id="selectPersonAnalysisBasicInfo" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO">
select
coalesce(staff.name, result.staff_name) as name,
result.staff_id_card as idNo,
result.staff_code as staffCode,
dept.dept_name as department,
staff.phone as phone,
case
when result.risk_level_code = 'HIGH' then '高风险'
when result.risk_level_code = 'MEDIUM' then '中风险'
else '低风险'
end as riskLevel,
project.project_name as projectName
from ccdi_project_overview_employee_result result
left join ccdi_base_staff staff
on staff.id_card = result.staff_id_card
left join sys_dept dept
on dept.dept_id = coalesce(staff.dept_id, result.dept_id)
left join ccdi_project project
on project.project_id = result.project_id
where result.project_id = #{projectId}
and result.staff_id_card = #{staffIdCard}
limit 1
</select>
<select id="selectPersonAnalysisStatementRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO">
select distinct
bs.bank_statement_id as bankStatementId,
bs.TRX_DATE as trxDate,
bs.LE_ACCOUNT_NO as leAccountNo,
bs.LE_ACCOUNT_NAME as leAccountName,
bs.CUSTOMER_ACCOUNT_NAME as customerAccountName,
bs.CUSTOMER_ACCOUNT_NO as customerAccountNo,
bs.USER_MEMO as userMemo,
bs.CASH_TYPE as cashType,
case
when ifnull(bs.AMOUNT_CR, 0) > 0 then bs.AMOUNT_CR
when ifnull(bs.AMOUNT_DR, 0) > 0 then -bs.AMOUNT_DR
else 0
end as displayAmount
from ccdi_bank_statement bs
inner join ccdi_bank_statement_tag_result tr
on tr.project_id = bs.project_id
and tr.bank_statement_id = bs.bank_statement_id
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and relation.relation_cert_no = bs.cret_no
where bs.project_id = #{projectId}
and (
bs.cret_no = #{staffIdCard}
or relation.person_id = #{staffIdCard}
or tr.object_key = #{staffIdCard}
)
order by bs.bank_statement_id desc
</select>
<select id="selectPersonAnalysisObjectRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO">
select
coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title,
max(case
when tr.object_type = 'STAFF_ID_CARD' then '员工对象'
else tr.object_type
end) as subtitle,
max(tr.reason_detail) as reasonDetail,
max(tr.rule_name) as summary
from ccdi_bank_statement_tag_result tr
left join ccdi_base_staff staff
on tr.object_type = 'STAFF_ID_CARD'
and tr.object_key = staff.id_card
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and tr.object_key = relation.relation_cert_no
where tr.project_id = #{projectId}
and tr.bank_statement_id is null
and (
tr.object_key = #{staffIdCard}
or exists (
select 1
from ccdi_staff_fmy_relation relation_scope
where relation_scope.status = 1
and relation_scope.person_id = #{staffIdCard}
and relation_scope.relation_cert_no = tr.object_key
)
)
group by coalesce(tr.object_key, tr.object_type), tr.rule_code
order by title asc, tr.rule_code asc
</select>
<select id="selectRiskCountSummaryByProjectId" resultType="map"> <select id="selectRiskCountSummaryByProjectId" resultType="map">
select select
coalesce(sum(case when agg.rule_count >= 5 then 1 else 0 end), 0) as highRiskCount, coalesce(sum(case when agg.rule_count >= 5 then 1 else 0 end), 0) as highRiskCount,

View File

@@ -39,6 +39,34 @@
<result property="queryDate" column="query_date"/> <result property="queryDate" column="query_date"/>
</resultMap> </resultMap>
<resultMap id="ExtendedPurchaseListItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO">
<id property="purchaseId" column="purchase_id"/>
<result property="projectName" column="project_name"/>
<result property="subjectName" column="subject_name"/>
<result property="applicantName" column="applicant_name"/>
<result property="applyDate" column="apply_date"/>
</resultMap>
<resultMap id="ExtendedRecruitmentListItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO">
<id property="recruitId" column="recruit_id"/>
<result property="recruitName" column="recruit_name"/>
<result property="posName" column="pos_name"/>
<result property="interviewerNameSummary" column="interviewer_name_summary"/>
<result property="admitStatus" column="admit_status"/>
</resultMap>
<resultMap id="ExtendedTransferListItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListItemVO">
<id property="id" column="id"/>
<result property="staffName" column="staff_name"/>
<result property="transferType" column="transfer_type"/>
<result property="deptNameBefore" column="dept_name_before"/>
<result property="deptNameAfter" column="dept_name_after"/>
<result property="transferDate" column="transfer_date"/>
</resultMap>
<resultMap id="FamilyAssetLiabilityDetailResultMap" <resultMap id="FamilyAssetLiabilityDetailResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO"> type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO">
<association property="incomeDetail" <association property="incomeDetail"
@@ -462,4 +490,230 @@
debt.debt_name asc debt.debt_name asc
</select> </select>
<select id="selectExtendedPurchasePage" resultMap="ExtendedPurchaseListItemResultMap">
<bind name="projectId" value="query.projectId"/>
select
p.purchase_id,
p.project_name,
p.subject_name,
p.applicant_name,
p.apply_date
from ccdi_purchase_transaction p
inner join (
select distinct scope.staff_name
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
) scoped_staff
on scoped_staff.staff_name = p.applicant_name
<where>
<if test="query.applicantName != null and query.applicantName != ''">
and p.applicant_name like concat('%', #{query.applicantName}, '%')
</if>
<if test="query.applyDateStart != null and query.applyDateStart != ''">
and p.apply_date &gt;= #{query.applyDateStart}
</if>
<if test="query.applyDateEnd != null and query.applyDateEnd != ''">
and p.apply_date &lt;= #{query.applyDateEnd}
</if>
</where>
order by p.apply_date desc, p.create_time desc, p.purchase_id desc
</select>
<select id="selectExtendedPurchaseDetail"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO">
select
p.purchase_id,
p.purchase_category,
p.project_name,
p.subject_name,
p.subject_desc,
p.purchase_qty,
p.budget_amount,
p.bid_amount,
p.actual_amount,
p.contract_amount,
p.settlement_amount,
p.purchase_method,
p.supplier_name,
p.contact_person,
p.contact_phone,
p.supplier_uscc,
p.supplier_bank_account,
p.apply_date,
p.plan_approve_date,
p.announce_date,
p.bid_open_date,
p.contract_sign_date,
p.expected_delivery_date,
p.actual_delivery_date,
p.acceptance_date,
p.settlement_date,
p.applicant_id,
p.applicant_name,
p.apply_department,
p.purchase_leader_id,
p.purchase_leader_name,
p.purchase_department,
p.created_by,
p.create_time,
p.updated_by,
p.update_time
from ccdi_purchase_transaction p
inner join (
select distinct scope.staff_name
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
) scoped_staff
on scoped_staff.staff_name = p.applicant_name
where p.purchase_id = #{purchaseId}
</select>
<select id="selectExtendedRecruitmentPage" resultMap="ExtendedRecruitmentListItemResultMap">
<bind name="projectId" value="query.projectId"/>
select distinct r.recruit_id,
r.recruit_name,
r.pos_name,
concat_ws(' / ',
nullif(trim(r.interviewer_name1), ''),
nullif(trim(r.interviewer_name2), '')
) as interviewer_name_summary,
r.admit_status
from ccdi_staff_recruitment r
where exists (
select 1
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
and (
scope.staff_name = r.interviewer_name1
or scope.staff_name = r.interviewer_name2
)
)
<if test="query.interviewerName != null and query.interviewerName != ''">
and (
r.interviewer_name1 like concat('%', #{query.interviewerName}, '%')
or r.interviewer_name2 like concat('%', #{query.interviewerName}, '%')
)
</if>
order by r.create_time desc, r.recruit_id desc
</select>
<select id="selectExtendedRecruitmentDetail"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO">
select
r.recruit_id,
r.recruit_name,
r.pos_name,
r.pos_category,
r.pos_desc,
r.cand_name,
r.cand_edu,
r.cand_id,
r.cand_school,
r.cand_major,
r.cand_grad,
r.admit_status,
r.interviewer_name1,
r.interviewer_id1,
r.interviewer_name2,
r.interviewer_id2,
r.created_by,
r.create_time,
r.updated_by,
r.update_time
from ccdi_staff_recruitment r
where r.recruit_id = #{recruitId}
and exists (
select 1
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
and (
scope.staff_name = r.interviewer_name1
or scope.staff_name = r.interviewer_name2
)
)
</select>
<select id="selectExtendedTransferPage" resultMap="ExtendedTransferListItemResultMap">
<bind name="projectId" value="query.projectId"/>
select
t.id,
s.name as staff_name,
t.transfer_type,
t.dept_name_before,
t.dept_name_after,
t.transfer_date
from ccdi_staff_transfer t
inner join ccdi_base_staff s
on t.staff_id = s.staff_id
inner join (
select distinct scope.staff_name
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name is not null
and scope.staff_name != ''
) scope
on scope.staff_name = s.name
<where>
<if test="query.staffName != null and query.staffName != ''">
and s.name like concat('%', #{query.staffName}, '%')
</if>
<if test="query.transferDateStart != null and query.transferDateStart != ''">
and t.transfer_date &gt;= #{query.transferDateStart}
</if>
<if test="query.transferDateEnd != null and query.transferDateEnd != ''">
and t.transfer_date &lt;= #{query.transferDateEnd}
</if>
</where>
order by t.transfer_date desc, t.create_time desc, t.id desc
</select>
<select id="selectExtendedTransferDetail"
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO">
select
t.id,
t.staff_id,
s.name as staff_name,
t.transfer_type,
t.transfer_sub_type,
t.dept_id_before,
t.dept_name_before,
t.grade_before,
t.position_before,
t.salary_level_before,
t.dept_id_after,
t.dept_name_after,
t.grade_after,
t.position_after,
t.salary_level_after,
t.transfer_date,
t.created_by,
t.create_time,
t.updated_by,
t.update_time
from ccdi_staff_transfer t
inner join ccdi_base_staff s
on t.staff_id = s.staff_id
where t.id = #{id}
and exists (
select 1
from (
<include refid="projectEmployeeScopeSql"/>
) scope
where scope.staff_name = s.name
)
</select>
</mapper> </mapper>

View File

@@ -0,0 +1,49 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
import io.swagger.v3.oas.annotations.Operation;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class CcdiProjectControllerContractTest {
@Test
void shouldExposeArchiveProjectEndpointContract() throws Exception {
RequestMapping requestMapping = CcdiProjectController.class.getAnnotation(RequestMapping.class);
Method method = CcdiProjectController.class.getMethod("archiveProject", Long.class);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(requestMapping);
assertEquals("/ccdi/project", requestMapping.value()[0]);
assertNotNull(postMapping);
assertEquals("/{projectId}/archive", postMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:edit')", preAuthorize.value());
assertNotNull(operation);
assertEquals("归档项目", operation.summary());
}
@Test
void shouldExposeHistoryImportEndpointContracts() throws Exception {
Method history = CcdiProjectController.class.getMethod("listHistoryProjects", CcdiProjectQueryDTO.class);
GetMapping historyMapping = history.getAnnotation(GetMapping.class);
assertNotNull(historyMapping);
assertEquals("/history", historyMapping.value()[0]);
Method importing = CcdiProjectController.class.getMethod("importFromHistory", CcdiProjectImportHistoryDTO.class);
PostMapping importMapping = importing.getAnnotation(PostMapping.class);
assertNotNull(importMapping);
assertEquals("/import", importMapping.value()[0]);
}
}

View File

@@ -0,0 +1,82 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class CcdiProjectControllerTest {
@InjectMocks
private CcdiProjectController controller;
@Mock
private ICcdiProjectService projectService;
@Test
void shouldArchiveProject() throws Exception {
try (MockedStatic<com.ruoyi.common.utils.SecurityUtils> mocked = mockStatic(com.ruoyi.common.utils.SecurityUtils.class)) {
mocked.when(com.ruoyi.common.utils.SecurityUtils::getUsername).thenReturn("tester");
AjaxResult result = controller.archiveProject(40L);
assertEquals(200, result.get("code"));
assertEquals("项目归档成功", result.get("msg"));
verify(projectService).archiveProject(40L, "tester");
}
Method method = CcdiProjectController.class.getMethod("archiveProject", Long.class);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/{projectId}/archive", postMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:edit')", preAuthorize.value());
assertNotNull(operation);
assertEquals("归档项目", operation.summary());
}
@Test
void shouldImportFromHistoryAndReturnCreatedProject() {
CcdiProjectImportHistoryDTO dto = new CcdiProjectImportHistoryDTO();
dto.setProjectName("新建项目");
CcdiProjectVO project = new CcdiProjectVO();
project.setProjectId(88L);
project.setProjectName("新建项目");
when(projectService.importFromHistory(eq(dto), eq("tester"))).thenReturn(project);
try (MockedStatic<com.ruoyi.common.utils.SecurityUtils> mocked = mockStatic(com.ruoyi.common.utils.SecurityUtils.class)) {
mocked.when(com.ruoyi.common.utils.SecurityUtils::getUsername).thenReturn("tester");
AjaxResult result = controller.importFromHistory(dto);
assertEquals(200, result.get("code"));
assertEquals("项目创建成功", result.get("msg"));
assertSame(project, result.get("data"));
verify(projectService).importFromHistory(dto, "tester");
}
}
}

View File

@@ -1,13 +1,16 @@
package com.ruoyi.ccdi.project.controller; package com.ruoyi.ccdi.project.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -63,4 +66,119 @@ class CcdiProjectOverviewControllerContractTest {
assertTrue(fieldNames.contains("pageNum")); assertTrue(fieldNames.contains("pageNum"));
assertTrue(fieldNames.contains("pageSize")); assertTrue(fieldNames.contains("pageSize"));
} }
@Test
void shouldExposePersonAnalysisDetailEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Class<?> queryDtoClass =
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO");
Method method = controllerClass.getMethod("getPersonAnalysisDetail", queryDtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/person-analysis/detail", getMapping.value()[0]);
assertNotNull(operation);
assertEquals(queryDtoClass, method.getParameterTypes()[0]);
assertEquals(AjaxResult.class, method.getReturnType());
}
@Test
void shouldExposePersonAnalysisDetailQueryDtoFields() throws Exception {
Class<?> dtoClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO");
List<String> fieldNames = Arrays.stream(dtoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertEquals(List.of("projectId", "staffIdCard"), fieldNames);
}
@Test
void shouldExposeSuspiciousTransactionsEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Class<?> queryDtoClass =
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
Method method = controllerClass.getMethod("getSuspiciousTransactions", queryDtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/suspicious-transactions", getMapping.value()[0]);
assertNotNull(operation);
assertEquals(queryDtoClass, method.getParameterTypes()[0]);
assertEquals(AjaxResult.class, method.getReturnType());
}
@Test
void shouldExposeEmployeeCreditNegativeEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Class<?> queryDtoClass =
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO");
Method method = controllerClass.getMethod("getEmployeeCreditNegative", queryDtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/employee-credit-negative", getMapping.value()[0]);
assertNotNull(operation);
assertEquals(queryDtoClass, method.getParameterTypes()[0]);
assertEquals(AjaxResult.class, method.getReturnType());
}
@Test
void shouldExposeSuspiciousTransactionsExportEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Class<?> queryDtoClass =
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
Method method = controllerClass.getMethod(
"exportSuspiciousTransactions",
HttpServletResponse.class,
queryDtoClass
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/suspicious-transactions/export", postMapping.value()[0]);
assertNotNull(operation);
}
@Test
void shouldExposeRiskDetailsExportEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Method method = controllerClass.getMethod(
"exportRiskDetails",
HttpServletResponse.class,
Long.class
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/risk-details/export", postMapping.value()[0]);
assertNotNull(operation);
}
@Test
void shouldExposeSuspiciousTransactionsQueryDtoFields() throws Exception {
Class<?> dtoClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
List<String> fieldNames = Arrays.stream(dtoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertEquals(List.of("projectId", "suspiciousType", "pageNum", "pageSize"), fieldNames);
}
@Test
void shouldExposeEmployeeCreditNegativeQueryDtoFields() throws Exception {
Class<?> dtoClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO");
List<String> fieldNames = Arrays.stream(dtoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertEquals(List.of("projectId", "pageNum", "pageSize"), fieldNames);
}
} }

View File

@@ -1,26 +1,43 @@
package com.ruoyi.ccdi.project.controller; package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -69,20 +86,31 @@ class CcdiProjectOverviewControllerTest {
riskPointTag.setRuleName("多工资转入"); riskPointTag.setRuleName("多工资转入");
item.setRiskPointTagList(List.of(riskPointTag)); item.setRiskPointTagList(List.of(riskPointTag));
CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO(); CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO();
overview.setOverviewList(List.of(item)); Method setRowsMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("setRows", List.class);
when(overviewService.getRiskPeopleOverview(40L)).thenReturn(overview); Method setTotalMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("setTotal", Long.class);
Method setPageNumMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("setPageNum", Long.class);
Method setPageSizeMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("setPageSize", Long.class);
Method getRowsMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("getRows");
Method getTotalMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("getTotal");
Method getPageNumMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("getPageNum");
Method getPageSizeMethod = CcdiProjectRiskPeopleOverviewVO.class.getMethod("getPageSize");
setRowsMethod.invoke(overview, List.of(item));
setTotalMethod.invoke(overview, 1L);
setPageNumMethod.invoke(overview, 1L);
setPageSizeMethod.invoke(overview, 5L);
AjaxResult result = controller.getRiskPeople(40L); assertEquals("中风险", ((List<CcdiProjectRiskPeopleOverviewItemVO>) getRowsMethod.invoke(overview)).getFirst().getRiskLevel());
assertEquals("warning", ((List<CcdiProjectRiskPeopleOverviewItemVO>) getRowsMethod.invoke(overview)).getFirst().getRiskLevelType());
assertEquals(4, ((List<CcdiProjectRiskPeopleOverviewItemVO>) getRowsMethod.invoke(overview)).getFirst().getModelCount());
assertEquals("SALARY", ((List<CcdiProjectRiskPeopleOverviewItemVO>) getRowsMethod.invoke(overview)).getFirst().getRiskPointTagList().getFirst().getModelCode());
assertEquals(1L, getTotalMethod.invoke(overview));
assertEquals(1L, getPageNumMethod.invoke(overview));
assertEquals(5L, getPageSizeMethod.invoke(overview));
assertEquals(200, result.get("code")); Method method = CcdiProjectOverviewController.class.getMethod(
CcdiProjectRiskPeopleOverviewVO data = (CcdiProjectRiskPeopleOverviewVO) result.get("data"); "getRiskPeople",
assertEquals("中风险", data.getOverviewList().getFirst().getRiskLevel()); Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO")
assertEquals("warning", data.getOverviewList().getFirst().getRiskLevelType()); );
assertEquals(4, data.getOverviewList().getFirst().getModelCount());
assertEquals("SALARY", data.getOverviewList().getFirst().getRiskPointTagList().getFirst().getModelCode());
verify(overviewService).getRiskPeopleOverview(40L);
Method method = CcdiProjectOverviewController.class.getMethod("getRiskPeople", Long.class);
GetMapping getMapping = method.getAnnotation(GetMapping.class); GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
@@ -110,4 +138,207 @@ class CcdiProjectOverviewControllerTest {
assertNotNull(preAuthorize); assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value()); assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
} }
@Test
void shouldExposePersonAnalysisDetailEndpoint() throws Exception {
CcdiProjectPersonAnalysisDetailQueryDTO queryDTO = new CcdiProjectPersonAnalysisDetailQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setStaffIdCard("330000000000000001");
CcdiProjectPersonAnalysisBasicInfoVO basicInfo = new CcdiProjectPersonAnalysisBasicInfoVO();
basicInfo.setName("李四");
CcdiProjectPersonAnalysisDetailVO detail = new CcdiProjectPersonAnalysisDetailVO();
detail.setBasicInfo(basicInfo);
when(overviewService.getPersonAnalysisDetail(queryDTO)).thenReturn(detail);
AjaxResult result = controller.getPersonAnalysisDetail(queryDTO);
assertEquals(200, result.get("code"));
CcdiProjectPersonAnalysisDetailVO data = (CcdiProjectPersonAnalysisDetailVO) result.get("data");
assertEquals("李四", data.getBasicInfo().getName());
verify(overviewService).getPersonAnalysisDetail(queryDTO);
Method method = CcdiProjectOverviewController.class.getMethod(
"getPersonAnalysisDetail",
CcdiProjectPersonAnalysisDetailQueryDTO.class
);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/person-analysis/detail", getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation);
}
@Test
void shouldExposeSuspiciousTransactionsEndpoint() throws Exception {
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("ALL");
queryDTO.setPageNum(1);
queryDTO.setPageSize(10);
CcdiProjectSuspiciousTransactionPageVO pageVO = new CcdiProjectSuspiciousTransactionPageVO();
pageVO.setRows(List.of());
pageVO.setTotal(0L);
when(overviewService.getSuspiciousTransactions(queryDTO)).thenReturn(pageVO);
AjaxResult result = controller.getSuspiciousTransactions(queryDTO);
assertEquals(200, result.get("code"));
assertEquals(pageVO, result.get("data"));
verify(overviewService).getSuspiciousTransactions(queryDTO);
Method method = CcdiProjectOverviewController.class.getMethod(
"getSuspiciousTransactions",
CcdiProjectSuspiciousTransactionQueryDTO.class
);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
assertNotNull(getMapping);
assertEquals("/suspicious-transactions", getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
}
@Test
void shouldExposeEmployeeCreditNegativeEndpoint() throws Exception {
CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO = new CcdiProjectEmployeeCreditNegativeQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setPageNum(1);
queryDTO.setPageSize(5);
CcdiProjectEmployeeCreditNegativeItemVO item = new CcdiProjectEmployeeCreditNegativeItemVO();
item.setPersonName("李四");
item.setPersonId("330000000000000001");
item.setCivilCnt(1);
item.setCivilLmt(new BigDecimal("10000.00"));
CcdiProjectEmployeeCreditNegativePageVO pageVO = new CcdiProjectEmployeeCreditNegativePageVO();
pageVO.setRows(List.of(item));
pageVO.setTotal(1L);
when(overviewService.getEmployeeCreditNegative(queryDTO)).thenReturn(pageVO);
AjaxResult result = controller.getEmployeeCreditNegative(queryDTO);
assertEquals(200, result.get("code"));
assertEquals(pageVO, result.get("data"));
verify(overviewService).getEmployeeCreditNegative(queryDTO);
Method method = CcdiProjectOverviewController.class.getMethod(
"getEmployeeCreditNegative",
CcdiProjectEmployeeCreditNegativeQueryDTO.class
);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/employee-credit-negative", getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation);
}
@Test
void shouldExposeSuspiciousTransactionsExportEndpoint() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
row.setSuspiciousPersonName("张三");
row.setDisplayAmount(new java.math.BigDecimal("10.00"));
when(overviewService.exportSuspiciousTransactions(same(queryDTO))).thenReturn(List.of(row));
controller.exportSuspiciousTransactions(response, queryDTO);
verify(overviewService).exportSuspiciousTransactions(same(queryDTO));
assertTrue(response.getContentType().startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
assertTrue(response.getContentAsByteArray().length > 0);
Method method = CcdiProjectOverviewController.class.getMethod(
"exportSuspiciousTransactions",
jakarta.servlet.http.HttpServletResponse.class,
CcdiProjectSuspiciousTransactionQueryDTO.class
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/suspicious-transactions/export", postMapping.value()[0]);
assertNotNull(operation);
}
@Test
void shouldExposeRiskDetailsExportEndpoint() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
controller.exportRiskDetails(response, 40L);
verify(overviewService).exportRiskDetails(same(response), same(40L));
Method method = CcdiProjectOverviewController.class.getMethod(
"exportRiskDetails",
jakarta.servlet.http.HttpServletResponse.class,
Long.class
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/risk-details/export", postMapping.value()[0]);
assertNotNull(operation);
}
@Test
void shouldExposeRiskPeopleExportContract() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectRiskPeopleOverviewExcel row = new CcdiProjectRiskPeopleOverviewExcel();
row.setName("张三");
row.setRiskPoint("薪酬异常");
when(overviewService.exportRiskPeopleOverview(40L)).thenReturn(List.of(row));
controller.exportRiskPeople(response, 40L);
verify(overviewService).exportRiskPeopleOverview(40L);
assertTrue(response.getContentType().startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
assertTrue(response.getContentAsByteArray().length > 0);
Method controllerMethod = CcdiProjectOverviewController.class.getMethod(
"exportRiskPeople",
jakarta.servlet.http.HttpServletResponse.class,
Long.class
);
PostMapping postMapping = controllerMethod.getAnnotation(PostMapping.class);
Operation operation = controllerMethod.getAnnotation(Operation.class);
PreAuthorize preAuthorize = controllerMethod.getAnnotation(PreAuthorize.class);
assertNotNull(postMapping);
assertEquals("/risk-people/export", postMapping.value()[0]);
assertNotNull(operation);
assertEquals("导出风险人员总览", operation.summary());
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
Method serviceMethod = ICcdiProjectOverviewService.class.getMethod("exportRiskPeopleOverview", Long.class);
assertEquals(List.class, serviceMethod.getReturnType());
assertExcelColumnName("name", "姓名");
assertExcelColumnName("idNo", "身份证号");
assertExcelColumnName("department", "所属部门");
assertExcelColumnName("riskCount", "疑似违规数");
assertExcelColumnName("riskLevel", "风险等级");
assertExcelColumnName("modelCount", "命中模型数");
assertExcelColumnName("riskPoint", "核心异常点");
}
private void assertExcelColumnName(String fieldName, String expectedColumnName) throws Exception {
Field field = CcdiProjectRiskPeopleOverviewExcel.class.getDeclaredField(fieldName);
Excel excel = field.getAnnotation(Excel.class);
assertNotNull(excel);
assertEquals(expectedColumnName, excel.name());
}
} }

View File

@@ -1,7 +1,19 @@
package com.ruoyi.ccdi.project.controller; package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService; import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
@@ -87,4 +99,76 @@ class CcdiProjectSpecialCheckControllerTest {
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value()); assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation); assertNotNull(operation);
} }
@Test
void shouldReturnAjaxResultSuccessForExtendedPurchaseEndpoints() {
CcdiProjectExtendedPurchaseQueryDTO listQuery = new CcdiProjectExtendedPurchaseQueryDTO();
listQuery.setProjectId(40L);
CcdiProjectExtendedPurchaseListVO listVO = new CcdiProjectExtendedPurchaseListVO();
when(specialCheckService.getExtendedPurchaseList(listQuery)).thenReturn(listVO);
AjaxResult listResult = controller.getExtendedPurchaseList(listQuery);
assertEquals(200, listResult.get("code"));
assertEquals(listVO, listResult.get("data"));
CcdiProjectExtendedPurchaseDetailQueryDTO detailQuery = new CcdiProjectExtendedPurchaseDetailQueryDTO();
detailQuery.setProjectId(40L);
detailQuery.setPurchaseId("CG-001");
CcdiProjectExtendedPurchaseDetailVO detailVO = new CcdiProjectExtendedPurchaseDetailVO();
when(specialCheckService.getExtendedPurchaseDetail(detailQuery)).thenReturn(detailVO);
AjaxResult detailResult = controller.getExtendedPurchaseDetail(detailQuery);
assertEquals(200, detailResult.get("code"));
assertEquals(detailVO, detailResult.get("data"));
}
@Test
void shouldReturnAjaxResultSuccessForExtendedRecruitmentEndpoints() {
CcdiProjectExtendedRecruitmentQueryDTO listQuery = new CcdiProjectExtendedRecruitmentQueryDTO();
listQuery.setProjectId(40L);
CcdiProjectExtendedRecruitmentListVO listVO = new CcdiProjectExtendedRecruitmentListVO();
when(specialCheckService.getExtendedRecruitmentList(listQuery)).thenReturn(listVO);
AjaxResult listResult = controller.getExtendedRecruitmentList(listQuery);
assertEquals(200, listResult.get("code"));
assertEquals(listVO, listResult.get("data"));
CcdiProjectExtendedRecruitmentDetailQueryDTO detailQuery = new CcdiProjectExtendedRecruitmentDetailQueryDTO();
detailQuery.setProjectId(40L);
detailQuery.setRecruitId("ZP-001");
CcdiProjectExtendedRecruitmentDetailVO detailVO = new CcdiProjectExtendedRecruitmentDetailVO();
when(specialCheckService.getExtendedRecruitmentDetail(detailQuery)).thenReturn(detailVO);
AjaxResult detailResult = controller.getExtendedRecruitmentDetail(detailQuery);
assertEquals(200, detailResult.get("code"));
assertEquals(detailVO, detailResult.get("data"));
}
@Test
void shouldReturnAjaxResultSuccessForExtendedTransferEndpoints() {
CcdiProjectExtendedTransferQueryDTO listQuery = new CcdiProjectExtendedTransferQueryDTO();
listQuery.setProjectId(40L);
CcdiProjectExtendedTransferListVO listVO = new CcdiProjectExtendedTransferListVO();
when(specialCheckService.getExtendedTransferList(listQuery)).thenReturn(listVO);
AjaxResult listResult = controller.getExtendedTransferList(listQuery);
assertEquals(200, listResult.get("code"));
assertEquals(listVO, listResult.get("data"));
CcdiProjectExtendedTransferDetailQueryDTO detailQuery = new CcdiProjectExtendedTransferDetailQueryDTO();
detailQuery.setProjectId(40L);
detailQuery.setId(1L);
CcdiProjectExtendedTransferDetailVO detailVO = new CcdiProjectExtendedTransferDetailVO();
when(specialCheckService.getExtendedTransferDetail(detailQuery)).thenReturn(detailVO);
AjaxResult detailResult = controller.getExtendedTransferDetail(detailQuery);
assertEquals(200, detailResult.get("code"));
assertEquals(detailVO, detailResult.get("data"));
}
} }

View File

@@ -0,0 +1,186 @@
package com.ruoyi.ccdi.project.controller;
import io.swagger.v3.oas.annotations.Operation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckExtendedQueryContractTest {
@Test
void shouldExposeExtendedPurchaseEndpointsAndContracts() throws Exception {
assertEndpoint(
"getExtendedPurchaseList",
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO",
"/extended-query/purchase/list"
);
assertEndpoint(
"getExtendedPurchaseDetail",
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO",
"/extended-query/purchase/detail"
);
assertFields(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO",
"projectId",
"applicantName",
"applyDateStart",
"applyDateEnd",
"pageNum",
"pageSize"
);
assertFields(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO",
"projectId",
"purchaseId"
);
assertFields(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO",
"rows",
"total"
);
assertExactFields(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO",
"purchaseId",
"projectName",
"subjectName",
"applicantName",
"applyDate"
);
assertDetailVoOwnedByProject("com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO");
}
@Test
void shouldExposeExtendedRecruitmentEndpointsAndContracts() throws Exception {
assertEndpoint(
"getExtendedRecruitmentList",
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO",
"/extended-query/recruitment/list"
);
assertEndpoint(
"getExtendedRecruitmentDetail",
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO",
"/extended-query/recruitment/detail"
);
assertFields(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO",
"projectId",
"interviewerName",
"pageNum",
"pageSize"
);
assertFields(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO",
"projectId",
"recruitId"
);
assertFields(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListVO",
"rows",
"total"
);
assertExactFields(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO",
"recruitId",
"recruitName",
"posName",
"interviewerNameSummary",
"admitStatus"
);
assertDetailVoOwnedByProject("com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO");
}
@Test
void shouldExposeExtendedTransferEndpointsAndContracts() throws Exception {
assertEndpoint(
"getExtendedTransferList",
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO",
"/extended-query/transfer/list"
);
assertEndpoint(
"getExtendedTransferDetail",
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO",
"/extended-query/transfer/detail"
);
assertFields(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO",
"projectId",
"staffName",
"transferDateStart",
"transferDateEnd",
"pageNum",
"pageSize"
);
assertFields(
"com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO",
"projectId",
"id"
);
assertFields(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO",
"rows",
"total"
);
assertExactFields(
"com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListItemVO",
"id",
"staffName",
"transferType",
"deptNameBefore",
"deptNameAfter",
"transferDate"
);
assertDetailVoOwnedByProject("com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO");
}
private void assertEndpoint(String methodName, String dtoClassName, String expectedPath) throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectSpecialCheckController");
Class<?> dtoClass = Class.forName(dtoClassName);
Method method = controllerClass.getMethod(methodName, dtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals(expectedPath, getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation);
}
private void assertFields(String className, String... expectedFields) throws Exception {
Class<?> type = Class.forName(className);
List<String> fieldNames = Arrays.stream(type.getDeclaredFields()).map(Field::getName).collect(Collectors.toList());
for (String expectedField : expectedFields) {
assertTrue(fieldNames.contains(expectedField), className + " 缺少字段 " + expectedField);
}
}
private void assertExactFields(String className, String... expectedFields) throws Exception {
Class<?> type = Class.forName(className);
List<String> fieldNames = Arrays.stream(type.getDeclaredFields()).map(Field::getName).collect(Collectors.toList());
assertEquals(expectedFields.length, fieldNames.size(), className + " 字段数量不符合预期");
for (String expectedField : expectedFields) {
assertTrue(fieldNames.contains(expectedField), className + " 缺少字段 " + expectedField);
}
}
private void assertDetailVoOwnedByProject(String className) throws Exception {
Class<?> type = Class.forName(className);
assertTrue(type.getName().startsWith("com.ruoyi.ccdi.project.domain.vo."));
assertTrue(!type.getName().contains("info.collection"));
}
}

View File

@@ -91,4 +91,17 @@ class CcdiBankStatementTest {
assertEquals(1, entity.getInternalFlag(), "Integer 类型应该正确复制"); assertEquals(1, entity.getInternalFlag(), "Integer 类型应该正确复制");
assertEquals(100, entity.getTrxType(), "Integer 类型应该正确复制"); assertEquals(100, entity.getTrxType(), "Integer 类型应该正确复制");
} }
@Test
void testFromResponse_ShouldMapCounterpartyIdentityFields() {
BankStatementItem item = new BankStatementItem();
item.setCustomerCertNo("330101199001011234");
item.setCustomerSocialCreditCode("91330100123456789X");
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
assertNotNull(entity, "转换结果不应为 null");
assertEquals("330101199001011234", entity.getCustomerCertNo());
assertEquals("91330100123456789X", entity.getCustomerSocialCreditCode());
}
} }

View File

@@ -16,6 +16,7 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -132,6 +133,67 @@ class CcdiBankStatementMapperXmlTest {
} }
} }
@Test
void fileUploadRecordMapper_shouldContainHistoryImportSourceFields() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/ccdi/project/CcdiFileUploadRecordMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("source_type"), xml);
assertTrue(xml.contains("source_project_id"), xml);
assertTrue(xml.contains("source_project_name"), xml);
}
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("fur.source_project_name"), xml);
}
}
@Test
void historyImportQueries_shouldFilterSuccessfulSourceBatchesAndDateRange() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/ccdi/project/CcdiFileUploadRecordMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("selectSuccessfulRecordsByProjectIds"), xml);
assertTrue(xml.contains("file_status = 'parsed_success'"), xml);
assertTrue(xml.contains("log_id is not null"), xml);
}
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("selectStatementsForHistoryImport"), xml);
assertTrue(xml.contains("bs.project_id = #{projectId}"), xml);
assertTrue(xml.contains("bs.batch_id = #{batchId}"), xml);
assertTrue(xml.contains("#{startDate}"), xml);
assertTrue(xml.contains("#{endDate}"), xml);
}
}
@Test
void selectStatementsForHistoryImport_shouldNotGenerateDuplicatedSelectKeyword() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper.selectStatementsForHistoryImport");
Map<String, Object> params = new HashMap<>();
params.put("projectId", 48L);
params.put("batchId", 17094);
params.put("startDate", "2026-03-17");
params.put("endDate", "2026-03-18");
BoundSql boundSql = mappedStatement.getBoundSql(params);
String sql = boundSql.getSql().replaceAll("\\s+", " ").trim();
assertFalse(sql.startsWith("SELECT select"), sql);
assertTrue(sql.startsWith("SELECT bank_statement_id"), sql);
assertFalse(sql.contains("FROM ccdi_bank_statement FROM ccdi_bank_statement"), sql);
assertFalse(sql.contains("?AND"), sql);
assertTrue(sql.contains("WHERE (bs.project_id = ?) AND (bs.batch_id = ?) AND ( CASE"), sql);
}
@Test @Test
void insertBatch_shouldAvoidUpdatingAutoIncrementPrimaryKeyInDuplicateBranch() throws Exception { void insertBatch_shouldAvoidUpdatingAutoIncrementPrimaryKeyInDuplicateBranch() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) { try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
@@ -143,6 +205,21 @@ class CcdiBankStatementMapperXmlTest {
} }
} }
@Test
void mapperXml_shouldContainCounterpartyIdentityColumns() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("<result property=\"customerCertNo\" column=\"customer_cert_no\" />"), xml);
assertTrue(
xml.contains("<result property=\"customerSocialCreditCode\" column=\"customer_social_credit_code\" />"),
xml
);
assertTrue(xml.contains("customer_bank, customer_reference, customer_cert_no, customer_social_credit_code,"), xml);
assertTrue(xml.contains("#{item.customerBank}, #{item.customerReference}, #{item.customerCertNo}, #{item.customerSocialCreditCode},"), xml);
}
}
private MappedStatement loadMappedStatement(String statementId) throws Exception { private MappedStatement loadMappedStatement(String statementId) throws Exception {
Configuration configuration = new Configuration(); Configuration configuration = new Configuration();
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource())); configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));

View File

@@ -13,17 +13,32 @@ class CcdiProjectOverviewMapperSqlTest {
@Test @Test
void shouldReadOverviewQueriesFromEmployeeResultTable() throws Exception { void shouldReadOverviewQueriesFromEmployeeResultTable() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml")); String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String riskPeopleSql = extractSelect(xml, "selectRiskPeopleOverviewByProjectId"); String riskPeopleSql = extractSelect(xml, "selectRiskPeopleOverviewPage");
String riskPeopleExportSql = extractSelect(xml, "selectRiskPeopleOverviewList");
String topRiskPeopleSql = extractSelect(xml, "selectTopRiskPeopleByProjectId"); String topRiskPeopleSql = extractSelect(xml, "selectTopRiskPeopleByProjectId");
String riskModelCardsSql = extractSelect(xml, "selectRiskModelCardsByProjectId"); String riskModelCardsSql = extractSelect(xml, "selectRiskModelCardsByProjectId");
String riskModelPeopleSql = extractSelect(xml, "selectRiskModelPeoplePage"); String riskModelPeopleSql = extractSelect(xml, "selectRiskModelPeoplePage");
assertTrue(xml.contains("<sql id=\"riskPeopleOverviewSelectColumns\">"), xml);
assertTrue(xml.contains("<sql id=\"riskPeopleOverviewOrderBy\">"), xml);
assertTrue(riskPeopleSql.contains("from ccdi_project_overview_employee_result")); assertTrue(riskPeopleSql.contains("from ccdi_project_overview_employee_result"));
assertTrue(riskPeopleSql.contains("risk_level_code")); assertTrue(riskPeopleSql.contains("result.project_id = #{query.projectId}"), riskPeopleSql);
assertTrue(riskPeopleSql.contains("model_count")); assertTrue(riskPeopleSql.contains("<include refid=\"riskPeopleOverviewSelectColumns\"/>"), riskPeopleSql);
assertTrue(riskPeopleSql.contains("risk_point")); assertTrue(riskPeopleSql.contains("<include refid=\"riskPeopleOverviewOrderBy\"/>"), riskPeopleSql);
assertTrue(
xml.contains(
"order by risk_level_sort asc, result.model_count desc, result.rule_count desc, result.staff_id_card asc"
),
xml
);
assertFalse(riskPeopleSql.contains("limit 10"), riskPeopleSql);
assertFalse(riskPeopleSql.contains("resolvedEmployeeRiskBaseSql")); assertFalse(riskPeopleSql.contains("resolvedEmployeeRiskBaseSql"));
assertTrue(riskPeopleExportSql.contains("from ccdi_project_overview_employee_result"), riskPeopleExportSql);
assertTrue(riskPeopleExportSql.contains("result.project_id = #{projectId}"), riskPeopleExportSql);
assertTrue(riskPeopleExportSql.contains("<include refid=\"riskPeopleOverviewSelectColumns\"/>"), riskPeopleExportSql);
assertTrue(riskPeopleExportSql.contains("<include refid=\"riskPeopleOverviewOrderBy\"/>"), riskPeopleExportSql);
assertTrue(topRiskPeopleSql.contains("from ccdi_project_overview_employee_result")); assertTrue(topRiskPeopleSql.contains("from ccdi_project_overview_employee_result"));
assertTrue(topRiskPeopleSql.contains("risk_level_code in ('HIGH', 'MEDIUM')")); assertTrue(topRiskPeopleSql.contains("risk_level_code in ('HIGH', 'MEDIUM')"));
assertFalse(topRiskPeopleSql.contains("resolvedEmployeeRiskBaseSql")); assertFalse(topRiskPeopleSql.contains("resolvedEmployeeRiskBaseSql"));
@@ -45,10 +60,73 @@ class CcdiProjectOverviewMapperSqlTest {
assertFalse(xml.contains("json_table("), xml); assertFalse(xml.contains("json_table("), xml);
} }
@Test
void shouldExposePersonAnalysisDetailQueries() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String basicInfoSql = extractSelect(xml, "selectPersonAnalysisBasicInfo");
String statementRowsSql = extractSelect(xml, "selectPersonAnalysisStatementRows");
String objectRowsSql = extractSelect(xml, "selectPersonAnalysisObjectRows");
assertTrue(basicInfoSql.contains("ccdi_base_staff"), basicInfoSql);
assertTrue(basicInfoSql.contains("left join sys_dept"), basicInfoSql);
assertTrue(basicInfoSql.contains("ccdi_project_overview_employee_result"), basicInfoSql);
assertTrue(statementRowsSql.contains("from ccdi_bank_statement"), statementRowsSql);
assertTrue(statementRowsSql.contains("ccdi_bank_statement_tag_result"), statementRowsSql);
assertTrue(statementRowsSql.contains("bs.project_id = #{projectId}"), statementRowsSql);
assertTrue(objectRowsSql.contains("from ccdi_bank_statement_tag_result"), objectRowsSql);
assertTrue(objectRowsSql.contains("tr.object_type"), objectRowsSql);
assertTrue(objectRowsSql.contains("tr.reason_detail"), objectRowsSql);
assertTrue(objectRowsSql.contains("as reasonDetail"), objectRowsSql);
assertTrue(objectRowsSql.contains("tr.rule_code"), objectRowsSql);
assertTrue(objectRowsSql.contains("group by coalesce(tr.object_key, tr.object_type), tr.rule_code"), objectRowsSql);
assertFalse(objectRowsSql.contains("group_concat(distinct tr.reason_detail"), objectRowsSql);
assertFalse(objectRowsSql.contains("group_concat(distinct tr.rule_name"), objectRowsSql);
assertTrue(objectRowsSql.contains("tr.staff_id_card = #{staffIdCard}") || objectRowsSql.contains("#{staffIdCard}"), objectRowsSql);
}
@Test
void shouldExposeSuspiciousTransactionAggregationQuery() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
assertTrue(suspiciousSql.contains("rule_name like '%可疑%'"), suspiciousSql);
assertTrue(suspiciousSql.contains("ccdi_biz_intermediary"), suspiciousSql);
assertTrue(suspiciousSql.contains("ccdi_enterprise_base_info"), suspiciousSql);
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
}
@Test
void shouldExposeEmployeeCreditNegativeQuery() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String employeeCreditSql = extractSelect(xml, "selectEmployeeCreditNegativePage");
String employeeCreditExportSql = extractSelect(xml, "selectEmployeeCreditNegativeList");
assertTrue(employeeCreditSql.contains("from ccdi_project_overview_employee_result"), employeeCreditSql);
assertTrue(employeeCreditSql.contains("inner join ccdi_credit_negative_info"), employeeCreditSql);
assertTrue(employeeCreditSql.contains("result.project_id = #{query.projectId}"), employeeCreditSql);
assertTrue(employeeCreditSql.contains("order by neg.query_date desc, neg.person_id asc"), employeeCreditSql);
assertFalse(employeeCreditSql.contains("ccdi_debts_info"), employeeCreditSql);
assertTrue(employeeCreditExportSql.contains("from ccdi_project_overview_employee_result"), employeeCreditExportSql);
assertTrue(employeeCreditExportSql.contains("inner join ccdi_credit_negative_info"), employeeCreditExportSql);
assertTrue(employeeCreditExportSql.contains("result.project_id = #{projectId}"), employeeCreditExportSql);
assertTrue(
employeeCreditExportSql.contains("order by neg.query_date desc, neg.person_id asc"),
employeeCreditExportSql
);
assertFalse(employeeCreditExportSql.contains("ccdi_debts_info"), employeeCreditExportSql);
}
private String extractSelect(String xml, String selectId) { private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\""; String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start); int startIndex = xml.indexOf(start);
assertTrue(startIndex >= 0, "missing select: " + selectId);
int endIndex = xml.indexOf("</select>", startIndex); int endIndex = xml.indexOf("</select>", startIndex);
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
return xml.substring(startIndex, endIndex); return xml.substring(startIndex, endIndex);
} }
} }

View File

@@ -0,0 +1,48 @@
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckExtendedPurchaseSqlTest {
@Test
void shouldExposeExtendedPurchaseMapperMethodsAndSqlCaliber() throws Exception {
Class<?> mapperClass = Class.forName("com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper");
Class<?> queryClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO");
Method listMethod = mapperClass.getMethod("selectExtendedPurchasePage", Page.class, queryClass);
Method detailMethod = mapperClass.getMethod("selectExtendedPurchaseDetail", Long.class, String.class);
assertEquals("Page", listMethod.getReturnType().getSimpleName());
assertEquals("CcdiProjectExtendedPurchaseDetailVO", detailMethod.getReturnType().getSimpleName());
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
String listSql = extractSelect(xml, "selectExtendedPurchasePage");
String detailSql = extractSelect(xml, "selectExtendedPurchaseDetail");
assertTrue(listSql.contains("projectEmployeeScopeSql"));
assertTrue(listSql.contains("name=\"projectId\" value=\"query.projectId\""));
assertTrue(listSql.contains("select distinct scope.staff_name"));
assertTrue(listSql.contains("applicant_name"));
assertTrue(listSql.contains("apply_date"));
assertTrue(listSql.contains("purchase_id"));
assertTrue(listSql.contains("project_name"));
assertTrue(listSql.contains("subject_name"));
assertTrue(listSql.contains("order by p.apply_date desc, p.create_time desc, p.purchase_id desc"));
assertTrue(detailSql.contains("projectEmployeeScopeSql"));
assertTrue(detailSql.contains("purchase_id = #{purchaseId}"));
assertTrue(detailSql.contains("applicant_name"));
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);
int endIndex = xml.indexOf("</select>", startIndex);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckExtendedRecruitmentSqlTest {
@Test
void shouldExposeExtendedRecruitmentMapperMethodsAndSqlCaliber() throws Exception {
Class<?> mapperClass = Class.forName("com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper");
Class<?> queryClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO");
Method listMethod = mapperClass.getMethod("selectExtendedRecruitmentPage", Page.class, queryClass);
Method detailMethod = mapperClass.getMethod("selectExtendedRecruitmentDetail", Long.class, String.class);
assertEquals("Page", listMethod.getReturnType().getSimpleName());
assertEquals("CcdiProjectExtendedRecruitmentDetailVO", detailMethod.getReturnType().getSimpleName());
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
String listSql = extractSelect(xml, "selectExtendedRecruitmentPage");
String detailSql = extractSelect(xml, "selectExtendedRecruitmentDetail");
assertTrue(listSql.contains("projectEmployeeScopeSql"));
assertTrue(listSql.contains("name=\"projectId\" value=\"query.projectId\""));
assertTrue(listSql.contains("select distinct r.recruit_id"));
assertTrue(listSql.contains("interviewer_name1"));
assertTrue(listSql.contains("interviewer_name2"));
assertTrue(listSql.contains("concat_ws(' / '"));
assertTrue(listSql.contains("admit_status"));
assertTrue(detailSql.contains("projectEmployeeScopeSql"));
assertTrue(detailSql.contains("recruit_id = #{recruitId}"));
assertTrue(detailSql.contains("interviewer_name1"));
assertTrue(detailSql.contains("interviewer_name2"));
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);
int endIndex = xml.indexOf("</select>", startIndex);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -0,0 +1,46 @@
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectSpecialCheckExtendedTransferSqlTest {
@Test
void shouldExposeExtendedTransferMapperMethodsAndSqlCaliber() throws Exception {
Class<?> mapperClass = Class.forName("com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper");
Class<?> queryClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO");
Method listMethod = mapperClass.getMethod("selectExtendedTransferPage", Page.class, queryClass);
Method detailMethod = mapperClass.getMethod("selectExtendedTransferDetail", Long.class, Long.class);
assertEquals("Page", listMethod.getReturnType().getSimpleName());
assertEquals("CcdiProjectExtendedTransferDetailVO", detailMethod.getReturnType().getSimpleName());
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
String listSql = extractSelect(xml, "selectExtendedTransferPage");
String detailSql = extractSelect(xml, "selectExtendedTransferDetail");
assertTrue(listSql.contains("projectEmployeeScopeSql"));
assertTrue(listSql.contains("name=\"projectId\" value=\"query.projectId\""));
assertTrue(listSql.contains("ccdi_staff_transfer t"));
assertTrue(listSql.contains("ccdi_base_staff s"));
assertTrue(listSql.contains("scope.staff_name = s.name"));
assertTrue(listSql.contains("transfer_date"));
assertTrue(listSql.contains("order by t.transfer_date desc, t.create_time desc, t.id desc"));
assertTrue(detailSql.contains("projectEmployeeScopeSql"));
assertTrue(detailSql.contains("t.id = #{id}"));
assertTrue(detailSql.contains("scope.staff_name = s.name"));
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);
int endIndex = xml.indexOf("</select>", startIndex);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -11,8 +11,27 @@ class CcdiProjectOverviewServiceStructureTest {
Class<?> clazz = Class.forName("com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService"); Class<?> clazz = Class.forName("com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService");
assertNotNull(clazz.getMethod("getDashboard", Long.class)); assertNotNull(clazz.getMethod("getDashboard", Long.class));
assertNotNull(clazz.getMethod("getRiskPeopleOverview", Long.class)); assertNotNull(clazz.getMethod(
"getRiskPeopleOverview",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO")
));
assertNotNull(clazz.getMethod("getTopRiskPeople", Long.class)); assertNotNull(clazz.getMethod("getTopRiskPeople", Long.class));
assertNotNull(clazz.getMethod(
"getPersonAnalysisDetail",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO")
));
assertNotNull(clazz.getMethod(
"getSuspiciousTransactions",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
));
assertNotNull(clazz.getMethod(
"getEmployeeCreditNegative",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO")
));
assertNotNull(clazz.getMethod(
"exportSuspiciousTransactions",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
));
assertNotNull(clazz.getMethod("refreshOverviewEmployeeResults", Long.class, String.class)); assertNotNull(clazz.getMethod("refreshOverviewEmployeeResults", Long.class, String.class));
assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class)); assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class));
} }

View File

@@ -154,6 +154,8 @@ class CcdiFileUploadServiceImplTest {
assertEquals("admin", inserted.get().get(0).getUploadUser()); assertEquals("admin", inserted.get().get(0).getUploadUser());
assertEquals("uploading", inserted.get().get(0).getFileStatus()); assertEquals("uploading", inserted.get().get(0).getFileStatus());
assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size()); assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size());
verify(projectService).ensureProjectNotArchived(PROJECT_ID, "已归档项目暂不允许上传或拉取数据");
verify(projectService).ensureProjectWritable(PROJECT_ID, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
} finally { } finally {
TransactionSynchronizationManager.clearSynchronization(); TransactionSynchronizationManager.clearSynchronization();
} }
@@ -409,6 +411,20 @@ class CcdiFileUploadServiceImplTest {
assertTrue(exception.getMessage().contains("仅支持删除解析成功文件")); assertTrue(exception.getMessage().contains("仅支持删除解析成功文件"));
} }
@Test
void deleteFileUploadRecord_shouldRejectHistoryImportRecord() {
CcdiFileUploadRecord record = buildRecord();
record.setFileStatus("parsed_success");
record.setSourceType("HISTORY_IMPORT");
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
assertTrue(exception.getMessage().contains("历史导入文件不支持删除"));
verify(lsfxClient, never()).deleteFiles(any());
}
@Test @Test
void deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails() { void deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails() {
CcdiFileUploadRecord record = buildRecord(); CcdiFileUploadRecord record = buildRecord();

View File

@@ -152,6 +152,7 @@ class CcdiModelParamServiceImplTest {
service.saveAllParams(buildSaveAllDto()); service.saveAllParams(buildSaveAllDto());
} }
verify(projectService).ensureProjectNotArchived(40L, "已归档项目暂不允许修改参数");
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE); verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
} }
@@ -178,6 +179,7 @@ class CcdiModelParamServiceImplTest {
service.saveParams(saveDTO); service.saveParams(saveDTO);
} }
verify(projectService).ensureProjectNotArchived(40L, "已归档项目暂不允许修改参数");
verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE); verify(bankTagService).submitAutoRebuild(40L, TriggerType.AUTO_PARAM_CHANGE);
} }

View File

@@ -0,0 +1,196 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectHistoryImportServiceImplTest {
@InjectMocks
private CcdiProjectHistoryImportServiceImpl service;
@Mock
private CcdiBankStatementMapper bankStatementMapper;
@Mock
private CcdiFileUploadRecordMapper recordMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Mock
private Executor fileUploadExecutor;
@Mock
private ICcdiBankTagService bankTagService;
@Test
void shouldFilterStatementsByTrxDateAndDeduplicateAcrossSourceProjects() {
CcdiProjectImportHistoryDTO dto = buildImportDto();
when(recordMapper.selectSuccessfulRecordsByProjectIds(dto.getSourceProjectIds()))
.thenReturn(List.of(buildSourceRecord(11L, 101, "批次A"), buildSourceRecord(12L, 202, "批次B")));
when(bankStatementMapper.selectStatementsForHistoryImport(11L, 101, "2026-01-01", "2026-01-31"))
.thenReturn(List.of(buildStatement("2026-01-10", "6222", "100.00", "备注A")));
when(bankStatementMapper.selectStatementsForHistoryImport(12L, 202, "2026-01-01", "2026-01-31"))
.thenReturn(List.of(
buildStatement("2026-01-10", "6222", "100.00", "备注A"),
buildStatement("2026-01-11", "6333", "200.00", "备注B")
));
doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(fileUploadExecutor).execute(any(Runnable.class));
AtomicReference<List<CcdiBankStatement>> insertedStatements = new AtomicReference<>();
doAnswer(invocation -> {
insertedStatements.set(List.copyOf(invocation.getArgument(0)));
return insertedStatements.get().size();
}).when(bankStatementMapper).insertBatch(anyList());
when(projectMapper.selectById(11L)).thenReturn(buildProject(11L, "历史项目A"));
when(projectMapper.selectById(12L)).thenReturn(buildProject(12L, "历史项目B"));
service.submitImport(90L, 3001, dto, "tester");
assertEquals(2, insertedStatements.get().size());
assertTrue(insertedStatements.get().stream().allMatch(item -> Long.valueOf(90L).equals(item.getProjectId())));
assertTrue(insertedStatements.get().stream().allMatch(item -> Integer.valueOf(3001).equals(item.getGroupId())));
verify(bankStatementMapper).selectStatementsForHistoryImport(11L, 101, "2026-01-01", "2026-01-31");
verify(bankStatementMapper).selectStatementsForHistoryImport(12L, 202, "2026-01-01", "2026-01-31");
}
@Test
void shouldGenerateNewBatchIdsAndHistoryImportFileRecordsOnlyForSuccessfulSourceBatches() {
CcdiProjectImportHistoryDTO dto = buildImportDto();
CcdiFileUploadRecord sourceRecord = buildSourceRecord(11L, 101, "批次A");
when(recordMapper.selectSuccessfulRecordsByProjectIds(dto.getSourceProjectIds()))
.thenReturn(List.of(sourceRecord, buildSourceRecord(12L, 202, "批次B")));
when(bankStatementMapper.selectStatementsForHistoryImport(11L, 101, "2026-01-01", "2026-01-31"))
.thenReturn(List.of(buildStatement("2026-01-12", "6444", "300.00", "备注C")));
when(bankStatementMapper.selectStatementsForHistoryImport(12L, 202, "2026-01-01", "2026-01-31"))
.thenReturn(List.of());
doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(fileUploadExecutor).execute(any(Runnable.class));
AtomicReference<List<CcdiBankStatement>> insertedStatements = new AtomicReference<>();
doAnswer(invocation -> {
insertedStatements.set(List.copyOf(invocation.getArgument(0)));
return insertedStatements.get().size();
}).when(bankStatementMapper).insertBatch(anyList());
AtomicReference<List<CcdiFileUploadRecord>> insertedRecords = new AtomicReference<>();
doAnswer(invocation -> {
insertedRecords.set(List.copyOf(invocation.getArgument(0)));
return insertedRecords.get().size();
}).when(recordMapper).insertBatch(anyList());
when(projectMapper.selectById(11L)).thenReturn(buildProject(11L, "历史项目A"));
service.submitImport(90L, 3001, dto, "tester");
assertEquals(1, insertedStatements.get().size());
assertNotEquals(101, insertedStatements.get().get(0).getBatchId());
assertEquals(1, insertedRecords.get().size());
assertEquals("HISTORY_IMPORT", insertedRecords.get().get(0).getSourceType());
assertEquals(11L, insertedRecords.get().get(0).getSourceProjectId());
assertEquals("历史项目A", insertedRecords.get().get(0).getSourceProjectName());
assertEquals("parsed_success", insertedRecords.get().get(0).getFileStatus());
assertNotEquals(sourceRecord.getLogId(), insertedRecords.get().get(0).getLogId());
}
@Test
void shouldRefreshTargetCountAndSubmitAutoRebuildAfterImport() {
CcdiProjectImportHistoryDTO dto = buildImportDto();
when(recordMapper.selectSuccessfulRecordsByProjectIds(dto.getSourceProjectIds()))
.thenReturn(List.of(buildSourceRecord(11L, 101, "批次A")));
when(bankStatementMapper.selectStatementsForHistoryImport(11L, 101, "2026-01-01", "2026-01-31"))
.thenReturn(List.of(buildStatement("2026-01-12", "6444", "300.00", "备注C")));
doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(fileUploadExecutor).execute(any(Runnable.class));
when(projectMapper.selectById(11L)).thenReturn(buildProject(11L, "历史项目A"));
when(projectMapper.selectById(90L)).thenReturn(buildProject(90L, "新项目"));
when(bankStatementMapper.countMatchedStaffCountByProjectId(90L)).thenReturn(3);
service.submitImport(90L, 3001, dto, "tester");
verify(projectMapper).updateById(org.mockito.ArgumentMatchers.<CcdiProject>argThat(project ->
Long.valueOf(90L).equals(project.getProjectId()) && Integer.valueOf(3).equals(project.getTargetCount())
));
verify(bankTagService).submitAutoRebuild(90L, TriggerType.AUTO_BATCH_UPLOAD);
}
private CcdiProjectImportHistoryDTO buildImportDto() {
CcdiProjectImportHistoryDTO dto = new CcdiProjectImportHistoryDTO();
dto.setProjectName("新项目");
dto.setDescription("从历史复制");
dto.setSourceProjectIds(List.of(11L, 12L));
dto.setStartDate("2026-01-01");
dto.setEndDate("2026-01-31");
return dto;
}
private CcdiFileUploadRecord buildSourceRecord(Long sourceProjectId, Integer logId, String fileName) {
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
record.setProjectId(sourceProjectId);
record.setLogId(logId);
record.setFileName(fileName);
record.setFileStatus("parsed_success");
return record;
}
private CcdiProject buildProject(Long projectId, String projectName) {
CcdiProject project = new CcdiProject();
project.setProjectId(projectId);
project.setProjectName(projectName);
return project;
}
private CcdiBankStatement buildStatement(String trxDate, String accountNo, String amountCr, String memo) {
CcdiBankStatement statement = new CcdiBankStatement();
statement.setTrxDate(trxDate);
statement.setLeAccountNo(accountNo);
statement.setAmountCr(new BigDecimal(amountCr));
statement.setUserMemo(memo);
statement.setCustomerAccountName("对手方");
statement.setCustomerAccountNo("7000");
statement.setBatchSequence(1);
return statement;
}
}

View File

@@ -0,0 +1,124 @@
package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.common.exception.ServiceException;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectOverviewServiceEmployeeCreditNegativeTest {
@InjectMocks
private CcdiProjectOverviewServiceImpl service;
@Mock
private CcdiProjectOverviewMapper overviewMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Mock
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
@Mock
private CcdiBankTagResultMapper bankTagResultMapper;
@Mock
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@Test
void shouldReturnEmployeeCreditNegativePage() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectEmployeeCreditNegativeItemVO item = new CcdiProjectEmployeeCreditNegativeItemVO();
item.setPersonName("李四");
item.setPersonId("330000000000000001");
item.setQueryDate("2026-03-20");
item.setCivilCnt(1);
item.setCivilLmt(new BigDecimal("10000.00"));
Page<CcdiProjectEmployeeCreditNegativeItemVO> page = new Page<>(1, 5);
page.setRecords(List.of(item));
page.setTotal(1L);
when(overviewMapper.selectEmployeeCreditNegativePage(any(Page.class), any(CcdiProjectEmployeeCreditNegativeQueryDTO.class)))
.thenReturn(page);
CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO = new CcdiProjectEmployeeCreditNegativeQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setPageNum(1);
queryDTO.setPageSize(5);
CcdiProjectEmployeeCreditNegativePageVO result = service.getEmployeeCreditNegative(queryDTO);
assertEquals(1, result.getRows().size());
assertEquals(1L, result.getTotal());
assertEquals("李四", result.getRows().getFirst().getPersonName());
verify(overviewMapper).selectEmployeeCreditNegativePage(
any(Page.class),
argThat(query -> query.getProjectId().equals(40L))
);
}
@Test
void shouldThrowWhenEmployeeCreditNegativeProjectDoesNotExist() {
when(projectMapper.selectById(99L)).thenReturn(null);
CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO = new CcdiProjectEmployeeCreditNegativeQueryDTO();
queryDTO.setProjectId(99L);
assertThrows(ServiceException.class, () -> service.getEmployeeCreditNegative(queryDTO));
}
@Test
void shouldExportEmployeeCreditNegativeRows() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectEmployeeCreditNegativeItemVO item = new CcdiProjectEmployeeCreditNegativeItemVO();
item.setPersonName("李四");
item.setPersonId("330000000000000001");
item.setQueryDate("2026-03-20");
item.setCivilCnt(1);
item.setCivilLmt(new BigDecimal("10000.00"));
when(overviewMapper.selectEmployeeCreditNegativeList(40L)).thenReturn(List.of(item));
List<CcdiProjectEmployeeCreditNegativeExcel> rows = service.exportEmployeeCreditNegative(40L);
assertEquals(1, rows.size());
assertEquals("李四", rows.getFirst().getPersonName());
assertEquals("330000000000000001", rows.getFirst().getPersonId());
verify(overviewMapper).selectEmployeeCreditNegativeList(eq(40L));
}
@Test
void shouldThrowWhenExportEmployeeCreditNegativeProjectDoesNotExist() {
when(projectMapper.selectById(100L)).thenReturn(null);
assertThrows(ServiceException.class, () -> service.exportEmployeeCreditNegative(100L));
}
}

View File

@@ -2,25 +2,38 @@ package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.springframework.mock.web.MockHttpServletResponse;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@@ -33,6 +46,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -51,9 +66,15 @@ class CcdiProjectOverviewServiceImplTest {
@Mock @Mock
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper; private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
@Mock
private CcdiBankTagResultMapper bankTagResultMapper;
@Mock @Mock
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder; private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@Mock
private CcdiProjectRiskDetailWorkbookExporter workbookExporter;
@Test @Test
void shouldBuildDashboardWithNoRiskCount() { void shouldBuildDashboardWithNoRiskCount() {
CcdiProject project = new CcdiProject(); CcdiProject project = new CcdiProject();
@@ -76,6 +97,7 @@ class CcdiProjectOverviewServiceImplTest {
CcdiProject project = new CcdiProject(); CcdiProject project = new CcdiProject();
project.setProjectId(40L); project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project); when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectRiskPeopleQueryDTO queryDTO = buildRiskPeopleQuery(40L);
CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO(); CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO();
aggregate.setStaffName("李四"); aggregate.setStaffName("李四");
@@ -86,24 +108,70 @@ class CcdiProjectOverviewServiceImplTest {
aggregate.setRiskLevelCode("HIGH"); aggregate.setRiskLevelCode("HIGH");
aggregate.setModelCount(3); aggregate.setModelCount(3);
aggregate.setRiskPoint("大额单笔收入、疑似兼职"); aggregate.setRiskPoint("大额单笔收入、疑似兼职");
when(overviewMapper.selectRiskPeopleOverviewByProjectId(40L)).thenReturn(List.of(aggregate)); Page<CcdiProjectEmployeeRiskAggregateVO> resultPage = new Page<>(1, 5);
resultPage.setRecords(List.of(aggregate));
resultPage.setTotal(1L);
when(overviewMapper.selectRiskPeopleOverviewPage(any(Page.class), any(CcdiProjectRiskPeopleQueryDTO.class)))
.thenReturn(resultPage);
when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of( when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of(
buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"), buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"),
buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM") buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM")
)); ));
CcdiProjectRiskPeopleOverviewVO overview = service.getRiskPeopleOverview(40L); CcdiProjectRiskPeopleOverviewVO overview = service.getRiskPeopleOverview(queryDTO);
assertEquals(1, overview.getOverviewList().size()); assertEquals(1, overview.getRows().size());
assertEquals(8, overview.getOverviewList().getFirst().getRiskCount()); assertEquals(1L, overview.getTotal());
assertEquals("高风险", overview.getOverviewList().getFirst().getRiskLevel()); assertEquals(1L, overview.getPageNum());
assertEquals("danger", overview.getOverviewList().getFirst().getRiskLevelType()); assertEquals(5L, overview.getPageSize());
assertEquals(3, overview.getOverviewList().getFirst().getModelCount()); assertEquals(8, overview.getRows().getFirst().getRiskCount());
assertEquals(2, overview.getOverviewList().getFirst().getRiskPointTagList().size()); assertEquals("高风险", overview.getRows().getFirst().getRiskLevel());
assertEquals("LARGE_TRANSACTION", overview.getOverviewList().getFirst().getRiskPointTagList().getFirst().getModelCode()); assertEquals("danger", overview.getRows().getFirst().getRiskLevelType());
assertEquals("大额交易模型", overview.getOverviewList().getFirst().getRiskPointTagList().getFirst().getModelName()); assertEquals(3, overview.getRows().getFirst().getModelCount());
assertEquals("大额单笔收入、疑似兼职", overview.getOverviewList().getFirst().getRiskPoint()); assertEquals(2, overview.getRows().getFirst().getRiskPointTagList().size());
assertEquals("查看详情", overview.getOverviewList().getFirst().getActionLabel()); assertEquals("LARGE_TRANSACTION", overview.getRows().getFirst().getRiskPointTagList().getFirst().getModelCode());
assertEquals("大额交易模型", overview.getRows().getFirst().getRiskPointTagList().getFirst().getModelName());
assertEquals("大额单笔收入、疑似兼职", overview.getRows().getFirst().getRiskPoint());
assertEquals("查看详情", overview.getRows().getFirst().getActionLabel());
verify(overviewMapper).selectRiskPeopleOverviewPage(
argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
argThat(query -> query.getProjectId().equals(40L))
);
}
@Test
void shouldDefaultRiskPeoplePageNumAndPageSizeToOneAndFive() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
Page<CcdiProjectEmployeeRiskAggregateVO> emptyPage = new Page<>(1, 5);
emptyPage.setRecords(List.of());
emptyPage.setTotal(0L);
when(overviewMapper.selectRiskPeopleOverviewPage(any(Page.class), any(CcdiProjectRiskPeopleQueryDTO.class)))
.thenReturn(emptyPage);
CcdiProjectRiskPeopleQueryDTO queryDTO = new CcdiProjectRiskPeopleQueryDTO();
queryDTO.setProjectId(40L);
service.getRiskPeopleOverview(queryDTO);
verify(overviewMapper).selectRiskPeopleOverviewPage(
argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
any(CcdiProjectRiskPeopleQueryDTO.class)
);
clearInvocations(overviewMapper);
CcdiProjectRiskPeopleQueryDTO invalidQuery = new CcdiProjectRiskPeopleQueryDTO();
invalidQuery.setProjectId(40L);
invalidQuery.setPageNum(0);
invalidQuery.setPageSize(-1);
service.getRiskPeopleOverview(invalidQuery);
verify(overviewMapper, times(1)).selectRiskPeopleOverviewPage(
argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L),
any(CcdiProjectRiskPeopleQueryDTO.class)
);
} }
@Test @Test
@@ -128,14 +196,168 @@ class CcdiProjectOverviewServiceImplTest {
assertEquals("查看详情", topRiskPeople.getTopRiskList().getFirst().getActionLabel()); assertEquals("查看详情", topRiskPeople.getTopRiskList().getFirst().getActionLabel());
} }
@Test
void shouldExportRiskPeopleOverviewRowsWithSameFieldsAsPage() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO();
aggregate.setStaffName("李四");
aggregate.setStaffIdCard("330000000000000001");
aggregate.setDeptName("信息二部");
aggregate.setRuleCount(5);
aggregate.setHitCount(8);
aggregate.setRiskLevelCode("HIGH");
aggregate.setModelCount(3);
aggregate.setRiskPoint("大额单笔收入、疑似兼职");
when(overviewMapper.selectRiskPeopleOverviewList(40L)).thenReturn(List.of(aggregate));
when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of(
buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"),
buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM")
));
List<CcdiProjectRiskPeopleOverviewExcel> rows = service.exportRiskPeopleOverview(40L);
assertEquals(1, rows.size());
assertEquals("李四", rows.getFirst().getName());
assertEquals("330000000000000001", rows.getFirst().getIdNo());
assertEquals("信息二部", rows.getFirst().getDepartment());
assertEquals(8, rows.getFirst().getRiskCount());
assertEquals("高风险", rows.getFirst().getRiskLevel());
assertEquals(3, rows.getFirst().getModelCount());
assertEquals("大额单笔收入、疑似兼职", rows.getFirst().getRiskPoint());
verify(overviewMapper).selectRiskPeopleOverviewList(40L);
}
@Test @Test
void shouldThrowWhenProjectDoesNotExist() { void shouldThrowWhenProjectDoesNotExist() {
when(projectMapper.selectById(99L)).thenReturn(null); when(projectMapper.selectById(99L)).thenReturn(null);
assertThrows(ServiceException.class, () -> service.getRiskPeopleOverview(99L)); assertThrows(ServiceException.class, () -> service.getRiskPeopleOverview(buildRiskPeopleQuery(99L)));
assertThrows(ServiceException.class, () -> service.exportRiskPeopleOverview(99L));
assertThrows(ServiceException.class, () -> service.getTopRiskPeople(99L)); assertThrows(ServiceException.class, () -> service.getTopRiskPeople(99L));
assertThrows(ServiceException.class, () -> service.getRiskModelCards(99L)); assertThrows(ServiceException.class, () -> service.getRiskModelCards(99L));
assertThrows(ServiceException.class, () -> service.getRiskModelPeople(buildRiskModelPeopleQuery(99L))); assertThrows(ServiceException.class, () -> service.getRiskModelPeople(buildRiskModelPeopleQuery(99L)));
assertThrows(ServiceException.class, () -> service.getPersonAnalysisDetail(buildPersonAnalysisDetailQuery(99L)));
}
@Test
void shouldExportRiskDetailsWorkbook() throws Exception {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO suspiciousItem = new CcdiProjectSuspiciousTransactionItemVO();
suspiciousItem.setTrxDate("2026-03-20 10:00:00");
suspiciousItem.setSuspiciousPersonName("张三");
suspiciousItem.setRelatedPersonName("张三");
suspiciousItem.setRelatedStaffName("张三");
suspiciousItem.setRelatedStaffCode("1001");
suspiciousItem.setRelationType("本人");
suspiciousItem.setUserMemo("转账");
suspiciousItem.setCashType("转账");
suspiciousItem.setDisplayAmount(new BigDecimal("100.00"));
when(overviewMapper.selectSuspiciousTransactionList(any())).thenReturn(List.of(suspiciousItem));
CcdiProjectEmployeeCreditNegativeItemVO creditItem = new CcdiProjectEmployeeCreditNegativeItemVO();
creditItem.setPersonName("李四");
creditItem.setPersonId("330000000000000001");
creditItem.setQueryDate("2026-03-20");
creditItem.setCivilCnt(1);
creditItem.setCivilLmt(new BigDecimal("20000.00"));
when(overviewMapper.selectEmployeeCreditNegativeList(40L)).thenReturn(List.of(creditItem));
MockHttpServletResponse response = new MockHttpServletResponse();
service.exportRiskDetails(response, 40L);
verify(overviewMapper).selectSuspiciousTransactionList(argThat(query ->
query.getProjectId().equals(40L) && "ALL".equals(query.getSuspiciousType())
));
verify(workbookExporter).export(
eq(response),
eq(40L),
argThat((List<CcdiProjectSuspiciousTransactionExcel> rows) ->
rows.size() == 1 && "张三".equals(rows.getFirst().getSuspiciousPersonName())
),
argThat((List<CcdiProjectEmployeeCreditNegativeExcel> rows) ->
rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName())
)
);
}
@Test
void shouldReturnPersonAnalysisDetailWithBasicInfoAndGroupedAbnormalDetail() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectPersonAnalysisBasicInfoVO basicInfo = new CcdiProjectPersonAnalysisBasicInfoVO();
basicInfo.setName("李四");
basicInfo.setIdNo("330000000000000001");
basicInfo.setStaffCode("10001");
basicInfo.setDepartment("信息二部");
basicInfo.setPhone("13800000000");
basicInfo.setRiskLevel("高风险");
basicInfo.setProjectName("测试项目");
when(overviewMapper.selectPersonAnalysisBasicInfo(40L, "330000000000000001")).thenReturn(basicInfo);
CcdiBankStatementListVO statementRow = new CcdiBankStatementListVO();
statementRow.setBankStatementId(1L);
statementRow.setTrxDate("2026-03-01 10:00:00");
statementRow.setLeAccountNo("62220001");
statementRow.setLeAccountName("李四");
statementRow.setCustomerAccountName("张三");
statementRow.setCustomerAccountNo("62220002");
statementRow.setUserMemo("转账");
statementRow.setCashType("转账汇款");
statementRow.setDisplayAmount(new BigDecimal("1000.00"));
when(overviewMapper.selectPersonAnalysisStatementRows(40L, "330000000000000001"))
.thenReturn(List.of(statementRow));
com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO hitTag =
new com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO();
hitTag.setBankStatementId(1L);
hitTag.setRuleCode("RULE_A");
hitTag.setRuleName("大额转账");
hitTag.setRiskLevel("HIGH");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(40L, List.of(1L)))
.thenReturn(List.of(hitTag));
CcdiProjectPersonAnalysisObjectRecordVO objectRow = new CcdiProjectPersonAnalysisObjectRecordVO();
objectRow.setTitle("张三");
objectRow.setSubtitle("关联人员");
objectRow.setRiskTags(List.of("频繁往来"));
objectRow.setReasonDetail("命中近30日高频往来规则存在多笔短周期回流");
objectRow.setSummary("高频往来");
CcdiProjectPersonAnalysisObjectRecordVO objectRowTwo = new CcdiProjectPersonAnalysisObjectRecordVO();
objectRowTwo.setTitle("张三");
objectRowTwo.setSubtitle("关联人员");
objectRowTwo.setRiskTags(List.of("异常关联"));
objectRowTwo.setReasonDetail("命中跨主体异常关联规则,存在关键时间点往来");
objectRowTwo.setSummary("跨主体关联");
when(overviewMapper.selectPersonAnalysisObjectRows(40L, "330000000000000001"))
.thenReturn(List.of(objectRow, objectRowTwo));
CcdiProjectPersonAnalysisDetailVO result = service.getPersonAnalysisDetail(buildPersonAnalysisDetailQuery(40L));
assertNotNull(result.getBasicInfo());
assertEquals("李四", result.getBasicInfo().getName());
assertEquals("高风险", result.getBasicInfo().getRiskLevel());
assertNotNull(result.getAbnormalDetail());
assertNotNull(result.getAbnormalDetail().getGroups());
assertEquals(2, result.getAbnormalDetail().getGroups().size());
assertEquals("BANK_STATEMENT", result.getAbnormalDetail().getGroups().get(0).getGroupType());
assertEquals("OBJECT", result.getAbnormalDetail().getGroups().get(1).getGroupType());
List<?> statementRecords = result.getAbnormalDetail().getGroups().get(0).getRecords();
assertEquals(1, ((CcdiBankStatementListVO) statementRecords.getFirst()).getHitTags().size());
List<?> objectRecords = result.getAbnormalDetail().getGroups().get(1).getRecords();
assertEquals(2, objectRecords.size());
assertEquals(
"命中近30日高频往来规则存在多笔短周期回流",
((CcdiProjectPersonAnalysisObjectRecordVO) objectRecords.getFirst()).getReasonDetail()
);
assertNotNull(((CcdiProjectPersonAnalysisObjectRecordVO) objectRecords.getFirst()).getExtraFields());
} }
@Test @Test
@@ -274,6 +496,14 @@ class CcdiProjectOverviewServiceImplTest {
); );
} }
private CcdiProjectRiskPeopleQueryDTO buildRiskPeopleQuery(Long projectId) {
CcdiProjectRiskPeopleQueryDTO queryDTO = new CcdiProjectRiskPeopleQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setPageNum(1);
queryDTO.setPageSize(5);
return queryDTO;
}
private CcdiProjectRiskModelPeopleQueryDTO buildRiskModelPeopleQuery(Long projectId) { private CcdiProjectRiskModelPeopleQueryDTO buildRiskModelPeopleQuery(Long projectId) {
CcdiProjectRiskModelPeopleQueryDTO queryDTO = new CcdiProjectRiskModelPeopleQueryDTO(); CcdiProjectRiskModelPeopleQueryDTO queryDTO = new CcdiProjectRiskModelPeopleQueryDTO();
queryDTO.setProjectId(projectId); queryDTO.setProjectId(projectId);
@@ -282,6 +512,13 @@ class CcdiProjectOverviewServiceImplTest {
return queryDTO; return queryDTO;
} }
private CcdiProjectPersonAnalysisDetailQueryDTO buildPersonAnalysisDetailQuery(Long projectId) {
CcdiProjectPersonAnalysisDetailQueryDTO queryDTO = new CcdiProjectPersonAnalysisDetailQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setStaffIdCard("330000000000000001");
return queryDTO;
}
private CcdiProjectOverviewEmployeeResult buildEmployeeResult(String riskLevelCode) { private CcdiProjectOverviewEmployeeResult buildEmployeeResult(String riskLevelCode) {
CcdiProjectOverviewEmployeeResult result = new CcdiProjectOverviewEmployeeResult(); CcdiProjectOverviewEmployeeResult result = new CcdiProjectOverviewEmployeeResult();
result.setRiskLevelCode(riskLevelCode); result.setRiskLevelCode(riskLevelCode);

View File

@@ -0,0 +1,134 @@
package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.common.exception.ServiceException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectOverviewServiceSuspiciousTransactionTest {
@InjectMocks
private CcdiProjectOverviewServiceImpl service;
@Mock
private CcdiProjectOverviewMapper overviewMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Mock
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
@Mock
private CcdiBankTagResultMapper bankTagResultMapper;
@Mock
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@Test
void shouldReturnDeduplicatedSuspiciousTransactions() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
item.setBankStatementId(101L);
item.setSuspiciousPersonName("王五");
item.setRelatedPersonName("孙七");
item.setRelatedStaffName("孙七");
item.setRelatedStaffCode("809901");
item.setRelationType("配偶");
item.setUserMemo("零钱商户消费");
item.setCashType("转账");
item.setDisplayAmount(new BigDecimal("200000.00"));
item.setHasModelRuleHit(true);
item.setHasNameListHit(true);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
page.setRecords(List.of(item));
page.setTotal(1);
when(overviewMapper.selectSuspiciousTransactionPage(any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
.thenReturn(page);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("name_list");
queryDTO.setPageNum(1);
queryDTO.setPageSize(10);
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
assertEquals(1, result.getRows().size());
assertEquals(1L, result.getTotal());
assertTrue(result.getRows().getFirst().getHasModelRuleHit());
assertTrue(result.getRows().getFirst().getHasNameListHit());
verify(overviewMapper).selectSuspiciousTransactionPage(
any(Page.class),
argThat(query -> "NAME_LIST".equals(query.getSuspiciousType()))
);
}
@Test
void shouldExportSuspiciousTransactionsWithCurrentFilter() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
item.setTrxDate("2024-01-15 10:00:00");
item.setSuspiciousPersonName("孙七");
item.setRelatedPersonName("孙七");
item.setRelatedStaffName("孙七");
item.setRelatedStaffCode("809901");
item.setRelationType("本人");
item.setUserMemo("");
item.setCashType("转账");
item.setDisplayAmount(new BigDecimal("500000.00"));
when(overviewMapper.selectSuspiciousTransactionList(any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
.thenReturn(List.of(item));
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
List<CcdiProjectSuspiciousTransactionExcel> rows = service.exportSuspiciousTransactions(queryDTO);
assertEquals(1, rows.size());
assertEquals("孙七(809901)", rows.getFirst().getRelatedStaffDisplay());
assertEquals("/转账", rows.getFirst().getSummaryAndCashType());
}
@Test
void shouldThrowWhenSuspiciousTransactionProjectDoesNotExist() {
when(projectMapper.selectById(99L)).thenReturn(null);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(99L);
assertThrows(ServiceException.class, () -> service.getSuspiciousTransactions(queryDTO));
assertThrows(ServiceException.class, () -> service.exportSuspiciousTransactions(queryDTO));
}
}

View File

@@ -0,0 +1,55 @@
package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import java.io.ByteArrayInputStream;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectRiskDetailWorkbookExporterTest {
@Test
void shouldExportWorkbookWithThreeOrderedSheets() throws Exception {
CcdiProjectRiskDetailWorkbookExporter exporter = new CcdiProjectRiskDetailWorkbookExporter();
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectSuspiciousTransactionExcel suspiciousRow = new CcdiProjectSuspiciousTransactionExcel();
suspiciousRow.setTrxDate("2026-03-20 10:00:00");
suspiciousRow.setSuspiciousPersonName("张三");
suspiciousRow.setRelatedPersonName("张三");
suspiciousRow.setRelatedStaffDisplay("张三(1001)");
suspiciousRow.setRelationType("本人");
suspiciousRow.setSummaryAndCashType("转账/转账");
suspiciousRow.setDisplayAmount(new BigDecimal("100.00"));
CcdiProjectEmployeeCreditNegativeExcel creditRow = new CcdiProjectEmployeeCreditNegativeExcel();
creditRow.setPersonName("李四");
creditRow.setPersonId("330000000000000001");
creditRow.setQueryDate("2026-03-20");
creditRow.setCivilCnt(1);
creditRow.setCivilLmt(new BigDecimal("20000.00"));
exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow));
assertTrue(response.getContentType().startsWith(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
));
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
assertEquals(3, workbook.getNumberOfSheets());
assertEquals("涉疑交易明细", workbook.getSheetAt(0).getSheetName());
assertEquals("员工负面征信信息", workbook.getSheetAt(1).getSheetName());
assertEquals("异常账户人员信息", workbook.getSheetAt(2).getSheetName());
assertEquals("账号", workbook.getSheetAt(2).getRow(0).getCell(0).getStringCellValue());
assertEquals("状态", workbook.getSheetAt(2).getRow(0).getCell(5).getStringCellValue());
assertEquals(1, workbook.getSheetAt(2).getPhysicalNumberOfRows());
}
}
}

View File

@@ -4,25 +4,38 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender; import ch.qos.logback.core.read.ListAppender;
import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
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.event.CcdiProjectHistoryImportSubmittedEvent;
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.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;
import com.ruoyi.lsfx.domain.response.GetTokenResponse; import com.ruoyi.lsfx.domain.response.GetTokenResponse;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.support.TransactionSynchronizationManager;
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.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@@ -37,6 +50,9 @@ class CcdiProjectServiceImplTest {
@Mock @Mock
private LsfxAnalysisClient lsfxAnalysisClient; private LsfxAnalysisClient lsfxAnalysisClient;
@Mock
private ApplicationEventPublisher applicationEventPublisher;
@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);
@@ -68,6 +84,105 @@ class CcdiProjectServiceImplTest {
() -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数")); () -> service.ensureProjectWritable(40L, "当前项目正在进行银行流水打标,暂不允许修改参数"));
} }
@Test
void shouldArchiveCompletedProject() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setProjectName("专案A");
project.setStatus("1");
project.setIsArchived(0);
when(projectMapper.selectById(40L)).thenReturn(project);
service.archiveProject(40L, "tester");
assertEquals("2", project.getStatus());
assertEquals(1, project.getIsArchived());
verify(projectMapper).updateById(project);
}
@Test
void shouldRejectArchivingProjectWhenStatusIsNotCompleted() {
CcdiProject project = new CcdiProject();
project.setProjectId(41L);
project.setStatus("0");
when(projectMapper.selectById(41L)).thenReturn(project);
assertThrows(ServiceException.class, () -> service.archiveProject(41L, "tester"));
}
@Test
void shouldRejectWritingWhenProjectIsArchived() {
CcdiProject archived = new CcdiProject();
archived.setProjectId(42L);
archived.setStatus("2");
when(projectMapper.selectById(42L)).thenReturn(archived);
assertThrows(ServiceException.class,
() -> service.ensureProjectNotArchived(42L, "已归档项目暂不允许修改参数"));
}
@Test
void shouldOnlyReturnCompletedAndArchivedHistoryProjects() {
CcdiProjectQueryDTO queryDTO = new CcdiProjectQueryDTO();
queryDTO.setProjectName("历史");
CcdiProjectHistoryListItemVO completed = new CcdiProjectHistoryListItemVO();
completed.setProjectId(1L);
completed.setStatus("1");
CcdiProjectHistoryListItemVO archived = new CcdiProjectHistoryListItemVO();
archived.setProjectId(2L);
archived.setStatus("2");
when(projectMapper.selectHistoryProjects(queryDTO)).thenReturn(List.of(completed, archived));
List<CcdiProjectHistoryListItemVO> result = service.listHistoryProjects(queryDTO);
assertEquals(2, result.size());
assertEquals(List.of(completed, archived), result);
verify(projectMapper).selectHistoryProjects(queryDTO);
}
@Test
void shouldCreateProjectThenPublishHistoryImportEventAfterCommit() {
CcdiProjectImportHistoryDTO dto = new CcdiProjectImportHistoryDTO();
dto.setProjectName("新项目");
dto.setDescription("从历史导入");
dto.setSourceProjectIds(List.of(11L, 12L));
dto.setStartDate("2026-01-01");
dto.setEndDate("2026-01-31");
when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(3001));
doAnswer(invocation -> {
CcdiProject project = invocation.getArgument(0);
project.setProjectId(90L);
return 1;
}).when(projectMapper).insert(any(CcdiProject.class));
TransactionSynchronizationManager.initSynchronization();
try {
CcdiProjectVO project = service.importFromHistory(dto, "tester");
assertNotNull(project);
assertEquals(90L, project.getProjectId());
assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size());
verify(applicationEventPublisher, never()).publishEvent(any());
TransactionSynchronizationManager.getSynchronizations().forEach(sync -> sync.afterCommit());
ArgumentCaptor<Object> eventCaptor = ArgumentCaptor.forClass(Object.class);
verify(applicationEventPublisher).publishEvent(eventCaptor.capture());
CcdiProjectHistoryImportSubmittedEvent event =
(CcdiProjectHistoryImportSubmittedEvent) eventCaptor.getValue();
assertEquals(90L, event.getTargetProjectId());
assertEquals(3001, event.getTargetLsfxProjectId());
assertEquals("tester", event.getOperator());
assertEquals(dto, event.getDto());
} finally {
TransactionSynchronizationManager.clearSynchronization();
}
}
@Test @Test
void shouldLogProjectInitialStatusWhenProjectIsCreated() { void shouldLogProjectInitialStatusWhenProjectIsCreated() {
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO(); CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();

View File

@@ -0,0 +1,218 @@
package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper;
import com.ruoyi.common.exception.ServiceException;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectSpecialCheckExtendedQueryServiceImplTest {
@InjectMocks
private CcdiProjectSpecialCheckServiceImpl service;
@Mock
private CcdiProjectSpecialCheckMapper specialCheckMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Test
void shouldThrowProjectNotFoundForExtendedQueries() {
when(projectMapper.selectById(99L)).thenReturn(null);
ServiceException purchaseListException = assertThrows(
ServiceException.class,
() -> service.getExtendedPurchaseList(buildPurchaseQuery(99L, 1, 10))
);
ServiceException purchaseDetailException = assertThrows(
ServiceException.class,
() -> service.getExtendedPurchaseDetail(buildPurchaseDetailQuery(99L, "CG-001"))
);
ServiceException recruitmentListException = assertThrows(
ServiceException.class,
() -> service.getExtendedRecruitmentList(buildRecruitmentQuery(99L, "张三", 1, 10))
);
ServiceException transferDetailException = assertThrows(
ServiceException.class,
() -> service.getExtendedTransferDetail(buildTransferDetailQuery(99L, 1L))
);
assertEquals("项目不存在", purchaseListException.getMessage());
assertEquals("项目不存在", purchaseDetailException.getMessage());
assertEquals("项目不存在", recruitmentListException.getMessage());
assertEquals("项目不存在", transferDetailException.getMessage());
}
@Test
void shouldBuildPurchaseListRowsAndTotalFromPage() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
Page<CcdiProjectExtendedPurchaseListItemVO> page = new Page<>(2, 5, 8);
CcdiProjectExtendedPurchaseListItemVO item = new CcdiProjectExtendedPurchaseListItemVO();
item.setPurchaseId("CG-001");
page.setRecords(List.of(item));
when(specialCheckMapper.selectExtendedPurchasePage(any(), any())).thenReturn(page);
CcdiProjectExtendedPurchaseListVO result = service.getExtendedPurchaseList(buildPurchaseQuery(40L, 2, 5));
assertNotNull(result.getRows());
assertEquals(1, result.getRows().size());
assertEquals(8L, result.getTotal());
ArgumentCaptor<Page<CcdiProjectExtendedPurchaseListItemVO>> pageCaptor = ArgumentCaptor.forClass(Page.class);
verify(specialCheckMapper).selectExtendedPurchasePage(pageCaptor.capture(), any());
assertEquals(2L, pageCaptor.getValue().getCurrent());
assertEquals(5L, pageCaptor.getValue().getSize());
}
@Test
void shouldReturnEmptyRecruitmentAndTransferRowsInsteadOfNull() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
when(specialCheckMapper.selectExtendedRecruitmentPage(any(), any())).thenReturn(new Page<>(1, 10, 0));
when(specialCheckMapper.selectExtendedTransferPage(any(), any())).thenReturn(new Page<>(1, 10, 0));
CcdiProjectExtendedRecruitmentListVO recruitmentResult =
service.getExtendedRecruitmentList(buildRecruitmentQuery(40L, "李四", 1, 10));
CcdiProjectExtendedTransferListVO transferResult = service.getExtendedTransferList(buildTransferQuery(40L, 1, 10));
assertNotNull(recruitmentResult.getRows());
assertEquals(0, recruitmentResult.getRows().size());
assertEquals(0L, recruitmentResult.getTotal());
assertNotNull(transferResult.getRows());
assertEquals(0, transferResult.getRows().size());
assertEquals(0L, transferResult.getTotal());
}
@Test
void shouldPassInterviewerNameAndPaginationToRecruitmentMapper() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
Page<CcdiProjectExtendedRecruitmentListItemVO> page = new Page<>(3, 4, 2);
page.setRecords(List.of(new CcdiProjectExtendedRecruitmentListItemVO()));
when(specialCheckMapper.selectExtendedRecruitmentPage(any(), any())).thenReturn(page);
CcdiProjectExtendedRecruitmentQueryDTO queryDTO = buildRecruitmentQuery(40L, "王五", 3, 4);
CcdiProjectExtendedRecruitmentListVO result = service.getExtendedRecruitmentList(queryDTO);
assertEquals(2L, result.getTotal());
ArgumentCaptor<Page<CcdiProjectExtendedRecruitmentListItemVO>> pageCaptor = ArgumentCaptor.forClass(Page.class);
ArgumentCaptor<CcdiProjectExtendedRecruitmentQueryDTO> queryCaptor = ArgumentCaptor.forClass(
CcdiProjectExtendedRecruitmentQueryDTO.class
);
verify(specialCheckMapper).selectExtendedRecruitmentPage(pageCaptor.capture(), queryCaptor.capture());
assertEquals(3L, pageCaptor.getValue().getCurrent());
assertEquals(4L, pageCaptor.getValue().getSize());
assertEquals("王五", queryCaptor.getValue().getInterviewerName());
}
@Test
void shouldThrowClearErrorWhenExtendedDetailDoesNotBelongToProjectScope() {
when(projectMapper.selectById(40L)).thenReturn(buildProject(40L));
when(specialCheckMapper.selectExtendedPurchaseDetail(40L, "CG-001")).thenReturn(null);
when(specialCheckMapper.selectExtendedRecruitmentDetail(40L, "ZP-001")).thenReturn(null);
when(specialCheckMapper.selectExtendedTransferDetail(40L, 1L)).thenReturn(null);
ServiceException purchaseException = assertThrows(
ServiceException.class,
() -> service.getExtendedPurchaseDetail(buildPurchaseDetailQuery(40L, "CG-001"))
);
ServiceException recruitmentException = assertThrows(
ServiceException.class,
() -> service.getExtendedRecruitmentDetail(buildRecruitmentDetailQuery(40L, "ZP-001"))
);
ServiceException transferException = assertThrows(
ServiceException.class,
() -> service.getExtendedTransferDetail(buildTransferDetailQuery(40L, 1L))
);
assertEquals("当前记录不属于该项目专项核查范围", purchaseException.getMessage());
assertEquals("当前记录不属于该项目专项核查范围", recruitmentException.getMessage());
assertEquals("当前记录不属于该项目专项核查范围", transferException.getMessage());
}
private CcdiProject buildProject(Long projectId) {
CcdiProject project = new CcdiProject();
project.setProjectId(projectId);
return project;
}
private CcdiProjectExtendedPurchaseQueryDTO buildPurchaseQuery(Long projectId, int pageNum, int pageSize) {
CcdiProjectExtendedPurchaseQueryDTO queryDTO = new CcdiProjectExtendedPurchaseQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
return queryDTO;
}
private CcdiProjectExtendedPurchaseDetailQueryDTO buildPurchaseDetailQuery(Long projectId, String purchaseId) {
CcdiProjectExtendedPurchaseDetailQueryDTO queryDTO = new CcdiProjectExtendedPurchaseDetailQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setPurchaseId(purchaseId);
return queryDTO;
}
private CcdiProjectExtendedRecruitmentQueryDTO buildRecruitmentQuery(
Long projectId,
String interviewerName,
int pageNum,
int pageSize
) {
CcdiProjectExtendedRecruitmentQueryDTO queryDTO = new CcdiProjectExtendedRecruitmentQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setInterviewerName(interviewerName);
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
return queryDTO;
}
private CcdiProjectExtendedRecruitmentDetailQueryDTO buildRecruitmentDetailQuery(Long projectId, String recruitId) {
CcdiProjectExtendedRecruitmentDetailQueryDTO queryDTO = new CcdiProjectExtendedRecruitmentDetailQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setRecruitId(recruitId);
return queryDTO;
}
private CcdiProjectExtendedTransferQueryDTO buildTransferQuery(Long projectId, int pageNum, int pageSize) {
CcdiProjectExtendedTransferQueryDTO queryDTO = new CcdiProjectExtendedTransferQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
return queryDTO;
}
private CcdiProjectExtendedTransferDetailQueryDTO buildTransferDetailQuery(Long projectId, Long id) {
CcdiProjectExtendedTransferDetailQueryDTO queryDTO = new CcdiProjectExtendedTransferDetailQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setId(id);
return queryDTO;
}
}

View File

@@ -0,0 +1,465 @@
# 专项核查拓展查询卡片设计文档
**模块**: 项目详情 - 专项核查
**日期**: 2026-03-24
**状态**: 已确认
## 一、背景
当前项目详情页的专项核查页签已包含两块内容:
1. 员工家庭资产负债专项核查
2. 图谱外链展示占位卡片
其中图谱外链卡片当前仅作为后续接入外链图谱页面的预留入口,尚未承载真实业务查询能力。
本轮需求是在图谱外链卡片下方新增一张“拓展查询”卡片,用于查询项目范围内员工的采购、招聘、调动记录,并支持按主题 Tab 切换、按主题对应姓名字段和时间范围检索,以及按记录查看完整详情。
## 二、目标
本次设计目标如下:
1. 在专项核查页签中新增“拓展查询”卡片,位置固定在图谱外链卡片下方。
2. 通过 Tab 切换展示 3 类拓展查询主题:
- 采购记录
- 招聘记录
- 调动记录
3. 列表数据只查询当前专项核查同范围员工,不扩展到项目全部目标员工。
4. 查询栏按主题独立配置,不共享无关筛选项。
5. 列表展示必要字段,操作列提供“查看详情”按钮。
6. 点击“查看详情”后,通过弹窗展示该条记录全部字段。
## 三、范围
### 3.1 本次范围
- 新增专项核查页的拓展查询卡片
- 新增采购、招聘、调动 3 个主题的 Tab 切换
- 新增项目范围拓展查询列表接口
- 新增项目范围拓展查询详情接口
- 新增对应前端查询、分页、详情弹窗交互
- 补充前后端测试与验证记录
### 3.2 不在本次范围
- 不接入真实图谱外链地址
- 不改动现有员工家庭资产负债专项核查逻辑
- 不新开独立路由页面
- 不扩展到项目全部目标员工
- 不为招聘记录新增时间筛选字段
- 不新增导出、编辑、删除等操作
- 不引入缓存、异步预计算或额外结果表
## 四、已确认业务口径
本次设计确认以下口径,后续实施必须严格遵守:
1. “项目范围内员工”定义为当前专项核查同范围员工,即现有专项核查链路中已归并出来的员工集合。
2. 不按项目全部目标员工查询。
3. 查询卡片位置固定在“图谱外链展示”卡片下方。
4. 采用横向 Tab 切换主题,不使用竖向主题导航。
5. 每个主题使用独立查询栏,不共享查询表单。
6. 采购记录按“申请人姓名 + 申请日期范围”筛选。
7. 招聘记录按“面试官姓名”筛选,本次不增加时间筛选。
8. 调动记录按“员工姓名 + 调动日期范围”筛选。
9. 列表只展示必要字段,详情弹窗展示该条记录全部字段。
## 五、方案对比
### 5.1 方案 A图谱卡片下方新增独立拓展查询卡片
做法:
- 保留现有图谱卡片不变
- 在图谱卡片下方新增一张“拓展查询”卡片
- 卡片内部顶部使用横向 Tab 切换主题
优点:
- 完全符合“在图谱外链卡片下面添加拓展查询功能卡片”的原始需求
- 不影响现有图谱占位卡片结构
- 页面层级清晰,用户认知成本低
- 前端改造路径最短
缺点:
- 页面纵向高度会增加
### 5.2 方案 B图谱卡片与拓展查询合并为一个大卡片
做法:
- 图谱占位与拓展查询共用同一张卡片
问题:
- 与“在图谱外链卡片下面添加卡片”的需求不一致
- 图谱占位与实际查询职责混在一起,层次不清
### 5.3 方案 C独立拓展查询卡片但使用左侧竖向主题导航
做法:
- 与方案 A 一样新增独立卡片
- 但主题切换改为左侧竖向导航
问题:
- 用户已明确要求通过 Tab 页切换主题
- 与当前页面已有横向区块风格不一致
### 5.4 推荐方案
采用方案 A在图谱外链卡片下方新增独立拓展查询卡片卡片内部使用横向 Tab 切换采购、招聘、调动 3 个主题。
## 六、页面结构设计
专项核查页整体结构调整后如下:
1. 员工家庭资产负债专项核查卡片
2. 图谱外链展示卡片
3. 拓展查询卡片
其中前两张卡片不修改现有交互,仅在其下方追加第三张卡片。
拓展查询卡片的渲染条件独立于家庭资产负债列表数据:
- 只要当前 `projectId` 有效,就应展示拓展查询卡片
- 即使家庭资产负债列表为空,拓展查询卡片仍需可见并可使用
拓展查询卡片结构如下:
### 6.1 卡片头部
- 标题:拓展查询
- 副标题:查询专项核查同范围员工的采购、招聘、调动记录
### 6.2 Tab 区
- Tab 1采购记录
- Tab 2招聘记录
- Tab 3调动记录
### 6.3 主题查询区
每个 Tab 使用独立查询栏:
- 采购记录:
- 申请人姓名
- 申请日期范围
- 搜索
- 重置
- 招聘记录:
- 面试官姓名
- 搜索
- 重置
- 调动记录:
- 员工姓名
- 调动日期范围
- 搜索
- 重置
### 6.4 列表区
Tab 下方展示当前主题列表与分页。
切换 Tab 时:
- 不共用查询表单
- 不互相覆盖列表数据
- 各主题保留自己的查询条件与分页状态
- 首次进入某个 Tab 时再触发该 Tab 的首查
### 6.5 详情弹窗
操作列统一提供“查看详情”按钮。
点击后:
- 采购记录弹出采购详情弹窗
- 招聘记录弹出招聘详情弹窗
- 调动记录弹出调动详情弹窗
详情弹窗展示该条记录全部字段,不裁剪字段内容。
## 七、列表字段设计
### 7.1 采购记录列表字段
- 采购事项 ID
- 项目名称
- 标的物名称
- 申请人姓名
- 申请日期
- 操作
### 7.2 招聘记录列表字段
- 招聘项目编号
- 招聘项目名称
- 职位名称
- 面试官姓名摘要
- 录用情况
- 操作
其中面试官姓名摘要定义为:
- 优先展示 `面试官1姓名 / 面试官2姓名`
- 如某一个为空,则只展示存在的姓名
### 7.3 调动记录列表字段
- 员工姓名
- 调动类型
- 调动前部门
- 调动后部门
- 调动日期
- 操作
## 八、后端设计
### 8.1 接口归属
本次不直接复用信息采集模块现有 `/list` 接口,而是在专项核查域新增项目范围拓展查询接口,统一挂在:
- `CcdiProjectSpecialCheckController`
原因:
1. 现有信息采集模块接口仅面向各自主表查询,不包含“专项核查同范围员工”的限制。
2. 如果前端调用现有接口后自行过滤,会导致项目范围口径不稳定。
3. 在专项核查域统一收口,后续维护更清晰。
### 8.2 接口设计
建议新增 6 个接口:
- `GET /ccdi/project/special-check/extended-query/purchase/list`
- `GET /ccdi/project/special-check/extended-query/purchase/detail`
- `GET /ccdi/project/special-check/extended-query/recruitment/list`
- `GET /ccdi/project/special-check/extended-query/recruitment/detail`
- `GET /ccdi/project/special-check/extended-query/transfer/list`
- `GET /ccdi/project/special-check/extended-query/transfer/detail`
### 8.3 项目范围员工口径复用
项目范围员工统一复用当前专项核查 mapper 中的员工范围定义,即:
- `CcdiProjectSpecialCheckMapper.xml` 中的 `projectEmployeeScopeSql`
该范围已经与当前专项核查页保持一致,本轮不重新定义口径,不新增第二套项目员工范围逻辑。
### 8.4 列表查询设计
#### 采购记录
数据来源:
- `ccdi_purchase_transaction`
范围限制:
- 记录必须命中专项核查同范围员工
筛选条件:
- `projectId`
- `applicantName`
- `applyDateStart`
- `applyDateEnd`
员工姓名口径:
- 按采购记录的 `applicant_name` 检索
时间口径:
- 按采购记录的 `apply_date` 检索
#### 招聘记录
数据来源:
- `ccdi_staff_recruitment`
范围限制:
- 记录必须命中专项核查同范围员工
筛选条件:
- `projectId`
- `interviewerName`
员工姓名口径:
- 按招聘记录的 `interviewer_name_1``interviewer_name_2` 检索
- 同一条招聘记录若同时命中 `interviewer_name_1``interviewer_name_2`,列表结果必须按招聘记录主键去重
时间口径:
- 本次不提供招聘时间筛选
#### 调动记录
数据来源:
- `ccdi_staff_transfer`
- `ccdi_base_staff`
范围限制:
- 记录必须命中专项核查同范围员工
筛选条件:
- `projectId`
- `staffName`
- `transferDateStart`
- `transferDateEnd`
员工姓名口径:
- 按调动记录对应员工的 `staff_name` 检索
时间口径:
-`transfer_date` 检索
### 8.5 详情查询设计
详情接口按各自主键查询,并返回该条记录全部字段:
- 采购详情:按 `purchaseId`
- 招聘详情:按 `recruitId`
- 调动详情:按 `id`
详情查询仍需校验该记录属于当前专项核查同范围员工;不属于时返回业务错误,避免越权或口径漂移。
### 8.6 DTO / VO 设计
建议在 `ccdi-project` 模块下新增独立 DTO
- 采购拓展查询 DTO
- 招聘拓展查询 DTO
- 调动拓展查询 DTO
其中招聘主题查询字段命名统一使用 `interviewerName`,不再使用泛化的 `staffName`
采购主题查询字段命名统一使用 `applicantName`,不再使用泛化的 `staffName`
列表返回对象按主题独立定义轻量 VO仅保留列表必要字段。
详情返回可优先复用现有 VO 字段结构:
- `CcdiPurchaseTransactionVO`
- `CcdiStaffRecruitmentVO`
- `CcdiStaffTransferVO`
如直接复用存在耦合问题,再在 `ccdi-project` 下补充专项核查详情 VO但本轮优先选择最短路径实现。
## 九、前端设计
### 9.1 页面挂载位置
继续以:
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
作为专项核查页签容器,不新增路由。
### 9.2 组件拆分
建议新增一张独立的拓展查询区块组件,例如:
- `ExtendedQuerySection.vue`
职责如下:
- 管理当前激活 Tab
- 管理各 Tab 的查询参数
- 驱动列表加载与分页
- 打开并关闭详情弹窗
详情展示建议按主题拆分为小组件或局部模板:
- 采购详情复用现有采购页的分组展示方式
- 招聘详情复用现有招聘页的分组展示方式
- 调动详情补齐与前两者一致的详情展示结构
### 9.3 状态管理
每个 Tab 独立维护以下状态:
- `query`
- `list`
- `loading`
- `pageNum`
- `pageSize`
- `total`
- `detailOpen`
- `detailLoading`
- `detailData`
原则如下:
1. 不共享查询表单
2. 不共享列表数据
3. 切换 Tab 时保留各自上次查询状态
4. 首次进入 Tab 时再请求列表
### 9.4 空态与异常
- 列表无数据时展示当前主题无数据空态
- 列表查询失败时提示错误信息,并清空当前主题列表
- 详情查询失败时只提示错误,不污染列表状态
- `projectId` 为空时沿用当前专项核查页已有空态逻辑
## 十、测试与验证设计
### 10.1 前端验证
补充以下验证:
1. 专项核查页新增拓展查询卡片结构断言
2. 卡片位于图谱外链卡片下方的结构断言
3. 3 个主题 Tab 的存在性断言
4. 不同 Tab 显示不同查询栏字段的断言
5. 操作列“查看详情”按钮存在性断言
6. 调动主题详情弹窗存在性断言
7. 前端构建验证
### 10.2 后端验证
补充以下验证:
1. 项目范围员工口径是否与 `projectEmployeeScopeSql` 一致
2. 采购列表按申请人姓名和申请日期范围过滤是否正确
3. 招聘列表按面试官姓名过滤是否正确
4. 调动列表按员工姓名和调动日期范围过滤是否正确
5. 详情接口是否能校验记录属于当前项目范围员工
6. 记录不属于当前项目范围员工时是否返回预期错误
### 10.3 联调验证
联调时重点验证:
1. Tab 切换后各自查询条件不会互相污染
2. 招聘主题不展示时间范围
3. 采购与调动主题展示时间范围
4. 详情弹窗字段完整性与原业务页一致
## 十一、实施约束
本轮实施需遵守以下要求:
1. 不新增兼容性分支或降级方案。
2. 不在需求外扩展更多主题。
3. 不新增导出、编辑、删除、批量操作。
4. 不改变现有图谱占位卡片行为。
5. 根据本设计文档产出两份实施计划:
- 后端实施计划放 `docs/plans/backend/`
- 前端实施计划放 `docs/plans/frontend/`
## 十二、结论
本次采用“图谱外链卡片下方新增独立拓展查询卡片”的方案,在专项核查页签内新增采购、招聘、调动 3 个主题查询能力。所有查询严格限制为当前专项核查同范围员工,采购与调动支持时间范围筛选,招聘仅支持面试官姓名筛选。前端不新增路由,后端在专项核查域新增统一接口,保持实现路径最短、页面职责清晰、业务口径稳定。

View File

@@ -0,0 +1,364 @@
# lsfx-mock all 模式 SQL 对齐强命中设计
## 1. 背景
当前 `lsfx-mock-server``--rule-hit-mode all` 已支持为多类规则生成流水样本,但生成策略仍带有早期“语义化样本”特征,未完全对齐主工程真实 SQL 的命中口径。
在最新排查中,以下 5 条已落地真实 SQL 的规则仍无法稳定命中:
- `SPECIAL_AMOUNT_TRANSACTION`
- `MONTHLY_FIXED_INCOME`
- `SUSPICIOUS_INCOME_KEYWORD`
- `FIXED_COUNTERPARTY_TRANSFER`
- `LOW_INCOME_RELATIVE_LARGE_TRANSACTION`
问题根因不是主工程打标链路异常,而是 Mock 样本和关联表数据与后端 XML SQL 条件不一致:
- 特殊金额样本金额错误
- 固定月收入样本月数不足
- 收入关键词样本摘要不在 SQL 词表内
- 固定对手转入样本主体落在家属证件号
- 低收入亲属规则缺少满足阈值的关系表收入基线
本次设计目标是在不修改主工程打标逻辑的前提下,只通过调整 `lsfx-mock-server``all` 模式生成策略,让以上 5 条真实 SQL 规则稳定命中,并尽量抑制额外噪声命中。
## 2. 目标
- 仅针对 `--rule-hit-mode all` 生效,`subset` 模式保持现状不变。
- 仅覆盖后端 XML 中已落地真实 SQL 的规则,不处理占位 SQL。
- 让上述 5 条规则在真实链路下稳定命中:
- Mock 返回流水
- 主工程入库 `ccdi_bank_statement`
- 触发重打标
-`ccdi_bank_statement_tag_result` 中看到命中结果
- 允许通过补写最小关联表基线满足 SQL 前置条件。
- 尽量压住因普通噪声流水导致的误命中或聚合口径污染。
## 3. 非目标
- 不修改主工程 `ccdi-project` 中的 XML SQL、Service 或打标逻辑。
- 不处理占位 SQL 规则:
- `ABNORMAL_CUSTOMER_TRANSACTION`
- `INTEREST_PAYMENT_BY_OTHERS`
- 以及其他仍为 `where 1 = 0` 的规则
- 不把 `subset` 模式升级成同样的强命中模型。
- 不引入 DSL、规则解释器或额外配置中心。
- 不做与本次 5 条规则无关的通用重构。
## 4. 范围
### 4.1 目标规则
- `SUSPICIOUS_RELATION / SPECIAL_AMOUNT_TRANSACTION`
- `SUSPICIOUS_PART_TIME / MONTHLY_FIXED_INCOME`
- `SUSPICIOUS_PART_TIME / SUSPICIOUS_INCOME_KEYWORD`
- `SUSPICIOUS_PART_TIME / FIXED_COUNTERPARTY_TRANSFER`
- `ABNORMAL_TRANSACTION / LOW_INCOME_RELATIVE_LARGE_TRANSACTION`
### 4.2 涉及模块
- `lsfx-mock-server/services/file_service.py`
- `lsfx-mock-server/services/statement_service.py`
- `lsfx-mock-server/services/statement_rule_samples.py`
- `lsfx-mock-server/services/phase2_baseline_service.py`
- `lsfx-mock-server/tests/test_file_service.py`
- `lsfx-mock-server/tests/test_statement_service.py`
- 视需要补充或调整集成测试
## 5. 方案对比
### 5.1 方案 ASQL 镜像驱动强命中
按后端 XML SQL 条件重写 `all` 模式样本和关联表基线。每条目标规则拥有专属、确定性的输入组,并对噪声数据施加约束。
优点:
- 与真实 SQL 口径一致
- 命中稳定,可重复验证
- 便于定位问题,后续逐条扩展也清晰
缺点:
- 需要逐条核对 SQL 条件并同步改写样本函数
### 5.2 方案 B最小命中修补
仅把当前明显不命中的样本做最小修修补补,不系统控制噪声和规则串扰。
优点:
- 改动量较小
缺点:
- 容易出现“目标规则命中了,但多出一批误命中”
- 后续 SQL 小改动后容易再次失效
### 5.3 方案 C配置化规则生成器
抽象规则配置层,由统一生成器按配置组装样本。
优点:
- 长期扩展性好
缺点:
- 明显超出本次最短路径诉求
- 需要引入新的抽象和维护成本
## 6. 推荐方案
采用方案 A。
原因:
- 这是当前最短且逻辑闭环的实现路径。
- 目标是“真实 SQL 下稳定命中”,因此 Mock 端必须直接对齐 XML 条件,而不是继续依靠语义相近的样本。
- 只在 `all` 模式启用该策略,可以在不破坏现有 `subset` 习惯的前提下完成强命中能力。
## 7. 设计原则
- `all` 模式中的目标规则样本必须是 deterministic 的,不依赖随机碰运气。
- 每条目标规则都要有独立的命中样本簇,尽量避免多条规则共享同一组核心流水。
- 规则所需的主体类型必须与 SQL 首层 join 一致:
- 仅员工本人:样本 `cret_no` 必须落在 `ccdi_base_staff.id_card`
- 依赖家属:样本 `cret_no` 必须落在 `ccdi_staff_fmy_relation.relation_cert_no`
- 对存在聚合或收口条件的规则,必须同时控制背景噪声,避免误触发额外对手方、额外月份或额外支出。
- 关联表基线只写“命中真实 SQL 所需的最小数据”,并且应可重复写入、可覆盖旧状态。
## 8. 模块设计
### 8.1 `file_service.py`
职责保持为“生成命中计划并记录文件级上下文”,但 `all` 模式逻辑调整为:
- 继续保留现有大额交易、第一期、第二期规则计划字段,避免影响调用方结构。
- 对本次 5 条目标规则,在 `all` 模式下强制加入计划。
- 占位 SQL 规则不进入 `all` 模式强命中计划。
- 在记录中保留足够的身份上下文,供基线服务和样本服务共享使用。
`subset` 模式不改。
### 8.2 `statement_rule_samples.py`
保留“每条规则一个构造函数”的现有结构,不引入新 DSL。
本次只重写以下函数:
- `build_special_amount_transaction_samples`
- `build_monthly_fixed_income_samples`
- `build_suspicious_income_keyword_samples`
- `build_fixed_counterparty_transfer_samples`
- `build_low_income_relative_large_transaction_samples`
同时对 `all` 模式下的噪声样本约束做最小增强,避免干扰:
- 不制造过多重复 `CUSTOMER_ACCOUNT_NAME`
- 不制造额外稳定季度转入
- 不制造会破坏“工资后无支出”或“固定月收入稳定性”的额外流水
### 8.3 `phase2_baseline_service.py`
继续作为关联表最小基线服务使用,但扩展为支持本次规则所需的关系表精确写入。
职责:
- 针对 `LOW_INCOME_RELATIVE_LARGE_TRANSACTION` 写入低收入家属基线
- 针对 `SPECIAL_AMOUNT_TRANSACTION` 保证目标对手不被识别成配偶/子女
- 只覆盖规则命中的必要字段,不额外灌入无关主数据
幂等策略:
- 对既有关系数据,按目标人员主键做“精确覆盖”而不是追加污染
- 保持同一员工域下重复执行结果一致
## 9. 规则级生成策略
### 9.1 `SPECIAL_AMOUNT_TRANSACTION`
对应 SQL 特征:
- 员工本人 `inner join ccdi_base_staff`
- 对手不是配偶/子女
- 金额必须为 `520``1314`
生成策略:
- 样本主体使用员工本人证件号
- 生成 1 至 2 笔支出或收入流水,金额固定取 `520``1314`
- 对手方名称使用专属固定名称
- 基线服务不为该对手建立“配偶/子女”关系
约束:
- 不再使用 `88888.88`
- 不复用现有家庭关系中的配偶、子女姓名,避免误入排除条件
### 9.2 `MONTHLY_FIXED_INCOME`
对应 SQL 特征:
- 员工本人 `inner join ccdi_base_staff`
- 近 12 个月至少 6 个月月收入大于阈值
- 排除工资代发主体和工资摘要
- 月度波动率不高于 `0.3`
生成策略:
- 样本主体切到员工本人
- 构造连续 6 个月固定转入,每月 1 笔
- 对手方固定
- 月金额保持完全一致或极小波动
- 对手方名称不是本行工资代发主体
- 摘要不包含工资相关关键词
约束:
- all 模式噪声流水不能再向该员工叠加大额杂散流入,避免抬高波动率
### 9.3 `SUSPICIOUS_INCOME_KEYWORD`
对应 SQL 特征:
- 员工本人 `inner join ccdi_base_staff`
- 收入流水
- 摘要或交易类型命中收入关键词词表
生成策略:
- 样本主体使用员工本人
- 生成 1 笔收入流水
- 摘要直接使用 SQL 关键词集合中的明确命中词,例如“劳务费发放”或“奖金发放”
- 交易类型可同步设置为“劳务费”或“代发收入”一类,形成双重保险
约束:
- 不再使用“咨询返现收入”“合作收入”这类 SQL 不识别的语义词
### 9.4 `FIXED_COUNTERPARTY_TRANSFER`
对应 SQL 特征:
- 员工本人 `inner join ccdi_base_staff`
- 近 12 个月内,固定对手方至少 2 个季度累计转入位于区间 `[3000,15000]`
- 满足条件的固定对手数量 `< 3`
生成策略:
- 样本主体改为员工本人
- 固定 1 个专属对手方
- 至少覆盖 2 个季度,建议 2 到 3 个季度
- 每季度累计转入控制在区间中部,避免靠边界
- 每季度仅保留 1 笔或少量固定笔数,便于验证
噪声控制:
- all 模式噪声流水中,员工本人不得再出现过多重复对手方季度转入
- 避免出现第二、第三个误满足区间条件的稳定对手
### 9.5 `LOW_INCOME_RELATIVE_LARGE_TRANSACTION`
对应 SQL 特征:
- 家属证件号与 `ccdi_staff_fmy_relation.relation_cert_no` 关联
- 家属年收入为空、为 0或折月小于 `3000`
- 家属累计交易金额大于 `100000`
生成策略:
- 使用目标员工的家属证件号作为流水主体
- 生成 2 笔以上累计超过 `100000` 的交易
- 基线服务把该家属 `annual_income` 精确设置为:
- `0`
- `null`
- 或低于 `36000`
约束:
- 不依赖项目库现存家属收入
- 每次 all 模式生成时都要确保目标家属收入状态被重置到可命中口径
## 10. 噪声控制设计
当前 `StatementService` 中的随机噪声会对聚合类规则造成干扰。本次只做最小必要控制:
- `all` 模式下继续保留背景噪声,但员工本人使用更收敛的对手方集合
- 对目标员工:
- 避免随机制造跨季度重复转入
- 避免随机制造大量同月对公收入
- 避免随机制造会抬高 `MONTHLY_FIXED_INCOME` 波动率的异常大额流入
- 对家属样本:
- 避免随机覆盖低收入亲属规则的专属家属域
实现上优先采用“all 模式下缩窄噪声候选集”而不是彻底禁掉噪声。
## 11. 数据流
1. `FileService``all` 模式下构建强命中规则计划。
2. `phase2_baseline_service` 根据计划写入最小关联表基线。
3. `StatementService` 根据规则计划生成专属流水样本,并补受控噪声。
4. 主工程按原流程拉取、入库并执行打标。
5. 目标规则在结果表中稳定出现命中记录。
## 12. 测试与验证
### 12.1 Mock 单元测试
至少补以下断言:
- `SPECIAL_AMOUNT_TRANSACTION` 样本金额为 `520/1314`
- `MONTHLY_FIXED_INCOME` 样本月份数不少于 `6`
- `SUSPICIOUS_INCOME_KEYWORD` 摘要或交易类型命中 SQL 关键词
- `FIXED_COUNTERPARTY_TRANSFER` 样本主体为员工本人而非家属
- `LOW_INCOME_RELATIVE_LARGE_TRANSACTION` 所需家属基线收入满足低收入条件
### 12.2 联调验证
沿用真实链路验证:
1. 启动 Mock`main.py --rule-hit-mode all`
2. 拉取流水并导入项目
3. 触发重打标
4. 查询 `ccdi_bank_statement_tag_result`
验收通过标准:
- 上述 5 条规则全部存在命中记录
- 命中主体与预期主体一致
- 没有明显失控的额外噪声命中
## 13. 风险与控制
风险:
- 规则间共享主体导致聚合口径互相污染
- 噪声流水再次引入额外固定对手或异常月收入
- 关系表覆盖不当污染其他联调项目
控制:
- 每条目标规则尽量使用独立样本簇
- all 模式下对目标员工域启用受控噪声
- 基线服务只覆盖当前命中域,不做全表灌数
## 14. 实施建议
按以下顺序执行最稳妥:
1. 先改 5 条样本函数
2. 再补低收入家属基线写入
3. 再收紧 all 模式噪声
4. 最后补单元测试和联调验证
这样可以分层确认:
- 样本本身对不对
- 主数据前置条件够不够
- 噪声是否把聚合口径冲坏
## 15. 结论
本次采用“只改 `all` 模式、只改真实 SQL 对应规则、按 XML 口径重写样本与最小基线”的方案,是满足当前需求的最短路径。
设计完成后,后续实施只需围绕 5 条目标规则逐条落地,不需要修改主工程,也不需要引入额外抽象层。

View File

@@ -0,0 +1,453 @@
# 结果总览项目分析弹窗设计文档
**日期**: 2026-03-25
**模块**: 初核项目详情 - 结果总览
**作者**: Codex
**状态**: 已批准
## 1. 概述
本设计用于补齐 `结果总览` 页面中两处人员列表的“查看项目”能力:
- `风险人员总览`
- `命中模型涉及人员列表`
点击后,统一打开同一个“项目分析”弹窗,在弹窗内展示人员分析工作台。视觉方向以用户提供的截图为基准,采用“左侧人物档案 + 右侧分析页签”的工作台布局;同时对左侧信息栏做适度收敛,避免在弹窗内重复完整页面级导航。
本轮目标是先完成前端高保真弹窗与基础切换能力,内容允许基于现有结果总览数据和静态占位组装,不在本轮扩展后端接口。
## 2. 设计范围
### 2.1 包含内容
-`结果总览` 页内新增统一的“项目分析”弹窗交互
- 两个入口列表共用一套弹窗壳子与信息架构
- 弹窗内保留 5 个页签:
- 异常明细
- 资产分析
- 征信摘要
- 关系图谱
- 资金流向
- 默认页签为 `异常明细`
- 左侧侧栏展示当前人员基础信息、命中模型摘要、排查记录摘要
- 右侧 `异常明细` 页签按截图风格做完整高保真布局
- 其余 4 个页签提供可切换的高保真静态承载
### 2.2 不包含内容
- 不新增后端接口
- 不改造项目详情页路由或导航结构
- 不把弹窗拆成新页面或新页签
- 不在本轮接通 5 个页签的真实分析链路
- 不扩展导出、权限、审批流等截图之外的新业务流程
- 不补充兜底、降级或兼容性分支方案
## 3. 当前上下文
当前结果总览主入口与相关区块如下:
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue`
当前状态:
1. 两个区块的操作列都已有“查看详情”按钮位
2. 现有按钮尚未绑定统一弹窗交互
3. `RiskPeopleSection.vue` 已具备人员基础信息、风险等级、核心异常点等展示字段
4. `RiskModelSection.vue` 已具备人员信息、命中模型、异常标签等展示字段
5. 结果总览当前真实接口仅覆盖仪表盘、风险人员总览、模型卡片与模型命中人员列表,不足以支撑完整项目分析工作台
因此,本轮设计应遵循最短路径实现:只在现有前端链路上新增统一弹窗,不扩散到后端。
## 4. 设计目标
### 4.1 业务目标
- 让用户在结果总览中直接查看某名人员对应的项目分析内容
- 让两个入口汇聚到同一个分析弹窗,避免形成两套详情语义
- 保持“从列表进入分析工作台”的浏览连贯性
### 4.2 交互目标
- 点击后立即打开弹窗,不跳页、不二次确认
- 默认落在最有信息密度的 `异常明细` 页签
- 若入口来自模型命中列表,应在弹窗顶部和侧栏中体现当前命中模型上下文
### 4.3 视觉目标
- 高保真贴近用户截图中的工作台气质
- 左栏稍微简化,避免页面级导航感过强
- 与当前结果总览页保持同一套蓝灰白色系和风险标签语义,不做成另一套产品
## 5. 视觉方向
### 5.1 Visual Thesis
以“调查工作台”为核心视觉意图,整体观感应当克制、聚焦、带有分析场景的秩序感,而不是普通业务弹窗。
### 5.2 Content Plan
- 首屏:人物身份与风险背景
- 主体:异常明细主视图
- 支撑:模型摘要、排查记录摘要、辅助页签
- 收束:让用户在一次打开中快速判断“这个人为什么值得继续查”
### 5.3 Interaction Thesis
- 打开弹窗后直接进入异常明细首屏,形成明确阅读起点
- 页签切换保持轻量、稳定,不做过多动画干扰
- 来源于模型命中列表时,强调当前模型上下文,形成“从哪里来、为什么点进来”的连贯感
## 6. 方案比较
### 6.1 方案 A工作台式高保真弹窗
结构:
- 左侧人物档案
- 右侧五页签工作区
- 异常明细为主视图
优点:
- 与用户截图最接近
- 能承载“查看项目”所需的分析工作台语义
- 后续逐步接真实页签数据时不需要推翻结构
缺点:
- 前端结构比普通详情弹窗更复杂
- 需要严格控制左栏信息量,避免显得臃肿
### 6.2 方案 B顶部信息带 + 页签主区
结构:
- 顶部统一人物信息带
- 下方五页签区域
优点:
- 结构规整
- 实现复杂度较低
缺点:
- 与截图差距较大
- 人员档案沉浸感较弱
### 6.3 方案 C轻量明细弹窗
结构:
- 人员概要
- 单屏明细区
优点:
- 实现最轻
缺点:
- 无法承载用户明确要求的完整页签工作台
- 容易退化成普通表格弹窗
### 6.4 方案结论
采用 **方案 A工作台式高保真弹窗**,同时将左侧栏从“完整工作台导航”收敛为“人物分析侧栏”,保证高保真方向正确且信息密度适中。
## 7. 信息架构设计
### 7.1 打开入口
以下两个入口统一打开同一个弹窗:
1. `风险人员总览` 的操作列
2. `命中模型涉及人员列表` 的操作列
### 7.2 弹窗顶栏
顶栏内容建议为:
- 主标题:`项目分析`
- 次级上下文:`姓名 | 工号 | 命中模型数/当前模型`
- 关闭按钮
不在顶栏继续放置页面级返回、导出报告等完整页面操作,避免弹窗承担页面壳子职责。
### 7.3 左侧人物分析侧栏
左侧侧栏只保留三组信息:
1. **人员基础信息**
- 姓名
- 工号
- 部门
- 风险等级
- 所属项目
2. **命中模型摘要**
- 命中模型数
- 模型名称摘要
- 核心异常标签
3. **排查记录摘要**
- 最近状态
- 简要备注或占位摘要
不保留完整项目侧边导航,不复刻截图中的“返回项目 / 生成报告 / 关注 / 推荐对象信息 / 排查记录”全量信息层级。
### 7.4 右侧主工作区
右侧主区保留五页签:
1. 异常明细
2. 资产分析
3. 征信摘要
4. 关系图谱
5. 资金流向
默认进入 `异常明细`,并作为本轮内容最完整的主视图。
## 8. 页面节奏与视觉结构
### 8.1 外层弹窗
- 使用大尺寸弹窗,宽度建议约 `1360px`
- 内容区高度按视口收敛,采用内部滚动
- 外层视觉重心在内容区,不依赖厚重边框和多层卡片堆叠
### 8.2 左侧侧栏
- 背景色较主内容区略暖或略灰
- 信息块之间通过分隔线与留白组织
- 不使用过多卡片和阴影,避免产生“页面套页面”感
### 8.3 右侧页签工作区
- 页签条作为稳定导航带
- 页签下方以“白底大区块 + 表格/摘要”的工作台形式展开
- `异常明细` 首屏强调三件事:
1. 当前人员风险背景
2. 异常明细主列表
3. 若干需要继续追查的异常摘要块
### 8.4 与当前结果总览页关系
- 保持同一套产品色与风险标签语义
- 弹窗整体更沉浸、更聚焦
- 不把结果总览主页面的四大区块原样塞进弹窗
## 9. 页签内容设计
### 9.1 异常明细页签
该页签为本轮主视图,按截图节奏组织:
- 顶部异常分组标题
- 主表格区:展示交易时间、本方账号、对方账号、摘要/交易类型、交易金额、标记状态等
- 补充摘要区:如频繁转账账户异常、关联交易异常等块级摘要
- 底部操作区按需保留,但不在本轮扩展真实业务动作
此页签允许使用当前行数据 + 本地静态映射方式组装高保真内容。
### 9.2 资产分析页签
- 展示高保真静态结构
- 体现资产概览、资产异动或资产分布分析的阅读节奏
- 本轮不接真实接口
### 9.3 征信摘要页签
- 展示高保真静态结构
- 体现授信、负债、逾期、查询记录等摘要位
- 本轮不接真实接口
### 9.4 关系图谱页签
- 展示高保真静态结构
- 体现中心人物、关系节点、关系类型与摘要说明
- 本轮不接真实接口
### 9.5 资金流向页签
- 展示高保真静态结构
- 体现流入流出方向、关键对手方、异常流向摘要
- 本轮不接真实接口
## 10. 组件拆分方案
### 10.1 入口编排组件
保留:
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
职责:
- 继续作为结果总览页面主入口
- 统一维护项目分析弹窗开关状态
- 保存当前选中人员、入口来源、来源上下文
### 10.2 列表区块组件
涉及:
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue`
职责调整:
- 将“查看项目”改为触发事件
- 向上抛出当前行信息
- 不在区块内部自行维护弹窗状态
### 10.3 新增统一弹窗组件
建议新增:
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
职责:
- 渲染大尺寸项目分析弹窗
- 负责左侧人物分析侧栏
- 负责右侧五页签导航与切换
- 管理默认页签与关闭行为
### 10.4 新增轻量子组件
建议按需拆分为:
- `ProjectAnalysisSidebar.vue`
- `ProjectAnalysisAbnormalTab.vue`
- `ProjectAnalysisPlaceholderTab.vue`
拆分原则:
- 服务于文件聚焦,不做过度抽象
- 异常明细单独成块,其他静态页签可共用一套占位骨架
## 11. 数据策略
### 11.1 本轮数据来源
本轮不新增后端接口,数据优先来自当前结果总览已拿到的列表行数据:
- `风险人员总览` 当前行
- `命中模型涉及人员列表` 当前行
### 11.2 左栏字段来源
优先映射:
- 姓名
- 工号
- 部门
- 风险等级
- 命中模型数
- 命中模型名称
- 异常标签
若来源于模型命中列表,则补充“当前命中模型”上下文;若来源于风险人员总览,则按人员风险概览口径展示。
### 11.3 右侧内容来源
- `异常明细`:由当前行信息 + 本地静态映射组合生成
- 其余页签:由静态高保真结构承载,不伪装成真实分析结果
### 11.4 缺字段策略
- 缺工号、部门、身份证号等字段时展示 `-`
- 缺异常标签时展示“暂无异常标签”
- 不在前端额外推导用户未提供的数据
## 12. 交互设计
### 12.1 打开行为
- 点击“查看项目”后立即打开弹窗
- 不跳页
- 不弹二次确认
- 默认进入 `异常明细`
- 默认滚动到弹窗顶部
### 12.2 来源感知
若入口来自 `命中模型涉及人员列表`
- 顶部摘要展示当前命中模型
- 左侧命中模型摘要中强调当前模型
若入口来自 `风险人员总览`
- 以人员风险背景为主,不额外强调某一模型
### 12.3 页签切换
- 五个页签均可点击切换
- 本轮只有 `异常明细` 是完整主视图
- 其他页签只承担高保真承载,不做真实联动
### 12.4 关闭与重开
- 关闭时清空当前选中人员和来源上下文
- 再次打开时始终回到 `异常明细`
- 不保留上次停留页签
## 13. 状态设计
### 13.1 打开前
- 页面原有列表与筛选行为保持不变
### 13.2 打开后
- 弹窗内部独立滚动
- 不影响结果总览页当前筛选、分页与卡片选中状态
### 13.3 空态
- 左栏缺字段时以 `-` 占位
- 标签为空时显示统一空文案
- 静态页签若无内容,使用高保真占位结构而不是纯 `el-empty`
### 13.4 异常态
本轮不扩展新的接口错误处理流,沿用前端静态组装范围,不引入额外异常流程。
## 14. 验证要点
本轮验证聚焦前端静态与交互边界:
1. 两个入口都能打开同一个弹窗
2. 不同来源可以正确带出当前行信息
3. 默认页签为 `异常明细`
4. 页签切换稳定,关闭重开后重置为默认页签
5. 左栏字段缺失、标签为空、标签过长时布局不塌
6. 弹窗桌面端宽度和常见页面宽度下布局稳定
7. 结果总览原有模型筛选、分页、标签高亮不受影响
## 15. 实施边界结论
本方案采用最短路径实现:
1. 不新增后端接口
2. 不扩展用户需求之外的新业务流程
3. 不做兼容性或补丁式平行方案
4. 只在结果总览现有前端链路上增加统一项目分析弹窗
该方案满足以下要求:
- 逻辑单一
- 入口统一
- 风格贴近用户截图
- 后续具备逐页签接入真实能力的扩展空间
## 16. 后续计划入口
设计确认后,下一步应输出两份实施计划:
- 后端实施计划:说明本轮后端保持不改动或仅补充边界说明
- 前端实施计划:说明弹窗、事件抛出、组件拆分、静态高保真页签和验证步骤
本轮实际开发应优先编写前端实施计划,并在计划中明确“不新增后端接口”的边界。

View File

@@ -0,0 +1,359 @@
# 结果总览查看详情窗口整体展示优化设计
**日期**: 2026-03-25
**模块**: 初核项目详情 - 结果总览 - 项目分析弹窗
**作者**: Codex
**状态**: 已确认
## 1. 概述
当前 `结果总览` 页中的“查看详情”弹窗已经具备统一入口、基础左右分栏和真实详情加载能力,但整体视觉仍存在以下问题:
- 弹窗内部存在明显“页面套页面”感,整体偏重
- 顶部留白过大,首屏可见内容不足
- 左右分栏比例与留白不够舒展,主内容区压缩感较强
- 左侧侧栏被主区高度强行拉长,人物档案区显得拖沓
- 左下角“核心异常标签”在多标签场景下无法完整展示
- 头部、侧栏、页签、表格、标签之间未形成统一的工作台视觉语言
本次设计只聚焦“整体展示效果优化”,允许对弹窗头部、侧栏组织和主区阅读节奏做明显重排,但不改变现有业务入口、路由和接口边界。
## 2. 设计范围
### 2.1 包含内容
- 重构 `ProjectAnalysisDialog.vue` 的整体视觉骨架
- 重排弹窗头带、左侧档案面板、右侧主工作区的层级关系
- 统一异常明细、占位页签、标签、摘要块、表格的视觉语言
- 保留现有“查看详情”入口与默认页签行为
- 保留现有真实详情接口调用链路
### 2.2 不包含内容
- 不新增后端接口
- 不调整结果总览列表层字段、按钮位置和交互入口
- 不新增页面、路由或抽屉式替代方案
- 不扩展导出、审批、关注等本轮需求之外的功能
- 不增加兼容性、补丁式或降级分支方案
## 3. 当前上下文
当前相关文件主要包括:
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisPlaceholderTab.vue`
当前实现特点:
1. 弹窗外层采用 `el-dialog`,内部再包一层 `project-analysis-shell`
2. 当前使用右侧主区独立滚动,左侧侧栏不滚动,造成阅读节奏割裂
3. 左侧和右侧虽已收进同一外壳,但仍有较强“卡片拼装”感
4. “当前命中模型”作为独立提示块出现在主区顶部,容易打断阅读
5. 侧栏以字段表单式平铺为主,人物档案感不足
6. 异常明细区块、占位页签和侧栏之间的边框、背景、圆角节奏不统一
## 4. 设计目标
### 4.1 视觉目标
- 去除“弹窗里再套一页”的套娃感
- 压缩顶部无效留白,让首屏尽量多展示真实内容
- 让窗口更像一个完整的分析工作台,而不是多张卡片临时拼接
- 在保持当前产品色系下,形成更稳定、更克制的调查分析气质
### 4.2 交互目标
- 用户打开弹窗后,首屏先看清“是谁、风险怎样、为什么点进来”
- 弹窗内容区统一滚动,左右两栏保持同一阅读路径
- 默认仍进入 `异常明细`,不改变用户现有操作路径
- 来源于模型命中列表时,上下文提示应更自然地并入头部信息
### 4.3 实现目标
- 维持现有数据链路与组件职责,不扩散到后端与页面壳层
- 将改动边界收敛在弹窗及其直属子组件内
## 5. 视觉方向
### 5.1 Visual Thesis
以“沉浸式分析工作台”为核心方向,整体界面强调一层主壳、两块功能面和连续阅读流,避免多层容器叠加造成的后台表单感。
### 5.2 Content Plan
1. 首屏识别人和风险上下文
2. 主体展示项目内异常分析内容
3. 辅助承载其余四个分析页签
4. 在一次打开中完成快速判断与继续追查的阅读起点
### 5.3 Interaction Thesis
- 通过头部信息头带建立进入时的第一落点
- 通过左侧档案面板维持人物语义,不让侧栏沦为字段表单
- 通过主区摘要条和页签导航建立清晰、稳定的操作路径
## 6. 方案比较
### 6.1 方案 A去壳化微调版
做法:
- 保留当前左右分栏与页签布局
- 仅去掉部分边框、圆角和外层容器感
优点:
- 改动边界最小
缺点:
- 只能缓解表层问题
- 套娃感和层级割裂无法根治
### 6.2 方案 B沉浸式分析工作台
做法:
- 保留左右工作台语义
- 弹窗本身作为唯一主壳,并显著放大工作区尺寸
- 头部、侧栏、主区阅读节奏全部重排
- 改为统一滚动,侧栏标签区完整展开
优点:
- 能同时解决套娃感、比例失衡和风格碎片化问题
- 与当前业务场景最匹配
- 后续继续补真实页签内容时不需要再次推翻结构
缺点:
- 结构调整比简单样式修饰更明显
### 6.3 方案 C单栏纵向叙事版
做法:
- 弱化或取消侧栏
- 全部内容按纵向单列展开
优点:
- 阅读路径最直接
缺点:
- 不符合当前“分析工作台”场景
- 会削弱多页签和多模块并存时的效率感
### 6.4 结论
采用 **方案 B沉浸式分析工作台**
原因:
1. 能一次性解决顶部留白、左右比例、标签展示不全和套娃感几类问题
2. 保留左右工作台语义,不偏离当前业务场景
3. 不需要引入新接口或新页面,仍然是最短路径实现
## 7. 总体设计
### 7.1 外层骨架
弹窗整体改为“三段式节奏”:
1. 顶部信息头带
2. 主体工作区
3. 底部留白收束
其中,`el-dialog` 内容区直接承担唯一外壳职责,不再在内部额外包一层大白卡式 `shell` 容器。
尺寸策略调整为:
- 弹窗整体尺寸较当前版本进一步放大,以“尽量多展示首屏内容”为优先目标
- 顶部与视口的空白显著压缩,不再保留当前过大的上边距观感
- 高度控制仍以视口内完整可操作为前提,不引入页面级滚动穿透问题
### 7.2 主体布局
主体仍保留左右分栏,但重设比例与层次:
- 左侧:固定窄栏,宽度约 `300px ~ 320px`
- 右侧:自适应主工作区
- 左右两区不再依赖粗边框硬切,而是通过背景层次、留白和局部分隔建立边界
- 左侧不再被右侧主区高度强行拉满,应按自身内容自然结束
- 弹窗内容区改为统一纵向滚动,左右两栏随同一滚动容器一起移动
### 7.3 头部信息头带
头部替代原有简单弹窗标题,组织方式如下:
- 左侧主信息:
- 姓名
- 风险等级状态
- 工号 / 部门 / 所属项目
- 右侧操作:
- 关闭按钮
- 右侧补充上下文:
- 若入口来自模型命中列表,则展示“当前命中模型”
“当前命中模型”不再占用主区独立一行,而是并入头带完成来源说明。
### 7.4 左侧人物档案面板
左栏改为“人物档案面板”,按以下顺序组织:
1. 人物身份区
2. 命中模型摘要区
3. 核心异常标签区
具体原则:
- 不再使用强表单感的左右对齐字段列表作为主要视觉形式
- 通过人物姓名、风险等级徽标和简洁元信息建立识别性
- 核心异常标签作为独立内容块完整展示
- 标签区必须占满可用宽度,并支持从左到右自动换行
- 不允许通过截断、隐藏或压缩到单列的方式“勉强塞下”
- 如果某组信息不足,则保持简洁,不硬凑空块
### 7.5 右侧主工作区
主区改为连续阅读流:
1. 头部信息头带
2. 页签导航
3. 当前页签主体内容
其中头带优先展示当前人员在本项目内最关键的进入上下文,包括:
- 姓名
- 风险等级
- 工号 / 部门 / 所属项目
- 当前命中模型(仅模型入口显示)
### 7.6 异常明细页签
`异常明细` 仍为默认页签和主视图,组织节奏调整为:
1. 分组标题与一句摘要
2. 异常明细表格
3. 对象异常或补充摘要区
目标是让主表格继续作为信息中心,同时不再被多余边框和零散块状结构打断。
滚动规则调整为:
- 不再保留主区内部独立滚动容器
- 表格、对象卡片和分页都放回统一文档流
- 用户滚动一次即可连续浏览左侧档案和右侧内容,不再出现左右阅读节奏脱节
### 7.7 其他页签
`资产分析``征信摘要``关系图谱``资金流向` 四个页签本轮仍可保持静态承载,但视觉上统一为同一套工作台区块,不再采用简单占位板式呈现。
## 8. 视觉规范
### 8.1 背景与层次
- 最外层弹窗:唯一主壳
- 左栏背景:较主区略深的浅灰蓝面
- 主区背景:白色或极浅白底
- 关键内容块:局部轻边框或轻底色区分
避免同屏重复出现多层边框、圆角和阴影。
### 8.2 圆角与边框
- 圆角只保留在外层弹窗和主区关键块
- 大面积容器减少边框存在感
- 优先通过留白、标题层级、背景差和局部轻分隔线建立秩序
### 8.3 间距体系
统一使用三档垂直节奏:
- 紧凑信息:`8px`
- 区块内常规间距:`16px`
- 大区块间距:`24px`
主区和侧栏的左右内边距保持同一基线,避免视觉错位。
### 8.4 标签与状态
- 风险等级、异常标签、命中模型提示统一为浅底色、细边框、小圆角体系
- 左侧“核心异常标签”需优先保证完整可读,再考虑一屏展示数量
- 不混用重阴影和高饱和整块底色
- 不同标签的差异主要通过语义色和轻度背景体现
### 8.5 表格
- 表格仍是主工作区核心阅读面
- 弱化网格感,减少厚边框
- 通过表头层级、行高、次级文字颜色和金额色建立信息密度
## 9. 交互与状态
### 9.1 保持不变
- `风险人员总览``命中模型涉及人员列表` 的“查看详情”入口不变
- 默认页签仍为 `异常明细`
- 仍使用现有详情接口按需拉取真实数据
### 9.2 调整点
- 加载态与报错态并入主区头部附近,减少打断感
- 来源为模型命中列表时,模型上下文移入头带
- 弹窗内容区改为统一滚动,左侧与右侧保持同一浏览路径
- 打开弹窗后的首屏信息顺序改为:
- 人员识别
- 风险背景
- 当前来源上下文
- 详细分析内容
## 10. 影响范围
本轮前端改动预计收敛在以下文件:
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisPlaceholderTab.vue`
必要时补充弹窗结构与视觉契约相关单测,但不涉及:
- 后端接口
- 项目详情页路由
- 结果总览列表层逻辑
- 其他业务模块详情弹窗
## 11. 验收标准
1. 打开弹窗后,用户能在首屏快速识别“是谁、风险怎样、为什么点进来”,且顶部空白明显收紧
2. 弹窗尺寸相较当前版本明显放大,但仍保持在视口内稳定展示
3. 窗口整体不再呈现“弹窗里再套一页”的视觉感受
4. 左右分栏比例更稳定,右侧主区成为明确视觉中心
5. 弹窗内容区统一滚动,不再保留右侧独立滚动
6. 左侧侧栏不再被主区强制拉成整块长面板
7. 左下角“核心异常标签”在多标签、长标签场景下可完整换行展示
8. 头带、侧栏、页签、表格、标签形成统一工作台语言
9. 不改变当前业务入口、接口和默认页签行为
## 12. 结论
本次设计采用“沉浸式分析工作台”作为结果总览查看详情窗口的整体优化方向:
1. 去掉多余外壳,收口为一层主壳
2. 重排头带、侧栏和主区节奏
3. 统一标签、摘要、页签和表格视觉语言
该方案满足用户确认的核心诉求:
- 去除套娃感
- 压缩顶部留白并放大首屏展示
- 调顺分栏比例
- 改为统一滚动
- 让左下角标签完整展示
- 统一整体展示效果
同时保持当前接口与业务路径不变,属于符合现有边界的最短路径优化方案。

View File

@@ -0,0 +1,399 @@
# 结果总览项目分析弹窗真实详情设计
**日期**: 2026-03-25
**模块**: 初核项目详情 - 结果总览
**作者**: Codex
**状态**: 已确认
## 1. 概述
现有 `结果总览` 页的“项目分析”弹窗已完成前端壳子,但弹窗内容仍依赖本地静态拼装:
- 弹窗宽度偏窄,流水类信息阅读空间不足
- `异常明细` 仍使用本地 mock 数据
- 左侧 `人员基础信息` 未读取员工信息表真实数据
- `命中模型摘要` 当前由弹窗内部拼装,和入口上下文绑定不清晰
本次设计只解决上述真实详情接入问题,目标是在保留现有弹窗结构的前提下,把“查看详情”改造成可查询真实人员详情和真实异常明细的工作台弹窗。
## 2. 设计目标
### 2.1 包含内容
- 调整 `项目分析` 弹窗宽度
- 新增结果总览详情专用后端接口
- `人员基础信息` 改为读取员工信息表真实数据
- `异常明细` 改为调用真实接口返回
- `命中模型摘要` 改为由外部列表透传进入弹窗
- `异常明细` 中:
- 流水维度异常按表格展示
- `object` 类型异常标签按摘要卡展示
### 2.2 不包含内容
- 不改造项目详情页路由和导航
- 不扩展弹窗内 `资产分析 / 征信摘要 / 关系图谱 / 资金流向` 的真实接口
- 不把弹窗拆成新页面
- 不向现有列表接口塞入详情字段
- 不增加兼容分支、降级分支或补丁式旁路方案
## 3. 当前上下文
当前相关前端文件:
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js`
当前相关后端文件:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBaseStaff.java`
当前问题:
1. 弹窗宽度固定为 `1280px`,流水类表格列空间不足
2. 异常明细页仍是“当前行数据 + 静态模板”拼装
3. 左侧基础信息没有使用员工主数据
4. 结果总览后端尚无详情接口,只有列表类接口
## 4. 方案比较
### 4.1 方案 A新增结果总览详情专用接口
做法:
- 在结果总览控制器下新增详情接口
- 一次返回人员基础信息和异常明细
- 前端弹窗只负责展示和状态管理
优点:
- 接口职责清晰,列表与详情边界明确
- 前端不需要并发拼多个真实接口
- 后续继续扩展其他页签时可以沿用同一详情链路
缺点:
- 需要补一条新的后端查询链路
### 4.2 方案 B前端并发查询员工信息和异常明细
做法:
- 员工信息单独查员工模块
- 异常明细单独查结果总览模块
- 前端弹窗自行合并两份结果
优点:
- 单个接口改动较小
缺点:
- 前端状态更散
- 打开弹窗时更容易出现半成功半失败
- 员工信息与详情上下文容易失去统一入口
### 4.3 方案 C把详情字段塞回现有列表接口
做法:
- 在风险人员总览和模型人员列表接口中直接追加详情字段
- 弹窗尽量少请求或不请求
优点:
- 表面改动快
缺点:
- 列表接口职责被拉坏
- 数据量被放大
- 不符合“查看详情单独获取真实信息”的语义
### 4.4 结论
采用 **方案 A**
原因:
1. 最符合“详情数据独立获取”的业务语义
2. 不破坏现有结果总览列表接口职责
3. 前后端边界最稳定,后续扩展成本最低
## 5. 总体设计
### 5.1 打开入口
以下两个入口继续共用同一个 `项目分析` 弹窗:
1. `风险人员总览`
2. `命中模型涉及人员列表`
### 5.2 打开流程
1. 用户点击列表中的 `查看详情`
2. 前端立即打开弹窗,默认进入 `异常明细`
3. 前端将入口行中的 `命中模型摘要` 直接透传给弹窗
4. 前端发起结果总览详情接口请求
5. 接口返回后刷新左侧真实人员基础信息和右侧真实异常明细
### 5.3 数据来源边界
- `人员基础信息`:后端详情接口内部查询员工信息表 `ccdi_base_staff`
- `命中模型摘要`:由外部列表透传,不由详情接口负责
- `异常明细`:由详情接口统一返回真实信息
## 6. 后端设计
### 6.1 接口设计
建议在结果总览控制器下新增接口:
- `GET /ccdi/project/overview/person-analysis/detail`
入参:
- `projectId`
- `staffIdCard`
只保留这两个字段,避免把外层上下文和真实详情查询混在一起。
### 6.2 返回结构
返回对象建议收口为:
```json
{
"basicInfo": {
"name": "张三",
"idNo": "3301********1234",
"staffCode": "A1023",
"department": "信息二部",
"phone": "138****0000",
"riskLevel": "高风险",
"projectName": "项目A"
},
"abnormalDetail": {
"groups": [
{
"groupCode": "BANK_STATEMENT",
"groupName": "流水异常明细",
"groupType": "BANK_STATEMENT",
"records": []
},
{
"groupCode": "RELATED_OBJECT",
"groupName": "异常对象摘要",
"groupType": "OBJECT",
"records": []
}
]
}
}
```
### 6.3 人员基础信息查询
人员基础信息统一从员工信息表读取:
- 主表:`ccdi_base_staff`
- 关联补充:
- 部门名称
- 当前项目名称
- 结果总览员工结果表中的风险等级
查询口径:
1. `staffIdCard` 作为员工识别主键
2. 员工表查不到时,仍允许返回异常明细,但 `basicInfo` 中对应字段为空
3. 不新增前端二次查员工接口
### 6.4 异常明细查询
异常明细统一组织为 `groups` 列表,每组包含:
- `groupCode`
- `groupName`
- `groupType`
- `records`
本轮只支持两种 `groupType`
- `BANK_STATEMENT`
- `OBJECT`
### 6.5 流水维度异常
`BANK_STATEMENT` 类型的 `records` 直接按流水明细展示口径返回,字段尽量贴近现有流水查询 VO
- `bankStatementId`
- `trxDate`
- `leAccountNo`
- `leAccountName`
- `customerAccountName`
- `customerAccountNo`
- `userMemo`
- `cashType`
- `displayAmount`
- `hitTags`
要求:
1. 展示口径与现有“流水明细查询”页面保持一致
2. 命中标签继续沿用当前流水标签结果表查询方式
3. 不再为弹窗单独造一套与流水查询不一致的字段命名
### 6.6 `OBJECT` 类型异常
`OBJECT` 类型记录用于卡片展示,返回摘要结构,不返回整对象平铺结构。
每条记录建议包含:
- `title`
- `subtitle`
- `riskTags`
- `summary`
- `extraFields`
其中:
- `title`:对象主识别名称
- `subtitle`:对象类型、关系或补充说明
- `riskTags`:异常标签数组
- `summary`:一句话摘要
- `extraFields`:最多 2 到 4 个补充字段
这样可满足“卡片展示核心信息”的要求,避免对象型数据在弹窗内变成字段堆叠。
### 6.7 查询范围
无论从哪个入口进入,详情接口都返回“该人员在当前项目下的全部异常明细”。
即使入口来自 `命中模型涉及人员列表`,也:
- 顶部继续显示当前命中模型上下文
- 但异常明细不按当前模型过滤
## 7. 前端设计
### 7.1 弹窗尺寸
`ProjectAnalysisDialog.vue` 中弹窗宽度从 `1280px` 调整为 `1440px`
布局建议:
- 左侧侧栏宽度从 `320px` 微调到 `340px`
- 右侧主工作区尽量为流水表格腾出空间
### 7.2 侧栏展示
左侧侧栏拆成两类来源:
1. 真实详情接口返回:
- 姓名
- 身份证号
- 工号
- 部门
- 联系方式
- 风险等级
- 所属项目
2. 外层透传:
- 命中模型数
- 当前命中模型
- 核心异常标签
`排查记录摘要` 本轮没有真实口径,继续保留占位文案,不新增接口。
### 7.3 异常明细渲染
`ProjectAnalysisAbnormalTab.vue` 改为分组渲染:
1. `BANK_STATEMENT`
- 渲染表格
- 表头风格参考 `DetailQuery.vue`
2. `OBJECT`
- 渲染摘要卡列表
- 每张卡只展示核心字段
空数据处理:
- 空分组不展示
- 全部为空时展示统一空态
### 7.4 Mock 替换策略
`preliminaryCheck.mock.js` 中的弹窗数据构造逻辑不再承担真实详情拼装职责。
保留内容:
- 占位页签文案
- 少量默认结构常量
移出内容:
- `basicInfo`
- `abnormalDetail`
这两部分改为完全由真实接口驱动。
## 8. 状态与交互设计
### 8.1 打开时机
点击后立即打开弹窗,不等待接口先返回。
### 8.2 加载态
- 左侧真实基础信息未返回前显示骨架或 `-`
- 右侧 `异常明细` 区域显示 `v-loading` 或骨架态
- 外层透传的命中模型摘要可先展示
### 8.3 错误态
如果详情接口失败:
- 弹窗保持打开
- 右侧异常明细区域展示错误提示和重试入口
- 左侧命中模型摘要继续保留
- 未获取到的基础信息字段显示为空
### 8.4 页签行为
- 默认页签始终为 `异常明细`
- 关闭弹窗后再次打开,仍回到 `异常明细`
- 其余 4 个页签继续展示占位内容,不在本次扩展真实数据
## 9. 测试与验收要点
### 9.1 后端验收
1. 详情接口可根据 `projectId + staffIdCard` 返回真实详情
2. 人员基础信息来源于员工信息表
3. 流水型异常可返回真实流水记录和命中标签
4. `OBJECT` 类型异常可返回摘要卡结构
5. 从模型人员列表进入时,异常明细仍返回该人的全部异常
### 9.2 前端验收
1. 弹窗宽度明显加宽,流水表格可读性提升
2. 点击 `查看详情` 后弹窗立即打开
3. 左侧命中模型摘要由外层透传
4. 左侧人员基础信息展示真实数据
5. 流水型异常按表格展示
6. `OBJECT` 类型异常按摘要卡展示
7. 详情请求失败时弹窗不关闭,可重试
## 10. 结论
本方案采用“**结果总览新增专用详情接口 + 前端弹窗按类型渲染真实数据**”的最短路径实现:
1. 不污染现有列表接口
2. 不让前端并发拼多条真实接口
3. 人员基础信息、异常明细、入口模型摘要三类职责边界清晰
4. 既满足本轮真实详情接入,也为后续逐步接通更多页签留出稳定扩展位

View File

@@ -0,0 +1,302 @@
# 专项核查员工家庭资产负债展开区改版设计
**日期**: 2026-03-25
**模块**: 项目详情 - 专项核查 - 员工家庭资产负债专项核查
**作者**: Codex
**状态**: 已确认
## 1. 概述
当前专项核查页中的“员工家庭资产负债专项核查”已经具备列表与展开详情能力,但展开区仍沿用“三列卡片 + 表格明细”的结构,与最新原型存在明显差异:
- 信息层级偏散,展开后阅读路径不稳定
- 逐条明细表过重,不符合当前原型的“汇总式详查”表达
- 资产、负债、指标和风险结论没有形成清晰的纵向阅读节奏
本次设计只改造展开后的详情展示效果,不调整列表层字段、列顺序、查看详情交互、接口契约和页面路由。
## 2. 设计目标
### 2.1 包含内容
- 仅改造 `FamilyAssetLiabilityDetail.vue` 的展开区展示结构
- 展开区改为自上而下 5 张纵向汇总卡片
- 每张卡片标题直接展示该模块汇总数字
- 卡片内部简洁展示汇总数据的来源项
- 资产与负债来源项改为按现有类型聚合后全部展示
- 风险结论按真实风险等级切换样式与文案
### 2.2 不包含内容
- 不调整列表层列顺序、列文案和操作按钮
- 不改造 `查看详情` 的交互入口和按需加载机制
- 不新增后端字段、后端接口或路由
- 不保留现有逐条表格明细展示
- 不增加兼容分支、降级分支、二级折叠或 tooltip 解释
## 3. 当前上下文
当前前端相关文件:
- `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilitySection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- `ruoyi-ui/src/api/ccdi/projectSpecialCheck.js`
当前详情接口已返回以下结构:
- `incomeDetail`
- `assetDetail`
- `debtDetail`
- `summary`
其中 `summary` 已包含:
- `totalIncome`
- `totalDebt`
- `totalAsset`
- `comparisonAmount`
- `riskLevelCode`
- `riskLevelName`
这意味着本次改版无需调整接口,即可直接支撑总额、净资产、关键指标和风险结果展示。
## 4. 视觉方向
### 4.1 Visual Thesis
展开区改为“工作台式纵向汇总带”,每段只承载一个核心结论,用浅灰标题条、白底内容区和简洁来源项建立稳定层级。
### 4.2 Content Plan
1. 总收入
2. 总负债
3. 总资产
4. 关键指标
5. 详查结果
### 4.3 Interaction Thesis
- 保留现有“点击列表行内展开”的唯一交互,不额外增加二级折叠
- 通过模块顺序和标题汇总值强化信息导向,不靠复杂动效制造层级
- 风险结果卡通过颜色和结论文案形成唯一视觉落点
## 5. 方案比较
### 5.1 方案 A完全沿用现有三列卡片仅替换表格样式
做法:
- 保留 `收入 / 负债 / 资产` 三列结构
- 仅把内部表格改成更轻的列表展示
优点:
- 改动最少
缺点:
- 与原型的纵向分段结构不一致
- 无法形成“总额 -> 指标 -> 结论”的阅读链路
### 5.2 方案 B双列摘要面板
做法:
- 左侧聚合资产和负债
- 右侧聚合指标和结果
优点:
- 信息比较紧凑
缺点:
- 更像通用后台摘要区,不够贴近原型
- 用户新确认的顺序无法自然落地
### 5.3 方案 C纵向 5 段汇总卡片
做法:
- 展开区改为 5 张单列卡片,顺序固定为:
- 总收入
- 总负债
- 总资产
- 关键指标
- 详查结果
- 每张卡片标题右侧直接展示汇总值
- 资产、负债卡片内部仅展示聚合后的来源项
优点:
- 最贴近当前确认原型和用户确认顺序
- 只动详情组件,改动边界最清晰
- 能自然去掉逐条表格,收拢成简洁汇总式详查
缺点:
- 放弃现有逐条明细表阅读方式
### 5.4 结论
采用 **方案 C纵向 5 段汇总卡片**
原因:
1. 完整匹配用户最终确认顺序
2. 不改接口即可实现
3. 视觉层级清晰,符合原型的“汇总式展开详情”目标
## 6. 总体设计
### 6.1 展开区结构
展开区固定改为以下顺序:
1. `总收入`
2. `总负债`
3. `总资产`
4. `关键指标`
5. `详查结果`
每个模块都采用统一结构:
- 标题左侧:模块名称
- 标题右侧:该模块汇总数字
- 内容区:来源项或指标项
### 6.2 模块规则
#### 6.2.1 总收入
标题右侧展示 `家庭总年收入`
内容区固定展示两条来源项:
- 本人收入
- 配偶收入
不新增额外说明文字。
#### 6.2.2 总负债
标题右侧展示 `家庭总负债`
内容区展示“按现有负债类型聚合后的来源项”,每条结构统一为:
- 负债类型名
- 聚合金额
- 占总负债比例
所有聚合项全部展示,不截断、不合并为“其他”。
#### 6.2.3 总资产
标题右侧展示 `家庭总资产`
内容区展示“按现有资产类型聚合后的来源项”,每条结构统一为:
- 资产类型名
- 聚合金额
- 占总资产比例
所有聚合项全部展示,不截断、不合并为“其他”。
#### 6.2.4 关键指标
标题右侧固定展示指标数 `3`
内容区固定展示以下 3 项:
- 资产负债率 = 总负债 / 总资产
- 资产/收入比 = 总资产 / 总收入
- 负债/收入比 = 总负债 / 总收入
展示格式:
- 百分比项使用 `%`
- 倍数项使用 `倍`
- 分母为 `0` 时显示 `-`
#### 6.2.5 详查结果
标题右侧展示 `riskLevelName`
内容区展示单条结论文案,按真实风险等级切换样式和文案:
- `NORMAL`:结构基本合理
- `RISK`:负债与收入压力偏高
- `HIGH`:资产负债结构明显异常
- `MISSING_INFO`:当前信息不完整
不新增二级解释区。
## 7. 数据映射与计算规则
### 7.1 直接取值
- 总收入:`incomeDetail.totalIncome``summary.totalIncome`
- 总负债:`debtDetail.totalDebt``summary.totalDebt`
- 总资产:`assetDetail.totalAsset``summary.totalAsset`
- 风险等级:`summary.riskLevelCode``summary.riskLevelName`
### 7.2 前端计算
- 净资产 = 总资产 - 总负债
- 资产负债率 = 总负债 / 总资产
- 资产/收入比 = 总资产 / 总收入
- 负债/收入比 = 总负债 / 总收入
### 7.3 聚合规则
资产来源项:
- 基于 `assetDetail.items`
- 按现有类型字段聚合
- 保留全部聚合项
负债来源项:
- 基于 `debtDetail.items`
- 按现有类型字段聚合
- 保留全部聚合项
本次不要求后端新增分类字段,统一由前端基于现有字段归并。
## 8. 样式约束
- 取消现有逐条明细表和表头
- 资产、负债来源项改为紧凑摘要条目
- 桌面端可使用两列或三列流式排布承载来源项
- 窄屏回落为单列
- 金额统一使用 `¥` + 千分位格式
- 标题区与内容区仍延续专项核查现有白底大区块语义,不引入新主题色体系
## 9. 验证要点
### 9.1 结构验证
- 展开区顺序必须为:总收入 -> 总负债 -> 总资产 -> 关键指标 -> 详查结果
- 每张卡片标题中必须出现对应汇总数字
- 资产与负债来源项不再使用表格
### 9.2 计算验证
- 关键指标基于当前总额值计算,不重复请求接口
- 分母为 `0` 时显示 `-`
- 风险结果卡的标题和结论文案与真实风险等级一致
### 9.3 范围验证
- 列表层结构、列顺序和交互不变
- 接口路径与返回结构不变
- 项目切换、展开缓存和首次展开按需加载机制不变
## 10. 实施边界
本次前端实施默认只涉及:
- `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue`
必要时补充与该组件直接相关的单元测试与样式断言,但不扩大到列表层和后端实现。

View File

@@ -0,0 +1,195 @@
# 项目管理列表重新分析确认与刷新设计文档
## 背景
项目管理列表页已经接入“重新分析”真实调用链路:
- 前端通过 `POST /ccdi/project/tags/rebuild` 提交项目级重打标
- 成功后提示“已开始重新分析”
- 随后调用 `getList()` 刷新列表与状态统计
当前缺口主要有两个:
1. 用户点击“重新分析”后会立即发起请求,缺少二次确认
2. 需要明确本轮仍然沿用成功后的全列表刷新策略,不做局部行补丁更新
本次需求是在现有真实调用能力之上补齐交互确认,并保持最短路径实现。
## 目标
- 为项目列表中的“重新分析”按钮增加确认弹窗
- 用户确认后才触发 `rebuildProjectTags`
- 调用成功后继续刷新项目列表与顶部状态统计
- 保持现有失败提示、按钮提交态和状态驱动展示逻辑
## 范围
### In Scope
- `ruoyi-ui/src/views/ccdiProject/index.vue` 中“重新分析”交互链路补充确认弹窗
- 沿用现有 `reAnalyzeLoadingMap` 防重复点击
- 成功后沿用 `getList()` 刷新列表和状态统计
- 取消确认、调用成功、调用失败三类前端行为收口
- 补充对应前端验证与实施文档
### Out of Scope
- 不新增后端接口
- 不新增轮询、进度提示、消息推送
- 不做局部列表行更新或乐观更新
- 不调整“重新分析”按钮显示条件
- 不变更后端重打标、风险人数计算或状态流转逻辑
## 现状分析
### 前端现状
`ruoyi-ui/src/views/ccdiProject/index.vue` 中的 `handleReAnalyze(row)` 当前流程为:
1. 判断当前项目是否处于重新分析提交中
2. 设置按钮 loading
3. 调用 `rebuildProjectTags({ projectId })`
4. 成功提示“已开始重新分析”
5. 调用 `getList()` 刷新列表
6. 失败时弹出错误提示
当前缺少用户确认步骤,因此点击即执行。
### 后端现状
后端现有 `/ccdi/project/tags/rebuild` 能力已满足本轮需求:
- 支持按 `projectId` 提交项目级重打标
- 会按既有流程更新项目状态
- 完成后由现有链路刷新结果与风险人数
因此本轮仍然只做前端交互补充,不引入后端源码改造。
## 方案选择
### 推荐方案
在列表页现有 `handleReAnalyze(row)` 中补充确认弹窗,确认后继续执行当前请求和刷新逻辑。
执行顺序:
1. 用户点击“重新分析”
2. 前端弹出确认框
3. 用户点击“确定”后再进入接口调用
4. 调用成功后提示“已开始重新分析”
5. 继续调用 `getList()` 刷新列表与统计
采用该方案的原因:
- 改动最小,符合最短路径要求
- 保持 `ProjectTable.vue` 只负责事件透传,不下沉业务交互逻辑
- 与当前页面级操作风格一致,便于复用现有 loading 和刷新逻辑
### 不采用的方案
#### 方案一:在 `ProjectTable.vue` 内直接处理确认弹窗
不采用原因:
- 表格组件职责变重
- 确认逻辑与接口逻辑分散在父子组件之间
- 不利于后续维护同类页面级操作
#### 方案二:抽象通用确认执行器
不采用原因:
- 对当前需求属于过度设计
- 只为单个按钮增加抽象,收益低于额外复杂度
## 详细设计
## 1. 交互设计
点击“重新分析”后,前端先弹出确认框,文案为:
`确认对项目“{项目名称}”重新分析吗?重新分析将重新计算项目标签。`
交互约束:
- 点击“确定”才继续发起请求
- 点击“取消”直接结束,不提示失败,不刷新列表
- 弹窗只承担确认职责,不展示额外解释信息
## 2. 数据流与状态设计
确认后的请求链路保持现有实现:
1. 读取 `projectId`
2. 设置当前项目按钮 loading
3. 调用 `rebuildProjectTags({ projectId })`
4. 成功后提示 `已开始重新分析`
5. 调用 `getList()` 刷新列表和顶部统计
6.`finally` 中清理 loading
说明:
- `getList()` 已并行请求项目列表和状态统计,继续复用即可保证界面一致性
- 不做局部行状态修改,避免列表和顶部统计出现时序不一致
## 3. 异常与边界设计
### 取消确认
- 用户主动取消时,不发请求
- 不设置 loading
- 不弹错误提示
### 接口失败
- 优先展示后端返回的 `error.message`
- 无明确业务文案时,统一提示 `重新分析失败,请稍后重试`
- 失败时不刷新列表,保持当前数据稳定
### 防重复提交
- 保持单项目维度的 `reAnalyzeLoadingMap`
- 只有在用户确认后才进入 loading
- 提交结束后恢复按钮状态
## 4. 测试设计
前端验证聚焦以下场景:
1. 点击“重新分析”先出现确认弹窗
2. 点击“取消”时不发请求、不刷新列表
3. 点击“确定”后发起接口调用
4. 请求成功时提示“已开始重新分析”并刷新列表
5. 请求失败时提示失败信息,按钮恢复可点击
回归重点:
- 重新分析按钮仍只在既有状态条件下显示
- 成功刷新后项目状态和顶部统计继续由后端返回结果驱动
## 5. 后端影响说明
本轮后端无需改造。
原因:
- 现有 `/ccdi/project/tags/rebuild` 已满足提交能力
- 现有 `getList()` 刷新结果已经满足页面回显需要
- 本次变更仅增加用户确认,不改变接口语义
## 6. 风险与约束
- 本轮仍然是“提交后刷新一次”的模式,不展示任务进度
- 如果后端异步处理时间较长,用户看到项目进入处理中状态属于预期
- 方案依赖现有后端重打标链路稳定可用,不额外建设旁路能力
## 7. 本次设计结论
本次需求采用“列表页现有重新分析链路上补确认弹窗,并在成功后继续全列表刷新”的方案。
该方案满足以下要求:
- 最短路径实现
- 不引入补丁式旁路方案
- 前后端职责边界清晰
- 列表数据与统计刷新保持一致

View File

@@ -0,0 +1,266 @@
# 结果总览卡片结构合并设计文档
**日期**: 2026-03-27
**模块**: 初核项目详情 - 结果总览
**作者**: Codex
**状态**: 已确认
## 1. 概述
当前结果总览页顶部存在两个连续区块:
- `风险仪表盘`
- `风险人员总览`
两者都服务于同一类“项目风险总览”信息,但当前被拆成两个独立白卡,导致首屏结构割裂,标题层级重复,且统计卡片虽然已有 `label``icon` 数据字段,视觉上没有形成统一的“总览卡片”语义。
本次需求要求在不新增页面元素、不扩展中风险 TOP10 等新区域的前提下,把现有结果总览页面中的顶部两块内容收拢为同一个大卡片,并统一后续区块命名。
## 2. 设计范围
### 2.1 包含内容
-`风险仪表盘``风险人员总览` 合并为一个 `风险总览` 卡片
- 为结果总览顶部 5 个统计卡片补充明确标题与小图标展示
- 将第二个卡片标题统一为 `风险模型`
- 将第三个卡片标题统一为 `风险明细`
- 补充本次设计文档与设计记录
### 2.2 不包含内容
- 不新增 `中风险人员TOP10``高风险人员清单` 等额外区块
- 不新增或修改后端接口
- 不修改统计口径、人员表格字段含义与查看详情链路
- 不调整风险模型区和风险明细区的业务内容
- 不做兼容性、补丁式或降级方案分支
## 3. 当前上下文
当前结果总览页主要由以下组件组成:
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/OverviewStats.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
当前实现特点如下:
1. `OverviewStats.vue` 独立渲染外层白卡、标题、副标题与统计卡片行。
2. `RiskPeopleSection.vue` 独立渲染另一张白卡,内部包含标题、副标题、导出按钮与风险人员表格。
3. `preliminaryCheck.mock.js` 和真实接口装配中,顶部统计项已经具备 `label``icon``tone` 字段。
4. 风险模型区和风险明细区已经作为独立区块存在,后续继续保留。
## 4. 设计目标
### 4.1 视觉目标
- 顶部信息收拢为一个清晰的“风险总览”首屏卡片
- 让统计卡片与风险人员表格形成上下连续的总览阅读流
- 去掉 `风险仪表盘``风险人员总览` 的重复壳层与重复标题感
- 统一页面主区块命名,形成:
- `风险总览`
- `风险模型`
- `风险明细`
### 4.2 实现目标
- 沿用当前组件和数据结构,不新增接口与平行组件体系
- 将改动控制在结果总览前端视图层
- 保持现有导出、查看详情、模型联动等交互行为不变
## 5. 方案比较
### 5.1 方案 A父层合并保留子组件职责
做法:
-`PreliminaryCheck.vue` 中新增统一的 `风险总览` 外层卡片
- `OverviewStats.vue` 仅负责统计卡片内容
- `RiskPeopleSection.vue` 仅负责风险人员表格内容
- 两个子组件不再各自渲染独立白卡壳层
优点:
- 最短路径实现
- 不改数据结构
- 组件职责仍然清晰
- 风险最小,最符合“按当前页面元素合并”的要求
缺点:
- 需要同步收掉两个子组件原有的外层样式壳
### 5.2 方案 B把统计区直接并入风险人员组件
做法:
-`RiskPeopleSection.vue` 同时负责标题、统计卡片、人员表格
优点:
- 页面结构集中在一个组件中,阅读更直观
缺点:
- 组件职责变重
- 后续维护与复用性变差
### 5.3 方案 C新增组合组件承接三段结构
做法:
- 新建结果总览组合组件,内部再调用统计与人员子块
优点:
- 语义完整
缺点:
- 对本次小范围需求来说偏重
- 会增加一层额外封装,不是最短路径
### 5.4 结论
采用 **方案 A父层合并保留子组件职责**
原因:
1. 与当前组件拆分方式最匹配
2. 不引入多余封装
3. 能在最小改动下完成结构合并和标题统一
## 6. 总体设计
### 6.1 页面区块结构
结果总览页主内容区调整为三张主卡片:
1. `风险总览`
2. `风险模型`
3. `风险明细`
其中第一张 `风险总览` 卡片内部结构为:
1. 卡片标题栏:`风险总览`
2. 第一部分5 个统计小卡片
3. 第二部分:原 `风险人员总览` 表格与导出按钮
### 6.2 风险总览卡片
`风险总览` 卡片承接原来的两块内容,但只保留一层外壳。
内容顺序固定为:
1. 标题栏
2. 统计卡片区
3. 风险人员表格区
说明:
- 不再单独显示 `风险仪表盘` 标题与副标题
- 不再单独显示 `风险人员总览` 作为平级卡片标题
- 风险人员列表继续展示当前已有字段和操作入口
### 6.3 统计卡片展示
顶部 5 个统计小卡片继续沿用当前数据项,不改口径:
- 总人数
- 高风险
- 中风险
- 低风险
- 无预警人数
每个卡片统一采用:
- 左侧小图标
- 右侧标题
- 下方主数值
图标、颜色继续复用现有 `icon``tone` 字段,不额外设计新的数据协议。
### 6.4 风险模型卡片
第二张主卡片标题统一为 `风险模型`
约束:
- 保持当前风险模型区内容、筛选、联动和查看详情行为不变
- 本次仅调整标题命名和与首卡片的层级关系,不扩展新功能
### 6.5 风险明细卡片
第三张主卡片标题统一为 `风险明细`
约束:
- 保持当前风险明细区内容和行为不变
- 本次仅统一标题和页面主区块语义
## 7. 组件改动边界
### 7.1 `PreliminaryCheck.vue`
- 负责重新编排顶部区块
-`OverviewStats``RiskPeopleSection` 包进统一的 `风险总览` 容器
- 保持 `RiskModelSection``RiskDetailSection` 继续作为独立区块
### 7.2 `OverviewStats.vue`
- 去掉自身独立大卡片壳层与原 `风险仪表盘` 标题、副标题
- 保留统计卡片栅格内容
- 完成统计卡片“标题 + 小图标 + 数值”展示
### 7.3 `RiskPeopleSection.vue`
- 去掉自身独立白卡壳层
- 作为 `风险总览` 卡片中的下半部分存在
- 保留当前导出按钮、表格字段与查看详情事件
### 7.4 `RiskModelSection.vue`
- 区块标题调整为 `风险模型`
- 其余数据与交互逻辑保持不变
### 7.5 `RiskDetailSection.vue`
- 区块标题调整为 `风险明细`
- 其余数据与交互逻辑保持不变
## 8. 数据与交互约束
本次设计不调整以下内容:
- `currentData.summary`
- `currentData.riskPeople`
- `currentData.riskModels`
- `currentData.riskDetails`
也就是说:
1. 不新增后端接口
2. 不改接口拼装逻辑
3. 不改统计卡片数量与字段口径
4. 不改风险人员表格字段含义
5. 不改模型区和风险明细区的业务行为
## 9. 验证口径
实施后需验证以下结果:
1. 首屏顶部只保留一个 `风险总览` 大卡片
2. `风险总览` 内同时包含统计卡片区和风险人员表格区
3. 5 个统计卡片都展示标题与小图标,数值与现有口径一致
4. 风险人员列表导出、查看详情与表格字段正常
5. 第二个卡片标题为 `风险模型`
6. 第三个卡片标题为 `风险明细`
7. 其他区块位置和现有交互不受影响
## 10. 后续动作
待用户审阅本设计文档后,继续输出两份实施计划:
- `docs/plans/backend/` 下的后端实施计划
- `docs/plans/frontend/` 下的前端实施计划

View File

@@ -0,0 +1,69 @@
# 结果总览涉疑交易明细与流水明细查询对齐设计
## 背景
- 结果总览页的“风险明细 > 涉疑交易明细”已接通真实接口,但当前列表列结构、详情弹窗内容和分页交互均与“流水明细查询”页面不一致。
- 用户要求以“流水明细查询”页为基准对齐交互与视觉,仅保留涉疑交易特有的“关联员工”字段,其余定制列移除。
## 目标
- 列表对齐“流水明细查询”页面的主表结构与样式。
- 详情弹窗对齐“流水明细查询”页面的字段布局、原始文件区域与命中异常标签区域。
- 分页固定每页 5 条,筛选切换后回到第一页。
- 保留“关联员工”列,作为涉疑交易明细相对流水明细查询的唯一额外业务列。
## 方案
### 列表
- `RiskDetailSection.vue` 保留“涉疑交易明细”区块与筛选下拉。
- 表格列改为:
- 交易时间
- 本方账户
- 对方账户
- 关联员工
- 摘要 / 交易类型
- 异常标签
- 交易金额
- 详情
- 保持与 `DetailQuery.vue` 相同的多行单元格结构、金额颜色和异常标签样式。
### 分页
- 组件内部新增独立分页状态:
- `suspiciousPageNum`
- `suspiciousPageSize` 固定为 `5`
- `suspiciousTotal`
- 初次加载、筛选切换和翻页都通过 `getOverviewSuspiciousTransactions` 重新请求。
- 分页组件沿用仓库现有 `Pagination`,但限制 `pageSizes``[5]`,并移除 `sizes` 布局项。
### 详情弹窗
- 详情弹窗结构对齐 `DetailQuery.vue`
- 基础字段宫格
- 原始文件信息
- 命中异常标签
- 详情数据继续复用 `getBankStatementDetail(bankStatementId)`,避免新增后端接口。
### 异常标签
- 结果总览涉疑交易列表接口当前不直接返回 `hitTags`
- 前端在列表加载完成后,按当前页流水 `bankStatementId` 逐条调用详情接口补齐 `hitTags`,仅处理当前页 5 条数据,保证逻辑闭环且不扩大后端改造范围。
## 影响范围
- 前端:
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
- `ruoyi-ui/tests/unit/`
- 文档:
- 当前设计文档
- 本轮实施记录
## 验证
- 单测验证列表列名、分页配置、详情弹窗字段与异常标签区域。
- 浏览器联调验证:
- 初始加载为 5 条
- 筛选切换可翻页
- 详情弹窗样式与字段对齐
- 导出仍可用

View File

@@ -0,0 +1,364 @@
# 项目详情风险总览员工列表分页设计文档
**模块**: 项目详情 - 结果总览 - 风险总览员工列表
**日期**: 2026-03-29
## 一、背景
当前项目详情页 `结果总览 -> 风险总览` 中的员工列表直接消费 `GET /ccdi/project/overview/risk-people` 返回的全量 `overviewList`。页面没有分页能力,项目内风险员工较多时会导致:
1. 单屏信息过长,浏览成本高。
2. 前端一次性渲染全量列表,交互体验不稳定。
3. 接口返回风格与当前结果总览域内其他分页接口不一致。
本轮需求要求为该员工列表增加分页,并固定为每页 5 条。同时,需求明确要求改造现有接口,不采用前端本地切片或新增补丁接口的方式。
## 二、目标
本次设计目标如下:
1.`GET /ccdi/project/overview/risk-people` 改造成真实分页接口。
2. 风险总览员工列表固定每页展示 5 条。
3. 前端翻页时仅刷新员工列表,不影响结果总览内其他区块。
4. 分页接口返回结构对齐项目现有分页风格,统一为 `rows + total + pageNum + pageSize`
## 三、范围
### 3.1 本次范围
- 改造 `risk-people` 接口入参与返回结构
- 为后端风险人员查询增加数据库分页
- 调整项目详情结果总览首屏数据装配
-`RiskPeopleSection.vue` 增加分页条与翻页请求
- 补充本次设计文档、设计记录,以及后续前后端实施计划入口
### 3.2 不在本次范围
- 不修改风险仪表盘接口
- 不修改风险模型卡片与模型命中人员查询
- 不修改风险明细区块
- 不新增筛选条件、搜索条件或排序条件
- 不修改风险等级、命中模型数、核心异常点的业务口径
- 不删除现有 `GET /ccdi/project/overview/top-risk-people`
## 四、现状分析
### 4.1 前端现状
当前 `PreliminaryCheck.vue` 在页面加载时并发请求:
- `GET /ccdi/project/overview/dashboard`
- `GET /ccdi/project/overview/risk-people`
- `GET /ccdi/project/overview/risk-models/cards`
- `GET /ccdi/project/overview/suspicious-transactions`
- `GET /ccdi/project/overview/employee-credit-negative`
其中 `risk-people` 的返回结果被直接注入 `currentData.riskPeople.overviewList``RiskPeopleSection.vue` 直接用该数组渲染表格,没有分页状态,也没有独立二次加载链路。
### 4.2 后端现状
当前 `CcdiProjectOverviewController.getRiskPeople(Long projectId)` 只接收项目 ID。
`CcdiProjectOverviewServiceImpl.getRiskPeopleOverview(Long projectId)` 的实现为:
1. 校验项目存在
2. 调用 `overviewMapper.selectRiskPeopleOverviewByProjectId(projectId)` 查询全量员工结果
3. 在 Java 层逐行映射为 `overviewList`
4. 返回 `CcdiProjectRiskPeopleOverviewVO { overviewList }`
当前 SQL 直接从 `ccdi_project_overview_employee_result` 查询并排序:
- `risk_level_sort asc`
- `model_count desc`
- `rule_count desc`
- `staff_id_card asc`
现状能够提供正确列表语义,但不具备分页能力。
## 五、方案对比
### 5.1 方案 A改造现有 `risk-people` 为标准分页接口
做法:
- 保持接口路径不变
- 入参扩展为 `projectId + pageNum + pageSize`
- 返回结构改为 `rows + total + pageNum + pageSize`
- 后端通过数据库分页查询返回当前页数据
- 前端首屏与翻页统一走该接口
优点:
- 满足“改接口”的明确要求
-`risk-models/people``employee-credit-negative` 的分页风格一致
- 逻辑单一,没有重复接口
- 数据量增大时性能与语义都正确
缺点:
- 需要同步调整前后端契约与测试
### 5.2 方案 B新增 `risk-people/page`,保留原接口不动
做法:
-`risk-people` 继续返回全量列表
- 新增单独分页接口供前端切换
问题:
- 留下两个语义重复的接口
- 不符合“改接口”的要求
- 增加后续维护成本
### 5.3 方案 C后端仍全量查Java 或前端再切页
做法:
- 接口表面返回分页结构
- 实际仍走全量查询后截断
问题:
- 不是真分页
- 数据量增大时性能与语义都不成立
- 属于补丁式方案,不符合最短路径要求
### 5.4 结论
采用方案 A。
## 六、接口设计
### 6.1 接口路径
- `GET /ccdi/project/overview/risk-people`
### 6.2 入参
- `projectId`: 项目 ID必填
- `pageNum`: 页码,非必填,默认 `1`
- `pageSize`: 每页条数,非必填,默认 `5`
说明:
- 前端固定传 `pageSize = 5`
- 后端默认值同样收敛为 `5`,避免前端漏传时行为偏移
### 6.3 返回结构
返回结构统一为:
```json
{
"rows": [
{
"name": "李四",
"idNo": "330000000000000001",
"department": "信息二部",
"riskCount": 5,
"riskLevel": "中风险",
"riskLevelType": "warning",
"modelCount": 4,
"riskPoint": "大额单笔收入、疑似兼职",
"actionLabel": "查看项目"
}
],
"total": 18,
"pageNum": 1,
"pageSize": 5
}
```
说明:
-`overviewList` 字段移除,统一改为 `rows`
- 单行字段保持现有页面绑定语义不变
- 本次不引入额外统计字段
## 七、后端设计
### 7.1 控制器
`CcdiProjectOverviewController.getRiskPeople` 改为接收独立 DTO例如
- `CcdiProjectRiskPeopleQueryDTO`
DTO 仅包含:
- `projectId`
- `pageNum`
- `pageSize`
不额外引入筛选项,保持最短路径。
### 7.2 服务层
`ICcdiProjectOverviewService.getRiskPeopleOverview` 改为接收查询 DTO并返回分页 VO。
服务层职责:
1. 校验项目存在
2. 规范化分页参数,默认 `pageNum=1``pageSize=5`
3. 构造 MyBatis Plus `Page`
4. 调用 mapper 分页查询
5. 将记录映射为现有员工列表行结构
6. 返回 `rows + total + pageNum + pageSize`
### 7.3 VO 调整
`CcdiProjectRiskPeopleOverviewVO` 改为标准分页 VO字段包括
- `List<CcdiProjectRiskPeopleOverviewItemVO> rows`
- `Long total`
- `Long pageNum`
- `Long pageSize`
说明:
- `CcdiProjectRiskPeopleOverviewItemVO` 本身字段不做语义调整
- 既有的风险等级映射与异常点标签来源保持不变
### 7.4 Mapper 与 SQL
Mapper 从“全量列表查询”改为“分页查询”,直接在数据库层完成分页。
排序规则保持现状不变:
- `risk_level_sort asc`
- `model_count desc`
- `rule_count desc`
- `staff_id_card asc`
数据来源继续使用 `ccdi_project_overview_employee_result`,不新增统计口径,不回退到历史复杂聚合链路。
### 7.5 默认值与边界
- `projectId` 为空时沿用现有参数校验/项目不存在校验逻辑
- `pageNum <= 0` 时按 `1` 处理
- `pageSize <= 0` 或为空时按 `5` 处理
- 本次不开放前端修改每页条数,接口虽接收 `pageSize`,但页面固定使用 5
## 八、前端设计
### 8.1 API 封装
`ruoyi-ui/src/api/ccdi/projectOverview.js` 中:
- `getOverviewRiskPeople` 从接收单个 `projectId` 改为接收 `params`
- 透传:
- `projectId`
- `pageNum`
- `pageSize`
### 8.2 首屏加载
`PreliminaryCheck.vue` 首次加载结果总览时,请求:
- `getOverviewRiskPeople({ projectId, pageNum: 1, pageSize: 5 })`
返回结果注入 `currentData.riskPeople` 时,直接保存分页结构:
- `rows`
- `total`
- `pageNum`
- `pageSize`
页面是否进入 `loaded` 状态的判断,从原来的 `overviewList.length` 改为 `rows.length`
### 8.3 风险总览员工列表组件
`RiskPeopleSection.vue` 调整为:
1. 表格数据源从 `sectionData.overviewList` 改为 `sectionData.rows`
2. 在表格下方增加分页组件
3. 分页组件固定:
- `:page-sizes="[5]"`
- `layout="total, prev, pager, next, jumper"`
4. 页码变化时触发独立请求,仅刷新员工列表分页数据
分页条展示规则:
- `total > 0` 时展示
- `total = 0` 时隐藏
### 8.4 页面刷新策略
翻页时只刷新风险总览员工列表,不重新拉取:
- 风险仪表盘
- 风险模型卡片
- 风险明细涉疑交易
- 风险明细员工负面征信
这样可以避免其他区块闪动,也避免把简单翻页放大成整页重载。
### 8.5 交互保持不变
以下内容本次保持不变:
- 表格列顺序
- 风险等级标签渲染
- 核心异常点标签拆分与色板逻辑
- 操作列文案与点击事件
- 空态文案
## 九、测试设计
### 9.1 后端测试
新增或调整以下验证:
1. Controller 测试
- 断言 `/risk-people` 接口改为接收 DTO
- 断言返回 `rows + total + pageNum + pageSize`
2. Service 测试
- 断言服务层使用分页参数构造 `Page`
- 断言现有字段映射未变化
- 断言默认分页参数回落为 `1 / 5`
3. Mapper/SQL 测试
- 断言风险人员查询改为分页查询方法
- 断言排序字段未变化
- 断言数据来源仍为 `ccdi_project_overview_employee_result`
### 9.2 前端测试
新增或调整以下验证:
1. API 封装测试
- 断言 `getOverviewRiskPeople(params)` 透传 `projectId/pageNum/pageSize`
2. 页面接入测试
- 断言 `PreliminaryCheck.vue` 首次加载传 `pageNum: 1`
- 断言固定传 `pageSize: 5`
- 断言风险人员数据改为读取 `rows`
3. 风险总览组件测试
- 断言 `RiskPeopleSection.vue` 存在分页组件
- 断言分页绑定 `rows/total/pageNum/pageSize`
- 断言分页大小固定为 5
## 十、实施文档要求
本次设计确认后,按仓库规范继续产出两份实施计划:
- 后端实施计划:`docs/plans/backend/`
- 前端实施计划:`docs/plans/frontend/`
实施完成后,补充对应实施记录,记录本次真实改动内容。
## 十一、结论
本次需求本质是将结果总览中的风险员工列表从“全量列表展示”升级为“标准分页列表展示”。
最终方案为:
1. 保持 `GET /ccdi/project/overview/risk-people` 路径不变
2. 改为标准分页接口,返回 `rows + total + pageNum + pageSize`
3. 后端在数据库层做真分页,默认每页 5 条
4. 前端首屏和翻页统一走该接口
5. 翻页仅刷新员工列表,不重载结果总览其他区块
该方案满足需求边界明确、链路完整、实现路径最短,且不引入额外补丁接口或兼容分支。

Some files were not shown because too many files have changed in this diff Show More