Compare commits

...

6 Commits

Author SHA1 Message Date
wkc
cf91be838f 部署脚本 2026-04-28 17:27:24 +08:00
wkc
592c58534a Use browser-use for page testing instructions 2026-04-27 10:22:31 +08:00
wkc
e48c9b4d49 新增关联业务自动补入实体库实施计划 2026-04-26 16:41:26 +08:00
wkc
c0eedfaaa1 修订实体库自动补入设计文档 2026-04-26 16:25:54 +08:00
wkc
344b115038 更新 AGENTS 协作规则 2026-04-26 16:18:41 +08:00
wkc
de9a7b3099 新增关联业务自动补入实体库设计文档 2026-04-26 16:12:12 +08:00
18 changed files with 4031 additions and 2 deletions

3
.gitignore vendored
View File

@@ -74,6 +74,7 @@ db_config.conf
# Local deployment bundles
.deploy/
/ccdi_????????.zip
output/
@@ -93,4 +94,4 @@ tongweb_62318.properties
.superpowers/
tmp/
tmp/

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
14.21.3

View File

@@ -1,5 +1,44 @@
# AGENTS.md - AI Coding Assistant Guide
## 全局执行规则
### Git
- Git 提交时使用中文添加描述
- 无视 `.DS_Store`
### AGENT
- `using-superpowers` 只有在用户明确声明调用时才允许启用;不要因为“会话开始”“任务较复杂”或“可能适用”而自动调用
- 当用户没有明确声明 `using-superpowers` 时,按普通流程直接处理需求
- 默认不开启 subagent
- 如用户明确要求启用 subagent所有 subagent 必须使用 `gpt-5.5`;默认推理强度为高,审查类 subagent 的推理强度为超高,测试类 subagent 的推理强度为中
### 文档
- 写完的设计文档必须要由审查类子代理进行内容审查,确保方案与实施方法符合实际需求,并检查是否还有需要明确的功能点
- 当功能设计涉及到前端和后端都有改动时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划。如果不是前后端架构的项目不需要输出两份执行文档
- 当功能修改只涉及到前端或只涉及到后端,只需要输出对应的实施计划
- 每一次改动都需要留下实施文档,记录修改的内容
- 每次写设计文档的时候,都要检查一下保存路径是否正确
### 测试
- 测试结束后,自动结束测试时开启的前后端进程
- 前端 Node 需要使用 nvm 进行控制版本
- 在完成页面功能开发后,必须使用 `browser-use` 技能打开浏览器进行实际页面测试,禁止打开 prototype 原型页面进行测试
- 所有生成的测试文件不需要上传到 Git
### 方案规范
当需要给出方案时必须符合以下规范:
- 不允许给出兼容性或补丁性的方案,不允许过度设计,保持最短路径实现且不能违反第一条要求
- 不允许自行给出用户提供的需求以外的方案,例如一些兜底和降级方案,这可能导致业务逻辑偏移问题
- 必须确保方案的逻辑正确,必须经过全链路的逻辑验证
---
## 项目概述
本仓库是纪检初核系统主仓库,基于若依 `v3.9.1`,当前技术栈以 `Java 21 + Spring Boot 3 + Vue 2` 为主,并包含独立的流水分析 Mock 服务、Docker 部署文件、SQL 脚本、实施文档与测试文档。
@@ -229,7 +268,7 @@ return AjaxResult.success(result);
- 导入功能测试必须进入真实业务页面执行,先在页面内下载当前导入模板,再基于该模板生成测试文件,禁止手工凭记忆新建表头或脱离页面直接构造上传文件
- 双 Sheet 模板的导入测试必须覆盖两个 Sheet 的联动关系;除“缺少 Sheet / 空 Sheet”专项场景外默认两个 Sheet 都要准备测试数据
- 导入测试文件优先放在 `output/spreadsheet/``output/playwright/`,不提交到 git
- 导入测试文件优先放在 `output/spreadsheet/``output/browser-use/`,不提交到 git
- 需要按场景拆分测试文件,避免多个互斥校验互相覆盖;至少覆盖空模板、主信息必填、主信息格式与金额、主从关系异常、供应商校验、缺少/空 Sheet、成功导入、成功与失败混合、失败记录查看、导入后清理回滚
- 主从关系异常测试至少覆盖:已存在主键、供应商有数据但主信息缺失、主信息重复、供应商 Sheet 中采购事项 ID 为空
- 供应商校验测试至少覆盖:重复供应商、多条中标、供应商名称为空、名称超长、联系人超长、银行账户超长、联系电话非法、统一信用代码非法、是否中标枚举非法

92
build_release_ccdi.sh Executable file
View File

@@ -0,0 +1,92 @@
#!/bin/sh
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
DATE_STAMP=$(date "+%Y%m%d")
RELEASE_ZIP="$ROOT_DIR/ccdi_${DATE_STAMP}.zip"
STAGE_DIR="$ROOT_DIR/.deploy/ccdi-release-package"
WORK_DIR="$STAGE_DIR/files"
BACKEND_JAR_SOURCE="$ROOT_DIR/ruoyi-admin/target/ruoyi-admin.jar"
FRONTEND_DIR="$ROOT_DIR/ruoyi-ui"
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
FRONTEND_DIST_ZIP="$WORK_DIR/dist.zip"
log_info() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
}
log_error() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
reset_stage_dir() {
rm -rf "$STAGE_DIR"
mkdir -p "$WORK_DIR"
}
build_backend() {
log_info "开始构建后端生产 jar"
(
cd "$ROOT_DIR"
mvn -pl ruoyi-admin -am clean package -DskipTests
)
if [ ! -f "$BACKEND_JAR_SOURCE" ]; then
log_error "未生成后端 jar: $BACKEND_JAR_SOURCE"
exit 1
fi
}
build_frontend() {
log_info "开始构建前端生产 dist"
FRONTEND_DIR="$FRONTEND_DIR" zsh -lic 'cd "$FRONTEND_DIR" && nvm use >/dev/null && npm run build:prod'
if [ ! -f "$FRONTEND_DIST_DIR/index.html" ]; then
log_error "前端生产构建失败,未找到: $FRONTEND_DIST_DIR/index.html"
exit 1
fi
(
cd "$FRONTEND_DIR"
zip -qr "$FRONTEND_DIST_ZIP" dist
)
if [ ! -f "$FRONTEND_DIST_ZIP" ]; then
log_error "未生成前端压缩包: $FRONTEND_DIST_ZIP"
exit 1
fi
}
package_release() {
cp "$BACKEND_JAR_SOURCE" "$WORK_DIR/ruoyi-admin.jar"
rm -f "$RELEASE_ZIP"
(
cd "$WORK_DIR"
zip -qr "$RELEASE_ZIP" ruoyi-admin.jar dist.zip
)
log_info "上线压缩包已生成: $RELEASE_ZIP"
log_info "压缩包根层内容: ruoyi-admin.jar, dist.zip"
}
main() {
require_command mvn
require_command zsh
require_command zip
reset_stage_dir
build_backend
build_frontend
package_release
}
main "$@"

212
deploy/deploy-release-prod.sh Executable file
View File

@@ -0,0 +1,212 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
BACKEND_DIR="${SCRIPT_DIR}/backend"
FRONTEND_DIR="${SCRIPT_DIR}/frontend"
START_SCRIPT="${SCRIPT_DIR}/start-java-backend-prod.sh"
BACKUP_ROOT="${SCRIPT_DIR}/backups"
WORK_ROOT="${SCRIPT_DIR}/.deploy-work"
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
WORK_DIR="${WORK_ROOT}/release-${TIMESTAMP}"
RELEASE_ZIP="${1:-}"
RELEASE_JAR=""
RELEASE_DIST_ZIP=""
FRONTEND_SOURCE_DIR=""
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
usage() {
cat <<'EOF'
用法: ./deploy-release-prod.sh [上线压缩包路径]
目录要求:
deploy-release-prod.sh
start-java-backend-prod.sh
backend/
frontend/
ccdi_YYYYMMDD.zip
说明:
未传上线压缩包路径时,脚本会自动使用当前脚本目录下唯一的 .zip 文件。
上线压缩包根层必须包含 ruoyi-admin.jar 和 dist.zip。
EOF
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log_error "缺少命令: $1"
exit 1
fi
}
resolve_path() {
input_path="$1"
case "${input_path}" in
/*)
printf '%s\n' "${input_path}"
;;
*)
input_dir=$(dirname "${input_path}")
input_base=$(basename "${input_path}")
printf '%s/%s\n' "$(CDPATH= cd -- "${input_dir}" && pwd)" "${input_base}"
;;
esac
}
resolve_release_zip() {
if [ -n "${RELEASE_ZIP}" ]; then
RELEASE_ZIP=$(resolve_path "${RELEASE_ZIP}")
else
mkdir -p "${WORK_ROOT}"
candidate_file="${WORK_ROOT}/zip-candidates-${TIMESTAMP}.txt"
find "${SCRIPT_DIR}" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip' | sort > "${candidate_file}"
candidate_count=$(wc -l < "${candidate_file}" | tr -d ' ')
if [ "${candidate_count}" -eq 0 ]; then
log_error "未在脚本目录找到上线压缩包,请传入压缩包路径"
exit 1
fi
if [ "${candidate_count}" -gt 1 ]; then
log_error "脚本目录存在多个上线压缩包,请显式传入压缩包路径"
cat "${candidate_file}" >&2
exit 1
fi
RELEASE_ZIP=$(sed -n '1p' "${candidate_file}")
rm -f "${candidate_file}"
fi
if [ ! -f "${RELEASE_ZIP}" ]; then
log_error "上线压缩包不存在: ${RELEASE_ZIP}"
exit 1
fi
}
assert_layout() {
if [ ! -d "${BACKEND_DIR}" ]; then
log_error "未找到后端目录: ${BACKEND_DIR}"
exit 1
fi
if [ ! -d "${FRONTEND_DIR}" ]; then
log_error "未找到前端目录: ${FRONTEND_DIR}"
exit 1
fi
if [ ! -f "${START_SCRIPT}" ]; then
log_error "未找到后端启动脚本: ${START_SCRIPT}"
exit 1
fi
}
backup_dir() {
source_dir="$1"
target_dir="$2"
mkdir -p "${target_dir}"
if [ -n "$(find "${source_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
cp -a "${source_dir}/." "${target_dir}/"
log_info "已备份 ${source_dir}${target_dir}"
else
log_info "目录为空,已创建空备份目录: ${target_dir}"
fi
}
backup_current_files() {
mkdir -p "${BACKUP_DIR}"
backup_dir "${BACKEND_DIR}" "${BACKUP_DIR}/backend"
backup_dir "${FRONTEND_DIR}" "${BACKUP_DIR}/frontend"
}
extract_release_package() {
mkdir -p "${WORK_DIR}/release" "${WORK_DIR}/frontend"
log_info "开始解压上线压缩包: ${RELEASE_ZIP}"
unzip -q "${RELEASE_ZIP}" -d "${WORK_DIR}/release"
RELEASE_JAR="${WORK_DIR}/release/ruoyi-admin.jar"
RELEASE_DIST_ZIP="${WORK_DIR}/release/dist.zip"
if [ ! -f "${RELEASE_JAR}" ]; then
log_error "上线压缩包根层缺少 ruoyi-admin.jar"
exit 1
fi
if [ ! -f "${RELEASE_DIST_ZIP}" ]; then
log_error "上线压缩包根层缺少 dist.zip"
exit 1
fi
unzip -q "${RELEASE_DIST_ZIP}" -d "${WORK_DIR}/frontend"
FRONTEND_SOURCE_DIR="${WORK_DIR}/frontend/dist"
if [ ! -f "${FRONTEND_SOURCE_DIR}/index.html" ]; then
log_error "dist.zip 解压后未找到 dist/index.html"
exit 1
fi
}
deploy_backend() {
target_jar="${BACKEND_DIR}/ruoyi-admin.jar"
deploying_jar="${BACKEND_DIR}/.ruoyi-admin.jar.deploying"
cp "${RELEASE_JAR}" "${deploying_jar}"
mv "${deploying_jar}" "${target_jar}"
log_info "后端 Jar 已部署: ${target_jar}"
}
deploy_frontend() {
find "${FRONTEND_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a "${FRONTEND_SOURCE_DIR}/." "${FRONTEND_DIR}/"
log_info "前端文件已部署到: ${FRONTEND_DIR}"
}
cleanup_work_dir() {
rm -rf "${WORK_DIR}"
}
main() {
case "${1:-}" in
-h|--help|help)
usage
exit 0
;;
esac
if [ "$#" -gt 1 ]; then
usage
exit 1
fi
require_command "unzip"
require_command "find"
resolve_release_zip
assert_layout
backup_current_files
trap cleanup_work_dir 0
extract_release_package
deploy_backend
deploy_frontend
cleanup_work_dir
trap - 0
log_info "部署完成,备份目录: ${BACKUP_DIR}"
log_info "开始重启后端并输出日志"
bash "${START_SCRIPT}" restart
}
main "$@"

310
deploy/start-java-backend-prod.sh Executable file
View File

@@ -0,0 +1,310 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ==================== 生产配置区:按服务器实际路径修改 ====================
# JDK 安装目录。留空时使用服务器已有 JAVA_HOME仍为空时使用 PATH 中的 java。
BACKEND_JAVA_HOME=""
# 后端 Jar 所在目录。生产目录结构为启动脚本在外层Jar 位于 backend/ruoyi-admin.jar。
APP_HOME="${SCRIPT_DIR}/backend"
# 后端 Jar 文件名。
JAR_NAME="ruoyi-admin.jar"
# Spring Profile。
SPRING_PROFILES_ACTIVE="uat"
# JVM 参数。
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
# 额外应用启动参数,例如:--server.port=8080
APP_ARGS=""
# 停止进程等待秒数。
STOP_WAIT_SECONDS=30
# ==================== 以下为脚本逻辑,一般不需要修改 ====================
if [[ "${APP_HOME}" != /* ]]; then
APP_HOME="${SCRIPT_DIR}/${APP_HOME}"
fi
JAR_PATH="${APP_HOME}/${JAR_NAME}"
RELATIVE_JAR_PATH=""
if [[ "${APP_HOME}" == "${SCRIPT_DIR}/"* ]]; then
RELATIVE_JAR_PATH="${APP_HOME#${SCRIPT_DIR}/}/${JAR_NAME}"
fi
LOG_DIR="${APP_HOME}/logs"
CONSOLE_LOG="${LOG_DIR}/backend-console.log"
PID_FILE="${LOG_DIR}/backend-java.pid"
APP_MARKER="-Dccdi.backend.prod.home=${APP_HOME}"
JAVA_CMD="java"
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
log_info() {
printf '[%s] %s\n' "$(timestamp)" "$1"
}
log_error() {
printf '[%s] %s\n' "$(timestamp)" "$1" >&2
}
usage() {
cat <<'EOF'
用法: ./start-java-backend-prod.sh [start|stop|restart|status|logs]
默认动作:
start 先关闭旧后端进程,再启动生产后端 Jar启动成功后持续输出控制台日志
常用配置:
配置统一写在脚本顶部“生产配置区”,包括 BACKEND_JAVA_HOME、APP_HOME、SPRING_PROFILES_ACTIVE、JAVA_OPTS。
示例:
./start-java-backend-prod.sh restart
EOF
}
ensure_command() {
local command_name="$1"
if ! command -v "${command_name}" >/dev/null 2>&1; then
log_error "缺少命令: ${command_name}"
exit 1
fi
}
resolve_java_cmd() {
local configured_java_home="${BACKEND_JAVA_HOME}"
if [[ -z "${configured_java_home}" ]]; then
configured_java_home="${JAVA_HOME:-}"
fi
if [[ -n "${configured_java_home}" ]]; then
configured_java_home="${configured_java_home%/}"
if [[ ! -x "${configured_java_home}/bin/java" ]]; then
log_error "配置的 JAVA_HOME 无效,未找到可执行文件: ${configured_java_home}/bin/java"
exit 1
fi
export JAVA_HOME="${configured_java_home}"
JAVA_CMD="${JAVA_HOME}/bin/java"
else
ensure_command "java"
JAVA_CMD="java"
fi
log_info "使用 Java 命令: ${JAVA_CMD}"
}
get_process_table() {
local process_table
if ! process_table="$(ps -ef 2>/dev/null)"; then
log_error "执行 ps -ef 失败,无法扫描旧进程"
return 1
fi
printf '%s\n' "${process_table}"
}
is_managed_pid() {
local pid="$1"
if [[ -z "${pid}" ]] || ! kill -0 "${pid}" 2>/dev/null; then
return 1
fi
local process_table
if ! process_table="$(get_process_table)"; then
return 1
fi
local line
while IFS= read -r line; do
set -- ${line}
if [[ "${2:-}" == "${pid}" ]] && is_backend_process_line "${line}"; then
return 0
fi
done <<<"${process_table}"
return 1
}
is_backend_process_line() {
local line="$1"
[[ "${line}" != *"<defunct>"* ]] || return 1
[[ "${line}" == *" -jar ${JAR_PATH}"* ]] && return 0
[[ -n "${RELATIVE_JAR_PATH}" && "${line}" == *" -jar ${RELATIVE_JAR_PATH}"* ]]
}
collect_pids() {
local all_pids=""
local pid
local process_table
if ! process_table="$(get_process_table)"; then
return 1
fi
if [[ -f "${PID_FILE}" ]]; then
pid="$(cat "${PID_FILE}" 2>/dev/null || true)"
if is_managed_pid "${pid}"; then
all_pids="${all_pids} ${pid}"
fi
fi
local line
while IFS= read -r line; do
set -- ${line}
pid="${2:-}"
if [[ "${pid}" =~ ^[0-9]+$ ]] && is_backend_process_line "${line}"; then
all_pids="${all_pids} ${pid}"
fi
done <<<"${process_table}"
local unique_pids=""
for pid in ${all_pids}; do
case " ${unique_pids} " in
*" ${pid} "*) ;;
*) unique_pids="${unique_pids} ${pid}" ;;
esac
done
xargs <<<"${unique_pids}" 2>/dev/null || true
}
start_backend() {
resolve_java_cmd
if [[ ! -f "${JAR_PATH}" ]]; then
log_error "未找到后端 Jar: ${JAR_PATH}"
exit 1
fi
local running_pids
running_pids="$(collect_pids)"
if [[ -n "${running_pids}" ]]; then
log_error "检测到后端已在运行: ${running_pids}"
exit 1
fi
mkdir -p "${LOG_DIR}"
printf '\n===== %s start =====\n' "$(timestamp)" >>"${CONSOLE_LOG}"
local profile_arg=""
if [[ -n "${SPRING_PROFILES_ACTIVE}" ]]; then
profile_arg="--spring.profiles.active=${SPRING_PROFILES_ACTIVE}"
fi
log_info "开始启动后端 Jar: ${JAR_PATH}"
nohup "${JAVA_CMD}" "${APP_MARKER}" ${JAVA_OPTS} -jar "${JAR_PATH}" ${profile_arg} ${APP_ARGS} >>"${CONSOLE_LOG}" 2>&1 &
echo $! >"${PID_FILE}"
sleep 3
local starter_pid
starter_pid="$(cat "${PID_FILE}" 2>/dev/null || true)"
if [[ -z "${starter_pid}" ]] || ! kill -0 "${starter_pid}" 2>/dev/null; then
log_error "启动命令未保持运行,请检查日志: ${CONSOLE_LOG}"
exit 1
fi
log_info "后端启动完成PID: ${starter_pid}"
}
stop_backend() {
local pids
pids="$(collect_pids)"
if [[ -z "${pids}" ]]; then
log_info "未发现运行中的后端进程"
rm -f "${PID_FILE}"
return 0
fi
log_info "准备停止后端进程: ${pids}"
local pid
for pid in ${pids}; do
kill -TERM "${pid}" 2>/dev/null || true
done
local elapsed=0
local remaining_pids="${pids}"
while [[ -n "${remaining_pids}" && "${elapsed}" -lt "${STOP_WAIT_SECONDS}" ]]; do
sleep 1
elapsed=$((elapsed + 1))
remaining_pids=""
for pid in ${pids}; do
if kill -0 "${pid}" 2>/dev/null; then
remaining_pids="${remaining_pids} ${pid}"
fi
done
remaining_pids="$(xargs <<<"${remaining_pids}" 2>/dev/null || true)"
done
if [[ -n "${remaining_pids}" ]]; then
log_info "仍有进程未退出,执行强制停止: ${remaining_pids}"
for pid in ${remaining_pids}; do
kill -KILL "${pid}" 2>/dev/null || true
done
fi
rm -f "${PID_FILE}"
log_info "后端停止完成"
}
status_backend() {
local pids
pids="$(collect_pids)"
if [[ -n "${pids}" ]]; then
log_info "后端正在运行,进程: ${pids}"
return 0
fi
log_info "后端未运行"
}
follow_logs() {
mkdir -p "${LOG_DIR}"
touch "${CONSOLE_LOG}"
log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看,不会停止后端进程"
tail -n 200 -F "${CONSOLE_LOG}"
}
start_action() {
stop_backend
start_backend
follow_logs
}
main() {
local action="${1:-start}"
case "${action}" in
start)
start_action
;;
stop)
stop_backend
;;
restart)
start_action
;;
status)
status_backend
;;
logs)
follow_logs
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,493 @@
# 关联业务自动补入实体库 Backend Implementation Plan
> **执行约束:** 按当前项目 `AGENTS.md` 执行;未获得用户明确要求时不启用 subagent。Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 新建和导入员工亲属实体关联、中介实体关联、信贷客户实体关联、招投标供应商时,实体库缺失的企业自动写入 `ccdi_enterprise_base_info`
**Architecture:** 新增一个后端内部实体库自动补全服务,统一处理“已存在不覆盖、缺失则最小插入、同批去重、来源和风险等级映射”。各业务 Service 在业务校验通过、业务数据落库前调用该能力;`EnterpriseSource` 枚举新增 `SUPPLIER` 并继续由现有 `/ccdi/enum/enterpriseSource` 接口驱动前端。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito, Maven.
---
## File Structure
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/EnterpriseSource.java`
- 新增 `SUPPLIER("SUPPLIER", "供应商")`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java`
- 内部补全服务,封装单条和批量实体补入。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationServiceImpl.java`
- 新建员工亲属实体关联前补实体库。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationImportServiceImpl.java`
- 导入成功行批量补实体库。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationServiceImpl.java`
- 新建信贷客户实体关联前补实体库。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java`
- 导入成功行批量补实体库。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
- 中介实体关联新建时取消实体库必须已存在校验,改为补实体库。
- 中介库管理新增实体时风险等级默认高风险。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java`
- 取消“机构表不存在”失败条件,改为成功行批量补实体库。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiEnterpriseBaseInfoImportServiceImpl.java`
- 中介库管理导入实体风险等级默认高风险。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionServiceImpl.java`
- 招投标新建时供应商补实体库。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java`
- 招投标导入成功采购事项的供应商批量补实体库。
- Test: existing unit tests under `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/`
- 扩展或新增对应 Service/Import/Controller 测试。
- Create: `docs/reports/implementation/2026-04-26-enterprise-auto-fill-implementation.md`
- 记录修改内容、影响范围、验证情况。
## Task 1: EnterpriseSource 枚举与接口契约
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/EnterpriseSource.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiEnumControllerTest.java`
- [ ] **Step 1: 写失败测试**
`CcdiEnumControllerTest#getEnterpriseSourceOptions_shouldReturnConfiguredOptions` 中断言返回值包含 `SUPPLIER/供应商`
```java
assertTrue(data.stream()
.map(EnumOptionVO.class::cast)
.anyMatch(option ->
"SUPPLIER".equals(option.getValue()) && "供应商".equals(option.getLabel())));
```
- [ ] **Step 2: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest test
```
Expected: FAIL提示未找到 `SUPPLIER`
- [ ] **Step 3: 实现枚举**
`EnterpriseSource` 中新增:
```java
SUPPLIER("SUPPLIER", "供应商"),
```
保持 `contains``resolveCode``getDescByCode` 通过 `values()` 自动生效。
- [ ] **Step 4: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest test
```
Expected: PASS。
## Task 2: 实体库自动补全服务
**Files:**
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillServiceTest.java`
- [ ] **Step 1: 写服务测试**
覆盖以下行为:
- 已存在实体不插入、不覆盖。
- 缺失实体插入最小记录。
- 中介来源写 `riskLevel=1`
- 员工亲属、信贷客户、供应商来源写 `riskLevel=null`
- 批量同一信用代码只插一次,并使用首次有效名称。
- 插入时遇到主键重复按已存在处理。
核心断言示例:
```java
assertEquals("SUPPLIER", captured.getEntSource());
assertNull(captured.getRiskLevel());
assertEquals("IMPORT", captured.getDataSource());
```
- [ ] **Step 2: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=EnterpriseAutoFillServiceTest test
```
Expected: FAIL类不存在。
- [ ] **Step 3: 实现服务接口**
创建内部记录类型和方法:
```java
@Service
public class EnterpriseAutoFillService {
@Resource
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
public record EnterpriseFillItem(
String socialCreditCode,
String enterpriseName,
String entSource,
String dataSource,
String userName
) {}
@Transactional
public void ensureExists(EnterpriseFillItem item) {
ensureExistsBatch(List.of(item));
}
@Transactional
public void ensureExistsBatch(List<EnterpriseFillItem> items) {
// trim、过滤空信用代码、按 socialCreditCode 首次出现去重
// selectBatchIds 查询已存在记录
// 组装 CcdiEnterpriseBaseInfo 最小实体
// riskLevel: INTERMEDIARY -> "1",其他 -> null
// dataSource: MANUAL 或 IMPORT
// 分批调用 enterpriseBaseInfoMapper.insertBatch
// 捕获 DuplicateKeyException 后继续逐条 selectById/insert重复则忽略
}
}
```
实现注意:
- 不调用 `CcdiEnterpriseBaseInfoServiceImpl#insertEnterpriseBaseInfo`,避免复用手工新增风险等级校验。
- 对非中介来源显式 `setRiskLevel(null)`
- 不更新已存在实体。
- `enterpriseName` 使用来源业务已通过校验的名称,不增加额外兜底。
- [ ] **Step 4: 运行服务测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=EnterpriseAutoFillServiceTest test
```
Expected: PASS。
## Task 3: 员工亲属实体关联接入
**Files:**
- Modify: `CcdiStaffEnterpriseRelationServiceImpl.java`
- Modify: `CcdiStaffEnterpriseRelationImportServiceImpl.java`
- Modify: `CcdiStaffEnterpriseRelationServiceImplTest.java`
- Modify: `CcdiStaffEnterpriseRelationImportServiceImplTest.java`
- [ ] **Step 1: 写新建测试**
`insertRelation_shouldAllowValidFamily` 中注入 `EnterpriseAutoFillService` mock并验证
```java
verify(enterpriseAutoFillService).ensureExists(argThat(item ->
"91310000123456789A".equals(item.socialCreditCode())
&& "测试企业".equals(item.enterpriseName())
&& "EMP_RELATION".equals(item.entSource())
&& "MANUAL".equals(item.dataSource())));
```
- [ ] **Step 2: 写导入测试**
扩展导入测试,验证成功行调用批量补入,失败行不进入补入集合。
- [ ] **Step 3: 运行员工亲属测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest test
```
Expected: FAIL尚未调用自动补全服务。
- [ ] **Step 4: 实现新建接入**
`insertRelation` 中,`existsByPersonIdAndSocialCreditCode` 通过后、`relationMapper.insert` 前调用:
```java
enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem(
addDTO.getSocialCreditCode(),
addDTO.getEnterpriseName(),
EnterpriseSource.EMP_RELATION.getCode(),
DataSource.MANUAL.getCode(),
SecurityUtils.getUsername()
));
```
- [ ] **Step 5: 实现导入接入**
`importRelationAsync` 成功构建 `newRecords` 后、`saveBatch(newRecords, 500)` 前,按成功记录组装补入集合:
```java
enterpriseAutoFillService.ensureExistsBatch(newRecords.stream()
.map(item -> new EnterpriseFillItem(item.getSocialCreditCode(), item.getEnterpriseName(),
EnterpriseSource.EMP_RELATION.getCode(), DataSource.IMPORT.getCode(), userName))
.toList());
```
- [ ] **Step 6: 运行员工亲属测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest test
```
Expected: PASS。
## Task 4: 信贷客户实体关联接入
**Files:**
- Modify: `CcdiCustEnterpriseRelationServiceImpl.java`
- Modify: `CcdiCustEnterpriseRelationImportServiceImpl.java`
- Test: add `CcdiCustEnterpriseRelationServiceImplTest.java` if missing
- Test: add or extend `CcdiCustEnterpriseRelationImportServiceImplTest.java`
- [ ] **Step 1: 写新建测试**
验证 `insertRelation` 成功时调用自动补全:
```java
assertEquals("CREDIT_CUSTOMER", item.entSource());
assertEquals("MANUAL", item.dataSource());
```
- [ ] **Step 2: 写导入测试**
准备一条成功、一条重复组合失败,验证只有成功行传入 `ensureExistsBatch`
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest test
```
Expected: FAIL尚未调用自动补全服务。
- [ ] **Step 4: 实现新建接入**
`insertRelation` 唯一性校验后、插入前调用自动补全,来源 `CREDIT_CUSTOMER`,数据来源 `MANUAL`
- [ ] **Step 5: 实现导入接入**
`importRelationAsync` 成功记录批量插入前调用自动补全,来源 `CREDIT_CUSTOMER`,数据来源 `IMPORT`
- [ ] **Step 6: 运行测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest test
```
Expected: PASS。
## Task 5: 中介实体关联和中介实体管理规则
**Files:**
- Modify: `CcdiIntermediaryServiceImpl.java`
- Modify: `CcdiIntermediaryEnterpriseRelationImportServiceImpl.java`
- Modify: `CcdiEnterpriseBaseInfoImportServiceImpl.java`
- Modify: `CcdiIntermediaryServiceImplTest.java`
- Modify: `CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java`
- Modify: `CcdiEnterpriseBaseInfoImportServiceImplTest.java`
- [ ] **Step 1: 写中介实体关联新建测试**
验证实体库缺失不再抛“关联机构不存在”,而是调用自动补全并插入关联:
```java
when(enterpriseRelationMapper.existsByIntermediaryBizIdAndSocialCreditCode("owner-biz", uscc)).thenReturn(false);
verify(enterpriseAutoFillService).ensureExists(argThat(item ->
"INTERMEDIARY".equals(item.entSource()) && "MANUAL".equals(item.dataSource())));
verify(enterpriseRelationMapper).insert(any(CcdiIntermediaryEnterpriseRelation.class));
```
- [ ] **Step 2: 写中介实体关联导入测试**
将现有 `importEnterpriseRelationAsync_shouldFailWhenEnterpriseDoesNotExist` 改成成功场景,断言:
- 不再产生失败记录。
- 调用 `ensureExistsBatch`
- 插入关联记录。
- [ ] **Step 3: 写中介库管理默认高风险测试**
`CcdiEnterpriseBaseInfoImportServiceImplTest` 增加:
```java
excel.setRiskLevel(null);
excel.setEntSource("中介");
CcdiEnterpriseBaseInfo entity = service.validateAndBuildEntity(excel, Set.of(), new HashSet<>(), "admin");
assertEquals("1", entity.getRiskLevel());
assertEquals("INTERMEDIARY", entity.getEntSource());
```
`CcdiIntermediaryServiceImplTest` 验证 `insertIntermediaryEntity` 未传风险等级时写入 `1`
- [ ] **Step 4: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest test
```
Expected: FAIL。
- [ ] **Step 5: 实现新建接入**
修改 `validateEnterpriseRelation`:保留中介本人和重复组合校验,删除 `enterpriseBaseInfoMapper.selectById(socialCreditCode) == null` 抛错。
`insertIntermediaryEnterpriseRelation` 插入前调用自动补全,来源 `INTERMEDIARY`,数据来源 `MANUAL`
- [ ] **Step 6: 实现导入接入**
在导入服务中删除 `getExistingEnterpriseCodes` 的失败判断。成功记录插入前按 Excel 行组装实体补入,来源 `INTERMEDIARY`,数据来源 `IMPORT`
- [ ] **Step 7: 实现中介实体默认高风险**
`insertIntermediaryEntity` 中,如果 `riskLevel` 为空,设置为 `"1"`
`CcdiEnterpriseBaseInfoImportServiceImpl#validateAndBuildEntity` 中,当解析出的 `entSource``INTERMEDIARY``riskLevel` 为空时,设置 `"1"`
- [ ] **Step 8: 运行测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest test
```
Expected: PASS。
## Task 6: 招投标供应商接入
**Files:**
- Modify: `CcdiPurchaseTransactionServiceImpl.java`
- Modify: `CcdiPurchaseTransactionImportServiceImpl.java`
- Test: add `CcdiPurchaseTransactionServiceImplTest.java` if missing
- Modify: `CcdiPurchaseTransactionFeatureContractTest.java` or add import service unit test
- [ ] **Step 1: 写新建测试**
验证 `insertTransaction` 成功时,仅对 `supplierUscc` 不为空的供应商调用自动补全:
```java
assertEquals("SUPPLIER", item.entSource());
assertEquals("MANUAL", item.dataSource());
assertEquals("供应商A", item.enterpriseName());
```
- [ ] **Step 2: 写导入测试**
准备一个成功采购事项和一个失败采购事项,断言只有成功事项的供应商进入 `ensureExistsBatch`,且来源为 `SUPPLIER`、数据来源为 `IMPORT`
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest,CcdiPurchaseTransactionFeatureContractTest test
```
Expected: FAIL。
- [ ] **Step 4: 实现新建接入**
`insertTransaction` 中,`buildSupplierEntities` 和校验完成后、写主从表前,收集供应商:
```java
enterpriseAutoFillService.ensureExistsBatch(supplierList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getSupplierUscc()))
.map(item -> new EnterpriseFillItem(item.getSupplierUscc(), item.getSupplierName(),
EnterpriseSource.SUPPLIER.getCode(), DataSource.MANUAL.getCode(), SecurityUtils.getUsername()))
.toList());
```
- [ ] **Step 5: 实现导入接入**
`importTransactionAsync` 中,按成功构建的 `newSuppliers` 收集供应商实体,在 `saveBatch(newTransactions, 500)` 之前调用自动补全。失败事项的供应商不进入 `newSuppliers`,天然不补。
- [ ] **Step 6: 运行测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest,CcdiPurchaseTransactionFeatureContractTest test
```
Expected: PASS。
## Task 7: 集成验证与实施记录
**Files:**
- Create: `docs/reports/implementation/2026-04-26-enterprise-auto-fill-implementation.md`
- [ ] **Step 1: 运行后端相关测试集合**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest,EnterpriseAutoFillServiceTest,CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest,CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiEnterpriseBaseInfoImportServiceImplTest,CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest,CcdiPurchaseTransactionFeatureContractTest test
```
Expected: BUILD SUCCESS。
- [ ] **Step 2: 如涉及数据库实测,确认 `risk_level` 落库值**
验证样本:
- 员工亲属自动补入:`risk_level IS NULL`
- 信贷客户自动补入:`risk_level IS NULL`
- 招投标供应商自动补入:`risk_level IS NULL`
- 中介自动补入:`risk_level = '1'`
- [ ] **Step 3: 写实施记录**
实施记录至少包含:
```markdown
# 关联业务自动补入实体库实施记录
## 修改内容
- 新增实体库自动补全服务
- 接入员工亲属、中介、信贷客户、招投标链路
- 新增 SUPPLIER 企业来源
## 影响范围
- ccdi-info-collection 后端服务
- 实体库管理企业来源枚举接口
## 验证情况
- 列出 Maven 测试命令与结果
- 列出页面或数据库验证结果
```
- [ ] **Step 4: 检查工作区**
Run:
```bash
git status --short
```
Expected: 仅包含本次功能相关源码、测试和实施记录,不包含 `.DS_Store` 或生成测试文件。
- [ ] **Step 5: 提交后端改动**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection ccdi-info-collection/src/test/java/com/ruoyi/info/collection docs/reports/implementation/2026-04-26-enterprise-auto-fill-implementation.md
git commit -m "实现关联业务自动补入实体库"
```

View File

@@ -0,0 +1,244 @@
# 关联业务自动补入实体库 Frontend Implementation Plan
> **执行约束:** 按当前项目 `AGENTS.md` 执行;未获得用户明确要求时不启用 subagent。Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 验证前端通过现有企业来源枚举接口展示新增 `SUPPLIER=供应商`,并在真实页面确认自动补入实体库后的展示链路可用。
**Architecture:** 本次不新增前端交互,不修改前端源码。企业来源选项由后端 `/ccdi/enum/enterpriseSource` 返回,实体库管理页与招投标详情页沿用 `getEnterpriseSourceOptions()` 展示新增来源;前端工作重点是运行真实页面验证并记录结果。
**Tech Stack:** Vue 2, Element UI, npm, nvm, Playwright.
---
## File Structure
- No source changes expected: `ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue`
- 已通过 `getEnterpriseSourceOptions()` 获取企业来源。
- No source changes expected: `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- 企业详情弹窗已通过 `getEnterpriseSourceOptions()` 格式化企业来源。
- Create or update: `docs/reports/implementation/2026-04-26-enterprise-auto-fill-implementation.md`
- 补充前端真实页面验证结果。
- Generated test files:
- 如需生成导入样本,放在 `output/playwright/``output/spreadsheet/`,不提交到 git。
## Task 1: 前端源码确认
**Files:**
- Read: `ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue`
- Read: `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- Read: `ruoyi-ui/src/api/ccdiEnum.js`
- [ ] **Step 1: 确认企业来源接口使用点**
Run:
```bash
rg -n "getEnterpriseSourceOptions|formatEnterpriseSource|enterpriseSourceOptions" ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue ruoyi-ui/src/api/ccdiEnum.js
```
Expected:
- 实体库管理页调用 `getEnterpriseSourceOptions()`
- 招投标详情企业弹窗调用 `getEnterpriseSourceOptions()`
- API 路径为 `/ccdi/enum/enterpriseSource`
- [ ] **Step 2: 确认不存在本地硬编码映射**
Expected: 未发现页面本地写死企业来源映射;如发现硬编码映射,停止实施并先修订本计划。
## Task 2: 前端启动准备
**Files:**
- Read: `ruoyi-ui/package.json`
- Use: `ruoyi-ui/.nvmrc` if present
- [ ] **Step 1: 使用 nvm 确认 Node 版本**
Run:
```bash
cd ruoyi-ui
source ~/.nvm/nvm.sh
nvm use
node -v
```
Expected: 切换到项目要求的 Node 版本。
- [ ] **Step 2: 启动前端开发服务**
Run:
```bash
cd ruoyi-ui
source ~/.nvm/nvm.sh
nvm use
npm run dev
```
Expected: 前端服务启动成功,记录实际 URL。若端口占用按 Vite/Vue CLI 输出使用实际端口。
- [ ] **Step 3: 启动后端**
Run:
```bash
sh bin/restart_java_backend.sh
```
Expected: 后端 `62318` 可访问。
测试结束后必须关闭本次启动的前后端进程。
## Task 3: 实体库管理页面验证
**Files:**
- Verify: real page `ruoyi-ui/src/views/ccdiEnterpriseBaseInfo/index.vue`
- Do not use: prototype pages
- [ ] **Step 1: Playwright 打开真实实体库管理页面**
进入实际路由:
```text
http://localhost:8080/maintain/enterpriseBaseInfo
```
Expected: 页面正常加载。
- [ ] **Step 2: 验证企业来源下拉包含供应商**
操作:
- 打开查询区“企业来源”下拉。
- 检查存在“供应商”选项。
Expected: 下拉出现“供应商”。
- [ ] **Step 3: 验证列表/详情展示**
准备后端自动补入的一条 `ent_source=SUPPLIER` 测试企业后:
- 在实体库管理页面搜索该统一社会信用代码。
- 检查列表企业来源显示“供应商”。
- 打开详情,检查企业来源显示“供应商”,风险等级为空时显示为空值占位。
Expected: 枚举中文展示正确。
## Task 4: 招投标真实页面验证
**Files:**
- Verify: `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- [ ] **Step 1: 打开真实招投标信息维护页面**
进入实际路由,例如:
```text
http://localhost:8080/maintain/purchaseTransaction
```
Expected: 页面正常加载。
- [ ] **Step 2: 新建含供应商统一信用代码的招投标记录**
使用真实页面新增测试数据:
- 采购事项 ID 使用本轮唯一测试值。
- 供应商明细中至少一条填写供应商名称和统一信用代码。
Expected: 保存成功。
- [ ] **Step 3: 回到实体库管理验证供应商自动补入**
用供应商统一信用代码查询实体库。
Expected:
- 能查到实体库记录。
- 企业名称为供应商名称。
- 企业来源显示“供应商”。
- 风险等级为空。
- [ ] **Step 4: 清理测试数据**
删除本轮新建的招投标测试数据和自动补入的实体库测试数据。若实体库记录已有关联限制,先删除业务数据再删除实体库记录。
Expected: 页面列表回到测试前状态。
## Task 5: 导入页面验证
**Files:**
- Generated samples: `output/playwright/` or `output/spreadsheet/`
- [ ] **Step 1: 在真实页面下载导入模板**
必须从当前业务页面点击下载模板,不手工凭记忆构造表头。
Expected: 获取当前模板。
- [ ] **Step 2: 基于模板生成测试文件**
至少覆盖:
- 员工亲属实体关联:页面 `/maintain/staffEnterpriseRelation`,点击“导入”后在弹窗中点击“下载模板”,接口 `ccdi/staffEnterpriseRelation/importTemplate`,上传接口 `/ccdi/staffEnterpriseRelation/importData`,验证成功行实体自动补入 `EMP_RELATION`
- 信贷客户实体关联:页面 `/maintain/custEnterpriseRelation`,点击“导入”后在弹窗中点击“下载模板”,接口 `ccdi/custEnterpriseRelation/importTemplate`,上传接口 `/ccdi/custEnterpriseRelation/importData`,验证成功行实体自动补入 `CREDIT_CUSTOMER`
- 中介实体关联:页面 `/maintain/intermediary`,点击“导入中介实体关联关系”,在导入弹窗下载模板,接口 `ccdi/intermediary/importEnterpriseRelationTemplate`,上传接口 `/ccdi/intermediary/importEnterpriseRelationData`,验证成功行实体自动补入 `INTERMEDIARY` 且风险等级高风险。
- 招投标信息维护:页面 `/maintain/purchaseTransaction`,点击“导入”后在弹窗中点击“下载模板”,接口 `ccdi/purchaseTransaction/importTemplate`,上传接口 `/ccdi/purchaseTransaction/importData`,验证供应商统一信用代码自动补入 `SUPPLIER`
- 每个页面至少包含一个混合成功失败样本,验证失败行不补实体。
Expected: 测试文件保存在 `output/playwright/``output/spreadsheet/`,不提交 git。
- [ ] **Step 3: 上传并核对导入状态**
在真实页面上传文件,核对:
- 页面提示
- 导入状态
- 失败记录弹窗
- 列表总数变化
- 实体库是否新增对应实体
Expected: 成功行补实体,失败行不补实体。
- [ ] **Step 4: 清理测试数据和任务缓存**
删除本轮成功写入的业务数据和实体库数据,清理页面本地导入任务缓存。
Expected: 页面和数据库不残留本轮测试数据。
## Task 6: 记录验证结果
**Files:**
- Modify: `docs/reports/implementation/2026-04-26-enterprise-auto-fill-implementation.md`
- [ ] **Step 1: 补充前端验证记录**
记录:
```markdown
## 前端验证
- Node 版本:
- 前端 URL
- 后端 URL
- 实体库企业来源“供应商”展示:
- 招投标供应商自动补入页面验证:
- 导入页面验证:
- 测试数据清理:
```
- [ ] **Step 2: 停止测试进程**
停止本次启动的前端和后端进程。
Expected: 无测试进程残留。
- [ ] **Step 3: 检查生成文件未进入 git**
Run:
```bash
git status --short
```
Expected: `output/playwright/``output/spreadsheet/` 下生成测试文件不在待提交范围。

View File

@@ -0,0 +1,27 @@
# AGENTS.md 使用 browser-use 技能同步实施记录
## 变更内容
- 更新 `/Users/wkc/Desktop/ccdi/ccdi/AGENTS.md`
- 将页面功能开发完成后的真实页面测试要求由调用 Playwright 调整为使用 `browser-use` 技能打开浏览器测试。
- 将导入测试文件推荐目录由 `output/playwright/` 调整为 `output/browser-use/`
## 影响范围
- 影响后续代理执行前端页面功能开发后的浏览器测试方式。
- 影响后续导入测试临时产物的推荐保存目录。
- 不涉及业务代码、脚本、接口或数据库变更。
## 保存路径确认
- 本次实施记录保存路径为 `/Users/wkc/Desktop/ccdi/ccdi/docs/reports/implementation/`
- 已确认该路径为 `ccdi` 项目既有实施记录目录。
## 验证情况
- 已确认 `AGENTS.md` 中页面功能测试规则改为使用 `browser-use` 技能。
- 已确认 `AGENTS.md` 中测试产物目录改为 `output/browser-use/`
## 说明
- 历史实施记录、历史计划和历史设计文档中的 Playwright 表述保留为当时执行记录,不作为本次规则同步范围。

View File

@@ -0,0 +1,22 @@
# AGENTS.md 更新实施记录
## 修改时间
2026-04-26
## 修改内容
- 在根目录 `AGENTS.md` 中补充全局执行规则,覆盖 Git、AGENT、文档、测试与方案规范。
- 明确 `using-superpowers` 与 subagent 的启用条件及模型要求。
- 明确设计文档审查、前后端实施计划拆分、实施文档留存、Playwright 页面测试、测试文件不提交等要求。
## 影响范围
- 仅影响 AI 编码助手在本仓库内的协作规则与执行约束。
- 未修改业务代码、数据库脚本、前端页面或后端接口。
## 验证情况
- 已确认 `AGENTS.md` 位于仓库根目录。
- 已确认实施记录保存路径为 `docs/reports/implementation/`
- 本次为文档规则更新,不涉及编译、单元测试或页面测试。

View File

@@ -0,0 +1,16 @@
# nvmrc 配置实施记录
## 修改内容
- 在仓库根目录新增 `.nvmrc`,统一指定 Node 版本为 `14.21.3`
- 保留并对齐前端目录 `ruoyi-ui/.nvmrc` 的既有配置,确保在仓库根目录或前端目录执行 `nvm use` 时使用同一 Node 版本。
## 影响范围
- 仅影响本地前端开发、构建、调试命令的 Node 版本选择。
- 不涉及后端代码、数据库结构、菜单权限或业务逻辑调整。
## 验证情况
- 已检查 `ruoyi-ui/.nvmrc` 当前内容为 `14.21.3`
- 已检查 `ruoyi-ui/package.json`,当前前端为 Vue 2 / Vue CLI 4 依赖栈,适合继续使用 Node `14.21.3`

View File

@@ -0,0 +1,88 @@
# 生产服务器 Java 后端启动脚本实施记录
## 保存路径确认
- 本次新增生产服务器后端启停脚本,实施记录保存到 `docs/reports/implementation/`,符合仓库实施文档目录规范。
## 修改目标
- 新写一个可在生产服务器上运行的 Java 后端启停脚本。
- 脚本支持配置 Java Home不依赖 Maven不执行本地构建只负责运行已上传到服务器的 `ruoyi-admin.jar`
## 修改内容
- 新增 `deploy/start-java-backend-prod.sh`
- 在脚本顶部新增“生产配置区”,生产服务器上的 Java Home、Jar 目录、Profile、JVM 参数和额外应用参数均直接写在脚本文件中。
- 按生产服务器目录结构调整默认 Jar 路径:启动脚本位于外层目录,后端 Jar 位于 `backend/ruoyi-admin.jar`
- 通过脚本内 `APP_HOME="${SCRIPT_DIR}/backend"` 指定生产服务器上的 Jar 所在目录。
- 通过脚本内 `BACKEND_JAVA_HOME` 指定脚本使用的 JDK优先级高于系统 `JAVA_HOME`
- 脚本内 `BACKEND_JAVA_HOME` 留空时读取系统 `JAVA_HOME`;两者都未配置时使用 `PATH` 中的 `java`
- 支持 `start``stop``restart``status``logs` 操作。
- `start` 会先调用 `stop_backend`,通过 `ps -ef` 关闭旧后端进程,再启动新的 `backend/ruoyi-admin.jar`
- `start``restart` 在后端启动成功后会自动持续输出 `backend/logs/backend-console.log`,按 `Ctrl+C` 仅退出日志查看,不停止后端进程。
- 支持 `stop` 单独停止后端进程。
- 使用 `APP_MARKER` 标记脚本启动的新进程,停止旧进程时统一通过 `ps -ef` 扫描进程列表,匹配当前 Jar 绝对路径或生产目录下的相对路径 `backend/ruoyi-admin.jar`
- `stop` 可停止没有脚本标记但由同一 `backend/ruoyi-admin.jar` 启动的旧进程,用于覆盖生产服务器已有手工启动进程。
- 进程扫描会忽略 `<defunct>` 行,避免僵尸进程或历史残留干扰启停判断。
-`ps -ef` 执行失败,脚本会明确报错并中止旧进程扫描,避免误判为“后端未运行”。
- 默认 Spring Profile 为 `uat`,可通过 `SPRING_PROFILES_ACTIVE` 覆盖。
## 使用方式
`deploy/start-java-backend-prod.sh` 放到生产服务器,并先修改脚本顶部“生产配置区”:
```bash
BACKEND_JAVA_HOME=""
APP_HOME="${SCRIPT_DIR}/backend"
JAR_NAME="ruoyi-admin.jar"
SPRING_PROFILES_ACTIVE="uat"
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
APP_ARGS=""
```
配置完成后直接执行:
常用命令:
```bash
./start-java-backend-prod.sh start
./start-java-backend-prod.sh stop
./start-java-backend-prod.sh restart
./start-java-backend-prod.sh status
./start-java-backend-prod.sh logs
```
## 验证记录
- 执行 `bash -n deploy/start-java-backend-prod.sh`
- 结果:通过
- 说明:脚本 Bash 语法正确。
- 执行 `bash deploy/start-java-backend-prod.sh help`
- 结果:通过
- 说明:帮助信息正常输出,并说明启动成功后会持续输出控制台日志。
- 执行 `rg -n "start_backend|follow_logs" deploy/start-java-backend-prod.sh`
- 结果:通过
- 说明:已确认 `start``restart` 分支均使用 `start_action`,流程为先 `stop_backend`,再 `start_backend`,最后 `follow_logs`
- 执行 `bash deploy/start-java-backend-prod.sh status`
- 结果:通过
- 说明:在允许执行 `ps -ef` 后,无后端进程时可正常输出未运行状态。
- 执行 `rg -n "pgrep" deploy/start-java-backend-prod.sh`
- 结果:无匹配
- 说明:已确认停止旧进程不再依赖 `pgrep`
- 执行 `rg -n "ps -ef" deploy/start-java-backend-prod.sh`
- 结果:通过
- 说明:已确认旧进程扫描逻辑使用 `ps -ef`
- 使用临时脚本副本和临时后端目录启动一个命令行包含 `-jar backend/ruoyi-admin.jar`、但不带脚本标记的模拟旧进程,再执行 `bash /tmp/start-java-backend-prod-test.sh stop`
- 结果:通过
- 说明:已验证 `stop` 可以停止同一 Jar 路径的旧进程,不要求旧进程必须由当前脚本启动。
- 修改临时脚本副本,将脚本内 `BACKEND_JAVA_HOME` 设置为 `/not-exist` 后执行 `bash /tmp/start-java-backend-prod-test.sh start`
- 结果:按预期失败
- 说明:脚本能在启动前拦截无效 Java Home并输出明确错误。
- 执行 `bash deploy/start-java-backend-prod.sh start`
- 结果:按预期失败
- 说明:脚本能正确解析当前 Java 命令,并在当前本地未提供 `deploy/backend/ruoyi-admin.jar` 时中止启动。
## 影响范围
- 仅新增生产服务器后端启停脚本与本实施记录。
- 不修改 Java 业务代码、数据库脚本、前端页面和现有发布包生成脚本。

View File

@@ -0,0 +1,68 @@
# 生产上线部署脚本实施记录
## 保存路径确认
- 本次为生产上线部署脚本改动,实施记录保存到 `docs/reports/implementation/`,符合仓库实施文档目录规范。
## 修改目标
- 生成一个可放在上线环境执行的部署脚本。
- 上线环境目录下已有 `backend/``frontend/` 和一个上线压缩包。
- 上线压缩包根层包含 `ruoyi-admin.jar``dist.zip`
- 执行脚本后先备份 `backend/``frontend/` 旧文件,再解压上线包并部署到对应目录,最后调用 `start-java-backend-prod.sh` 重启后端并输出日志。
## 修改内容
- 新增 `deploy/deploy-release-prod.sh`
- 默认按脚本同级目录解析 `backend/``frontend/``start-java-backend-prod.sh` 和上线压缩包。
- 使用 `/bin/sh` 写法,避免依赖 Bash 进程替换等服务器环境不一定支持的语法。
- 支持显式传入上线压缩包路径:`./deploy-release-prod.sh /path/to/ccdi_YYYYMMDD.zip`
- 未传入压缩包时,自动使用脚本同级目录下唯一的 `.zip` 文件,并排除 `dist.zip`
- 部署前将 `backend/``frontend/` 当前内容备份到 `backups/YYYYMMDDHHMMSS/`
- 解压上线包后校验根层必须存在 `ruoyi-admin.jar``dist.zip`
- 解压 `dist.zip` 后校验必须存在 `dist/index.html`
- 后端部署为覆盖 `backend/ruoyi-admin.jar`
- 前端部署为清空 `frontend/` 后复制 `dist/` 内文件到 `frontend/`
- 部署完成后执行 `bash start-java-backend-prod.sh restart`,由现有启动脚本完成后端重启并持续输出后端日志。
## 使用方式
生产环境目录结构:
```text
上线目录/
├── deploy-release-prod.sh
├── start-java-backend-prod.sh
├── backend/
├── frontend/
└── ccdi_YYYYMMDD.zip
```
执行:
```bash
./deploy-release-prod.sh
```
或显式指定压缩包:
```bash
./deploy-release-prod.sh /path/to/ccdi_YYYYMMDD.zip
```
## 验证记录
- 执行 `sh -n deploy/deploy-release-prod.sh`
- 结果:通过
- 说明:脚本 Shell 语法正确。
- 执行 `sh deploy/deploy-release-prod.sh --help`
- 结果:通过
- 说明:帮助信息正常输出。
- 使用 `/tmp` 构造最小上线目录、旧 `backend/`、旧 `frontend/`、上线压缩包和假的 `start-java-backend-prod.sh` 后执行部署脚本
- 结果:通过
- 说明:已验证旧文件备份、新 Jar 覆盖、前端 `dist/` 文件部署,以及最终调用启动脚本 `restart`
## 影响范围
- 仅新增生产上线部署脚本与本实施记录。
- 不修改 Java 业务代码、前端业务代码、数据库脚本和现有后端启动脚本。

View File

@@ -0,0 +1,78 @@
# 生产上线初始化 SQL 生成实施记录
## 保存路径确认
- 生产初始化 SQL`sql/ccdi_prod_init_20260428.sql`
- 实施记录:`docs/reports/implementation/2026-04-28-production-init-sql-implementation.md`
## 修改内容
- 新增 `sql/ccdi_prod_init_20260428.sql`,用于生产空库初始化。
- SQL 内容包含当前 `ccdi` 库最终态的 57 张表结构。
- SQL 必要数据范围:
- 若依基础配置、部门、岗位、用户、角色、菜单、角色菜单、字典、定时任务、公告。
- CCDI 默认模型参数,仅包含 `ccdi_model_param.project_id = 0` 的系统默认参数。
- 流水打标规则 `ccdi_bank_tag_rule`
- SQL 不包含运行期业务数据:
- 项目、员工、流水、导入记录、风险结果、采购事项、实体库、中介库、操作日志、登录日志等数据均保持空表。
- 将导出结构中的非规范排序规则统一修正为 `utf8mb4_general_ci`,未保留 `utf8mb4_0900_ai_ci`
- 针对生产执行时报错 `Specified key was too long; max key length is 767 bytes`,按生产要求删除旧库 767 bytes 限制下会超长的索引定义,保留字段长度、表结构和必要初始化数据不变。
- 删除的超长索引范围:
- Quartz 表中的长字符复合主键和依赖这些长字符复合键的外键索引。
- `ccdi_account_info.idx_ccdi_account_info_account_no`
- `ccdi_asset_info.idx_family_person`
- `ccdi_bank_statement.uk_bank_statement_dedup`
- `ccdi_bank_statement.idx_batch_id_account`
- `ccdi_bank_statement.c4c_bank_statement_stg_batch_id_IDX`
- `ccdi_bank_statement_tag_result.uk_ccdi_bank_tag_object_hit`
- `ccdi_enterprise_base_info.idx_enterprise_name`
- `ccdi_evidence.idx_ccdi_evidence_source`
- `ccdi_model_param.uk_project_model_param`
- `ccdi_project.idx_project_name`
## 验证情况
- 使用本机临时 MySQL 实例导入 `sql/ccdi_prod_init_20260428.sql` 验证通过。
- 导入后验证结果:
- 表数量57。
-`utf8mb4_general_ci` 表数量0。
- 基础数据行数:
- `sys_config`8。
- `sys_dept`10。
- `sys_dict_type`26。
- `sys_dict_data`98。
- `sys_menu`166。
- `sys_role`2。
- `sys_role_menu`134。
- `sys_user`3。
- `sys_job`3。
- `sys_notice`2。
- `ccdi_bank_tag_rule`35。
- `ccdi_model_param`17且全部为 `project_id = 0`
- 业务数据抽查为空:
- `ccdi_project`0。
- `ccdi_base_staff`0。
- `ccdi_bank_statement`0。
- `ccdi_file_upload_record`0。
- `ccdi_purchase_transaction`0。
- 测试完成后已关闭本机临时 MySQL 实例。
- 生产索引长度修复后,再次计算脚本内所有剩余索引长度,确认超过 767 bytes 的索引数量为 0。
- 删除超长索引后,再次使用本机临时 MySQL 实例导入验证通过:
- 表数量57。
-`utf8mb4_general_ci` 表数量0。
- `ccdi_model_param`17。
- `ccdi_bank_tag_rule`35。
- `sys_menu`166。
- `ccdi_project`0。
- `ccdi_bank_statement`0。
## 执行说明
- 目标生产库需为空库。
- 目标库字符集和排序规则建议使用:
```sql
CREATE DATABASE ccdi DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
```
- 导入时需使用 `utf8mb4` 会话字符集。

View File

@@ -0,0 +1,35 @@
# CCDI 上线压缩包生成脚本实施记录
## 修改内容
- 新增根目录脚本 `build_release_ccdi.sh`
- 脚本执行后会重新构建后端 `ruoyi-admin.jar`,并进入 `ruoyi-ui` 通过 `nvm use` 切换前端 Node 版本后执行 `npm run build:prod`
- 脚本会在根目录生成 `ccdi_YYYYMMDD.zip`,压缩包根层仅包含 `ruoyi-admin.jar``dist.zip`,不再额外包裹 `deploy` 目录。
- `.gitignore` 新增 `/ccdi_????????.zip`,避免生成的上线压缩包进入 Git。
## 影响范围
- 仅新增发布包生成脚本与忽略规则,不修改业务代码。
- 临时打包目录使用 `.deploy/ccdi-release-package/`,该目录已作为本地部署产物被 Git 忽略。
## 使用方式
```bash
./build_release_ccdi.sh
```
生成结果示例:
```text
ccdi_20260428.zip
├── ruoyi-admin.jar
└── dist.zip
```
## 验证情况
- 已执行 `sh -n build_release_ccdi.sh`,脚本语法检查通过。
- 已执行 `git diff --check`,未发现空白错误。
- 已执行 `./build_release_ccdi.sh`,后端 Maven 打包成功,前端生产构建成功,并生成 `ccdi_20260428.zip`
- 已执行 `unzip -l ccdi_20260428.zip`,确认压缩包根层仅包含 `ruoyi-admin.jar``dist.zip` 两个文件。
- 已执行 `git check-ignore -v ccdi_20260428.zip`,确认根目录上线压缩包会被 `.gitignore` 忽略。

View File

@@ -0,0 +1,337 @@
# 关联业务自动补入实体库设计
## 1. 背景
当前信息维护中多条业务链路都会录入或导入统一社会信用代码和企业名称,但实体库 `ccdi_enterprise_base_info` 不一定已经存在对应企业。现状中部分链路只保存业务关联表,部分链路会因为实体库不存在而失败,导致“关联业务已经知道企业信息,但实体库没有沉淀”的数据断点。
本次需求要求以下模块在新建、导入时,如果要关联的实体在系统实体库不存在,则自动将不存在的实体添加到实体库中:
- 员工亲属实体关联
- 中介库与实体关联
- 信贷客户实体关联
- 招投标信息维护
本次设计按最短路径实现,不增加兼容性分支,不扩展用户未提出的兜底或降级逻辑。
## 2. 目标与范围
### 2.1 目标
在保留现有页面交互和业务校验规则的前提下,为上述四类业务的新建和导入链路补齐实体库自动沉淀能力,保证业务数据成功落库时,对应实体也已存在于实体库。
### 2.2 本次范围
- 新增后端内部实体库自动补全能力
- 改造员工亲属实体关联新建、导入链路
- 改造中介实体关联新建、导入链路
- 改造信贷客户实体关联新建、导入链路
- 改造招投标信息维护新建、导入链路中的供应商实体补入
- 新增企业来源 `SUPPLIER=供应商`
- 保证中介库管理新增、导入实体时风险等级默认为高风险
- 补充对应后端、前端与测试文档
### 2.3 非本次范围
- 不修改实体库主键规则,仍以统一社会信用代码作为实体唯一标识
- 不在实体库已存在时覆盖企业名称、来源、风险等级、数据来源等字段
- 不从外部接口拉取企业工商详情
- 不为没有统一社会信用代码的招投标供应商创建实体库记录
- 不新增用户确认弹窗或前端交互分支
- 不修改关联表与招投标主从表结构
## 3. 现状分析
### 3.1 实体库
实体库表为 `ccdi_enterprise_base_info`,主键为 `social_credit_code`。核心字段包括:
- `social_credit_code`
- `enterprise_name`
- `risk_level`
- `ent_source`
- `data_source`
- `created_by`
- `updated_by`
当前企业来源枚举包含:
- `GENERAL`:一般企业
- `EMP_RELATION`:员工关系人
- `CREDIT_CUSTOMER`:信贷客户
- `INTERMEDIARY`:中介
- `BOTH`:兼有
当前缺少供应商来源,需要新增 `SUPPLIER`
### 3.2 各业务链路
员工亲属实体关联:
- 新建、导入会校验有效员工亲属
- 业务关联表保存统一社会信用代码和企业名称
- 当前不会补入实体库
中介实体关联:
- 新建时要求关联机构必须已存在
- 导入时当前会因“统一社会信用代码不存在于系统机构表”失败
- 本次需要改为实体库缺失时自动补入
信贷客户实体关联:
- 新建、导入保存统一社会信用代码和企业名称
- 当前不会补入实体库
招投标信息维护:
- 新建、导入维护供应商明细
- 供应商明细包含供应商名称和统一信用代码
- 当前不会补入实体库
## 4. 实现方案
### 4.1 新增后端内部实体库自动补全服务
本次采用单一路径实现:
- 新增一个内部复用能力,统一接收统一社会信用代码、企业名称、企业来源、数据来源和操作人
- 各业务 Service 在成功落业务数据前调用
- 已存在则不处理,不存在则插入最小实体记录
采用该方案的原因:
- 规则集中,避免四处重复
- 新建与导入可复用同一口径
- 后续新增业务来源时扩展点明确
- 需要各业务链路接入该内部服务
- 满足最短路径实现
- 业务规则集中可测
- 不改变前端交互
- 可以明确保证已存在实体不被覆盖
## 5. 数据规则
### 5.1 实体识别规则
自动补入实体库时,只按统一社会信用代码判断实体是否存在。
-`ccdi_enterprise_base_info.social_credit_code` 已存在:不更新实体库任何字段
- 若不存在:新增一条最小实体记录
### 5.2 最小实体记录字段
自动补入时写入以下字段:
| 字段 | 规则 |
|------|------|
| `social_credit_code` | 来源业务中的统一社会信用代码 |
| `enterprise_name` | 来源业务中的企业名称或供应商名称 |
| `ent_source` | 按业务来源映射 |
| `risk_level` | 仅中介来源默认为 `1`,其他来源按 `NULL` 落库 |
| `data_source` | 新建为 `MANUAL`,导入为 `IMPORT` |
| `created_by` | 当前操作人 |
| `updated_by` | 当前操作人 |
其他实体字段保持为空。自动补入能力必须使用独立插入路径,不复用实体库管理手工新增中要求风险等级必填的校验逻辑;员工亲属、信贷客户、供应商三类自动补入记录必须显式保证 `risk_level``NULL` 落库,不能吃到历史表默认低风险值。
### 5.3 企业来源映射
| 触发业务 | `ent_source` | `risk_level` |
|----------|--------------|--------------|
| 员工亲属实体关联 | `EMP_RELATION` | 空 |
| 中介实体关联 | `INTERMEDIARY` | `1` |
| 中介库管理新增实体 | `INTERMEDIARY` | `1` |
| 中介库管理导入实体 | `INTERMEDIARY` | `1` |
| 信贷客户实体关联 | `CREDIT_CUSTOMER` | 空 |
| 招投标供应商 | `SUPPLIER` | 空 |
需要新增企业来源枚举:
- `SUPPLIER("SUPPLIER", "供应商")`
企业来源选项由现有 `EnterpriseSource` 枚举接口下发,实体库管理页面应通过该接口正常显示和筛选“供应商”。
### 5.4 同批重复规则
导入场景中,如果同一批成功数据内同一个统一社会信用代码出现多次,且实体库当前不存在:
- 只补入一次实体库
- 使用首次有效出现的企业名称或供应商名称
- 后续相同统一社会信用代码不覆盖已补入实体
## 6. 业务链路设计
### 6.1 员工亲属实体关联
新建:
1. 校验亲属身份证号必须是有效员工亲属
2. 校验亲属身份证号和统一社会信用代码组合唯一
3. 调用实体库自动补全,来源为 `EMP_RELATION`,数据来源为 `MANUAL`
4. 插入员工亲属实体关联
导入:
1. 保持现有每行基础校验、有效亲属校验、重复组合校验
2. 仅对校验成功行收集实体信息
3. 批量自动补入缺失实体,来源为 `EMP_RELATION`,数据来源为 `IMPORT`
4. 批量插入员工亲属实体关联成功行
5. 校验失败行不补入实体库
### 6.2 中介实体关联
新建:
1. 校验中介本人存在
2. 不再将“关联机构不存在”作为失败条件
3. 校验中介和统一社会信用代码组合唯一
4. 调用实体库自动补全,来源为 `INTERMEDIARY`,风险等级为 `1`,数据来源为 `MANUAL`
5. 插入中介实体关联
导入:
1. 保持中介本人存在、字段格式、重复组合等校验
2. 取消“统一社会信用代码不存在于系统机构表”失败条件
3. 对校验成功行收集实体信息
4. 批量自动补入缺失实体,来源为 `INTERMEDIARY`,风险等级为 `1`,数据来源为 `IMPORT`
5. 批量插入中介实体关联成功行
中介库管理新增、导入实体:
- 新增实体时继续直接写实体库,风险等级默认为 `1`
- 导入实体时成功记录的风险等级默认为 `1`
### 6.3 信贷客户实体关联
新建:
1. 校验身份证号和统一社会信用代码组合唯一
2. 调用实体库自动补全,来源为 `CREDIT_CUSTOMER`,数据来源为 `MANUAL`
3. 插入信贷客户实体关联
导入:
1. 保持现有字段格式、重复组合等校验
2. 对校验成功行收集实体信息
3. 批量自动补入缺失实体,来源为 `CREDIT_CUSTOMER`,数据来源为 `IMPORT`
4. 批量插入信贷客户实体关联成功行
### 6.4 招投标信息维护
新建:
1. 保持采购事项 ID 唯一性校验
2. 保持供应商唯一中标、重复供应商等校验
3. 从供应商明细中收集 `supplierUscc` 不为空的供应商
4. 调用实体库自动补全,来源为 `SUPPLIER`,数据来源为 `MANUAL`
5. 插入招投标主信息和供应商明细
导入:
1. 保持双 Sheet 主从关系、主信息字段、供应商字段、重复供应商等校验
2. 仅对校验成功的采购事项收集供应商实体信息
3. 只处理供应商统一信用代码不为空的供应商
4. 批量自动补入缺失实体,来源为 `SUPPLIER`,数据来源为 `IMPORT`
5. 批量插入招投标主信息和供应商明细
没有统一信用代码的供应商不补实体库,也不因此失败。
## 7. 事务与并发
### 7.1 新建事务
新建场景中,实体库补全和业务数据插入在同一事务内执行。任一环节失败,本次新建整体回滚。
### 7.2 导入事务
导入场景保持现有异步任务机制。每个导入任务在成功数据批量落库前先执行实体库补全,再写业务表。若实体补全失败,对应成功候选数据不得静默写入业务表。
### 7.3 并发重复
如果两个请求同时补入同一统一社会信用代码:
- 查询时不存在但插入时遇到主键重复,应按“实体已存在”处理
- 不覆盖并发方已写入的实体字段
- 不影响当前业务数据继续落库
## 8. 异常处理
保留现有业务校验错误:
- 字段必填失败
- 统一社会信用代码格式错误
- 身份证号格式错误
- 亲属不存在或无效
- 中介本人不存在
- 重复关联组合
- 重复供应商
- 招投标主从 Sheet 关系异常
不再作为失败原因:
- 中介实体关联中的“统一社会信用代码不存在于系统机构表”
自动补入实体库时,企业名称为空或超过长度不单独新增兜底规则,沿用各业务现有字段校验结果。
## 9. 前端与枚举
前端不新增交互。
需要同步新增或调整:
- 后端 `EnterpriseSource` 枚举新增 `SUPPLIER("SUPPLIER", "供应商")`
- 现有企业来源枚举接口应返回供应商选项
- 实体库管理页面企业来源下拉、列表、详情中的枚举展示应能正确显示“供应商”
## 10. 测试设计
### 10.1 后端测试
需要覆盖:
- 员工亲属实体关联新建:实体库无记录时补入 `EMP_RELATION`,风险为空
- 员工亲属实体关联导入:成功行补入实体库,失败行不补
- 中介实体关联新建:实体库无记录时补入 `INTERMEDIARY`,风险为 `1`
- 中介实体关联导入:原实体不存在场景由失败改为成功
- 中介库管理新增实体:未填写风险等级时写入 `riskLevel=1`
- 中介库管理导入实体:成功记录写入 `riskLevel=1`
- 信贷客户实体关联新建:实体库无记录时补入 `CREDIT_CUSTOMER`,风险为空
- 信贷客户实体关联导入:成功行补入实体库,失败行不补
- 招投标新建:供应商统一信用代码存在时补入 `SUPPLIER`,风险为空
- 招投标导入:成功采购事项的供应商补入实体库,失败采购事项不补
- 已存在实体不覆盖原有名称、来源、风险等级、数据来源
- 同批重复统一社会信用代码只补一次,首次名称生效
- 并发或插入重复时按已存在处理
- 员工亲属、信贷客户、招投标供应商自动补入记录的 `risk_level``NULL`
### 10.2 前端测试
需要覆盖:
- 实体库管理企业来源筛选项包含“供应商”
- 实体库列表和详情能够显示供应商来源
- 招投标页面新建供应商后,实体库可查询到自动补入的供应商实体
完成页面功能开发后,需要使用 Playwright 打开真实业务页面验证,不使用原型页面。
### 10.3 导入测试
导入测试必须进入真实业务页面执行:
- 下载当前页面模板
- 基于模板生成测试文件
- 覆盖成功导入、失败行不补实体、混合成功失败、同批重复统一社会信用代码等场景
- 测试结束后清理本轮成功写入的测试数据和导入任务缓存
## 11. 实施文档要求
本次功能涉及后端服务、企业来源枚举和前端枚举展示,因此后续实施计划需要拆分:
- 后端实施计划:`docs/plans/backend/`
- 前端实施计划:`docs/plans/frontend/`
实现完成后需要新增实施记录:
- `docs/reports/implementation/`
实施记录需包含修改内容、影响范围和验证情况。

View File

@@ -0,0 +1,146 @@
# 开发环境配置
ruoyi:
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
profile: backend/uploadPath
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 62318
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://158.234.199.250:3306/ccdi?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: dbicm
password: Kfcx@1234
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
data:
# redis 配置
redis:
# 地址
host: r-kz640f6b20dac724.redis.rds.ops.dc-tst-zj96596.com
# 端口默认为6379
port: 6379
# 数据库索引
database: 9
# 密码
password: Kfcx@1234
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 流水分析平台配置
lsfx:
api:
base-url: http://158.234.196.5:82/c4c3
# 生产环境
# base-url: http://64.202.32.176/c4c3
# 认证配置
app-id: remote_app
app-secret: dXj6eHRmPv # 见知提供的密钥
client-id: c2017e8d105c435a96f86373635b6a09 # 测试环境固定值
# 接口路径配置
endpoints:
get-token: /account/common/getToken
upload-file: /watson/api/project/remoteUploadSplitFile
fetch-inner-flow: /watson/api/project/getJZFileOrZjrcuFile
check-parse-status: /watson/api/project/upload/getpendings
get-bank-statement: /watson/api/project/getBSByLogId
# 新增接口
get-file-upload-status: /watson/api/project/bs/upload
delete-files: /watson/api/project/batchDeleteUploadFile
# RestTemplate配置
connection-timeout: 30000 # 连接超时30秒
read-timeout: 60000 # 读取超时60秒
# 连接池配置
pool:
max-total: 100 # 最大连接数
default-max-per-route: 20 # 每个路由最大连接数
credit-parse:
api:
url: http://192.168.0.111:62320/xfeature-mngs/conversation/htmlEval

File diff suppressed because one or more lines are too long