Compare commits
13 Commits
fb388e22c5
...
c37456983f
| Author | SHA1 | Date | |
|---|---|---|---|
| c37456983f | |||
| 4b1593d249 | |||
| 164295b6d1 | |||
| 69e1c47940 | |||
| 71c5744b3d | |||
| 79c5317414 | |||
| 9fe1bffe0d | |||
| bc9f15d340 | |||
| 08416b01d0 | |||
| 36f3c32a48 | |||
| 3d4b9a6b29 | |||
| e865398ebf | |||
| 30a341ee73 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -45,3 +45,8 @@ nbdist/
|
||||
!*/build/*.java
|
||||
!*/build/*.html
|
||||
!*/build/*.xml
|
||||
|
||||
logs/
|
||||
.playwright-cli/
|
||||
ruoyi-ui/tests
|
||||
*/src/test
|
||||
27
AGENTS.md
Normal file
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 仓库协作指南
|
||||
|
||||
## 项目结构与模块组织
|
||||
本仓库采用 RuoYi-Vue 前后端分离架构。后端模块位于仓库根目录:`ruoyi-admin` 为 Spring Boot 启动入口,`ruoyi-framework` 负责安全与 Web 基础设施,`ruoyi-system` 承载业务服务与 MyBatis Mapper,`ruoyi-common` 存放公共工具,`ruoyi-quartz` 与 `ruoyi-generator` 分别提供定时任务和代码生成功能。前端代码位于 `ruoyi-ui`,页面放在 `src/views`,接口封装位于 `src/api`,公共组件位于 `src/components`,静态资源位于 `src/assets`。SQL 脚本统一放在 `sql/`,项目文档统一放在 `doc/`。
|
||||
|
||||
## 构建、测试与开发命令
|
||||
后端:
|
||||
- `mvn clean package -DskipTests`:构建全部 Java 模块。
|
||||
- `mvn test`:运行整个 Maven 聚合工程的后端测试。
|
||||
- `mvn -pl ruoyi-admin -am spring-boot:run`:启动管理端服务,并自动构建依赖模块。
|
||||
|
||||
前端:
|
||||
- `cd ruoyi-ui && nvm use && npm install`:通过 `nvm` 切换并使用项目要求的 Node 版本后安装依赖。
|
||||
- `cd ruoyi-ui && npm run dev`:启动 Vue 2 开发服务。
|
||||
- `cd ruoyi-ui && npm run build:prod`:构建生产环境前端产物。
|
||||
|
||||
## 编码风格与命名规范
|
||||
Java 代码统一使用 4 个空格缩进,包路径保持在 `com.ruoyi.<module>` 下。请沿用现有后缀命名,例如 `*Controller`、`*Service`、`*ServiceImpl`、`*Mapper`;Mapper XML 文件应继续放在 `src/main/resources/mapper/**` 下。Vue 文件遵循现有功能目录组织方式,例如 `src/views/system/user/index.vue` 与 `src/api/system/user.js`。新增代码时请保持与仓库现有命名风格一致,不要引入新的命名体系。
|
||||
|
||||
## 测试规范
|
||||
当前仓库中尚未发现已提交的 `src/test` 测试文件,因此新增后端功能时,应在对应模块的 `src/test/java` 下补充聚焦型测试类,并使用 `*Test` 作为类名后缀。涉及前端改动时,至少需要验证 `npm run build:prod` 可以成功执行,并对相关页面流程进行本地冒烟验证。测试范围应聚焦于本次改动所在模块,避免无关扩散。
|
||||
|
||||
## 提交与 Pull Request 规范
|
||||
当前仓库提交历史较少,仅有 `init`,因此提交信息请保持简短、明确,并统一使用中文,例如 `新增贷款定价审批接口`。忽略 `.DS_Store` 文件的变更。提交 PR 时,需要说明影响的模块、概述行为变化、列出 SQL 或配置变更,并在涉及界面调整时附上截图。
|
||||
|
||||
## Agent 协作说明
|
||||
每次改动都必须在 `doc/` 中留下实施记录。如果一个功能同时涉及前端和后端,则需要分别维护前端实施计划和后端实施计划;如果仅涉及单侧改动,则只记录对应侧的实施内容。
|
||||
257
bin/prod/deploy_from_package.sh
Executable file
257
bin/prod/deploy_from_package.sh
Executable file
@@ -0,0 +1,257 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
JAVA_BIN="/home/webapp/env/java/bin/java"
|
||||
BACKEND_PORT=63310
|
||||
SPRING_PROFILE="uat"
|
||||
JAVA_OPTS="-Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
BACKEND_DIR="$SCRIPT_DIR/backend"
|
||||
FRONTEND_DIR="$SCRIPT_DIR/frontend"
|
||||
BACKEND_JAR_TARGET="$BACKEND_DIR/ruoyi-admin.jar"
|
||||
BACKEND_PID_FILE="$BACKEND_DIR/backend.pid"
|
||||
BACKEND_LOG_FILE="$BACKEND_DIR/backend-console.log"
|
||||
FRONTEND_DIST_ARCHIVE="$FRONTEND_DIR/dist.zip"
|
||||
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
|
||||
BACKEND_MARKER="-Dloan.pricing.home=$SCRIPT_DIR"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./deploy_from_package.sh
|
||||
EOF
|
||||
}
|
||||
|
||||
timestamp() {
|
||||
date "+%Y%m%d%H%M%S"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
require_dir() {
|
||||
if [ ! -d "$1" ]; then
|
||||
log_error "缺少目录: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
find_release_archive() {
|
||||
archives=$(find "$SCRIPT_DIR" -maxdepth 1 -type f -name '*.zip' ! -name 'dist.zip')
|
||||
count=$(printf '%s\n' "$archives" | sed '/^$/d' | wc -l | tr -d ' ')
|
||||
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "脚本同目录发布 zip 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$archives"
|
||||
}
|
||||
|
||||
extract_release_package() {
|
||||
release_archive="$1"
|
||||
release_extract_dir="$2"
|
||||
|
||||
mkdir -p "$release_extract_dir"
|
||||
unzip -oq "$release_archive" -d "$release_extract_dir"
|
||||
}
|
||||
|
||||
assert_single_jar() {
|
||||
search_dir="$1"
|
||||
count=$(find "$search_dir" -type f -name '*.jar' ! -path '*/__MACOSX/*' ! -name '._*' | wc -l | tr -d ' ')
|
||||
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "后端 jar 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$search_dir" -type f -name '*.jar' ! -path '*/__MACOSX/*' ! -name '._*' | head -n 1
|
||||
}
|
||||
|
||||
assert_single_dist_zip() {
|
||||
search_dir="$1"
|
||||
count=$(find "$search_dir" -type f -name 'dist.zip' ! -path '*/__MACOSX/*' ! -name '._*' | wc -l | tr -d ' ')
|
||||
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "前端 dist.zip 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$search_dir" -type f -name 'dist.zip' ! -path '*/__MACOSX/*' ! -name '._*' | head -n 1
|
||||
}
|
||||
|
||||
backup_backend_jar() {
|
||||
if [ -f "$BACKEND_JAR_TARGET" ]; then
|
||||
mv "$BACKEND_JAR_TARGET" "$BACKEND_JAR_TARGET.$(timestamp).bak"
|
||||
fi
|
||||
}
|
||||
|
||||
backup_frontend_dist() {
|
||||
if [ -d "$FRONTEND_DIST_DIR" ]; then
|
||||
mv "$FRONTEND_DIST_DIR" "$FRONTEND_DIR/dist-$(timestamp)"
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_backend_jar() {
|
||||
source_jar="$1"
|
||||
mv "$source_jar" "$BACKEND_JAR_TARGET"
|
||||
}
|
||||
|
||||
deploy_frontend_dist() {
|
||||
source_dist_zip="$1"
|
||||
rm -f "$FRONTEND_DIST_ARCHIVE"
|
||||
rm -rf "$FRONTEND_DIST_DIR"
|
||||
mv "$source_dist_zip" "$FRONTEND_DIST_ARCHIVE"
|
||||
unzip -oq "$FRONTEND_DIST_ARCHIVE" -d "$FRONTEND_DIR"
|
||||
|
||||
if [ ! -d "$FRONTEND_DIST_DIR" ]; then
|
||||
log_error "dist.zip 解压后未找到 $FRONTEND_DIST_DIR"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
collect_backend_pids() {
|
||||
ps -ef | awk -v marker="$BACKEND_MARKER" -v jar="$BACKEND_JAR_TARGET" '
|
||||
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
|
||||
for (i = 1; i < NF; i++) {
|
||||
if ($i == "-jar" && $(i + 1) == jar) {
|
||||
print $2
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
' | xargs 2>/dev/null || true
|
||||
}
|
||||
|
||||
stop_backend() {
|
||||
pids=$(collect_backend_pids)
|
||||
|
||||
if [ -z "${pids:-}" ]; then
|
||||
rm -f "$BACKEND_PID_FILE"
|
||||
log_info "未发现运行中的后端进程"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "停止后端进程: $pids"
|
||||
for pid in $pids; do
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
done
|
||||
|
||||
elapsed=0
|
||||
remaining="$pids"
|
||||
while [ "$elapsed" -lt 30 ]; do
|
||||
remaining=""
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
remaining="$remaining $pid"
|
||||
fi
|
||||
done
|
||||
|
||||
remaining=$(echo "$remaining" | xargs 2>/dev/null || true)
|
||||
if [ -z "${remaining:-}" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
if [ -n "${remaining:-}" ]; then
|
||||
log_info "执行强制停止: $remaining"
|
||||
for pid in $remaining; do
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
rm -f "$BACKEND_PID_FILE"
|
||||
}
|
||||
|
||||
start_backend() {
|
||||
if [ ! -x "$JAVA_BIN" ]; then
|
||||
log_error "未检测到可执行 Java: $JAVA_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BACKEND_JAR_TARGET" ]; then
|
||||
log_error "未找到后端 jar: $BACKEND_JAR_TARGET"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$(collect_backend_pids)" ]; then
|
||||
log_error "检测到后端已在运行,请先停止旧进程"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\n===== %s deploy =====\n' "$(date '+%Y-%m-%d %H:%M:%S')" >> "$BACKEND_LOG_FILE"
|
||||
|
||||
nohup "$JAVA_BIN" $JAVA_OPTS "$BACKEND_MARKER" -jar "$BACKEND_JAR_TARGET" \
|
||||
--spring.profiles.active="$SPRING_PROFILE" \
|
||||
--server.port="$BACKEND_PORT" >> "$BACKEND_LOG_FILE" 2>&1 &
|
||||
backend_pid=$!
|
||||
printf '%s\n' "$backend_pid" > "$BACKEND_PID_FILE"
|
||||
|
||||
sleep 1
|
||||
|
||||
if ! kill -0 "$backend_pid" 2>/dev/null; then
|
||||
log_error "后端启动失败,请检查日志: $BACKEND_LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "后端已启动,PID: $backend_pid"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ "$#" -ne 0 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_dir "$BACKEND_DIR"
|
||||
require_dir "$FRONTEND_DIR"
|
||||
require_command unzip
|
||||
require_command find
|
||||
require_command ps
|
||||
require_command nohup
|
||||
|
||||
release_archive=$(find_release_archive)
|
||||
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/deploy_from_package.XXXXXX")
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
extract_release_package "$release_archive" "$WORK_DIR/package"
|
||||
|
||||
backend_jar_source=$(assert_single_jar "$WORK_DIR/package")
|
||||
frontend_dist_source=$(assert_single_dist_zip "$WORK_DIR/package")
|
||||
|
||||
backup_backend_jar
|
||||
backup_frontend_dist
|
||||
stop_backend
|
||||
deploy_backend_jar "$backend_jar_source"
|
||||
deploy_frontend_dist "$frontend_dist_source"
|
||||
start_backend
|
||||
|
||||
log_info "部署完成"
|
||||
log_info "后端 jar: $BACKEND_JAR_TARGET"
|
||||
log_info "前端目录: $FRONTEND_DIST_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
262
bin/prod/deploy_from_package_test.sh
Executable file
262
bin/prod/deploy_from_package_test.sh
Executable file
@@ -0,0 +1,262 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
|
||||
SCRIPT_UNDER_TEST="$ROOT_DIR/bin/prod/deploy_from_package.sh"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_file_exists() {
|
||||
file_path="$1"
|
||||
[ -e "$file_path" ] || fail "expected file to exist: $file_path"
|
||||
}
|
||||
|
||||
assert_grep() {
|
||||
pattern="$1"
|
||||
target="$2"
|
||||
if ! grep -Eq "$pattern" "$target"; then
|
||||
fail "expected pattern [$pattern] in $target"
|
||||
fi
|
||||
}
|
||||
|
||||
create_fake_java() {
|
||||
fake_java="$1"
|
||||
|
||||
cat > "$fake_java" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
port=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--server.port=*)
|
||||
port=${arg#--server.port=}
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$port" ]; then
|
||||
echo "missing port" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while :; do
|
||||
sleep 1
|
||||
done
|
||||
EOF
|
||||
|
||||
chmod +x "$fake_java"
|
||||
}
|
||||
|
||||
create_release_zip() {
|
||||
release_dir="$1"
|
||||
release_zip_name="$2"
|
||||
|
||||
mkdir -p "$release_dir/package/deploy" "$release_dir/package/__MACOSX/deploy"
|
||||
mkdir -p "$release_dir/package/frontend_payload/dist" "$release_dir/package/frontend_payload/__MACOSX/dist"
|
||||
printf 'new-jar\n' > "$release_dir/package/deploy/ruoyi-admin.jar"
|
||||
printf 'macos-meta\n' > "$release_dir/package/__MACOSX/deploy/._ruoyi-admin.jar"
|
||||
printf '<html>new</html>\n' > "$release_dir/package/frontend_payload/dist/index.html"
|
||||
printf 'macos-meta\n' > "$release_dir/package/frontend_payload/__MACOSX/dist/._index.html"
|
||||
(
|
||||
cd "$release_dir/package/frontend_payload"
|
||||
zip -qr "$release_dir/package/dist.zip" dist __MACOSX
|
||||
)
|
||||
mv "$release_dir/package/dist.zip" "$release_dir/package/deploy/dist.zip"
|
||||
(
|
||||
cd "$release_dir/package"
|
||||
zip -qr "$release_dir/$release_zip_name" deploy __MACOSX
|
||||
)
|
||||
}
|
||||
|
||||
find_free_port() {
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
|
||||
sock = socket.socket()
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
print(sock.getsockname()[1])
|
||||
sock.close()
|
||||
PY
|
||||
}
|
||||
|
||||
prepare_release_dir() {
|
||||
release_dir="$1"
|
||||
backend_port="$2"
|
||||
|
||||
mkdir -p "$release_dir/backend" "$release_dir/frontend" "$release_dir/fake-java-bin"
|
||||
printf 'old-jar\n' > "$release_dir/backend/ruoyi-admin.jar"
|
||||
mkdir -p "$release_dir/frontend/dist"
|
||||
printf '<html>old</html>\n' > "$release_dir/frontend/dist/index.html"
|
||||
|
||||
create_fake_java "$release_dir/fake-java-bin/java"
|
||||
create_release_zip "$release_dir" "deploy.zip"
|
||||
cp "$SCRIPT_UNDER_TEST" "$release_dir/deploy_from_package.sh"
|
||||
perl -0pi -e "s#JAVA_BIN=\"/home/webapp/env/java/bin/java\"#JAVA_BIN=\"$release_dir/fake-java-bin/java\"#" \
|
||||
"$release_dir/deploy_from_package.sh"
|
||||
perl -0pi -e "s/BACKEND_PORT=63310/BACKEND_PORT=$backend_port/" \
|
||||
"$release_dir/deploy_from_package.sh"
|
||||
chmod +x "$release_dir/deploy_from_package.sh"
|
||||
}
|
||||
|
||||
cleanup_release_dir() {
|
||||
release_dir="$1"
|
||||
|
||||
if [ -f "$release_dir/backend/backend.pid" ]; then
|
||||
backend_pid=$(cat "$release_dir/backend/backend.pid" 2>/dev/null || true)
|
||||
if [ -n "${backend_pid:-}" ]; then
|
||||
kill "$backend_pid" 2>/dev/null || true
|
||||
wait "$backend_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$release_dir"
|
||||
}
|
||||
|
||||
test_deploy_success() {
|
||||
release_dir=$(mktemp -d)
|
||||
backend_port=$(find_free_port)
|
||||
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
|
||||
|
||||
prepare_release_dir "$release_dir" "$backend_port"
|
||||
(
|
||||
cd "$release_dir"
|
||||
./deploy_from_package.sh
|
||||
)
|
||||
|
||||
assert_file_exists "$release_dir/backend/ruoyi-admin.jar"
|
||||
assert_file_exists "$release_dir/frontend/dist.zip"
|
||||
assert_file_exists "$release_dir/frontend/dist/index.html"
|
||||
assert_file_exists "$release_dir/backend/backend.pid"
|
||||
assert_file_exists "$release_dir/backend/backend-console.log"
|
||||
assert_grep 'new' "$release_dir/frontend/dist/index.html"
|
||||
|
||||
backup_jar_count=$(find "$release_dir/backend" -maxdepth 1 -type f -name 'ruoyi-admin.jar.*.bak' | wc -l | tr -d ' ')
|
||||
[ "$backup_jar_count" -eq 1 ] || fail "expected one backup jar, got $backup_jar_count"
|
||||
|
||||
backup_dist_count=$(find "$release_dir/frontend" -maxdepth 1 -type d -name 'dist-*' | wc -l | tr -d ' ')
|
||||
[ "$backup_dist_count" -eq 1 ] || fail "expected one backup dist dir, got $backup_dist_count"
|
||||
|
||||
backend_pid=$(cat "$release_dir/backend/backend.pid")
|
||||
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running"
|
||||
|
||||
trap - EXIT INT TERM
|
||||
cleanup_release_dir "$release_dir"
|
||||
}
|
||||
|
||||
test_multiple_release_zip_should_fail() {
|
||||
release_dir=$(mktemp -d)
|
||||
backend_port=$(find_free_port)
|
||||
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
|
||||
|
||||
prepare_release_dir "$release_dir" "$backend_port"
|
||||
cp "$release_dir/deploy.zip" "$release_dir/deploy-copy.zip"
|
||||
|
||||
if (
|
||||
cd "$release_dir"
|
||||
./deploy_from_package.sh >/tmp/deploy_from_package_test.stderr 2>&1
|
||||
); then
|
||||
fail "expected deploy_from_package.sh to fail when multiple release zips exist"
|
||||
fi
|
||||
|
||||
assert_file_exists /tmp/deploy_from_package_test.stderr
|
||||
assert_grep '发布 zip 数量不正确' /tmp/deploy_from_package_test.stderr
|
||||
|
||||
rm -f /tmp/deploy_from_package_test.stderr
|
||||
trap - EXIT INT TERM
|
||||
cleanup_release_dir "$release_dir"
|
||||
}
|
||||
|
||||
test_defunct_process_should_be_ignored() {
|
||||
release_dir=$(mktemp -d)
|
||||
backend_port=$(find_free_port)
|
||||
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
|
||||
|
||||
prepare_release_dir "$release_dir" "$backend_port"
|
||||
mkdir -p "$release_dir/fake-ps-bin"
|
||||
cat > "$release_dir/fake-ps-bin/ps" <<EOF
|
||||
#!/bin/sh
|
||||
if [ "\$1" = "-ef" ]; then
|
||||
cat <<'PSOUT'
|
||||
UID PID PPID C STIME TTY TIME CMD
|
||||
root 99999 1 0 00:00 ? 00:00:00 [java] <defunct> -Dloan.pricing.home=$release_dir -jar $release_dir/backend/ruoyi-admin.jar
|
||||
PSOUT
|
||||
exit 0
|
||||
fi
|
||||
/bin/ps "\$@"
|
||||
EOF
|
||||
chmod +x "$release_dir/fake-ps-bin/ps"
|
||||
|
||||
(
|
||||
cd "$release_dir"
|
||||
PATH="$release_dir/fake-ps-bin:/usr/bin:/bin" ./deploy_from_package.sh
|
||||
)
|
||||
|
||||
backend_pid=$(cat "$release_dir/backend/backend.pid")
|
||||
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running when defunct process is ignored"
|
||||
|
||||
trap - EXIT INT TERM
|
||||
cleanup_release_dir "$release_dir"
|
||||
}
|
||||
|
||||
test_only_current_project_jar_should_match() {
|
||||
release_dir=$(mktemp -d)
|
||||
backend_port=$(find_free_port)
|
||||
trap 'cleanup_release_dir "$release_dir"' EXIT INT TERM
|
||||
|
||||
prepare_release_dir "$release_dir" "$backend_port"
|
||||
mkdir -p "$release_dir/fake-ps-bin"
|
||||
cat > "$release_dir/fake-ps-bin/ps" <<EOF
|
||||
#!/bin/sh
|
||||
if [ "\$1" = "-ef" ]; then
|
||||
cat <<'PSOUT'
|
||||
UID PID PPID C STIME TTY TIME CMD
|
||||
root 88888 1 0 00:00 ? 00:00:00 java -Dloan.pricing.home=$release_dir -jar $release_dir/backend/ruoyi-admin.jar.bak --spring.profiles.active=pro
|
||||
PSOUT
|
||||
exit 0
|
||||
fi
|
||||
/bin/ps "\$@"
|
||||
EOF
|
||||
chmod +x "$release_dir/fake-ps-bin/ps"
|
||||
|
||||
(
|
||||
cd "$release_dir"
|
||||
PATH="$release_dir/fake-ps-bin:/usr/bin:/bin" ./deploy_from_package.sh
|
||||
)
|
||||
|
||||
backend_pid=$(cat "$release_dir/backend/backend.pid")
|
||||
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend pid to be running when non-target jar process is ignored"
|
||||
|
||||
trap - EXIT INT TERM
|
||||
cleanup_release_dir "$release_dir"
|
||||
}
|
||||
|
||||
test_should_use_ps_ef_for_process_detection() {
|
||||
if rg -n 'pgrep' "$SCRIPT_UNDER_TEST" >/dev/null 2>&1; then
|
||||
fail "expected deploy_from_package.sh not to depend on pgrep"
|
||||
fi
|
||||
|
||||
if ! rg -n 'ps -ef' "$SCRIPT_UNDER_TEST" >/dev/null 2>&1; then
|
||||
fail "expected deploy_from_package.sh to use ps -ef for process detection"
|
||||
fi
|
||||
|
||||
if rg -n '\b(ss|lsof|netstat|resolve_frontend_source_dir|is_port_listening)\b' "$SCRIPT_UNDER_TEST" >/dev/null 2>&1; then
|
||||
fail "expected deploy_from_package.sh to remove port detection and unzip compatibility helpers"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
[ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
|
||||
test_should_use_ps_ef_for_process_detection
|
||||
test_deploy_success
|
||||
test_multiple_release_zip_should_fail
|
||||
test_defunct_process_should_be_ignored
|
||||
test_only_current_project_jar_should_match
|
||||
printf 'PASS: deploy_from_package tests\n'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
245
bin/prod/deploy_release.sh
Executable file
245
bin/prod/deploy_release.sh
Executable file
@@ -0,0 +1,245 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
WEBAPP_ROOT="/home/webapp"
|
||||
ENV_ROOT="$WEBAPP_ROOT/env"
|
||||
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
|
||||
JAVA_HOME="$ENV_ROOT/java"
|
||||
NGINX_HOME="$ENV_ROOT/nginx"
|
||||
NGINX_CONF="$NGINX_HOME/conf/nginx.conf"
|
||||
BACKEND_DIR="$APP_ROOT/backend"
|
||||
FRONTEND_DIR="$APP_ROOT/frontend"
|
||||
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
|
||||
BACKUP_DIR="$APP_ROOT/backup"
|
||||
LOG_DIR="$APP_ROOT/logs"
|
||||
RUN_DIR="$APP_ROOT/run"
|
||||
TMP_DIR="$APP_ROOT/tmp"
|
||||
BACKEND_JAR="$BACKEND_DIR/ruoyi-admin.jar"
|
||||
FRONTEND_PORT=63311
|
||||
JAVA_RESTART_SCRIPT="$WEBAPP_ROOT/restart_java.sh"
|
||||
|
||||
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'
|
||||
用法:
|
||||
./bin/prod/deploy_release.sh <发布压缩包路径>
|
||||
EOF
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
log_error "请使用 root 用户执行部署脚本"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_runtime_dirs() {
|
||||
mkdir -p "$BACKEND_DIR" "$FRONTEND_DIR" "$BACKUP_DIR" "$LOG_DIR" "$RUN_DIR" "$TMP_DIR"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_release_package() {
|
||||
release_archive="$1"
|
||||
release_extract_dir="$2"
|
||||
|
||||
mkdir -p "$release_extract_dir"
|
||||
|
||||
case "$release_archive" in
|
||||
*.zip)
|
||||
unzip -oq "$release_archive" -d "$release_extract_dir"
|
||||
;;
|
||||
*.tar.gz|*.tgz)
|
||||
tar -xzf "$release_archive" -C "$release_extract_dir"
|
||||
;;
|
||||
*.tar)
|
||||
tar -xf "$release_archive" -C "$release_extract_dir"
|
||||
;;
|
||||
*)
|
||||
log_error "不支持的发布包格式: $release_archive"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_single_file() {
|
||||
search_dir="$1"
|
||||
file_name="$2"
|
||||
description="$3"
|
||||
count=$(find "$search_dir" -type f -name "$file_name" | wc -l | tr -d ' ')
|
||||
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "$description 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$search_dir" -type f -name "$file_name" | head -n 1
|
||||
}
|
||||
|
||||
assert_single_jar() {
|
||||
search_dir="$1"
|
||||
count=$(find "$search_dir" -type f -name '*.jar' | wc -l | tr -d ' ')
|
||||
|
||||
if [ "$count" -ne 1 ]; then
|
||||
log_error "后端 jar 数量不正确,期望 1 个,实际 $count 个"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$search_dir" -type f -name '*.jar' | head -n 1
|
||||
}
|
||||
|
||||
backup_current_release() {
|
||||
backup_stamp=$(date "+%Y%m%d%H%M%S")
|
||||
CURRENT_BACKUP_DIR="$BACKUP_DIR/$backup_stamp"
|
||||
|
||||
mkdir -p "$CURRENT_BACKUP_DIR/backend" "$CURRENT_BACKUP_DIR/frontend"
|
||||
|
||||
if [ -f "$BACKEND_JAR" ]; then
|
||||
cp -a "$BACKEND_JAR" "$CURRENT_BACKUP_DIR/backend/"
|
||||
fi
|
||||
|
||||
if [ -d "$FRONTEND_DIST_DIR" ]; then
|
||||
cp -a "$FRONTEND_DIST_DIR" "$CURRENT_BACKUP_DIR/frontend/"
|
||||
fi
|
||||
|
||||
log_info "旧版本已备份到: $CURRENT_BACKUP_DIR"
|
||||
}
|
||||
|
||||
deploy_backend() {
|
||||
source_jar="$1"
|
||||
|
||||
rm -f "$BACKEND_DIR"/*.jar
|
||||
cp "$source_jar" "$BACKEND_JAR"
|
||||
}
|
||||
|
||||
resolve_frontend_source_dir() {
|
||||
unzip_dir="$1"
|
||||
|
||||
if [ -f "$unzip_dir/index.html" ]; then
|
||||
printf '%s\n' "$unzip_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "$unzip_dir/dist/index.html" ]; then
|
||||
printf '%s\n' "$unzip_dir/dist"
|
||||
return 0
|
||||
fi
|
||||
|
||||
candidate=$(find "$unzip_dir" -type f -name 'index.html' | head -n 1)
|
||||
if [ -z "${candidate:-}" ]; then
|
||||
log_error "dist.zip 解压后未找到 index.html"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dirname "$candidate"
|
||||
}
|
||||
|
||||
deploy_frontend() {
|
||||
dist_zip="$1"
|
||||
dist_unpack_dir="$WORK_DIR/frontend"
|
||||
|
||||
mkdir -p "$dist_unpack_dir"
|
||||
unzip -oq "$dist_zip" -d "$dist_unpack_dir"
|
||||
|
||||
frontend_source_dir=$(resolve_frontend_source_dir "$dist_unpack_dir")
|
||||
rm -rf "$FRONTEND_DIST_DIR"
|
||||
mkdir -p "$FRONTEND_DIST_DIR"
|
||||
cp -a "$frontend_source_dir"/. "$FRONTEND_DIST_DIR"/
|
||||
}
|
||||
|
||||
reload_nginx() {
|
||||
nginx_pid_file="$RUN_DIR/nginx.pid"
|
||||
|
||||
"$NGINX_HOME/sbin/nginx" -t -c "$NGINX_CONF"
|
||||
|
||||
if [ -f "$nginx_pid_file" ]; then
|
||||
nginx_pid=$(cat "$nginx_pid_file" 2>/dev/null || true)
|
||||
if [ -n "${nginx_pid:-}" ] && kill -0 "$nginx_pid" 2>/dev/null; then
|
||||
"$NGINX_HOME/sbin/nginx" -c "$NGINX_CONF" -s reload
|
||||
log_info "Nginx 已重载,前端端口: $FRONTEND_PORT"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
"$NGINX_HOME/sbin/nginx" -c "$NGINX_CONF"
|
||||
log_info "Nginx 已启动,前端端口: $FRONTEND_PORT"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ "$#" -ne 1 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_root
|
||||
require_command tar
|
||||
require_command unzip
|
||||
require_command find
|
||||
|
||||
release_archive="$1"
|
||||
if [ ! -f "$release_archive" ]; then
|
||||
log_error "发布压缩包不存在: $release_archive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVA_HOME/bin/java" ]; then
|
||||
log_error "未检测到 Java,请先执行 ./bin/prod/install_env.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$NGINX_HOME/sbin/nginx" ]; then
|
||||
log_error "未检测到 Nginx,请先执行 ./bin/prod/install_env.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVA_RESTART_SCRIPT" ]; then
|
||||
log_error "未检测到 Java 重启脚本: $JAVA_RESTART_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_runtime_dirs
|
||||
WORK_DIR=$(mktemp -d "$TMP_DIR/release.XXXXXX")
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
extract_release_package "$release_archive" "$WORK_DIR/package"
|
||||
|
||||
backend_jar_source=$(assert_single_jar "$WORK_DIR/package")
|
||||
frontend_dist_source=$(assert_single_file "$WORK_DIR/package" 'dist.zip' '前端 dist.zip')
|
||||
|
||||
backup_current_release
|
||||
"$JAVA_RESTART_SCRIPT" stop
|
||||
deploy_backend "$backend_jar_source"
|
||||
deploy_frontend "$frontend_dist_source"
|
||||
"$JAVA_RESTART_SCRIPT" start
|
||||
reload_nginx
|
||||
|
||||
log_info "部署完成"
|
||||
log_info "后端 jar: $BACKEND_JAR"
|
||||
log_info "前端目录: $FRONTEND_DIST_DIR"
|
||||
log_info "备份目录: $CURRENT_BACKUP_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
244
bin/prod/install_env.sh
Executable file
244
bin/prod/install_env.sh
Executable file
@@ -0,0 +1,244 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
WEBAPP_ROOT="/home/webapp"
|
||||
ENV_ROOT="$WEBAPP_ROOT/env"
|
||||
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
|
||||
JAVA_HOME="$ENV_ROOT/java"
|
||||
NGINX_HOME="$ENV_ROOT/nginx"
|
||||
NGINX_CONF="$NGINX_HOME/conf/nginx.conf"
|
||||
BACKEND_DIR="$APP_ROOT/backend"
|
||||
FRONTEND_DIR="$APP_ROOT/frontend"
|
||||
BACKUP_DIR="$APP_ROOT/backup"
|
||||
LOG_DIR="$APP_ROOT/logs"
|
||||
RUN_DIR="$APP_ROOT/run"
|
||||
TMP_DIR="$APP_ROOT/tmp"
|
||||
BACKEND_PORT=63310
|
||||
FRONTEND_PORT=63311
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
log_error "请使用 root 用户执行安装脚本"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_base_dirs() {
|
||||
mkdir -p "$ENV_ROOT" "$BACKEND_DIR" "$FRONTEND_DIR" "$BACKUP_DIR" "$LOG_DIR" "$RUN_DIR" "$TMP_DIR"
|
||||
}
|
||||
|
||||
find_archive() {
|
||||
search_kind="$1"
|
||||
found=""
|
||||
|
||||
case "$search_kind" in
|
||||
java)
|
||||
set -- "$WEBAPP_ROOT/openjdk" "$WEBAPP_ROOT"
|
||||
patterns='openjdk*.tar.gz openjdk*.tgz jdk*.tar.gz jdk*.tgz'
|
||||
;;
|
||||
nginx)
|
||||
set -- "$WEBAPP_ROOT/nginx" "$ENV_ROOT" "$WEBAPP_ROOT"
|
||||
patterns='nginx-*.tar.gz nginx-*.tgz'
|
||||
;;
|
||||
*)
|
||||
log_error "未知的安装包类型: $search_kind"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
for dir in "$@"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for pattern in $patterns; do
|
||||
candidate=$(find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | head -n 1)
|
||||
if [ -n "${candidate:-}" ]; then
|
||||
found="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${found:-}" ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${found:-}" ]; then
|
||||
log_error "未找到 $search_kind 安装包"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$found"
|
||||
}
|
||||
|
||||
install_yum_dependencies() {
|
||||
require_command yum
|
||||
|
||||
log_info "安装系统依赖"
|
||||
yum install -y \
|
||||
gcc \
|
||||
make \
|
||||
pcre \
|
||||
pcre-devel \
|
||||
zlib \
|
||||
zlib-devel \
|
||||
openssl \
|
||||
openssl-devel \
|
||||
tar \
|
||||
gzip \
|
||||
unzip \
|
||||
which \
|
||||
findutils \
|
||||
procps-ng \
|
||||
iproute
|
||||
}
|
||||
|
||||
install_java() {
|
||||
java_archive="$1"
|
||||
|
||||
log_info "安装 Java: $java_archive"
|
||||
rm -rf "$JAVA_HOME"
|
||||
mkdir -p "$JAVA_HOME"
|
||||
tar -xzf "$java_archive" -C "$JAVA_HOME" --strip-components=1
|
||||
|
||||
if [ ! -x "$JAVA_HOME/bin/java" ]; then
|
||||
log_error "Java 安装失败,未找到 $JAVA_HOME/bin/java"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$JAVA_HOME/bin/java" -version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_nginx() {
|
||||
nginx_archive="$1"
|
||||
build_dir=$(mktemp -d "$ENV_ROOT/nginx-build.XXXXXX")
|
||||
|
||||
log_info "编译安装 Nginx: $nginx_archive"
|
||||
rm -rf "$NGINX_HOME"
|
||||
mkdir -p "$NGINX_HOME"
|
||||
|
||||
tar -xzf "$nginx_archive" -C "$build_dir"
|
||||
source_dir=$(find "$build_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
if [ -z "${source_dir:-}" ]; then
|
||||
rm -rf "$build_dir"
|
||||
log_error "Nginx 源码目录解析失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jobs=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
|
||||
(
|
||||
cd "$source_dir"
|
||||
./configure --prefix="$NGINX_HOME" --with-http_ssl_module
|
||||
make -j"$jobs"
|
||||
make install
|
||||
)
|
||||
|
||||
rm -rf "$build_dir"
|
||||
|
||||
if [ ! -x "$NGINX_HOME/sbin/nginx" ]; then
|
||||
log_error "Nginx 安装失败,未找到 $NGINX_HOME/sbin/nginx"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_nginx_conf() {
|
||||
log_info "写入 Nginx 配置: $NGINX_CONF"
|
||||
|
||||
mkdir -p "$NGINX_HOME/conf" "$NGINX_HOME/logs" "$FRONTEND_DIR/dist"
|
||||
|
||||
cat > "$NGINX_CONF" <<EOF
|
||||
user nobody;
|
||||
worker_processes 1;
|
||||
|
||||
error_log $LOG_DIR/nginx-error.log warn;
|
||||
pid $RUN_DIR/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include $NGINX_HOME/conf/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
|
||||
'\$status \$body_bytes_sent "\$http_referer" '
|
||||
'"\$http_user_agent" "\$http_x_forwarded_for"';
|
||||
|
||||
access_log $LOG_DIR/nginx-access.log main;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 100m;
|
||||
|
||||
server {
|
||||
listen $FRONTEND_PORT;
|
||||
server_name _;
|
||||
|
||||
root $FRONTEND_DIR/dist;
|
||||
index index.html;
|
||||
|
||||
location /prod-api/ {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
"$NGINX_HOME/sbin/nginx" -t -c "$NGINX_CONF"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
require_command tar
|
||||
require_command find
|
||||
ensure_base_dirs
|
||||
install_yum_dependencies
|
||||
|
||||
java_archive=$(find_archive java)
|
||||
nginx_archive=$(find_archive nginx)
|
||||
|
||||
log_info "检测到 Java 安装包: $java_archive"
|
||||
log_info "检测到 Nginx 安装包: $nginx_archive"
|
||||
|
||||
install_java "$java_archive"
|
||||
install_nginx "$nginx_archive"
|
||||
write_nginx_conf
|
||||
|
||||
log_info "环境安装完成"
|
||||
log_info "JAVA_HOME=$JAVA_HOME"
|
||||
log_info "NGINX_HOME=$NGINX_HOME"
|
||||
log_info "前端端口=$FRONTEND_PORT,后端端口=$BACKEND_PORT"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
208
bin/prod/restart_java.sh
Executable file
208
bin/prod/restart_java.sh
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
WEBAPP_ROOT="/home/webapp"
|
||||
ENV_ROOT="$WEBAPP_ROOT/env"
|
||||
APP_ROOT="$WEBAPP_ROOT/loan-pricing"
|
||||
JAVA_HOME="$ENV_ROOT/jdk"
|
||||
BACKEND_DIR="$APP_ROOT/backend"
|
||||
LOG_DIR="$APP_ROOT/logs"
|
||||
RUN_DIR="$APP_ROOT/run"
|
||||
BACKEND_PID_FILE="$RUN_DIR/backend.pid"
|
||||
BACKEND_JAR="$BACKEND_DIR/ruoyi-admin.jar"
|
||||
BACKEND_CONSOLE_LOG="$LOG_DIR/backend-console.log"
|
||||
BACKEND_PORT=63310
|
||||
BACKEND_MARKER="-Dloan.pricing.home=$APP_ROOT"
|
||||
JAVA_OPTS="$BACKEND_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
|
||||
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'
|
||||
用法: ./restart_java.sh [start|stop|restart|status]
|
||||
|
||||
默认动作:
|
||||
restart 重启后端 Java 进程
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_runtime_dirs() {
|
||||
mkdir -p "$BACKEND_DIR" "$LOG_DIR" "$RUN_DIR"
|
||||
}
|
||||
|
||||
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
|
||||
*"$BACKEND_MARKER"*"$BACKEND_JAR"*|*"$BACKEND_JAR"*"$BACKEND_MARKER"*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
collect_backend_pids() {
|
||||
pids=""
|
||||
|
||||
if [ -f "$BACKEND_PID_FILE" ]; then
|
||||
file_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
|
||||
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
|
||||
pids="$pids $file_pid"
|
||||
fi
|
||||
fi
|
||||
|
||||
marker_pids=$(ps -ef | awk -v marker="$BACKEND_MARKER" -v jar="$BACKEND_JAR" '
|
||||
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
|
||||
for (i = 1; i < NF; i++) {
|
||||
if ($i == "-jar" && $(i + 1) == jar) {
|
||||
print $2
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
' | xargs 2>/dev/null || true)
|
||||
if [ -n "${marker_pids:-}" ]; then
|
||||
for pid in $marker_pids; do
|
||||
if is_managed_backend_pid "$pid"; then
|
||||
pids="$pids $pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s\n' "$(echo "$pids" | xargs 2>/dev/null || true)"
|
||||
}
|
||||
|
||||
stop_backend() {
|
||||
pids=$(collect_backend_pids)
|
||||
|
||||
if [ -z "${pids:-}" ]; then
|
||||
rm -f "$BACKEND_PID_FILE"
|
||||
log_info "未发现运行中的后端进程"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "停止后端进程: $pids"
|
||||
for pid in $pids; do
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
done
|
||||
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt 30 ]; do
|
||||
remaining=""
|
||||
for pid in $pids; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
remaining="$remaining $pid"
|
||||
fi
|
||||
done
|
||||
|
||||
remaining=$(echo "$remaining" | xargs 2>/dev/null || true)
|
||||
if [ -z "${remaining:-}" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
if [ -n "${remaining:-}" ]; then
|
||||
log_info "执行强制停止: $remaining"
|
||||
for pid in $remaining; do
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
rm -f "$BACKEND_PID_FILE"
|
||||
}
|
||||
|
||||
start_backend() {
|
||||
ensure_runtime_dirs
|
||||
|
||||
if [ ! -x "$JAVA_HOME/bin/java" ]; then
|
||||
log_error "未检测到可执行 Java: $JAVA_HOME/bin/java"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BACKEND_JAR" ]; then
|
||||
log_error "未找到后端 jar: $BACKEND_JAR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$(collect_backend_pids)" ]; then
|
||||
log_error "检测到后端已在运行,请先执行 stop 或 restart"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\n===== %s restart =====\n' "$(timestamp)" >> "$BACKEND_CONSOLE_LOG"
|
||||
|
||||
(
|
||||
cd "$BACKEND_DIR"
|
||||
nohup "$JAVA_HOME/bin/java" $JAVA_OPTS -jar "$BACKEND_JAR" --spring.profiles.active=pro --server.port="$BACKEND_PORT" >> "$BACKEND_CONSOLE_LOG" 2>&1 &
|
||||
echo $! > "$BACKEND_PID_FILE"
|
||||
)
|
||||
|
||||
sleep 3
|
||||
|
||||
backend_pid=$(cat "$BACKEND_PID_FILE" 2>/dev/null || true)
|
||||
if [ -z "${backend_pid:-}" ] || ! kill -0 "$backend_pid" 2>/dev/null; then
|
||||
log_error "后端启动失败,请检查日志: $BACKEND_CONSOLE_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "后端已启动,PID: $backend_pid"
|
||||
}
|
||||
|
||||
status_backend() {
|
||||
pids=$(collect_backend_pids)
|
||||
if [ -n "${pids:-}" ]; then
|
||||
log_info "后端正在运行,进程: $pids"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "后端未运行"
|
||||
}
|
||||
|
||||
main() {
|
||||
action="${1:-restart}"
|
||||
|
||||
case "$action" in
|
||||
start)
|
||||
start_backend
|
||||
;;
|
||||
stop)
|
||||
stop_backend
|
||||
;;
|
||||
restart)
|
||||
stop_backend
|
||||
start_backend
|
||||
;;
|
||||
status)
|
||||
status_backend
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
116
bin/prod/restart_java_test.sh
Normal file
116
bin/prod/restart_java_test.sh
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
|
||||
SCRIPT_UNDER_TEST="$ROOT_DIR/bin/prod/restart_java.sh"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_grep() {
|
||||
pattern="$1"
|
||||
target="$2"
|
||||
if ! grep -Eq -- "$pattern" "$target"; then
|
||||
fail "expected pattern [$pattern] in $target"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_grep() {
|
||||
pattern="$1"
|
||||
target="$2"
|
||||
if grep -Eq -- "$pattern" "$target"; then
|
||||
fail "did not expect pattern [$pattern] in $target"
|
||||
fi
|
||||
}
|
||||
|
||||
create_fake_java() {
|
||||
fake_java="$1"
|
||||
|
||||
cat > "$fake_java" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
while :; do
|
||||
sleep 1
|
||||
done
|
||||
EOF
|
||||
|
||||
chmod +x "$fake_java"
|
||||
}
|
||||
|
||||
prepare_script_env() {
|
||||
work_dir="$1"
|
||||
|
||||
mkdir -p "$work_dir/env/jdk/bin" "$work_dir/loan-pricing/backend" "$work_dir/loan-pricing/logs" "$work_dir/loan-pricing/run"
|
||||
create_fake_java "$work_dir/env/jdk/bin/java"
|
||||
printf 'fake-jar\n' > "$work_dir/loan-pricing/backend/ruoyi-admin.jar"
|
||||
cp "$SCRIPT_UNDER_TEST" "$work_dir/restart_java.sh"
|
||||
perl -0pi -e "s#WEBAPP_ROOT=\"/home/webapp\"#WEBAPP_ROOT=\"$work_dir\"#" "$work_dir/restart_java.sh"
|
||||
chmod +x "$work_dir/restart_java.sh"
|
||||
}
|
||||
|
||||
cleanup_work_dir() {
|
||||
work_dir="$1"
|
||||
|
||||
if [ -f "$work_dir/loan-pricing/run/backend.pid" ]; then
|
||||
backend_pid=$(cat "$work_dir/loan-pricing/run/backend.pid" 2>/dev/null || true)
|
||||
if [ -n "${backend_pid:-}" ]; then
|
||||
kill "$backend_pid" 2>/dev/null || true
|
||||
wait "$backend_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$work_dir"
|
||||
}
|
||||
|
||||
test_script_contract() {
|
||||
assert_grep 'JAVA_HOME="\$ENV_ROOT/jdk"' "$SCRIPT_UNDER_TEST"
|
||||
assert_grep '--spring\.profiles\.active=pro' "$SCRIPT_UNDER_TEST"
|
||||
assert_grep 'ps -ef' "$SCRIPT_UNDER_TEST"
|
||||
assert_not_grep 'pgrep' "$SCRIPT_UNDER_TEST"
|
||||
assert_not_grep 'mvn' "$SCRIPT_UNDER_TEST"
|
||||
assert_not_grep 'require_root' "$SCRIPT_UNDER_TEST"
|
||||
assert_not_grep '\b(ss|lsof|netstat)\b' "$SCRIPT_UNDER_TEST"
|
||||
}
|
||||
|
||||
test_restart_flow() {
|
||||
work_dir=$(mktemp -d)
|
||||
trap 'cleanup_work_dir "$work_dir"' EXIT INT TERM
|
||||
|
||||
prepare_script_env "$work_dir"
|
||||
|
||||
"$work_dir/restart_java.sh" start
|
||||
if [ ! -f "$work_dir/loan-pricing/run/backend.pid" ]; then
|
||||
fail "expected backend pid file after start"
|
||||
fi
|
||||
|
||||
backend_pid=$(cat "$work_dir/loan-pricing/run/backend.pid")
|
||||
kill -0 "$backend_pid" 2>/dev/null || fail "expected backend process to be running after start"
|
||||
|
||||
status_output=$("$work_dir/restart_java.sh" status 2>&1 || true)
|
||||
printf '%s\n' "$status_output" | grep -q '后端正在运行' || fail "expected status output to show running"
|
||||
|
||||
"$work_dir/restart_java.sh" restart
|
||||
restarted_pid=$(cat "$work_dir/loan-pricing/run/backend.pid")
|
||||
kill -0 "$restarted_pid" 2>/dev/null || fail "expected backend process to be running after restart"
|
||||
|
||||
"$work_dir/restart_java.sh" stop
|
||||
if [ -f "$work_dir/loan-pricing/run/backend.pid" ]; then
|
||||
fail "expected backend pid file to be removed after stop"
|
||||
fi
|
||||
|
||||
trap - EXIT INT TERM
|
||||
cleanup_work_dir "$work_dir"
|
||||
}
|
||||
|
||||
main() {
|
||||
[ -f "$SCRIPT_UNDER_TEST" ] || fail "script under test not found: $SCRIPT_UNDER_TEST"
|
||||
test_script_contract
|
||||
test_restart_flow
|
||||
printf 'PASS: restart_java tests\n'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
279
bin/restart_java_backend.sh
Executable file
279
bin/restart_java_backend.sh
Executable file
@@ -0,0 +1,279 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
LOG_DIR="$ROOT_DIR/logs"
|
||||
CONSOLE_LOG="$LOG_DIR/backend-console.log"
|
||||
PID_FILE="$LOG_DIR/backend-java.pid"
|
||||
TARGET_DIR="$ROOT_DIR/ruoyi-admin/target"
|
||||
JAR_NAME="ruoyi-admin.jar"
|
||||
JAVA_HOME_1_8="/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home"
|
||||
JAVA_BIN="$JAVA_HOME_1_8/bin/java"
|
||||
SERVER_PORT=63310
|
||||
STOP_WAIT_SECONDS=30
|
||||
APP_MARKER="-Dloan.pricing.backend.root=$ROOT_DIR"
|
||||
JAVA_OPTS="$APP_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
|
||||
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'
|
||||
用法: ./bin/restart_java_backend.sh [start|stop|restart|status]
|
||||
|
||||
默认动作:
|
||||
restart 重新构建后端并重启,随后持续输出运行日志
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
setup_java_env() {
|
||||
if [ ! -x "$JAVA_BIN" ]; then
|
||||
log_error "未找到 JDK 1.8: $JAVA_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_HOME="$JAVA_HOME_1_8"
|
||||
export JAVA_HOME
|
||||
PATH="$JAVA_HOME/bin:$PATH"
|
||||
export PATH
|
||||
}
|
||||
|
||||
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() {
|
||||
all_pids=""
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
file_pid=$(cat "$PID_FILE" 2>/dev/null || true)
|
||||
if [ -n "${file_pid:-}" ] && is_managed_backend_pid "$file_pid"; then
|
||||
all_pids="$all_pids $file_pid"
|
||||
fi
|
||||
fi
|
||||
|
||||
marker_pids=$(ps -ef | awk -v marker="$APP_MARKER" -v jar="$JAR_NAME" '
|
||||
index($0, "<defunct>") == 0 && index($0, marker) > 0 {
|
||||
for (i = 1; i < NF; i++) {
|
||||
if ($i == "-jar" && $(i + 1) == jar) {
|
||||
print $2
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
' | xargs 2>/dev/null || true)
|
||||
if [ -n "${marker_pids:-}" ]; then
|
||||
for pid in $marker_pids; do
|
||||
if is_managed_backend_pid "$pid"; then
|
||||
all_pids="$all_pids $pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
unique_pids=""
|
||||
for pid in $all_pids; do
|
||||
case " $unique_pids " in
|
||||
*" $pid "*) ;;
|
||||
*)
|
||||
unique_pids="$unique_pids $pid"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$(echo "$unique_pids" | xargs 2>/dev/null || true)"
|
||||
}
|
||||
|
||||
build_backend() {
|
||||
log_info "开始构建后端: mvn -pl ruoyi-admin -am clean package -DskipTests"
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
mvn -pl ruoyi-admin -am clean package -DskipTests
|
||||
)
|
||||
}
|
||||
|
||||
stop_backend() {
|
||||
pids=$(collect_pids)
|
||||
|
||||
if [ -z "${pids:-}" ]; then
|
||||
log_info "未发现运行中的后端进程"
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "准备停止后端进程: $pids"
|
||||
for pid in $pids; do
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
done
|
||||
|
||||
remaining_pids="$pids"
|
||||
elapsed=0
|
||||
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=$(echo "$remaining_pids" | xargs 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 "后端停止完成"
|
||||
}
|
||||
|
||||
start_backend() {
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$CONSOLE_LOG"
|
||||
|
||||
printf '\n===== %s restart =====\n' "$(timestamp)" >> "$CONSOLE_LOG"
|
||||
|
||||
log_info "开始启动后端,控制台日志输出到: $CONSOLE_LOG"
|
||||
if [ ! -f "$TARGET_DIR/$JAR_NAME" ]; then
|
||||
log_error "未找到打包产物: $TARGET_DIR/$JAR_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$TARGET_DIR"
|
||||
nohup "$JAVA_BIN" $JAVA_OPTS -jar "$JAR_NAME" >> "$CONSOLE_LOG" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
)
|
||||
|
||||
sleep 3
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
status_backend() {
|
||||
pids=$(collect_pids)
|
||||
if [ -n "${pids:-}" ]; then
|
||||
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
|
||||
log_info "后端未运行"
|
||||
fi
|
||||
}
|
||||
|
||||
follow_logs() {
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$CONSOLE_LOG"
|
||||
log_info "持续输出日志中,按 Ctrl+C 仅退出日志查看"
|
||||
tail -n 200 -F "$CONSOLE_LOG"
|
||||
}
|
||||
|
||||
start_action() {
|
||||
running_pids=$(collect_pids)
|
||||
if [ -n "${running_pids:-}" ]; then
|
||||
log_error "检测到已有后端进程在运行: $running_pids,请先执行 stop 或 restart"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_backend
|
||||
start_backend
|
||||
follow_logs
|
||||
}
|
||||
|
||||
restart_action() {
|
||||
build_backend
|
||||
stop_backend
|
||||
start_backend
|
||||
follow_logs
|
||||
}
|
||||
|
||||
main() {
|
||||
setup_java_env
|
||||
ensure_command mvn
|
||||
ensure_command lsof
|
||||
ensure_command ps
|
||||
ensure_command tail
|
||||
|
||||
action="${1:-restart}"
|
||||
case "$action" in
|
||||
start)
|
||||
start_action
|
||||
;;
|
||||
stop)
|
||||
stop_backend
|
||||
;;
|
||||
restart)
|
||||
restart_action
|
||||
;;
|
||||
status)
|
||||
status_backend
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
190
deploy/2026-03-31-local-nginx-java-install-manual.md
Normal file
190
deploy/2026-03-31-local-nginx-java-install-manual.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# 本地安装 Nginx 和 Java 手册
|
||||
|
||||
## 适用范围
|
||||
|
||||
本手册适用于需要在本地 Linux 环境手动安装贷款定价系统运行环境的场景,安装结果与当前生产脚本约定保持一致:
|
||||
|
||||
- Java 安装到 `/home/webapp/env/java`
|
||||
- Nginx 安装到 `/home/webapp/env/nginx`
|
||||
- 项目部署目录使用 `/home/webapp/loan-pricing`
|
||||
- 后端服务端口固定为 `63310`
|
||||
- 前端 Nginx 端口固定为 `63311`
|
||||
|
||||
## 前置条件
|
||||
|
||||
安装前请先确认:
|
||||
|
||||
- 当前用户具备 `root` 权限
|
||||
- 本机已配置可用的 `yum` 源
|
||||
- `/home/webapp` 目录已存在
|
||||
- `/home/webapp` 下已准备安装包:
|
||||
- `openjdk-21.0.2_linux-aarch64_bin.tar.gz`
|
||||
- `nginx-1.20.2.tar.gz`
|
||||
|
||||
如果安装包文件名不同,只要仍是 Java 的 `tar.gz` 包和 Nginx 的源码 `tar.gz` 包,也可以使用同样步骤。
|
||||
|
||||
## 目录规划
|
||||
|
||||
安装完成后目录结构如下:
|
||||
|
||||
```text
|
||||
/home/webapp
|
||||
├── env
|
||||
│ ├── java
|
||||
│ └── nginx
|
||||
└── loan-pricing
|
||||
├── backend
|
||||
├── frontend
|
||||
├── backup
|
||||
├── logs
|
||||
├── run
|
||||
└── tmp
|
||||
```
|
||||
|
||||
## 第一步:安装系统依赖
|
||||
|
||||
执行以下命令安装编译 Nginx 和运行部署脚本所需依赖:
|
||||
|
||||
```sh
|
||||
yum install -y \
|
||||
gcc \
|
||||
make \
|
||||
pcre \
|
||||
pcre-devel \
|
||||
zlib \
|
||||
zlib-devel \
|
||||
openssl \
|
||||
openssl-devel \
|
||||
tar \
|
||||
gzip \
|
||||
unzip \
|
||||
which \
|
||||
findutils \
|
||||
procps-ng \
|
||||
iproute
|
||||
```
|
||||
|
||||
## 第二步:创建目录
|
||||
|
||||
执行以下命令初始化目录:
|
||||
|
||||
```sh
|
||||
mkdir -p \
|
||||
/home/webapp/env \
|
||||
/home/webapp/loan-pricing/backend \
|
||||
/home/webapp/loan-pricing/frontend \
|
||||
/home/webapp/loan-pricing/backup \
|
||||
/home/webapp/loan-pricing/logs \
|
||||
/home/webapp/loan-pricing/run \
|
||||
/home/webapp/loan-pricing/tmp
|
||||
```
|
||||
|
||||
## 第三步:安装 Java
|
||||
|
||||
解压 Java 安装包到目标目录:
|
||||
|
||||
```sh
|
||||
rm -rf /home/webapp/env/java
|
||||
mkdir -p /home/webapp/env/java
|
||||
tar -xzf /home/webapp/openjdk-21.0.2_linux-aarch64_bin.tar.gz -C /home/webapp/env/java --strip-components=1
|
||||
```
|
||||
|
||||
验证安装结果:
|
||||
|
||||
```sh
|
||||
/home/webapp/env/java/bin/java -version
|
||||
```
|
||||
|
||||
如果能正常输出 Java 版本,说明安装成功。
|
||||
|
||||
## 第四步:安装 Nginx
|
||||
|
||||
Nginx 安装包为源码包,需要先解压、编译、安装:
|
||||
|
||||
```sh
|
||||
rm -rf /home/webapp/env/nginx
|
||||
mkdir -p /home/webapp/env/nginx
|
||||
mkdir -p /home/webapp/env/nginx-build
|
||||
tar -xzf /home/webapp/nginx-1.20.2.tar.gz -C /home/webapp/env/nginx-build
|
||||
cd /home/webapp/env/nginx-build/nginx-1.20.2
|
||||
./configure --prefix=/home/webapp/env/nginx --with-http_ssl_module
|
||||
make -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)"
|
||||
make install
|
||||
```
|
||||
|
||||
安装完成后可执行文件位置为:
|
||||
|
||||
```text
|
||||
/home/webapp/env/nginx/sbin/nginx
|
||||
```
|
||||
|
||||
## 第五步:写入 Nginx 配置
|
||||
|
||||
仓库已提供可直接参考的配置文件:
|
||||
|
||||
```text
|
||||
deploy/nginx.conf
|
||||
```
|
||||
|
||||
将该文件内容写入 `/home/webapp/env/nginx/conf/nginx.conf` 即可。
|
||||
|
||||
## 第六步:校验 Nginx 配置
|
||||
|
||||
执行:
|
||||
|
||||
```sh
|
||||
/home/webapp/env/nginx/sbin/nginx -t -c /home/webapp/env/nginx/conf/nginx.conf
|
||||
```
|
||||
|
||||
如果输出 `syntax is ok` 和 `test is successful`,说明配置可用。
|
||||
|
||||
## 第七步:启动 Nginx
|
||||
|
||||
执行:
|
||||
|
||||
```sh
|
||||
/home/webapp/env/nginx/sbin/nginx -c /home/webapp/env/nginx/conf/nginx.conf
|
||||
```
|
||||
|
||||
如果后续修改了配置,可执行:
|
||||
|
||||
```sh
|
||||
/home/webapp/env/nginx/sbin/nginx -c /home/webapp/env/nginx/conf/nginx.conf -s reload
|
||||
```
|
||||
|
||||
## 第八步:验证端口
|
||||
|
||||
执行:
|
||||
|
||||
```sh
|
||||
ss -lnt | grep 63311
|
||||
```
|
||||
|
||||
如果能看到 `63311` 监听记录,说明前端 Nginx 已启动成功。
|
||||
|
||||
## 建议执行方式
|
||||
|
||||
如果本机已经放置了以下脚本,也可以直接使用脚本完成安装:
|
||||
|
||||
```sh
|
||||
cd /home/webapp
|
||||
./install_env.sh
|
||||
```
|
||||
|
||||
如果只需要管理后端 Java 进程,可执行:
|
||||
|
||||
```sh
|
||||
cd /home/webapp
|
||||
./restart_java.sh start
|
||||
./restart_java.sh stop
|
||||
./restart_java.sh restart
|
||||
./restart_java.sh status
|
||||
```
|
||||
|
||||
## 常见检查项
|
||||
|
||||
- `yum` 不可用:先确认系统已配置可用的 `yum` 源
|
||||
- Java 未安装成功:检查 `/home/webapp/openjdk-*.tar.gz` 是否存在且未损坏
|
||||
- Nginx 编译失败:检查 `gcc`、`make`、`pcre-devel`、`zlib-devel`、`openssl-devel` 是否已安装
|
||||
- Nginx 启动失败:先执行 `nginx -t` 查看配置是否正确
|
||||
- 前端无法访问后端:检查本机 `63310` 端口是否已有 Java 服务监听
|
||||
BIN
deploy/deploy.zip
Normal file
BIN
deploy/deploy.zip
Normal file
Binary file not shown.
45
deploy/nginx.conf
Normal file
45
deploy/nginx.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
user nobody;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /home/webapp/loan-pricing/logs/nginx-error.log warn;
|
||||
pid /home/webapp/loan-pricing/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /home/webapp/env/nginx/conf/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /home/webapp/loan-pricing/logs/nginx-access.log main;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 100m;
|
||||
|
||||
server {
|
||||
listen 63311;
|
||||
server_name _;
|
||||
|
||||
root /home/webapp/loan-pricing/frontend/dist;
|
||||
index index.html;
|
||||
|
||||
location /prod-api/ {
|
||||
proxy_pass http://127.0.0.1:63310/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
doc/2026-04-15-AGENTS中文化实施记录.md
Normal file
17
doc/2026-04-15-AGENTS中文化实施记录.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# AGENTS 中文化实施记录
|
||||
|
||||
## 修改内容
|
||||
- 将根目录 `AGENTS.md` 中英文版仓库协作指南完整转换为中文表述。
|
||||
- 保留原有章节结构与约束语义,仅调整为中文描述,未改变规则内容。
|
||||
|
||||
## 影响范围
|
||||
- 影响文件:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/AGENTS.md`
|
||||
- 本次实施记录:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-AGENTS中文化实施记录.md`
|
||||
|
||||
## 说明
|
||||
- 本次修改仅涉及文档文本,不涉及前端、后端逻辑或配置变更。
|
||||
- 未执行构建与测试,原因是本次仅为文档中文化调整。
|
||||
|
||||
## 保存路径确认
|
||||
- `AGENTS.md` 保存路径正确:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/AGENTS.md`
|
||||
- 实施记录保存路径正确:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-AGENTS中文化实施记录.md`
|
||||
15
doc/2026-04-15-AGENTS贡献指南实施文档.md
Normal file
15
doc/2026-04-15-AGENTS贡献指南实施文档.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# AGENTS 贡献指南实施文档
|
||||
|
||||
## 修改内容
|
||||
- 新增根目录 `AGENTS.md`,整理仓库贡献指南。
|
||||
- 文档内容基于当前仓库实际结构编写,覆盖后端多模块、前端 `ruoyi-ui`、常用构建命令、测试约定、提交与 PR 要求。
|
||||
|
||||
## 依据
|
||||
- 后端为 Maven 聚合工程,模块包括 `ruoyi-admin`、`ruoyi-framework`、`ruoyi-system`、`ruoyi-common`、`ruoyi-quartz`、`ruoyi-generator`。
|
||||
- 前端位于 `ruoyi-ui`,使用 Vue 2 与 Vue CLI。
|
||||
- 当前仓库历史提交极少,仅有 `init`,因此提交规范采用最小明确约束。
|
||||
- 当前未发现已提交的 `src/test` 测试文件,测试章节据此说明现状并给出新增约定。
|
||||
|
||||
## 保存路径确认
|
||||
- 贡献指南保存路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/AGENTS.md`
|
||||
- 本次实施文档保存路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-AGENTS贡献指南实施文档.md`
|
||||
90
doc/2026-04-15-Breadcrumb重复key修复前端实施记录.md
Normal file
90
doc/2026-04-15-Breadcrumb重复key修复前端实施记录.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Breadcrumb 重复 key 修复前端实施记录
|
||||
|
||||
## 1. 实际改动内容
|
||||
|
||||
### 1.1 修复 Breadcrumb 重复 key 告警
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-ui/src/components/Breadcrumb/index.vue`
|
||||
- `ruoyi-ui/src/components/Breadcrumb/utils.js`
|
||||
|
||||
改动内容:
|
||||
|
||||
- 将 Breadcrumb 列表项的 `key` 生成逻辑从直接使用 `item.path` 调整为统一调用 `buildBreadcrumbItemKey`
|
||||
- 新增 `buildBreadcrumbItemKey(item, index)` 工具方法,使用 `path + title + index` 组合生成稳定且唯一的 key
|
||||
- 保持现有面包屑展示逻辑不变,不调整路由结构、不修改首页与当前页的展示顺序
|
||||
|
||||
根因说明:
|
||||
|
||||
- 当前项目的 Breadcrumb 会在非首页场景外额外插入一个 `首页` 面包屑,路径固定为 `'/index'`
|
||||
- 当当前页面本身也对应 `'/index'` 时,原逻辑使用 `item.path` 作为 `transition-group` 的 key,会同时生成两个 `'/index'`
|
||||
- Vue 因此抛出 `Duplicate keys detected: '/index'`
|
||||
|
||||
### 1.2 增加最小回归测试
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-ui/tests/breadcrumb-duplicate-key.test.js`
|
||||
|
||||
改动内容:
|
||||
|
||||
- 新增最小 Node 断言脚本
|
||||
- 校验当两个 Breadcrumb 条目 path 同为 `'/index'` 时,生成的 key 仍然唯一
|
||||
- 锁定本次问题,避免后续调整 Breadcrumb 时再次引入相同告警
|
||||
|
||||
## 2. 验证结果
|
||||
|
||||
### 2.1 Node 版本
|
||||
|
||||
项目中未提供 `.nvmrc`,因此未能直接执行 `nvm use` 自动切换。
|
||||
|
||||
实际使用版本:
|
||||
|
||||
- `nvm use 14.21.3`
|
||||
|
||||
### 2.2 测试命令
|
||||
|
||||
已执行:
|
||||
|
||||
- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && node tests/breadcrumb-duplicate-key.test.js`
|
||||
|
||||
结果:
|
||||
|
||||
- 测试通过
|
||||
- 输出 `breadcrumb duplicate key assertions passed`
|
||||
|
||||
### 2.3 构建命令
|
||||
|
||||
已执行:
|
||||
|
||||
- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && npm run build:prod`
|
||||
|
||||
结果:
|
||||
|
||||
- 构建成功
|
||||
- 输出 `DONE Build complete. The dist directory is ready to be deployed.`
|
||||
|
||||
### 2.4 构建告警
|
||||
|
||||
存在 webpack 资源体积告警:
|
||||
|
||||
- `asset size limit`
|
||||
- `entrypoint size limit`
|
||||
|
||||
说明:
|
||||
|
||||
- 这些是现有项目静态资源体积告警
|
||||
- 本次 Breadcrumb 修复未引入新的构建错误或新的语法告警
|
||||
|
||||
## 3. 影响范围
|
||||
|
||||
- 仅涉及前端 Breadcrumb 组件
|
||||
- 未修改后端代码
|
||||
- 未修改贷款定价业务字段逻辑
|
||||
|
||||
## 4. 当前结论
|
||||
|
||||
- `Duplicate keys detected: '/index'` 的 Breadcrumb 告警已修复
|
||||
- 修复方式限定在组件 key 生成逻辑,属于最短路径处理
|
||||
- 前端回归测试与生产构建均已通过
|
||||
18
doc/2026-04-15-DictUtils泛型告警修复后端实施记录.md
Normal file
18
doc/2026-04-15-DictUtils泛型告警修复后端实施记录.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# DictUtils 泛型告警修复后端实施记录
|
||||
|
||||
## 变更时间
|
||||
- 2026-04-15
|
||||
|
||||
## 变更范围
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java`
|
||||
- `ruoyi-common/src/test/java/com/ruoyi/common/utils/DictUtilsTest.java`
|
||||
|
||||
## 变更内容
|
||||
- 修复 `DictUtils#getDictCache` 中对缓存对象直接强转 `List<SysDictData>` 触发的未检查类型转换告警。
|
||||
- 调整缓存读取顺序,优先处理 `JSONArray`,避免 JSON 数组被 `List` 分支提前命中后返回非 `SysDictData` 元素。
|
||||
- 对普通 `List` 缓存执行逐项类型校验并复制为强类型结果列表。
|
||||
- 新增 `JSONArray` 缓存场景测试,覆盖字典缓存反序列化读取逻辑。
|
||||
|
||||
## 验证结果
|
||||
- 执行 `mvn -pl ruoyi-common -Dtest=DictUtilsTest test`,测试通过。
|
||||
- 执行 `mvn -pl ruoyi-common -am clean compile`,编译通过,未再出现 `DictUtils.java` 的未检查类型转换告警。
|
||||
26
doc/2026-04-15-上虞对公展示指标对齐前端实施计划.md
Normal file
26
doc/2026-04-15-上虞对公展示指标对齐前端实施计划.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 2026-04-15 上虞对公展示指标对齐前端实施计划
|
||||
|
||||
## 改动内容
|
||||
- 对齐 [CorporateCreateDialog.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue) 的对公新增弹窗:
|
||||
- 新增 `repayMethod`
|
||||
- `loanTerm` 改为 `1-6` 年下拉
|
||||
- `collType` 改为 `一类/二类/三类/四类`
|
||||
- 对外提交字段改为 `isTradeBuildEnt`
|
||||
- `isGreenLoan`、`isTradeBuildEnt`、`collThirdParty` 统一提交 `0/1`
|
||||
- 移除 `isAgriGuar`、`isTechEnt`
|
||||
- 对齐 [CorporateWorkflowDetail.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue) 的流程详情录入字段展示:
|
||||
- 新增 `还款方式`
|
||||
- `贷款期限` 改为 `借款期限`
|
||||
- 保留 `绿色贷款`、`贸易和建筑业企业`、`抵质押类型`、`抵质押物是否三方所有`
|
||||
- 移除 `省农担担保贷款`、`科技型企业`
|
||||
- 对齐 [ModelOutputDisplay.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue) 的企业模型输出展示口径:
|
||||
- 展示 `repayMethod`、`isTradeBuildEnt`
|
||||
- 不再展示 `省农担担保贷款`、`科技型企业`
|
||||
- 新增/更新前端静态断言:
|
||||
- [corporate-create-input-params.test.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/tests/corporate-create-input-params.test.js)
|
||||
- [corporate-display-fields.test.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/tests/corporate-display-fields.test.js)
|
||||
|
||||
## 验证记录
|
||||
- `source ~/.nvm/nvm.sh && nvm use 14 >/dev/null && node tests/corporate-create-input-params.test.js`
|
||||
- `source ~/.nvm/nvm.sh && nvm use 14 >/dev/null && node tests/corporate-display-fields.test.js`
|
||||
- `source ~/.nvm/nvm.sh && nvm use 14 >/dev/null && npm run build:prod`
|
||||
39
doc/2026-04-15-上虞对公展示指标对齐后端实施计划.md
Normal file
39
doc/2026-04-15-上虞对公展示指标对齐后端实施计划.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 2026-04-15 上虞对公展示指标对齐后端实施计划
|
||||
|
||||
## 改动内容
|
||||
- 对齐对公创建接口 DTO 与模型调用 DTO:
|
||||
- [CorporateLoanPricingCreateDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java)
|
||||
- [ModelInvokeDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java)
|
||||
- 新增 `repayMethod`
|
||||
- 对外字段改为 `isTradeBuildEnt`
|
||||
- `loanTerm` 校验为 `1-6`
|
||||
- `collType` 校验为 `一类/二类/三类/四类`
|
||||
- 对齐流程实体、详情出参和模型输出镜像:
|
||||
- [LoanPricingWorkflow.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java)
|
||||
- [ModelCorpOutputFields.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java)
|
||||
- [LoanPricingConverter.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java)
|
||||
- [LoanPricingModelService.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java)
|
||||
- [LoanPricingWorkflowServiceImpl.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java)
|
||||
- 内部继续复用 `isTradeConstruction` 落库,外部统一返回 `isTradeBuildEnt`
|
||||
- `isAgriGuar`、`isTechEnt` 从对外 JSON 隐藏
|
||||
- 企业模型输出补充 `repayMethod`、`isTradeBuildEnt` 展示镜像
|
||||
- 对齐 mock 与 SQL 资产:
|
||||
- [corp_output.json](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/resources/data/corp_output.json)
|
||||
- [loan_pricing_workflow.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_workflow.sql)
|
||||
- [loan_pricing_schema_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_schema_20260328.sql)
|
||||
- [loan_pricing_prod_init_20260331.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_prod_init_20260331.sql)
|
||||
- [loan_pricing_required_data_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_required_data_20260328.sql)
|
||||
- [loan_pricing_alter_20260415_repay_method.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_alter_20260415_repay_method.sql)
|
||||
- [test_corporate_create.http](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/test_api/test_corporate_create.http)
|
||||
- [test_corporate_create.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/test_api/test_corporate_create.sh)
|
||||
- `loan_pricing_workflow` 增加 `repay_method`
|
||||
- mock 数据和接口样例统一为 Excel 字段名与 `0/1` 口径
|
||||
- 补充独立增量 SQL,便于其他环境按最小影响同步结构
|
||||
- 新增/更新后端定向单测:
|
||||
- [LoanPricingModelServiceCorporateParamsTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceCorporateParamsTest.java)
|
||||
- [LoanPricingModelServiceTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java)
|
||||
- [LoanPricingWorkflowServiceImplTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java)
|
||||
- [LoanPricingConverterTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java)
|
||||
|
||||
## 验证记录
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceCorporateParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest,LoanPricingConverterTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
403
doc/2026-04-15-全量迁移892-without-redis分支功能设计文档.md
Normal file
403
doc/2026-04-15-全量迁移892-without-redis分支功能设计文档.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# 全量迁移 `892-without-redis` 分支功能设计文档
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前分支为 `892-jdk-8-without-redis`,已经完成 JDK8 适配与 Redis 移除改造。目标分支 `origin/892-without-redis` 包含贷款定价业务、登录安全、部署脚本、生产初始化 SQL、文档与实施记录等多类改动。
|
||||
|
||||
本次需求已确认如下:
|
||||
|
||||
- 迁移范围为目标分支中的全部功能,不只限于贷款定价主链
|
||||
- 迁移目标以“功能行为完全对齐目标分支”为准
|
||||
- 不要求提交历史一致,也不要求逐文件原样搬运
|
||||
- 当前分支已完成的 JDK8 与去 Redis 基线必须保留,不能被回退
|
||||
|
||||
由于两个分支不存在共同 `merge base`,不能按常规增量合并处理。本次应采用“以当前分支为基线,按功能行为对齐迁移”的方式落地。
|
||||
|
||||
## 2. 已确认约束
|
||||
|
||||
- 当前分支保留:
|
||||
- JDK8 运行基线
|
||||
- 去 Redis 后的内存缓存实现
|
||||
- 目标分支中的所有业务能力、脚本能力、初始化能力都要迁入当前分支
|
||||
- 迁移完成的判断标准是“当前分支行为与目标分支一致”,不是“文件内容完全一致”
|
||||
- 不采用补丁式兜底方案,不引入额外兼容分支
|
||||
- 所有本次改动需要在 `doc/` 目录留下文档记录
|
||||
- 前后端同时涉及改动时,后续必须分别输出前端实施计划和后端实施计划
|
||||
|
||||
## 3. 现状分析
|
||||
|
||||
### 3.1 分支关系现状
|
||||
|
||||
当前仓库中:
|
||||
|
||||
- 本地分支:`892-jdk-8-without-redis`
|
||||
- 远端参考分支:`origin/892-without-redis`
|
||||
|
||||
两者没有共同合并基线,说明不能通过简单 `merge`、`cherry-pick` 或三点 diff 来安全迁移全部功能。
|
||||
|
||||
### 3.2 目标分支改动构成
|
||||
|
||||
经核对,目标分支包含以下几类内容:
|
||||
|
||||
1. 贷款定价主业务
|
||||
- 流程列表的测算利率、执行利率、更新时间
|
||||
- 流程详情字段补齐与详情卡片顺序调整
|
||||
- 个人最终测算利率展示
|
||||
- 个人测算入参对齐
|
||||
- 敏感信息存储加密、接口脱敏、模型调用前解密
|
||||
|
||||
2. 登录与账号相关
|
||||
- 登录页默认账号密码移除
|
||||
- 登录密码传输相关前后端改造
|
||||
|
||||
3. 运行与部署相关
|
||||
- 后端重启脚本
|
||||
- 生产一键部署脚本
|
||||
- 压缩包目录结构与进程识别规则调整
|
||||
- 多环境配置文件补充
|
||||
|
||||
4. 数据初始化与数据库脚本
|
||||
- 贷款定价相关表结构脚本
|
||||
- 菜单初始化脚本
|
||||
- 生产初始化数据库脚本
|
||||
|
||||
5. 文档与测试资产
|
||||
- 业务设计文档、实施计划、实施记录
|
||||
- `test_api` 示例
|
||||
- 前端静态测试脚本
|
||||
|
||||
### 3.3 当前分支迁移风险
|
||||
|
||||
如果直接套用目标分支文件,会出现以下风险:
|
||||
|
||||
- 覆盖当前分支已经完成的去 Redis 实现
|
||||
- 引入不适配 JDK8 的实现方式
|
||||
- 将目标分支中与当前分支冲突的配置整文件覆盖
|
||||
- 把脚本和 SQL 迁进来但没有同步前后端行为,导致联调失败
|
||||
|
||||
因此,本次需要以“保留当前底座、按业务行为重建目标功能”的方式推进。
|
||||
|
||||
## 4. 方案对比
|
||||
|
||||
### 方案一:按业务域做行为对齐迁移
|
||||
|
||||
做法:
|
||||
|
||||
- 以当前分支为基线
|
||||
- 按功能域拆分迁移目标分支能力
|
||||
- 对于冲突文件采用手工整合
|
||||
- 最终以页面表现、接口行为、脚本效果与目标分支一致为准
|
||||
|
||||
优点:
|
||||
|
||||
- 最适合当前两条独立历史的情况
|
||||
- 可以稳定保住当前分支 JDK8 与去 Redis 基线
|
||||
- 更容易分块验证与回归
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要逐模块核对行为
|
||||
- 人工整合成本高于直接覆盖
|
||||
|
||||
### 方案二:大面积覆盖目标分支文件后回补当前基线
|
||||
|
||||
做法:
|
||||
|
||||
- 尽量把目标分支文件整体覆盖到当前分支
|
||||
- 再回头修复 JDK8 和去 Redis 冲突
|
||||
|
||||
优点:
|
||||
|
||||
- 前期拷贝速度快
|
||||
|
||||
缺点:
|
||||
|
||||
- 极易冲掉当前分支已经验证过的基础改造
|
||||
- 后置修复成本高
|
||||
- 很难确认哪些回归来自覆盖行为
|
||||
|
||||
### 方案三:按提交批量迁移
|
||||
|
||||
做法:
|
||||
|
||||
- 尝试对目标分支提交做 `cherry-pick` 或补丁迁移
|
||||
|
||||
优点:
|
||||
|
||||
- 理论上来源更清晰
|
||||
|
||||
缺点:
|
||||
|
||||
- 两个分支无共同基线,冲突会非常多
|
||||
- 目标分支提交混杂,难以按提交边界安全迁移
|
||||
|
||||
## 5. 设计结论
|
||||
|
||||
采用方案一:按业务域做行为对齐迁移。
|
||||
|
||||
核心原则如下:
|
||||
|
||||
- 保留当前分支的 JDK8 与去 Redis 基线
|
||||
- 迁移目标分支中的全部功能,但不强求原样搬代码
|
||||
- 所有冲突位置以“行为一致 + 当前底座不回退”为判定标准
|
||||
- 迁移顺序固定为“基线对齐 -> 后端能力 -> 前端行为 -> 验证收口”
|
||||
|
||||
## 6. 迁移边界设计
|
||||
|
||||
本次将目标分支改动拆分为 5 个迁移包:
|
||||
|
||||
### 6.1 贷款定价主业务包
|
||||
|
||||
包含:
|
||||
|
||||
- 流程列表字段与刷新行为
|
||||
- 详情展示与卡片顺序调整
|
||||
- 个人与企业建单链路
|
||||
- 个人测算入参对齐
|
||||
- 最终测算利率展示
|
||||
- 敏感信息加密、脱敏、模型调用链路
|
||||
- 相关 SQL 脚本与测试文件
|
||||
|
||||
### 6.2 登录与账号包
|
||||
|
||||
包含:
|
||||
|
||||
- 登录页默认账号密码移除
|
||||
- 登录密码传输相关前后端链路
|
||||
- 控制器、API、工具函数与测试
|
||||
|
||||
### 6.3 部署与运行包
|
||||
|
||||
包含:
|
||||
|
||||
- 重启脚本
|
||||
- 生产一键部署脚本
|
||||
- 环境安装脚本
|
||||
- 目录结构与 jar 进程识别规则
|
||||
- 多环境配置文件
|
||||
|
||||
### 6.4 初始化与交付包
|
||||
|
||||
包含:
|
||||
|
||||
- 生产初始化数据库脚本
|
||||
- 菜单脚本
|
||||
- 表结构变更脚本
|
||||
- `deploy/` 目录与交付物配置
|
||||
|
||||
### 6.5 文档与验证包
|
||||
|
||||
包含:
|
||||
|
||||
- 本次迁移设计文档
|
||||
- 前端实施计划
|
||||
- 后端实施计划
|
||||
- 本次迁移实施记录
|
||||
- 必要测试说明
|
||||
|
||||
## 7. 迁移顺序设计
|
||||
|
||||
### 7.1 第一阶段:基线对齐
|
||||
|
||||
目标:
|
||||
|
||||
- 锁定当前分支可保留的 JDK8 与去 Redis 底座
|
||||
- 识别目标分支需要补入的依赖、配置、脚本、SQL 基础设施
|
||||
|
||||
输出:
|
||||
|
||||
- 迁移差异清单
|
||||
- 需整合的配置与脚本列表
|
||||
|
||||
### 7.2 第二阶段:后端能力迁移
|
||||
|
||||
目标:
|
||||
|
||||
- 先完成贷款定价、登录安全、脚本能力、初始化 SQL 的后端与脚本迁移
|
||||
|
||||
原因:
|
||||
|
||||
- 前端页面行为依赖后端接口契约
|
||||
- 先稳定接口、字段和脚本能力,后续前端才能一次性对齐
|
||||
|
||||
### 7.3 第三阶段:前端行为迁移
|
||||
|
||||
目标:
|
||||
|
||||
- 完成 `ruoyi-ui` 页面、API、工具函数、页面测试脚本迁移
|
||||
|
||||
重点页面:
|
||||
|
||||
- 贷款定价流程列表
|
||||
- 流程详情
|
||||
- 个人/企业建单弹窗
|
||||
- 登录页
|
||||
- 缓存监控页
|
||||
|
||||
### 7.4 第四阶段:验证与收口
|
||||
|
||||
目标:
|
||||
|
||||
- 对迁移后的当前分支做构建、测试与关键链路核验
|
||||
- 输出实施文档并确认最终行为一致
|
||||
|
||||
## 8. 冲突处理设计
|
||||
|
||||
### 8.1 Redis 冲突处理
|
||||
|
||||
凡是目标分支使用 Redis 的旧实现,统一保留当前分支的内存缓存能力,只迁移与业务行为相关的部分,不回退技术实现。
|
||||
|
||||
### 8.2 JDK8 冲突处理
|
||||
|
||||
凡是目标分支中使用高版本 JDK 写法的地方,统一改为当前分支可运行的 JDK8 写法,保持行为一致即可。
|
||||
|
||||
### 8.3 同文件多来源冲突处理
|
||||
|
||||
如果同一文件同时存在:
|
||||
|
||||
- 当前分支的去 Redis/JDK8 适配
|
||||
- 目标分支的新业务功能
|
||||
|
||||
则采用手工整合,不做整文件覆盖。
|
||||
|
||||
### 8.4 脚本与环境冲突处理
|
||||
|
||||
部署脚本与环境配置以目标分支行为为目标,但需要重新核对:
|
||||
|
||||
- 端口
|
||||
- 目录
|
||||
- 进程识别规则
|
||||
- 当前仓库的启动方式
|
||||
|
||||
### 8.5 文档路径处理
|
||||
|
||||
目标分支中存在 `docs/superpowers/...` 路径文档,但当前仓库规范要求将本次文档统一落到 `doc/`。因此文档只迁移内容,不沿用目标分支文档目录结构。
|
||||
|
||||
## 9. 详细迁移清单
|
||||
|
||||
### 9.1 贷款定价主业务
|
||||
|
||||
- 列表:
|
||||
- 测算利率
|
||||
- 执行利率
|
||||
- 更新时间
|
||||
- 返回列表自动刷新
|
||||
- 详情:
|
||||
- 个人详情字段补齐
|
||||
- 详情卡片顺序调整
|
||||
- 最终测算利率展示
|
||||
- 建单:
|
||||
- 个人测算入参对齐
|
||||
- 创建请求字段与模型调用字段统一
|
||||
- 安全:
|
||||
- `custName`
|
||||
- `idNum`
|
||||
- 存储加密、查询脱敏、模型调用前解密
|
||||
- 数据:
|
||||
- 流程表、输出表字段脚本
|
||||
- 菜单脚本
|
||||
- 测试 API 文件
|
||||
|
||||
### 9.2 登录与账号相关
|
||||
|
||||
- 登录页默认账号密码移除
|
||||
- 密码传输工具函数
|
||||
- 登录、注册、个人中心、用户管理相关前后端接口对齐
|
||||
- 对应测试补齐
|
||||
|
||||
### 9.3 运行与部署相关
|
||||
|
||||
- `bin/restart_java_backend.sh`
|
||||
- `bin/prod/*`
|
||||
- `deploy/*`
|
||||
- 环境配置文件
|
||||
- 目录结构与压缩包处理
|
||||
- 启停测试脚本
|
||||
|
||||
### 9.4 初始化与交付物
|
||||
|
||||
- 初始化数据库脚本
|
||||
- 贷款定价菜单脚本
|
||||
- 生产交付压缩包相关目录
|
||||
- 其他目标分支新增 SQL
|
||||
|
||||
## 10. 验收口径
|
||||
|
||||
迁移完成需同时满足以下条件:
|
||||
|
||||
### 10.1 后端
|
||||
|
||||
- 受影响模块测试可通过
|
||||
- 新增关键测试覆盖关键行为
|
||||
- 当前分支能正常启动后端服务
|
||||
|
||||
### 10.2 前端
|
||||
|
||||
- `ruoyi-ui` 在 `nvm use` 后可安装依赖
|
||||
- `npm run build:prod` 成功
|
||||
- 登录、流程列表、详情、建单、缓存监控等关键页面可正常访问
|
||||
|
||||
### 10.3 业务行为
|
||||
|
||||
- 贷款定价链路行为与目标分支一致
|
||||
- 登录行为与目标分支一致
|
||||
- 部署脚本的执行逻辑与目标分支一致
|
||||
- 目标分支新增 SQL 在当前分支具备等价能力
|
||||
|
||||
### 10.4 文档
|
||||
|
||||
- 在 `doc/` 目录补齐本次:
|
||||
- 设计文档
|
||||
- 后端实施计划
|
||||
- 前端实施计划
|
||||
- 实施记录
|
||||
|
||||
## 11. 测试与验证设计
|
||||
|
||||
### 11.1 后端验证
|
||||
|
||||
- 单元或控制器测试:
|
||||
- 列表字段返回
|
||||
- 详情脱敏
|
||||
- 登录密码传输
|
||||
- 缓存监控接口
|
||||
- 配置与脚本核对:
|
||||
- 多环境配置
|
||||
- 端口
|
||||
- 路径
|
||||
- 进程识别规则
|
||||
- 启动验证:
|
||||
- 至少确认迁移后可正常启动
|
||||
|
||||
### 11.2 前端验证
|
||||
|
||||
- 静态构建验证:
|
||||
- `nvm use`
|
||||
- `npm install`
|
||||
- `npm run build:prod`
|
||||
- 页面链路验证:
|
||||
- 登录
|
||||
- 流程列表
|
||||
- 流程详情
|
||||
- 个人建单
|
||||
- 缓存监控页
|
||||
|
||||
### 11.3 进程清理要求
|
||||
|
||||
若验证过程中启动前后端进程,验证结束后统一关闭,不保留测试进程。
|
||||
|
||||
## 12. 非目标
|
||||
|
||||
- 不追求与目标分支逐文件完全一致
|
||||
- 不迁移目标分支 git 历史
|
||||
- 不为了复刻目标分支而回退当前 JDK8 或去 Redis 基线
|
||||
- 不新增需求之外的兼容或降级逻辑
|
||||
|
||||
## 13. 后续实施方式
|
||||
|
||||
设计确认后,后续实施将拆分为两份计划:
|
||||
|
||||
- 后端实施计划
|
||||
- 前端实施计划
|
||||
|
||||
实施时采用“分包迁移 + 每包自验证”的方式推进,逐块完成并验证,不做一次性大批量落地后再集中排错。
|
||||
420
doc/2026-04-15-全量迁移892-without-redis前端实施计划.md
Normal file
420
doc/2026-04-15-全量迁移892-without-redis前端实施计划.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# 全量迁移 `892-without-redis` 前端实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在当前前端工程中迁入 `origin/892-without-redis` 的贷款定价页面、登录页与缓存监控相关行为,并与后端迁移后的接口契约完全对齐。
|
||||
|
||||
**Architecture:** 以前端 `ruoyi-ui` 为基线,新增贷款定价页面目录、API 封装、密码传输工具与页面测试脚本,按“接口先对齐、页面后对齐”的顺序迁移。对目标分支与当前分支冲突位置优先保留当前工程的 Vue2/RuoYi 组织方式,再把目标功能按现有结构接进去。
|
||||
|
||||
**Tech Stack:** Vue 2, Vue Router, Vuex, Element UI, Axios API wrappers, Node via nvm
|
||||
|
||||
---
|
||||
|
||||
> 仓库约束补充:执行前端验证时必须先在 `ruoyi-ui` 下执行 `nvm use`;若拉起前端 dev 进程,验证结束后要主动关闭。
|
||||
|
||||
## 文件结构映射
|
||||
|
||||
### 路由、登录、公共工具
|
||||
|
||||
- Modify: `ruoyi-ui/src/router/index.js`
|
||||
- Modify: `ruoyi-ui/src/views/login.vue`
|
||||
- Modify: `ruoyi-ui/src/api/login.js`
|
||||
- Create: `ruoyi-ui/src/utils/passwordTransfer.js`
|
||||
|
||||
### 贷款定价 API 与页面
|
||||
|
||||
- Create: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerTypeSelector.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
|
||||
|
||||
### 缓存监控页面
|
||||
|
||||
- Modify: `ruoyi-ui/src/api/monitor/cache.js`
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||
|
||||
### 前端静态测试脚本
|
||||
|
||||
- Create: `ruoyi-ui/tests/password-transfer-api.test.js`
|
||||
- Create: `ruoyi-ui/tests/personal-create-input-params.test.js`
|
||||
- Create: `ruoyi-ui/tests/retail-display-fields.test.js`
|
||||
- Create: `ruoyi-ui/tests/personal-final-calculate-rate-display.test.js`
|
||||
- Create: `ruoyi-ui/tests/workflow-detail-card-order.test.js`
|
||||
- Create: `ruoyi-ui/tests/workflow-index-refresh.test.js`
|
||||
- Create: `ruoyi-ui/tests/login-default-credentials.test.js`
|
||||
|
||||
### 前端实施记录
|
||||
|
||||
- Create: `doc/2026-04-15-全量迁移892-without-redis前端实施记录.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 接入贷款定价路由、API 和页面骨架
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/router/index.js`
|
||||
- Create: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerTypeSelector.vue`
|
||||
|
||||
- [ ] **Step 1: 先写路由/页面缺失断言脚本**
|
||||
|
||||
在 `ruoyi-ui/tests/workflow-index-refresh.test.js` 先写最小源码断言:
|
||||
|
||||
```js
|
||||
expect(routerSource).toContain("LoanPricingWorkflow")
|
||||
expect(routerSource).toContain("@/views/loanPricing/workflow/index")
|
||||
expect(routerSource).toContain("@/views/loanPricing/workflow/detail")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 API 封装**
|
||||
|
||||
`ruoyi-ui/src/api/loanPricing/workflow.js` 至少提供:
|
||||
|
||||
```js
|
||||
export function listWorkflow(query) {}
|
||||
export function getWorkflow(serialNum) {}
|
||||
export function createPersonalWorkflow(data) {}
|
||||
export function createCorporateWorkflow(data) {}
|
||||
export function updateExecuteRate(data) {}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 接入默认首页和详情路由**
|
||||
|
||||
`ruoyi-ui/src/router/index.js` 需要包含:
|
||||
|
||||
```js
|
||||
component: () => import('@/views/loanPricing/workflow/index')
|
||||
```
|
||||
|
||||
以及隐藏详情路由:
|
||||
|
||||
```js
|
||||
path: '/loanPricing/workflow-detail'
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 创建最小页面骨架**
|
||||
|
||||
`index.vue` 至少包含:
|
||||
|
||||
```vue
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
```
|
||||
|
||||
`detail.vue` 至少包含:
|
||||
|
||||
```vue
|
||||
<personal-workflow-detail v-if="form.custType === '个人'" />
|
||||
<corporate-workflow-detail v-else />
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行前端源码测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui && node tests/workflow-index-refresh.test.js
|
||||
```
|
||||
|
||||
Expected: 断言通过。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/router/index.js ruoyi-ui/src/api/loanPricing/workflow.js ruoyi-ui/src/views/loanPricing
|
||||
git commit -m "接入贷款定价前端页面骨架"
|
||||
```
|
||||
|
||||
### Task 2: 实现流程列表与详情展示行为
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
|
||||
- Test: `ruoyi-ui/tests/retail-display-fields.test.js`
|
||||
- Test: `ruoyi-ui/tests/personal-final-calculate-rate-display.test.js`
|
||||
- Test: `ruoyi-ui/tests/workflow-detail-card-order.test.js`
|
||||
|
||||
- [ ] **Step 1: 先写详情字段失败测试**
|
||||
|
||||
在源码断言里至少覆盖:
|
||||
|
||||
```js
|
||||
expect(source).toContain("loanTerm")
|
||||
expect(source).toContain("finalCalculateRate")
|
||||
expect(source).toContain("referenceRate")
|
||||
expect(source).toContain("smoothRange")
|
||||
```
|
||||
|
||||
并为卡片顺序加断言:
|
||||
|
||||
```js
|
||||
expect(detailSource.indexOf("基础信息")).toBeLessThan(detailSource.indexOf("测算结果"))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 改列表列定义**
|
||||
|
||||
`index.vue` 要明确展示:
|
||||
|
||||
```vue
|
||||
<el-table-column label="测算利率(%)" prop="calculateRate" />
|
||||
<el-table-column label="执行利率(%)" prop="executeRate" />
|
||||
<el-table-column label="更新时间" prop="updateTime" />
|
||||
```
|
||||
|
||||
个人最终测算利率按目标分支行为取后端返回的 `finalCalculateRate` 或已统一好的 `calculateRate`。
|
||||
|
||||
- [ ] **Step 3: 改详情页面与模型输出组件**
|
||||
|
||||
`PersonalWorkflowDetail.vue` 必须展示:
|
||||
|
||||
- `loanTerm`
|
||||
- `finalCalculateRate`
|
||||
- 调整后的详情卡片顺序
|
||||
|
||||
`ModelOutputDisplay.vue` 必须展示:
|
||||
|
||||
- `loanRateHistory`
|
||||
- `minRateProduct`
|
||||
- `smoothRange`
|
||||
- `finalCalculateRate`
|
||||
- `referenceRate`
|
||||
|
||||
- [ ] **Step 4: 运行源码测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui && node tests/retail-display-fields.test.js && node tests/personal-final-calculate-rate-display.test.js && node tests/workflow-detail-card-order.test.js
|
||||
```
|
||||
|
||||
Expected: 断言通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/loanPricing ruoyi-ui/tests
|
||||
git commit -m "迁移流程列表与详情展示逻辑"
|
||||
```
|
||||
|
||||
### Task 3: 实现个人/企业建单弹窗与输入参数对齐
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Test: `ruoyi-ui/tests/personal-create-input-params.test.js`
|
||||
|
||||
- [ ] **Step 1: 先写个人入参失败测试**
|
||||
|
||||
```js
|
||||
expect(source).toContain("loanPurpose")
|
||||
expect(source).toContain("loanTerm")
|
||||
expect(source).toContain("loanLoop")
|
||||
expect(source).toContain("collThirdParty")
|
||||
expect(source).toContain("['一类', '二类', '三类']")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现个人建单弹窗**
|
||||
|
||||
`PersonalCreateDialog.vue` 至少补齐:
|
||||
|
||||
```js
|
||||
form: {
|
||||
custIsn: '',
|
||||
custName: '',
|
||||
idType: '',
|
||||
idNum: '',
|
||||
guarType: '',
|
||||
applyAmt: '',
|
||||
loanPurpose: '',
|
||||
loanTerm: '',
|
||||
bizProof: false,
|
||||
loanLoop: false,
|
||||
collThirdParty: false,
|
||||
collType: ''
|
||||
}
|
||||
```
|
||||
|
||||
并在提交时直接调用 `createPersonalWorkflow`,不在前端增加补丁式兜底。
|
||||
|
||||
- [ ] **Step 3: 实现企业建单弹窗并接入列表页**
|
||||
|
||||
`index.vue` 中要能根据客户类型打开对应建单对话框,且提交后刷新列表。
|
||||
|
||||
- [ ] **Step 4: 跑源码测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui && node tests/personal-create-input-params.test.js
|
||||
```
|
||||
|
||||
Expected: 断言通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/loanPricing ruoyi-ui/tests/personal-create-input-params.test.js
|
||||
git commit -m "迁移贷款定价建单弹窗"
|
||||
```
|
||||
|
||||
### Task 4: 迁移登录密码传输与登录页展示
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/login.vue`
|
||||
- Modify: `ruoyi-ui/src/api/login.js`
|
||||
- Create: `ruoyi-ui/src/utils/passwordTransfer.js`
|
||||
- Test: `ruoyi-ui/tests/password-transfer-api.test.js`
|
||||
- Test: `ruoyi-ui/tests/login-default-credentials.test.js`
|
||||
|
||||
- [ ] **Step 1: 先写登录页失败测试**
|
||||
|
||||
`login-default-credentials.test.js` 要先断言旧默认账号密码提示不存在:
|
||||
|
||||
```js
|
||||
expect(source).not.toContain("admin")
|
||||
expect(source).not.toContain("123456")
|
||||
```
|
||||
|
||||
`password-transfer-api.test.js` 要断言登录 API 使用密码传输工具:
|
||||
|
||||
```js
|
||||
expect(apiSource).toContain("transferPassword")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 新增密码传输工具**
|
||||
|
||||
`ruoyi-ui/src/utils/passwordTransfer.js` 最小接口:
|
||||
|
||||
```js
|
||||
export function transferPassword(password) {
|
||||
return password
|
||||
}
|
||||
```
|
||||
|
||||
后续再替换为与后端一致的实际传输处理,但调用入口先统一。
|
||||
|
||||
- [ ] **Step 3: 改登录 API 与页面**
|
||||
|
||||
`src/api/login.js` 在登录、注册、修改密码等调用入口统一接 `transferPassword`。
|
||||
|
||||
`src/views/login.vue` 删除默认账号密码展示,保留验证码、记住密码和现有交互。
|
||||
|
||||
- [ ] **Step 4: 跑源码测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui && node tests/password-transfer-api.test.js && node tests/login-default-credentials.test.js
|
||||
```
|
||||
|
||||
Expected: 断言通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/api/login.js ruoyi-ui/src/utils/passwordTransfer.js ruoyi-ui/src/views/login.vue ruoyi-ui/tests
|
||||
git commit -m "迁移登录页与密码传输前端逻辑"
|
||||
```
|
||||
|
||||
### Task 5: 迁移缓存监控页并对齐内存缓存行为
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/api/monitor/cache.js`
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||
|
||||
- [ ] **Step 1: 先核对后端缓存接口字段**
|
||||
|
||||
执行前先读后端 `CacheController` 最终返回字段,记录页面需要显示哪些统计项,禁止继续按 Redis 键空间展示。
|
||||
|
||||
- [ ] **Step 2: 改 API 与页面展示**
|
||||
|
||||
`cache.js` 按后端实际接口字段更新。
|
||||
|
||||
`index.vue` / `list.vue` 改成展示内存缓存统计、命中率、键数量、过期数量等目标分支行为要求的内容。
|
||||
|
||||
- [ ] **Step 3: 做页面级静态检查**
|
||||
|
||||
至少确认源码里不再假设 Redis 特有结构,例如:
|
||||
|
||||
```js
|
||||
expect(source).not.toContain("redis")
|
||||
```
|
||||
|
||||
如需要,可新增一个简短源码测试脚本并与本任务一起提交。
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/api/monitor/cache.js ruoyi-ui/src/views/monitor/cache
|
||||
git commit -m "迁移缓存监控前端页面"
|
||||
```
|
||||
|
||||
### Task 6: 前端总验收与实施记录
|
||||
|
||||
**Files:**
|
||||
- Modify: `doc/2026-04-15-全量迁移892-without-redis前端实施计划.md`
|
||||
- Create: `doc/2026-04-15-全量迁移892-without-redis前端实施记录.md`
|
||||
|
||||
- [ ] **Step 1: 安装依赖并构建**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
nvm use
|
||||
npm install
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Expected: 生产构建成功。
|
||||
|
||||
- [ ] **Step 2: 启动前端进行关键页面冒烟**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
nvm use
|
||||
npm run dev
|
||||
```
|
||||
|
||||
检查:
|
||||
|
||||
- 登录页
|
||||
- 流程列表
|
||||
- 流程详情
|
||||
- 个人建单
|
||||
- 缓存监控页
|
||||
|
||||
验证完成后主动停止前端进程。
|
||||
|
||||
- [ ] **Step 3: 补前端实施记录**
|
||||
|
||||
`doc/2026-04-15-全量迁移892-without-redis前端实施记录.md` 至少记录:
|
||||
|
||||
- 实际迁入页面与 API
|
||||
- 与目标分支不做原样搬运的整合点
|
||||
- `nvm use` 的 Node 版本
|
||||
- 构建结果
|
||||
- 页面冒烟结果
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add doc/2026-04-15-全量迁移892-without-redis前端实施计划.md doc/2026-04-15-全量迁移892-without-redis前端实施记录.md
|
||||
git commit -m "补充全量迁移前端实施记录"
|
||||
```
|
||||
63
doc/2026-04-15-全量迁移892-without-redis前端实施记录.md
Normal file
63
doc/2026-04-15-全量迁移892-without-redis前端实施记录.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 全量迁移 `892-without-redis` 前端实施记录
|
||||
|
||||
## 修改时间
|
||||
|
||||
- 2026-04-15
|
||||
|
||||
## 本次完成内容
|
||||
|
||||
- 迁入贷款定价前端页面与组件:
|
||||
- [workflow/index.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/index.vue)
|
||||
- [workflow/detail.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/detail.vue)
|
||||
- [workflow/components/PersonalCreateDialog.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue)
|
||||
- [workflow/components/CorporateCreateDialog.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue)
|
||||
- [workflow/components/PersonalWorkflowDetail.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue)
|
||||
- [workflow/components/CorporateWorkflowDetail.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue)
|
||||
- [workflow/components/ModelOutputDisplay.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue)
|
||||
- 迁入贷款定价 API:
|
||||
- [workflow.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/api/loanPricing/workflow.js)
|
||||
- 迁入密码传输工具:
|
||||
- [passwordTransfer.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/utils/passwordTransfer.js)
|
||||
- 接入前端密码传输调用:
|
||||
- [src/api/login.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/api/login.js)
|
||||
- [src/api/system/user.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/api/system/user.js)
|
||||
- 调整登录页默认值为空,移除默认账号密码回填:
|
||||
- [src/views/login.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/login.vue)
|
||||
- 补充前端密码传输环境变量:
|
||||
- [ruoyi-ui/.env.development](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/.env.development)
|
||||
- [ruoyi-ui/.env.production](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/.env.production)
|
||||
- 补充前端依赖:
|
||||
- `crypto-js`
|
||||
- `html-webpack-plugin`
|
||||
- 迁入目标分支中的前端静态测试:
|
||||
- `ruoyi-ui/tests/*`
|
||||
|
||||
## 关键整合说明
|
||||
|
||||
- 前端密码传输使用目标分支的 AES ECB 方案,但仍按当前仓库结构挂载到现有 `src/api` 层
|
||||
- 登录页默认用户名和默认密码已清空,同时保留 cookie 回填逻辑
|
||||
- 依赖安装时使用 `nvm use 14.21.3`,满足仓库对前端 Node 版本由 `nvm` 控制的要求
|
||||
- `npm install` 后补了 `html-webpack-plugin`,用于修复现有构建链缺失 peer 依赖导致的生产构建失败
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 前端静态测试
|
||||
|
||||
- `source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && node tests/password-transfer-api.test.js && node tests/login-default-credentials.test.js && node tests/personal-create-input-params.test.js && node tests/retail-display-fields.test.js && node tests/personal-final-calculate-rate-display.test.js && node tests/workflow-detail-card-order.test.js && node tests/workflow-index-refresh.test.js`
|
||||
- 结果:通过
|
||||
|
||||
### 依赖安装
|
||||
|
||||
- `source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 && npm install`
|
||||
- 结果:通过
|
||||
|
||||
### 生产构建
|
||||
|
||||
- `source ~/.nvm/nvm.sh && cd ruoyi-ui && nvm use 14.21.3 >/dev/null && npm run build:prod`
|
||||
- 结果:通过
|
||||
- 备注:有 asset size warning,但构建成功,`dist/` 已生成
|
||||
|
||||
## 未在本记录中执行的内容
|
||||
|
||||
- 未启动前端 dev server 做交互式页面冒烟
|
||||
- 因此没有需要额外清理的前端测试进程
|
||||
587
doc/2026-04-15-全量迁移892-without-redis后端实施计划.md
Normal file
587
doc/2026-04-15-全量迁移892-without-redis后端实施计划.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# 全量迁移 `892-without-redis` 后端实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在保留当前分支 JDK8 与去 Redis 基线的前提下,把 `origin/892-without-redis` 的后端业务、脚本、SQL、配置能力完整迁移到当前分支,并保证行为对齐。
|
||||
|
||||
**Architecture:** 以后端主工程为基线,新增 `ruoyi-loan-pricing` 业务模块并接入 `ruoyi-admin`、根 `pom.xml` 与 SQL/脚本目录。所有与 Redis 或高版本 JDK 冲突的实现只迁业务行为,不回退当前分支的技术底座;同文件冲突采用手工整合,不做整文件覆盖。
|
||||
|
||||
**Tech Stack:** Java 8, Spring Boot 2.5.x, Maven multi-module, MyBatis/XML Mapper, JUnit, Shell scripts, SQL
|
||||
|
||||
---
|
||||
|
||||
> 仓库约束补充:本仓库 `AGENTS.md` 明确要求不开启 subagent,因此执行本计划时应在当前会话内分批完成,不走子代理评审流。
|
||||
|
||||
## 文件结构映射
|
||||
|
||||
### 根工程与模块接入
|
||||
|
||||
- Modify: `pom.xml`
|
||||
- Modify: `ruoyi-admin/pom.xml`
|
||||
- Create: `ruoyi-loan-pricing/pom.xml`
|
||||
|
||||
### 贷款定价后端模块
|
||||
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/LoanPricingWorkflowMapper.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/ModelRetailOutputFieldsMapper.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/ModelCorpOutputFieldsMapper.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ModelService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml`
|
||||
- Create: `ruoyi-loan-pricing/src/main/resources/data/corp_output.json`
|
||||
- Create: `ruoyi-loan-pricing/src/main/resources/data/retail_output.json`
|
||||
|
||||
### 登录密码传输与系统接口调整
|
||||
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java`
|
||||
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java`
|
||||
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java`
|
||||
|
||||
### 缓存、配置、部署与 SQL
|
||||
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
||||
- Create: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
- Create: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Modify: `bin/restart_java_backend.sh`
|
||||
- Create: `bin/restart_java_backend_test.sh`
|
||||
- Create: `bin/prod/deploy_from_package.sh`
|
||||
- Create: `bin/prod/deploy_from_package_test.sh`
|
||||
- Create: `bin/prod/deploy_release.sh`
|
||||
- Create: `bin/prod/install_env.sh`
|
||||
- Create: `bin/prod/restart_java.sh`
|
||||
- Create: `bin/prod/restart_java_test.sh`
|
||||
- Create: `deploy/nginx.conf`
|
||||
- Create: `deploy/2026-03-31-local-nginx-java-install-manual.md`
|
||||
- Create: `sql/loan_pricing_schema_20260328.sql`
|
||||
- Create: `sql/loan_pricing_required_data_20260328.sql`
|
||||
- Create: `sql/loan_pricing_prod_init_20260331.sql`
|
||||
- Create: `sql/loan_pricing_menu.sql`
|
||||
- Create: `sql/loan_pricing_workflow.sql`
|
||||
- Create: `sql/model_retail.sql`
|
||||
- Create: `sql/model_corp.sql`
|
||||
|
||||
### 后端测试
|
||||
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVOTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/mapper/LoanPricingWorkflowMapperXmlTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayServiceTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysLoginControllerPasswordTransferTest.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysRegisterControllerPasswordTransferTest.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysProfileControllerPasswordTransferTest.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysUserControllerPasswordTransferTest.java`
|
||||
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java`
|
||||
|
||||
### 后端实施记录
|
||||
|
||||
- Create: `doc/2026-04-15-全量迁移892-without-redis后端实施记录.md`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 接入 `ruoyi-loan-pricing` 模块到底座工程
|
||||
|
||||
**Files:**
|
||||
- Modify: `pom.xml`
|
||||
- Modify: `ruoyi-admin/pom.xml`
|
||||
- Create: `ruoyi-loan-pricing/pom.xml`
|
||||
|
||||
- [ ] **Step 1: 写出模块接入失败测试条件**
|
||||
|
||||
在计划执行时,先确认当前工程不存在贷款定价模块:
|
||||
|
||||
```bash
|
||||
test -d ruoyi-loan-pricing || echo "ruoyi-loan-pricing missing"
|
||||
```
|
||||
|
||||
Expected: 输出 `ruoyi-loan-pricing missing`
|
||||
|
||||
- [ ] **Step 2: 建立最小模块骨架**
|
||||
|
||||
创建:
|
||||
|
||||
```text
|
||||
ruoyi-loan-pricing/
|
||||
ruoyi-loan-pricing/pom.xml
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/
|
||||
ruoyi-loan-pricing/src/main/resources/
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/
|
||||
```
|
||||
|
||||
`ruoyi-loan-pricing/pom.xml` 最小内容应包含:
|
||||
|
||||
```xml
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 把模块接入聚合与启动工程**
|
||||
|
||||
修改 `pom.xml`:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
以及:
|
||||
|
||||
```xml
|
||||
<module>ruoyi-loan-pricing</module>
|
||||
```
|
||||
|
||||
修改 `ruoyi-admin/pom.xml` 增加:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行 Maven 校验模块被识别**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -DskipTests package
|
||||
```
|
||||
|
||||
Expected: Maven 能识别 `ruoyi-loan-pricing` 模块;若后续类未补齐可先因编译缺类失败,但不能再是“模块不存在”。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add pom.xml ruoyi-admin/pom.xml ruoyi-loan-pricing
|
||||
git commit -m "接入贷款定价后端模块骨架"
|
||||
```
|
||||
|
||||
### Task 2: 落地贷款定价后端主链与 Mapper
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/LoanPricingWorkflowMapper.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/ModelRetailOutputFieldsMapper.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/mapper/ModelCorpOutputFieldsMapper.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ModelService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/mapper/LoanPricingWorkflowMapperXmlTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
|
||||
|
||||
- [ ] **Step 1: 先写列表/详情契约失败测试**
|
||||
|
||||
在 `LoanPricingWorkflowMapperXmlTest` 中先写断言,覆盖:
|
||||
|
||||
```java
|
||||
assertThat(xml).contains("calculate_rate");
|
||||
assertThat(xml).contains("execute_rate");
|
||||
assertThat(xml).contains("update_time");
|
||||
assertThat(xml).contains("final_calculate_rate");
|
||||
```
|
||||
|
||||
以及在 `LoanPricingWorkflowServiceImplTest` 中先写:
|
||||
|
||||
```java
|
||||
assertEquals("个人", workflow.getCustType());
|
||||
assertEquals("1", dto.getRunType());
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 DTO / Entity / VO / Mapper 骨架**
|
||||
|
||||
最小要保证这些字段到位:
|
||||
|
||||
```java
|
||||
private String serialNum;
|
||||
private String custIsn;
|
||||
private String custName;
|
||||
private String idNum;
|
||||
private String custType;
|
||||
private BigDecimal executeRate;
|
||||
private BigDecimal calculateRate;
|
||||
private BigDecimal finalCalculateRate;
|
||||
private Date updateTime;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现列表 SQL 与详情装配**
|
||||
|
||||
`LoanPricingWorkflowMapper.xml` 中明确写联表,不做前端拼装:
|
||||
|
||||
```xml
|
||||
LEFT JOIN model_retail_output_fields mr ON lpw.model_output_id = mr.id
|
||||
LEFT JOIN model_corp_output_fields mc ON lpw.model_output_id = mc.id
|
||||
```
|
||||
|
||||
个人列表测算利率按目标分支行为取:
|
||||
|
||||
```sql
|
||||
CASE WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate
|
||||
WHEN lpw.cust_type = '企业' THEN mc.calculate_rate
|
||||
END AS calculateRate
|
||||
```
|
||||
|
||||
并返回:
|
||||
|
||||
```sql
|
||||
lpw.execute_rate AS executeRate,
|
||||
lpw.update_time AS updateTime
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 实现建单与模型调用链路**
|
||||
|
||||
`PersonalLoanPricingCreateDTO` 至少补齐:
|
||||
|
||||
```java
|
||||
private String loanPurpose;
|
||||
private Integer loanTerm;
|
||||
private String loanLoop;
|
||||
private String collThirdParty;
|
||||
```
|
||||
|
||||
`ModelInvokeDTO` 至少补齐:
|
||||
|
||||
```java
|
||||
private String loanPurpose;
|
||||
private Integer loanTerm;
|
||||
private String loanLoop;
|
||||
```
|
||||
|
||||
`LoanPricingConverter` 和 `LoanPricingModelService` 保证个人入参完整透传。
|
||||
|
||||
- [ ] **Step 5: 跑定向测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -Dtest=LoanPricingWorkflowMapperXmlTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest test
|
||||
```
|
||||
|
||||
Expected: 以上测试通过。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-loan-pricing
|
||||
git commit -m "迁移贷款定价后端主链"
|
||||
```
|
||||
|
||||
### Task 3: 落地敏感字段加密脱敏与个人展示字段
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayService.java`
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayServiceTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
|
||||
|
||||
- [ ] **Step 1: 先写加解密和脱敏失败测试**
|
||||
|
||||
```java
|
||||
assertThat(crypto.encrypt("张三")).isNotEqualTo("张三");
|
||||
assertThat(crypto.decrypt(cipher)).isEqualTo("张三");
|
||||
assertThat(display.maskIdNum("330102199001011234")).contains("****");
|
||||
```
|
||||
|
||||
再补个人输出字段断言:
|
||||
|
||||
```java
|
||||
assertThat(fieldsClassText).contains("loanRateHistory");
|
||||
assertThat(fieldsClassText).contains("minRateProduct");
|
||||
assertThat(fieldsClassText).contains("smoothRange");
|
||||
assertThat(fieldsClassText).contains("finalCalculateRate");
|
||||
assertThat(fieldsClassText).contains("referenceRate");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现敏感字段服务**
|
||||
|
||||
`SensitiveFieldCryptoService` 只做:
|
||||
|
||||
```java
|
||||
String encrypt(String plainText)
|
||||
String decrypt(String cipherText)
|
||||
```
|
||||
|
||||
`LoanPricingSensitiveDisplayService` 只做:
|
||||
|
||||
```java
|
||||
String maskName(String name)
|
||||
String maskIdNum(String idNum)
|
||||
void maskWorkflow(LoanPricingWorkflowVO vo)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 接入流程创建、详情、列表、模型调用**
|
||||
|
||||
在 `LoanPricingWorkflowServiceImpl` 中:
|
||||
|
||||
```java
|
||||
workflow.setCustName(cryptoService.encrypt(workflow.getCustName()));
|
||||
workflow.setIdNum(cryptoService.encrypt(workflow.getIdNum()));
|
||||
```
|
||||
|
||||
在返回前:
|
||||
|
||||
```java
|
||||
sensitiveDisplayService.maskWorkflow(vo);
|
||||
```
|
||||
|
||||
在 `LoanPricingModelService` 调模型前:
|
||||
|
||||
```java
|
||||
invokeDTO.setCustName(cryptoService.decrypt(workflow.getCustName()));
|
||||
invokeDTO.setIdNum(cryptoService.decrypt(workflow.getIdNum()));
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 补实体字段与详情组装**
|
||||
|
||||
`ModelRetailOutputFields` 增加:
|
||||
|
||||
```java
|
||||
private BigDecimal loanRateHistory;
|
||||
private BigDecimal minRateProduct;
|
||||
private BigDecimal smoothRange;
|
||||
private BigDecimal finalCalculateRate;
|
||||
private BigDecimal referenceRate;
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑定向测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest,ModelRetailOutputFieldsTest,LoanPricingModelServiceTest test
|
||||
```
|
||||
|
||||
Expected: 以上测试通过。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-loan-pricing
|
||||
git commit -m "补齐贷款定价敏感信息与详情字段"
|
||||
```
|
||||
|
||||
### Task 4: 迁移登录密码传输与缓存监控后端行为
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java`
|
||||
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginBody.java`
|
||||
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/RegisterBody.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysLoginControllerPasswordTransferTest.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysRegisterControllerPasswordTransferTest.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysProfileControllerPasswordTransferTest.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/SysUserControllerPasswordTransferTest.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java`
|
||||
|
||||
- [ ] **Step 1: 先写密码透传失败测试**
|
||||
|
||||
每个测试至少断言控制器能接收透传后的密码字段:
|
||||
|
||||
```java
|
||||
assertEquals("plain-password", body.getPassword());
|
||||
```
|
||||
|
||||
或断言 controller 调 service 前已拿到预期值。
|
||||
|
||||
- [ ] **Step 2: 调整请求体与控制器**
|
||||
|
||||
`LoginBody` / `RegisterBody` 保证可承接前端密码传输字段,控制器保持接口路径不变,只修正入参读取与传递行为。
|
||||
|
||||
- [ ] **Step 3: 校准缓存监控行为**
|
||||
|
||||
按当前分支去 Redis 底座整合目标分支缓存监控效果,不回退到 Redis 实现。`CacheController` 只输出内存缓存统计所需字段。
|
||||
|
||||
- [ ] **Step 4: 跑 admin 定向测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am -Dtest=SysLoginControllerPasswordTransferTest,SysRegisterControllerPasswordTransferTest,SysProfileControllerPasswordTransferTest,SysUserControllerPasswordTransferTest,CacheControllerTest test
|
||||
```
|
||||
|
||||
Expected: 以上测试通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin ruoyi-common
|
||||
git commit -m "迁移登录密码传输与缓存监控后端逻辑"
|
||||
```
|
||||
|
||||
### Task 5: 迁移环境配置、脚本和 SQL 资产
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
||||
- Create: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
- Create: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Modify: `bin/restart_java_backend.sh`
|
||||
- Create: `bin/restart_java_backend_test.sh`
|
||||
- Create: `bin/prod/deploy_from_package.sh`
|
||||
- Create: `bin/prod/deploy_from_package_test.sh`
|
||||
- Create: `bin/prod/deploy_release.sh`
|
||||
- Create: `bin/prod/install_env.sh`
|
||||
- Create: `bin/prod/restart_java.sh`
|
||||
- Create: `bin/prod/restart_java_test.sh`
|
||||
- Create: `deploy/nginx.conf`
|
||||
- Create: `deploy/2026-03-31-local-nginx-java-install-manual.md`
|
||||
- Create: `sql/loan_pricing_schema_20260328.sql`
|
||||
- Create: `sql/loan_pricing_required_data_20260328.sql`
|
||||
- Create: `sql/loan_pricing_prod_init_20260331.sql`
|
||||
- Create: `sql/loan_pricing_menu.sql`
|
||||
- Create: `sql/loan_pricing_workflow.sql`
|
||||
- Create: `sql/model_retail.sql`
|
||||
- Create: `sql/model_corp.sql`
|
||||
- Create: `test_api/test_personal_create.http`
|
||||
- Create: `test_api/test_personal_create.sh`
|
||||
- Create: `test_api/test_corporate_create.http`
|
||||
- Create: `test_api/test_corporate_create.sh`
|
||||
|
||||
- [ ] **Step 1: 先校验当前文件缺失**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
ls ruoyi-admin/src/main/resources/application-pro.yml bin/prod sql/loan_pricing_schema_20260328.sql test_api 2>/dev/null || true
|
||||
```
|
||||
|
||||
Expected: 当前分支大部分路径不存在。
|
||||
|
||||
- [ ] **Step 2: 迁入配置与脚本骨架**
|
||||
|
||||
先把文件原样引入,再逐个检查:
|
||||
|
||||
- 端口
|
||||
- profile 名称
|
||||
- jar 名称
|
||||
- 目录路径
|
||||
- `ps -ef` / 端口识别逻辑
|
||||
|
||||
禁止直接照搬掉当前分支去 Redis 配置。
|
||||
|
||||
- [ ] **Step 3: 迁入 SQL 与 API 示例**
|
||||
|
||||
SQL 必须覆盖:
|
||||
|
||||
- 贷款定价表结构
|
||||
- 初始化基础数据
|
||||
- 菜单
|
||||
- 模型输出表字段补齐
|
||||
|
||||
测试脚本只作为联调辅助,不允许替代单元测试。
|
||||
|
||||
- [ ] **Step 4: 跑脚本与 SQL 静态自检**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bash -n bin/restart_java_backend.sh
|
||||
bash -n bin/prod/deploy_from_package.sh
|
||||
bash -n bin/prod/deploy_release.sh
|
||||
```
|
||||
|
||||
Expected: Shell 语法检查通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/resources bin deploy sql test_api
|
||||
git commit -m "迁移部署脚本与贷款定价SQL资产"
|
||||
```
|
||||
|
||||
### Task 6: 后端总验收与实施记录
|
||||
|
||||
**Files:**
|
||||
- Modify: `doc/2026-04-15-全量迁移892-without-redis后端实施计划.md`
|
||||
- Create: `doc/2026-04-15-全量迁移892-without-redis后端实施记录.md`
|
||||
|
||||
- [ ] **Step 1: 运行后端回归测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
Expected: 全仓后端测试通过;若存在外部依赖导致个别测试不可跑,必须记录具体模块与原因。
|
||||
|
||||
- [ ] **Step 2: 运行后端启动验证**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am spring-boot:run
|
||||
```
|
||||
|
||||
Expected: 服务启动成功,贷款定价相关 Bean 可加载;验证后主动停止进程。
|
||||
|
||||
- [ ] **Step 3: 补实施记录**
|
||||
|
||||
`doc/2026-04-15-全量迁移892-without-redis后端实施记录.md` 至少记录:
|
||||
|
||||
- 实际迁入模块
|
||||
- 与目标分支不做原样复制的整合点
|
||||
- 测试命令与结果
|
||||
- 启动验证结果
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add doc/2026-04-15-全量迁移892-without-redis后端实施计划.md doc/2026-04-15-全量迁移892-without-redis后端实施记录.md
|
||||
git commit -m "补充全量迁移后端实施记录"
|
||||
```
|
||||
71
doc/2026-04-15-全量迁移892-without-redis后端实施记录.md
Normal file
71
doc/2026-04-15-全量迁移892-without-redis后端实施记录.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 全量迁移 `892-without-redis` 后端实施记录
|
||||
|
||||
## 修改时间
|
||||
|
||||
- 2026-04-15
|
||||
|
||||
## 本次完成内容
|
||||
|
||||
- 新增并接入 `ruoyi-loan-pricing` 模块,纳入根 `pom.xml` 与 `ruoyi-admin/pom.xml`
|
||||
- 保留 `MyBatis-Plus + Lombok`,将贷款定价模块中的 `jakarta.*` 兼容替换为当前基线可运行的 `javax.*`
|
||||
- 在 `ruoyi-framework` 中接入 `MyBatis-Plus`:
|
||||
- `MybatisSqlSessionFactoryBean`
|
||||
- `MybatisPlusInterceptor`
|
||||
- MySQL 分页拦截器
|
||||
- 迁入贷款定价后端主链:
|
||||
- Controller / DTO / Entity / VO / Mapper / Service / XML
|
||||
- 敏感字段加解密与脱敏服务
|
||||
- 个人测算入参对齐
|
||||
- 列表联表查询测算利率
|
||||
- 详情页个人最终测算利率取值
|
||||
- 补充 `HttpUtils#doPostFormUrlEncoded`,满足模型表单调用
|
||||
- 新增 `PasswordTransferCryptoService`
|
||||
- 接入登录密码传输后端链路:
|
||||
- [SysLoginController](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java)
|
||||
- [SysRegisterController](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java)
|
||||
- [SysProfileController](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java)
|
||||
- [SysUserController](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java)
|
||||
- 补齐密码传输配置:
|
||||
- `security.password-transfer.key`
|
||||
- 迁入目标分支中的部署脚本、环境配置和 SQL 资产:
|
||||
- `bin/prod/*`
|
||||
- `deploy/*`
|
||||
- `sql/loan_pricing_*.sql`
|
||||
- `sql/model_*.sql`
|
||||
- `test_api/*`
|
||||
|
||||
## 关键整合说明
|
||||
|
||||
- 没有回退当前分支的 `JDK8` 与“去 Redis 改为内存缓存”基线
|
||||
- 贷款定价模块没有改写为普通 MyBatis,而是保留 `MyBatis-Plus` 风格实现
|
||||
- 由于当前主工程是 `Spring Boot 2.5 / JDK8`,没有原样保留 `jakarta.*`,而是按你的确认改成 `javax.*`
|
||||
- 贷款定价模块中的 Swagger v3 注解未继续保留,避免为非业务注解引入额外运行时依赖
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 构建验证
|
||||
|
||||
- `mvn -pl ruoyi-loan-pricing -am -DskipTests package`
|
||||
- 结果:通过
|
||||
- `mvn -pl ruoyi-admin -am -DskipTests package`
|
||||
- 结果:通过
|
||||
|
||||
### 定向测试
|
||||
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowMapperXmlTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest,ModelRetailOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:通过
|
||||
- 统计:22 tests run, 0 failures, 0 errors
|
||||
|
||||
- `mvn -pl ruoyi-admin -am -Dtest=SysLoginControllerPasswordTransferTest,SysRegisterControllerPasswordTransferTest,SysProfileControllerPasswordTransferTest,SysUserControllerPasswordTransferTest,CacheControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:通过
|
||||
- 统计:7 tests run, 0 failures, 0 errors
|
||||
|
||||
### 全量后端测试
|
||||
|
||||
- `mvn test`
|
||||
- 结果:通过
|
||||
|
||||
## 未在本记录中执行的内容
|
||||
|
||||
- 未执行真实数据库初始化和真实模型接口联调
|
||||
- 未在本记录中启动长期运行的后端进程,因此无需额外清理测试进程
|
||||
235
doc/2026-04-15-前端缓存监控页适配内存缓存实施计划.md
Normal file
235
doc/2026-04-15-前端缓存监控页适配内存缓存实施计划.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# 前端缓存监控页适配内存缓存实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 调整前端缓存监控页面,使其不再展示 Redis 专属字段,而是展示后端返回的内存缓存统计信息,并保留缓存列表与清理交互。
|
||||
|
||||
**Architecture:** 保留现有缓存监控路由、API 文件和列表页交互,只更新首页统计面板与图表语义,确保最小改动完成页面适配。前端接口路径不变,主要改造点集中在 `monitor/cache/index.vue`。
|
||||
|
||||
**Tech Stack:** Vue 2, Element UI, ECharts, Node.js, npm
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
**Modify:**
|
||||
- `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
- `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||
- `ruoyi-ui/src/api/monitor/cache.js`
|
||||
|
||||
**Verify against:**
|
||||
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||
|
||||
### Task 1: 调整缓存监控首页文案与字段映射
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
|
||||
- [ ] **Step 1: 先根据后端新结构补充页面默认数据模型**
|
||||
|
||||
将默认字段统一为:
|
||||
|
||||
```js
|
||||
cache: {
|
||||
info: {
|
||||
cache_type: "IN_MEMORY",
|
||||
cache_mode: "single-instance",
|
||||
key_size: 0,
|
||||
hit_count: 0,
|
||||
miss_count: 0,
|
||||
expired_count: 0,
|
||||
write_count: 0
|
||||
},
|
||||
dbSize: 0,
|
||||
commandStats: []
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 将页面文案从 Redis 语义改为内存缓存语义**
|
||||
|
||||
替换方向:
|
||||
|
||||
- `Redis版本` -> `缓存类型`
|
||||
- `运行模式` -> `运行模式`
|
||||
- `数据库大小` -> `总键数`
|
||||
- `命令统计` -> `缓存统计`
|
||||
|
||||
- [ ] **Step 3: 实现数据格式化方法**
|
||||
|
||||
补齐:
|
||||
|
||||
- `formatCacheType`
|
||||
- `formatCacheMode`
|
||||
- `normalizeCacheData`
|
||||
- `normalizeCommandStats`
|
||||
- `formatSampleTime`
|
||||
- `toNumber`
|
||||
|
||||
- [ ] **Step 4: 增加命中率计算和采样时间显示**
|
||||
|
||||
```js
|
||||
hitRateText() {
|
||||
const total = hitCount + missCount
|
||||
return total ? ((hitCount / total) * 100).toFixed(2) + "%" : "0.00%"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/monitor/cache/index.vue
|
||||
git commit -m "调整缓存监控页基础信息展示"
|
||||
```
|
||||
|
||||
### Task 2: 调整首页图表为内存缓存统计
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
|
||||
- [ ] **Step 1: 将图表数据源改为后端返回的 `commandStats`**
|
||||
|
||||
约定图表项为:
|
||||
|
||||
- `hit_count`
|
||||
- `miss_count`
|
||||
- `expired_count`
|
||||
- `write_count`
|
||||
|
||||
- [ ] **Step 2: 调整图表标题和标签**
|
||||
|
||||
改造目标:
|
||||
|
||||
- 左侧图表展示“缓存统计分布”
|
||||
- 右侧图表展示“命中概览”或“读写概览”
|
||||
|
||||
- [ ] **Step 3: 处理空数据场景**
|
||||
|
||||
要求:
|
||||
|
||||
- 接口失败时页面不报错
|
||||
- 图表可渲染空数据
|
||||
- 采样时间回退为 `-`
|
||||
|
||||
- [ ] **Step 4: 处理窗口 resize 与组件销毁**
|
||||
|
||||
确保:
|
||||
|
||||
- `beforeDestroy` 中清理 resize 监听
|
||||
- 销毁 ECharts 实例,避免重复初始化
|
||||
|
||||
- [ ] **Step 5: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/monitor/cache/index.vue
|
||||
git commit -m "调整缓存监控图表为内存统计"
|
||||
```
|
||||
|
||||
### Task 3: 验证缓存列表页和 API 保持兼容
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||
- Modify: `ruoyi-ui/src/api/monitor/cache.js`
|
||||
|
||||
- [ ] **Step 1: 检查列表页是否依赖 Redis 专属字段**
|
||||
|
||||
确认页面仅依赖:
|
||||
|
||||
- 缓存分类名称
|
||||
- key 列表
|
||||
- cacheValue 文本
|
||||
|
||||
- [ ] **Step 2: 如有需要,仅做兼容性最小调整**
|
||||
|
||||
例如:
|
||||
|
||||
- 保证 `cacheValue` 为字符串时可直接展示
|
||||
- 保证刷新、清理按钮仍使用原接口路径
|
||||
|
||||
- [ ] **Step 3: 手工检查交互链路**
|
||||
|
||||
检查:
|
||||
|
||||
- 左侧缓存分类加载
|
||||
- 中间 key 列表加载
|
||||
- 右侧缓存值展示
|
||||
- 清理分类 / 清理 key / 清理全部
|
||||
|
||||
- [ ] **Step 4: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/monitor/cache/list.vue ruoyi-ui/src/api/monitor/cache.js
|
||||
git commit -m "校准缓存列表页内存缓存兼容性"
|
||||
```
|
||||
|
||||
### Task 4: 运行前端构建与页面冒烟
|
||||
|
||||
**Files:**
|
||||
- Modify if needed: `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
- Modify if needed: `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||
|
||||
- [ ] **Step 1: 使用 `nvm` 切换 Node 版本并安装依赖**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
source ~/.nvm/nvm.sh
|
||||
nvm use
|
||||
npm install
|
||||
```
|
||||
|
||||
Expected: 依赖安装成功,Node 版本符合项目要求
|
||||
|
||||
- [ ] **Step 2: 运行前端生产构建**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd ruoyi-ui
|
||||
source ~/.nvm/nvm.sh
|
||||
nvm use
|
||||
npm run build:prod
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESS,无 ESLint / 编译错误
|
||||
|
||||
- [ ] **Step 3: 联调缓存监控页面**
|
||||
|
||||
配合后端启动后验证:
|
||||
|
||||
- `/monitor/cache/index` 页面可正常打开
|
||||
- `/monitor/cache/list` 页面可正常打开
|
||||
- 统计信息、图表、缓存值和清理操作正常
|
||||
|
||||
- [ ] **Step 4: 如果启动过前端开发进程,停止测试进程**
|
||||
|
||||
结束 `npm run dev` 或其它前端测试进程,避免残留后台服务
|
||||
|
||||
- [ ] **Step 5: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/monitor/cache/index.vue ruoyi-ui/src/views/monitor/cache/list.vue ruoyi-ui/src/api/monitor/cache.js
|
||||
git commit -m "完成缓存监控页内存缓存适配"
|
||||
```
|
||||
|
||||
### Task 5: 补充实施记录
|
||||
|
||||
**Files:**
|
||||
- Modify: `doc/2026-04-15-前端缓存监控页适配内存缓存实施计划.md`
|
||||
- Create or Modify: `doc/2026-04-15-前端缓存监控页适配内存缓存实施记录.md`
|
||||
|
||||
- [ ] **Step 1: 记录最终前端变更与验证命令**
|
||||
|
||||
记录内容至少包括:
|
||||
|
||||
- 实际修改文件
|
||||
- `nvm use` 结果
|
||||
- 构建命令
|
||||
- 页面冒烟结果
|
||||
|
||||
- [ ] **Step 2: 提交文档**
|
||||
|
||||
```bash
|
||||
git add doc/2026-04-15-前端缓存监控页适配内存缓存实施计划.md doc/2026-04-15-前端缓存监控页适配内存缓存实施记录.md
|
||||
git commit -m "补充前端实施记录"
|
||||
```
|
||||
116
doc/2026-04-15-前端缓存监控页适配内存缓存实施记录.md
Normal file
116
doc/2026-04-15-前端缓存监控页适配内存缓存实施记录.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 前端缓存监控页适配内存缓存实施记录
|
||||
|
||||
## 1. 实际改动内容
|
||||
|
||||
### 1.1 调整缓存监控首页
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-ui/src/views/monitor/cache/index.vue`
|
||||
|
||||
改动内容:
|
||||
|
||||
- 页面展示从 Redis 服务器指标改为内存缓存统计
|
||||
- 增加默认数据结构:
|
||||
- `cache_type`
|
||||
- `cache_mode`
|
||||
- `key_size`
|
||||
- `hit_count`
|
||||
- `miss_count`
|
||||
- `expired_count`
|
||||
- `write_count`
|
||||
- 新增格式化与归一化方法:
|
||||
- `normalizeCacheData`
|
||||
- `normalizeCommandStats`
|
||||
- `formatCacheType`
|
||||
- `formatCacheMode`
|
||||
- `formatSampleTime`
|
||||
- `toNumber`
|
||||
- 基本信息区改为展示:
|
||||
- 缓存类型
|
||||
- 运行模式
|
||||
- 总键数
|
||||
- 写入次数
|
||||
- 命中次数
|
||||
- 未命中次数
|
||||
- 过期清理次数
|
||||
- 命中率
|
||||
- 图表改为展示内存缓存统计分布与条形概览
|
||||
|
||||
### 1.2 调整缓存列表页清理全部后的状态
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-ui/src/views/monitor/cache/list.vue`
|
||||
|
||||
改动内容:
|
||||
|
||||
- 清理全部缓存后主动清空:
|
||||
- `cacheKeys`
|
||||
- `cacheForm`
|
||||
- `nowCacheName`
|
||||
- 重新加载缓存分类列表,避免页面残留旧数据显示
|
||||
|
||||
### 1.3 API 文件
|
||||
|
||||
文件:
|
||||
|
||||
- `ruoyi-ui/src/api/monitor/cache.js`
|
||||
|
||||
结论:
|
||||
|
||||
- 接口路径与交互方式保持不变
|
||||
- 本次未对 API 文件做额外改动
|
||||
|
||||
## 2. 验证结果
|
||||
|
||||
### 2.1 Node 版本
|
||||
|
||||
项目中未提供 `.nvmrc`,因此未能直接执行 `nvm use` 自动切换。
|
||||
|
||||
本机 `nvm` 已安装版本:
|
||||
|
||||
- `v14.21.3`
|
||||
- `v25.9.0`
|
||||
|
||||
实际使用版本:
|
||||
|
||||
- `nvm use 14.21.3`
|
||||
|
||||
原因:
|
||||
|
||||
- Vue CLI 4 + Vue 2 对较新的 Node 版本存在潜在兼容风险,优先使用更稳妥的 `v14.21.3`
|
||||
|
||||
### 2.2 构建命令
|
||||
|
||||
已执行:
|
||||
|
||||
- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`
|
||||
|
||||
结果:
|
||||
|
||||
- 构建成功
|
||||
- 输出 `DONE Build complete. The dist directory is ready to be deployed.`
|
||||
|
||||
### 2.3 构建告警
|
||||
|
||||
存在 webpack 资源体积告警:
|
||||
|
||||
- `asset size limit`
|
||||
- `entrypoint size limit`
|
||||
|
||||
说明:
|
||||
|
||||
- 这些是现有项目静态资源体积告警,不影响本次缓存监控页适配结果
|
||||
- 本次未对该类历史体积问题做额外处理
|
||||
|
||||
## 3. 与计划差异
|
||||
|
||||
- 计划中预期通过 `nvm use` 自动切换版本,但项目没有 `.nvmrc`,因此改为显式执行 `nvm use 14.21.3`
|
||||
- 除此之外,前端实施路径与计划一致
|
||||
|
||||
## 4. 当前结论
|
||||
|
||||
- 缓存监控首页已适配后端内存缓存统计结构
|
||||
- 缓存列表页与现有接口保持兼容
|
||||
- 前端生产构建已通过
|
||||
19
doc/2026-04-15-后端启动脚本固定JDK18实施文档.md
Normal file
19
doc/2026-04-15-后端启动脚本固定JDK18实施文档.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 后端启动脚本固定 JDK 1.8 实施文档
|
||||
|
||||
## 修改内容
|
||||
- 在 `bin/restart_java_backend.sh` 中新增固定的 JDK 1.8 路径 `/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home`。
|
||||
- 脚本启动时统一设置 `JAVA_HOME` 和 `PATH`,确保脚本内执行的 `mvn` 与 `java -jar` 都使用 JDK 1.8。
|
||||
- 启动 Java 进程时改为显式调用固定路径下的 `bin/java`,避免落回系统默认的 JDK 21。
|
||||
|
||||
## 适配依据
|
||||
- 当前机器默认 `JAVA_HOME` 为 JDK 21,不满足本项目需要固定 JDK 1.8 的要求。
|
||||
- 当前机器已安装可用的 JDK 1.8:`/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home`。
|
||||
- 本次按“写死当前机器 JDK 1.8 路径”的方案实现,不引入额外的自动探测逻辑。
|
||||
|
||||
## 验证方式
|
||||
- 执行 `sh -n bin/restart_java_backend.sh` 校验脚本语法。
|
||||
- 执行 `/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home/bin/java -version` 校验固定路径下的 Java 为 1.8。
|
||||
|
||||
## 保存路径确认
|
||||
- 脚本路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/bin/restart_java_backend.sh`
|
||||
- 本次实施文档路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-后端启动脚本固定JDK18实施文档.md`
|
||||
360
doc/2026-04-15-后端移除Redis改造为内存缓存实施计划.md
Normal file
360
doc/2026-04-15-后端移除Redis改造为内存缓存实施计划.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 后端移除 Redis 改造为内存缓存实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 移除后端 Redis 依赖,并在单实例 JVM 内存中承接登录态、验证码、限流、防重提交、参数/字典缓存、在线用户和缓存监控能力。
|
||||
|
||||
**Architecture:** 保留现有 `RedisCache` 业务入口,底层替换为基于 `ConcurrentHashMap` 的本地缓存存储,减少对现有业务代码的侵入。限流、在线用户、缓存监控等能力继续沿用现有 key 规则,仅替换存储实现与统计来源。
|
||||
|
||||
**Tech Stack:** Java 8, Spring Boot 2.x, Spring Security, Maven, JUnit 5
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
**Create:**
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java`
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java`
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java`
|
||||
- `ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java`
|
||||
- `ruoyi-common/src/test/java/com/ruoyi/common/core/redis/RedisCacheTest.java`
|
||||
- `ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java`
|
||||
- `ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/TokenServiceLocalCacheTest.java`
|
||||
- `ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java`
|
||||
|
||||
**Modify:**
|
||||
- `pom.xml`
|
||||
- `ruoyi-common/pom.xml`
|
||||
- `ruoyi-framework/pom.xml`
|
||||
- `ruoyi-admin/pom.xml`
|
||||
- `ruoyi-admin/src/main/resources/application.yml`
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java`
|
||||
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||
|
||||
**Delete:**
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java`
|
||||
|
||||
**Verify existing behavior against:**
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java`
|
||||
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java`
|
||||
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java`
|
||||
- `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java`
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java`
|
||||
|
||||
### Task 1: 搭建内存缓存底座
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java`
|
||||
- Create: `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java`
|
||||
- Create: `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java`
|
||||
- Test: `ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java`
|
||||
|
||||
- [ ] **Step 1: 先写 `InMemoryCacheStore` 的失败测试**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldExpireEntryAfterTimeout() throws Exception {
|
||||
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||
store.set("captcha:1", "1234", 50L, TimeUnit.MILLISECONDS);
|
||||
Thread.sleep(80L);
|
||||
assertNull(store.get("captcha:1"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `mvn -pl ruoyi-common -Dtest=InMemoryCacheStoreTest test`
|
||||
Expected: FAIL,提示 `InMemoryCacheStore` 或相关方法不存在
|
||||
|
||||
- [ ] **Step 3: 实现最小缓存条目与存储类**
|
||||
|
||||
```java
|
||||
public final class InMemoryCacheEntry {
|
||||
private final Object value;
|
||||
private final Long expireAtMillis;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class InMemoryCacheStore {
|
||||
private final ConcurrentHashMap<String, InMemoryCacheEntry> entries = new ConcurrentHashMap<>();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 为底座补齐核心能力**
|
||||
|
||||
实现并覆盖测试:
|
||||
|
||||
- `set/get`
|
||||
- `set(key, value, timeout, unit)`
|
||||
- `hasKey`
|
||||
- `delete`
|
||||
- `delete(Collection<String>)`
|
||||
- `keys(pattern)`
|
||||
- `expire/getExpire`
|
||||
- `increment`
|
||||
- `clear`
|
||||
- `snapshot`
|
||||
|
||||
- [ ] **Step 5: 运行测试确认通过**
|
||||
|
||||
Run: `mvn -pl ruoyi-common -Dtest=InMemoryCacheStoreTest test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-common/src/main/java/com/ruoyi/common/core/cache ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
|
||||
git commit -m "新增内存缓存底座"
|
||||
```
|
||||
|
||||
### Task 2: 将 `RedisCache` 改为内存缓存门面
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java`
|
||||
- Test: `ruoyi-common/src/test/java/com/ruoyi/common/core/redis/RedisCacheTest.java`
|
||||
|
||||
- [ ] **Step 1: 先写 `RedisCache` 门面测试**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldReadAndDeleteCachedObject() {
|
||||
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||
cache.setCacheObject("login_tokens:abc", "value");
|
||||
assertEquals("value", cache.getCacheObject("login_tokens:abc"));
|
||||
assertTrue(cache.deleteObject("login_tokens:abc"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `mvn -pl ruoyi-common -Dtest=RedisCacheTest test`
|
||||
Expected: FAIL,提示当前 `RedisCache` 仍依赖 `RedisTemplate`
|
||||
|
||||
- [ ] **Step 3: 将 `RedisCache` 改成构造注入 `InMemoryCacheStore`**
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class RedisCache {
|
||||
private final InMemoryCacheStore cacheStore;
|
||||
public RedisCache(InMemoryCacheStore cacheStore) {
|
||||
this.cacheStore = cacheStore;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 保留原有业务方法并补充监控/限流能力**
|
||||
|
||||
补齐:
|
||||
|
||||
- `setCacheObject/getCacheObject`
|
||||
- `setCacheMap/getCacheMap`
|
||||
- `setCacheList/getCacheList`
|
||||
- `setCacheSet/getCacheSet`
|
||||
- `deleteObject`
|
||||
- `keys`
|
||||
- `increment`
|
||||
- `getCacheStats`
|
||||
- `clear`
|
||||
|
||||
- [ ] **Step 5: 运行测试确认通过**
|
||||
|
||||
Run: `mvn -pl ruoyi-common -Dtest=RedisCacheTest,InMemoryCacheStoreTest test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java ruoyi-common/src/test/java/com/ruoyi/common/core/redis/RedisCacheTest.java
|
||||
git commit -m "改造缓存门面为内存实现"
|
||||
```
|
||||
|
||||
### Task 3: 用内存计数替换限流实现
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java`
|
||||
- Test: `ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java`
|
||||
|
||||
- [ ] **Step 1: 为限流切换先写失败测试**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldRejectRequestWhenCountExceeded() throws Throwable {
|
||||
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||
RateLimiterAspect aspect = new RateLimiterAspect(cache);
|
||||
// 连续触发三次,阈值为2,第三次应抛 ServiceException
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `mvn -pl ruoyi-framework -am -Dtest=RateLimiterAspectTest test`
|
||||
Expected: FAIL,提示当前 `RateLimiterAspect` 仍依赖 `RedisTemplate/RedisScript`
|
||||
|
||||
- [ ] **Step 3: 删除 `RedisTemplate` 和 Lua 脚本依赖**
|
||||
|
||||
将核心调用改为:
|
||||
|
||||
```java
|
||||
long number = redisCache.increment(combineKey, time, TimeUnit.SECONDS);
|
||||
if (number > count) {
|
||||
throw new ServiceException("访问过于频繁,请稍候再试");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行测试确认通过**
|
||||
|
||||
Run: `mvn -pl ruoyi-framework -am -Dtest=RateLimiterAspectTest test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java
|
||||
git commit -m "改造限流为内存计数"
|
||||
```
|
||||
|
||||
### Task 4: 调整缓存监控接口到内存统计
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java`
|
||||
|
||||
- [ ] **Step 1: 为监控接口写失败测试**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldReturnInMemoryCacheStats() {
|
||||
// 断言返回 info.cache_type == "IN_MEMORY"
|
||||
// 断言返回 info.cache_mode == "single-instance"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
Run: `mvn -pl ruoyi-admin -am -Dtest=CacheControllerTest test`
|
||||
Expected: FAIL,提示当前接口仍返回 Redis 信息结构
|
||||
|
||||
- [ ] **Step 3: 将 `CacheController` 改为依赖 `RedisCache`**
|
||||
|
||||
实现行为:
|
||||
|
||||
- `getInfo()` 返回内存缓存统计
|
||||
- `getKeys()` 通过 `redisCache.keys(pattern)` 获取
|
||||
- `getCacheValue()` 将对象序列化为字符串
|
||||
- `clearCacheName()`、`clearCacheKey()`、`clearCacheAll()` 走内存门面
|
||||
|
||||
- [ ] **Step 4: 运行测试确认通过**
|
||||
|
||||
Run: `mvn -pl ruoyi-admin -am -Dtest=CacheControllerTest test`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: 手工冒烟现有依赖方**
|
||||
|
||||
检查以下类不需要额外逻辑改造:
|
||||
|
||||
- `TokenService`
|
||||
- `SysLoginService`
|
||||
- `SysRegisterService`
|
||||
- `SysPasswordService`
|
||||
- `SameUrlDataInterceptor`
|
||||
- `SysUserOnlineController`
|
||||
- `SysConfigServiceImpl`
|
||||
- `DictUtils`
|
||||
|
||||
Expected: 仅继续依赖 `RedisCache` 即可,无需改 key 规则
|
||||
|
||||
- [ ] **Step 6: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java
|
||||
git commit -m "改造缓存监控为内存统计"
|
||||
```
|
||||
|
||||
### Task 5: 删除 Redis 配置与依赖
|
||||
|
||||
**Files:**
|
||||
- Modify: `pom.xml`
|
||||
- Modify: `ruoyi-common/pom.xml`
|
||||
- Modify: `ruoyi-framework/pom.xml`
|
||||
- Modify: `ruoyi-admin/pom.xml`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
||||
- Delete: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java`
|
||||
- Delete: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java`
|
||||
|
||||
- [ ] **Step 1: 删除 Maven 中 Redis 相关依赖**
|
||||
|
||||
删除:
|
||||
|
||||
- `spring-boot-starter-data-redis`
|
||||
- `commons-pool2`
|
||||
|
||||
- [ ] **Step 2: 删除应用配置中的 `spring.redis` 配置块**
|
||||
|
||||
保留其它配置项不变,只移除 Redis 相关字段
|
||||
|
||||
- [ ] **Step 3: 删除 Redis 基础设施类**
|
||||
|
||||
删除:
|
||||
|
||||
- `RedisConfig`
|
||||
- `FastJson2JsonRedisSerializer`
|
||||
|
||||
- [ ] **Step 4: 全量编译验证**
|
||||
|
||||
Run: `mvn clean package -DskipTests`
|
||||
Expected: BUILD SUCCESS
|
||||
|
||||
- [ ] **Step 5: 运行后端相关测试**
|
||||
|
||||
Run: `mvn -pl ruoyi-common,ruoyi-framework,ruoyi-admin -am test`
|
||||
Expected: BUILD SUCCESS
|
||||
|
||||
- [ ] **Step 6: 启动后端做接口冒烟**
|
||||
|
||||
Run: `mvn -pl ruoyi-admin -am spring-boot:run`
|
||||
Expected:
|
||||
|
||||
- 应用可正常启动
|
||||
- 不再尝试连接 Redis
|
||||
- 登录、验证码、缓存监控接口正常可用
|
||||
|
||||
- [ ] **Step 7: 停止测试进程**
|
||||
|
||||
结束 `spring-boot:run` 启动的 Java 进程,避免遗留后台服务
|
||||
|
||||
- [ ] **Step 8: 提交这一小步**
|
||||
|
||||
```bash
|
||||
git add pom.xml ruoyi-common/pom.xml ruoyi-framework/pom.xml ruoyi-admin/pom.xml ruoyi-admin/src/main/resources/application.yml ruoyi-framework/src/main/java/com/ruoyi/framework/config
|
||||
git commit -m "移除Redis依赖与配置"
|
||||
```
|
||||
|
||||
### Task 6: 补充实施记录
|
||||
|
||||
**Files:**
|
||||
- Modify: `doc/2026-04-15-后端移除Redis改造为内存缓存实施计划.md`
|
||||
- Create or Modify: `doc/2026-04-15-移除Redis依赖改造为内存缓存后端实施记录.md`
|
||||
|
||||
- [ ] **Step 1: 记录最终实际修改文件和偏差**
|
||||
|
||||
记录内容至少包括:
|
||||
|
||||
- 实际变更文件
|
||||
- 测试命令
|
||||
- 冒烟结论
|
||||
- 与计划有无偏差
|
||||
|
||||
- [ ] **Step 2: 提交文档**
|
||||
|
||||
```bash
|
||||
git add doc/2026-04-15-后端移除Redis改造为内存缓存实施计划.md doc/2026-04-15-移除Redis依赖改造为内存缓存后端实施记录.md
|
||||
git commit -m "补充后端实施记录"
|
||||
```
|
||||
25
doc/2026-04-15-后端端口改为63310实施记录.md
Normal file
25
doc/2026-04-15-后端端口改为63310实施记录.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 后端端口改为 `63310` 实施记录
|
||||
|
||||
## 修改时间
|
||||
|
||||
- 2026-04-15
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将重启脚本 [bin/restart_java_backend.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/bin/restart_java_backend.sh) 中的 `SERVER_PORT` 从 `8080` 调整为 `63310`
|
||||
- 核对后端环境配置,确认 [application-dev.yml](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/resources/application-dev.yml)、[application-pro.yml](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/resources/application-pro.yml)、[application-uat.yml](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/resources/application-uat.yml) 已经使用 `63310`
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 后端重启脚本的端口监听检测
|
||||
- 后端脚本状态查询结果
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 通过源码检索确认后端主环境配置和脚本统一指向 `63310`
|
||||
- 通过 `bash -n bin/restart_java_backend.sh` 确认脚本语法正常
|
||||
|
||||
## 说明
|
||||
|
||||
- 当前开发、生产、UAT 环境配置原本已经是 `63310`
|
||||
- 本次主要修正的是脚本里仍残留的 `8080`
|
||||
19
doc/2026-04-15-后端重启脚本适配实施文档.md
Normal file
19
doc/2026-04-15-后端重启脚本适配实施文档.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 后端重启脚本适配实施文档
|
||||
|
||||
## 修改内容
|
||||
- 调整 `bin/restart_java_backend.sh` 中的后端端口,从 `63310` 修正为当前项目 `ruoyi-admin` 实际使用的 `8080`。
|
||||
- 恢复并改造脚本启动标识,新增 `-Dloan.pricing.backend.root=$ROOT_DIR` 作为当前项目的 Java 进程识别标记,避免 `set -u` 下因 `APP_MARKER` 未定义导致脚本执行失败。
|
||||
- 在脚本启动前增加 `java` 命令检查,避免构建成功后因运行环境缺少 JDK/JRE 才报错。
|
||||
|
||||
## 适配依据
|
||||
- 当前项目后端入口模块为 `ruoyi-admin`,打包产物为 `ruoyi-admin.jar`。
|
||||
- `ruoyi-admin/src/main/resources/application.yml` 中 `server.port` 配置为 `8080`。
|
||||
- 现有脚本的进程识别逻辑依赖 `APP_MARKER`,但工作区版本中该变量已被移除,和 `set -eu` 冲突。
|
||||
|
||||
## 验证方式
|
||||
- 执行 `sh -n bin/restart_java_backend.sh` 校验脚本语法。
|
||||
- 执行 `bin/restart_java_backend.sh status` 校验脚本可正常进入状态检查流程。
|
||||
|
||||
## 保存路径确认
|
||||
- 脚本路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/bin/restart_java_backend.sh`
|
||||
- 本次实施文档路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-后端重启脚本适配实施文档.md`
|
||||
22
doc/2026-04-15-审计字段自动填充后端实施记录.md
Normal file
22
doc/2026-04-15-审计字段自动填充后端实施记录.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 2026-04-15 审计字段自动填充后端实施记录
|
||||
|
||||
## 背景
|
||||
|
||||
- 贷款定价流程实体已经声明了 MyBatis-Plus 的 `FieldFill`,但当前分支缺少迁移源分支中的统一审计填充处理器。
|
||||
- 导致 `insert` 和 `updateById` 执行时,`createBy`、`createTime`、`updateBy`、`updateTime` 不会自动写入或刷新。
|
||||
|
||||
## 本次改动
|
||||
|
||||
- 新增 [MyMetaHandler.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-framework/src/main/java/com/ruoyi/framework/config/handler/MyMetaHandler.java),恢复与迁移源分支一致的统一审计填充逻辑。
|
||||
- 审计人格式保持与源分支一致,统一写入 `昵称-用户名`。
|
||||
- 新增 [MyMetaHandlerTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-framework/src/test/java/com/ruoyi/framework/config/handler/MyMetaHandlerTest.java),覆盖插入填充与更新填充两个核心场景。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 执行 `mvn -pl ruoyi-framework -am -Dtest=MyMetaHandlerTest -Dsurefire.failIfNoSpecifiedTests=false test`,通过。
|
||||
- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=MyMetaHandlerTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest,LoanPricingModelServiceCorporateParamsTest,LoanPricingModelServicePersonalParamsTest -Dsurefire.failIfNoSpecifiedTests=false test`,通过。
|
||||
|
||||
## 影响说明
|
||||
|
||||
- 所有使用 MyBatis-Plus 自动填充并声明对应字段的实体,在当前登录上下文下执行新增和更新时,都会自动维护审计字段。
|
||||
- 本次未改动贷款定价业务入参、SQL 结构和前端页面行为。
|
||||
24
doc/2026-04-15-对公流程详情测算结果与风险分析分组调整前端实施记录.md
Normal file
24
doc/2026-04-15-对公流程详情测算结果与风险分析分组调整前端实施记录.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 对公流程详情测算结果与风险分析分组调整前端实施记录
|
||||
|
||||
## 变更日期
|
||||
- 2026-04-15
|
||||
|
||||
## 变更范围
|
||||
- 前端页面:`ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
|
||||
- 前端校验:`ruoyi-ui/tests/corporate-create-input-params.test.js`
|
||||
- 前端校验:`ruoyi-ui/tests/corporate-display-fields.test.js`
|
||||
|
||||
## 实施内容
|
||||
- 将对公流程详情“模型输出”卡片中的“测算结果”从原“风险度与测算结果”合并分组中拆出。
|
||||
- 按页面要求将对公模型输出分组顺序调整为“基本信息 → 测算结果 → 忠诚度分析 → 贡献度分析 → 关联度分析 → 风险分析”。
|
||||
- 保留“风险分析”在模型输出卡片末尾,仅调整展示分组,不修改接口字段、父组件传参和格式化逻辑。
|
||||
- 补充前端断言,校验对公模型输出存在独立“测算结果”“风险分析”标题,且不再保留“风险度与测算结果”合并标题。
|
||||
|
||||
## 影响说明
|
||||
- 本次仅涉及前端详情页展示层,不涉及后端接口、数据库脚本和模型测算逻辑。
|
||||
- 对公流程详情页中,用户可在基本信息后直接查看测算结果,风险分析独立展示且位于模型输出末尾。
|
||||
|
||||
## 验证结果
|
||||
- 执行 `node ruoyi-ui/tests/corporate-create-input-params.test.js`,断言通过。
|
||||
- 执行 `node ruoyi-ui/tests/corporate-display-fields.test.js`,断言通过。
|
||||
- 执行 `cd ruoyi-ui && nvm use 14.21.3 && npm run build:prod`,前端生产构建通过。
|
||||
16
doc/2026-04-15-开发库补列SQL落盘实施记录.md
Normal file
16
doc/2026-04-15-开发库补列SQL落盘实施记录.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 2026-04-15 开发库补列 SQL 落盘实施记录
|
||||
|
||||
## 修改内容
|
||||
- 将开发库已执行的对公字段补列 SQL 整理并保存到 `sql/loan_pricing_alter_20260415_repay_method.sql`。
|
||||
- 在原有 `loan_pricing_workflow.repay_method` 基础上,补充 `model_corp_output_fields` 的以下字段变更语句:
|
||||
- `repay_method`
|
||||
- `is_trade_build_ent`
|
||||
- `loan_rate_history`
|
||||
- `min_rate_product`
|
||||
- `smooth_range`
|
||||
- `final_calculate_rate`
|
||||
- `reference_rate`
|
||||
|
||||
## 结果
|
||||
- 现有 SQL 文件已可直接用于同步开发库本次字段补齐变更。
|
||||
- 文件内容与本次实际执行到开发库的语句保持一致。
|
||||
21
doc/2026-04-15-流程详情模型输出平铺展示前端实施记录.md
Normal file
21
doc/2026-04-15-流程详情模型输出平铺展示前端实施记录.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 流程详情模型输出平铺展示前端实施记录
|
||||
|
||||
## 变更日期
|
||||
- 2026-04-15
|
||||
|
||||
## 变更范围
|
||||
- 前端页面:`ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
|
||||
|
||||
## 实施内容
|
||||
- 将流程详情页“模型输出”区域的 `tab` 切换结构改为按分组顺序平铺展示。
|
||||
- 按页面最新要求调整分组顺序,当前展示顺序为“基本信息”在前,“测算结果”紧跟其后。
|
||||
- 保留个人客户、企业客户原有字段、分组顺序和格式化逻辑,不调整接口入参、出参和父组件传值。
|
||||
- 移除仅用于 tab 默认选中的本地状态与监听逻辑,简化组件展示职责。
|
||||
|
||||
## 影响说明
|
||||
- 本次仅涉及前端展示层,不涉及后端接口、数据库脚本和业务计算逻辑。
|
||||
- 用户进入流程详情页后,可直接连续查看全部模型输出内容,无需逐个切换页签。
|
||||
|
||||
## 验证计划
|
||||
- 使用项目既有 Node 环境执行前端生产构建,确认编译通过。
|
||||
- 本地打开流程详情页,检查个人客户与企业客户两类模型输出均按分组平铺展示。
|
||||
16
doc/2026-04-15-直接复制源分支前端代码实施记录.md
Normal file
16
doc/2026-04-15-直接复制源分支前端代码实施记录.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 2026-04-15 直接复制源分支前端代码实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 按用户要求,直接将 `origin/892-without-redis` 的 `ruoyi-ui` 整体复制到当前分支。
|
||||
- 覆盖了源分支已有的前端页面、路由、API、布局、样式、构建配置与依赖锁文件。
|
||||
- 同步删除了当前分支中源分支不存在的前端文件,确保前端代码基线与源分支保持一致。
|
||||
|
||||
## 涉及范围
|
||||
|
||||
- `ruoyi-ui/`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 使用 `nvm` 切换 Node 版本后执行 `npm install`
|
||||
- 执行 `npm run build:prod`
|
||||
131
doc/2026-04-15-移除Redis依赖改造为内存缓存后端实施记录.md
Normal file
131
doc/2026-04-15-移除Redis依赖改造为内存缓存后端实施记录.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 移除 Redis 依赖改造为内存缓存后端实施记录
|
||||
|
||||
## 1. 实际改动内容
|
||||
|
||||
### 1.1 新增内存缓存底座
|
||||
|
||||
新增文件:
|
||||
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java`
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java`
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java`
|
||||
|
||||
实现内容:
|
||||
|
||||
- 使用 `ConcurrentHashMap` 存储缓存值
|
||||
- 支持 TTL、过期清理、按前缀查询 key、批量删除
|
||||
- 支持计数器递增能力,供限流使用
|
||||
- 提供命中、未命中、过期、写入、总键数统计快照
|
||||
|
||||
### 1.2 改造缓存门面
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java`
|
||||
|
||||
改造内容:
|
||||
|
||||
- 保留原 `RedisCache` 业务入口
|
||||
- 移除 `RedisTemplate` 依赖
|
||||
- 底层改为委托 `InMemoryCacheStore`
|
||||
- 补充 `increment`、`getCacheStats`、`clear` 能力
|
||||
|
||||
### 1.3 改造限流与缓存监控
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java`
|
||||
- `ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java`
|
||||
|
||||
改造内容:
|
||||
|
||||
- `RateLimiterAspect` 删除 Lua + Redis 依赖,改为内存计数
|
||||
- `CacheController` 改为返回内存缓存统计与缓存内容
|
||||
- 缓存清理接口改为操作本地缓存门面
|
||||
|
||||
### 1.4 删除 Redis 配置与依赖
|
||||
|
||||
修改文件:
|
||||
|
||||
- `ruoyi-common/pom.xml`
|
||||
- `ruoyi-framework/pom.xml`
|
||||
- `ruoyi-admin/pom.xml`
|
||||
- `ruoyi-admin/src/main/resources/application.yml`
|
||||
|
||||
删除文件:
|
||||
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java`
|
||||
- `ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java`
|
||||
|
||||
处理结果:
|
||||
|
||||
- 删除 `spring-boot-starter-data-redis`
|
||||
- 删除 `commons-pool2`
|
||||
- 删除 `spring.redis` 配置块
|
||||
- 删除 Redis 专属配置类
|
||||
- 为 `ruoyi-common`、`ruoyi-framework`、`ruoyi-admin` 增加 `spring-boot-starter-test` 测试依赖
|
||||
|
||||
### 1.5 新增后端测试
|
||||
|
||||
新增文件:
|
||||
|
||||
- `ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java`
|
||||
- `ruoyi-common/src/test/java/com/ruoyi/common/core/redis/RedisCacheTest.java`
|
||||
- `ruoyi-framework/src/test/java/com/ruoyi/framework/aspectj/RateLimiterAspectTest.java`
|
||||
- `ruoyi-framework/src/test/java/com/ruoyi/framework/web/service/TokenServiceLocalCacheTest.java`
|
||||
- `ruoyi-admin/src/test/java/com/ruoyi/web/controller/monitor/CacheControllerTest.java`
|
||||
|
||||
覆盖内容:
|
||||
|
||||
- 内存缓存读写 / TTL / keys / 统计
|
||||
- 缓存门面能力
|
||||
- 限流计数
|
||||
- token 登录态缓存
|
||||
- 验证码删除
|
||||
- 防重提交毫秒级窗口
|
||||
- 缓存监控接口
|
||||
|
||||
## 2. 执行结果
|
||||
|
||||
### 2.1 验证命令
|
||||
|
||||
已执行:
|
||||
|
||||
- `mvn -pl ruoyi-common -Dtest=InMemoryCacheStoreTest,RedisCacheTest test`
|
||||
- `mvn -pl ruoyi-framework -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=RateLimiterAspectTest,TokenServiceLocalCacheTest test`
|
||||
- `mvn -pl ruoyi-admin -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CacheControllerTest test`
|
||||
- `mvn clean package -DskipTests`
|
||||
- `mvn -pl ruoyi-common,ruoyi-framework,ruoyi-admin -am test`
|
||||
|
||||
结果:
|
||||
|
||||
- 上述命令全部执行成功
|
||||
|
||||
### 2.2 运行态冒烟
|
||||
|
||||
已执行:
|
||||
|
||||
- `java -jar ruoyi-admin/target/ruoyi-admin.jar --server.port=18080`
|
||||
- `curl http://127.0.0.1:18080/captchaImage`
|
||||
|
||||
结果:
|
||||
|
||||
- 应用可正常启动
|
||||
- 启动过程未出现 Redis 连接错误
|
||||
- `captchaImage` 返回:`{"msg":"操作成功","code":200,"captchaEnabled":false}`
|
||||
|
||||
说明:
|
||||
|
||||
- 默认端口 `8080` 被本机其它进程占用,因此改用临时端口 `18080` 做冒烟
|
||||
- 冒烟结束后已手动停止本次启动的 Java 进程
|
||||
|
||||
## 3. 与计划差异
|
||||
|
||||
- 计划中建议使用 `mvn -pl ruoyi-admin -am spring-boot:run` 启动应用,但本地 Maven 环境未解析到 `spring-boot` 插件前缀,因此改为直接运行已构建完成的 `ruoyi-admin.jar`
|
||||
- 除上述启动方式调整外,其余后端实施路径与计划一致
|
||||
|
||||
## 4. 当前结论
|
||||
|
||||
- 后端已完成 Redis 依赖移除
|
||||
- Redis 相关能力已切换为单实例 JVM 内存实现
|
||||
- 构建、单元测试、运行态公开接口冒烟均已完成
|
||||
250
doc/2026-04-15-移除Redis依赖改造为内存缓存设计文档.md
Normal file
250
doc/2026-04-15-移除Redis依赖改造为内存缓存设计文档.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 移除 Redis 依赖改造为内存缓存设计文档
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前项目中 Redis 不仅用于通用缓存,还承担了以下业务能力:
|
||||
|
||||
- 登录态缓存
|
||||
- 验证码缓存
|
||||
- 密码错误次数统计
|
||||
- 防重提交
|
||||
- 接口限流
|
||||
- 系统参数缓存
|
||||
- 数据字典缓存
|
||||
- 在线用户监控
|
||||
- 缓存监控页
|
||||
|
||||
本次改造目标是移除 Redis 依赖,并将以上依赖 Redis 的能力改造为基于单实例后端 JVM 内存的实现。
|
||||
|
||||
## 2. 设计约束
|
||||
|
||||
- 后端部署模型明确为单实例
|
||||
- 不引入 Redis 兼容层、中间件替代品或额外持久化方案
|
||||
- 不引入 Caffeine、Guava 等新的缓存框架
|
||||
- 不改变现有业务 key 前缀和主要调用方式
|
||||
- 不做多实例共享、重启恢复、降级兜底等额外设计
|
||||
- 保留缓存监控功能,但改为展示内存缓存信息
|
||||
|
||||
## 3. 参考实现
|
||||
|
||||
本次设计参考 git 远端分支 `origin/892-without-redis` 的处理方向,但仅借鉴其“保留业务缓存入口、底层改为内存存储”的思路,不引入与当前 JDK 1.8 / Spring Boot 2 技术栈无关的框架升级改造。
|
||||
|
||||
## 4. 总体方案
|
||||
|
||||
### 4.1 核心思路
|
||||
|
||||
保留现有 `com.ruoyi.common.core.redis.RedisCache` 作为统一业务缓存入口,将其底层实现从 `RedisTemplate` 改为进程内缓存存储。
|
||||
|
||||
这样可以保证以下调用方的业务逻辑基本不变:
|
||||
|
||||
- `TokenService`
|
||||
- `SysLoginService`
|
||||
- `SysRegisterService`
|
||||
- `SysPasswordService`
|
||||
- `SameUrlDataInterceptor`
|
||||
- `SysConfigServiceImpl`
|
||||
- `DictUtils`
|
||||
- `SysUserOnlineController`
|
||||
- `CacheController`
|
||||
|
||||
### 4.2 存储结构
|
||||
|
||||
在 `ruoyi-common` 中新增本地缓存存储组件,职责如下:
|
||||
|
||||
- 使用 `ConcurrentHashMap<String, CacheEntry>` 保存缓存数据
|
||||
- `CacheEntry` 保存:
|
||||
- 实际缓存值 `value`
|
||||
- 过期时间戳 `expireAtMillis`
|
||||
- 读操作时判断是否过期
|
||||
- 过期数据在读取、扫描 key、统计快照时清理
|
||||
|
||||
### 4.3 保留的缓存门面能力
|
||||
|
||||
`RedisCache` 对外继续保留以下常用方法,以降低改造面:
|
||||
|
||||
- `setCacheObject`
|
||||
- `getCacheObject`
|
||||
- `deleteObject`
|
||||
- `hasKey`
|
||||
- `expire`
|
||||
- `getExpire`
|
||||
- `keys`
|
||||
- `setCacheMap`
|
||||
- `getCacheMap`
|
||||
- `setCacheMapValue`
|
||||
- `getCacheMapValue`
|
||||
- `setCacheList`
|
||||
- `getCacheList`
|
||||
- `setCacheSet`
|
||||
- `getCacheSet`
|
||||
|
||||
另外补充两个内存实现需要的能力:
|
||||
|
||||
- `increment(key, timeout, unit)`:支持限流计数
|
||||
- `getCacheStats()`:支持缓存监控页展示统计信息
|
||||
|
||||
## 5. 功能映射设计
|
||||
|
||||
### 5.1 登录态缓存
|
||||
|
||||
- `TokenService` 继续使用 `LOGIN_TOKEN_KEY + token` 作为缓存 key
|
||||
- 登录成功时将 `LoginUser` 写入本地缓存,并设置与当前 token 配置一致的过期时间
|
||||
- 鉴权时仍通过 JWT 中的 token 标识查找缓存中的 `LoginUser`
|
||||
- token 临近过期时继续刷新本地缓存 TTL
|
||||
- 退出登录、强退用户时继续删除对应缓存 key
|
||||
|
||||
该设计保持当前认证链路不变,仅将状态存储位置从 Redis 改为 JVM 内存。
|
||||
|
||||
### 5.2 验证码缓存
|
||||
|
||||
- `CaptchaController` 继续将验证码写入 `CAPTCHA_CODE_KEY + uuid`
|
||||
- TTL 保持分钟级
|
||||
- `SysLoginService` 与 `SysRegisterService` 校验验证码后继续删除对应 key
|
||||
|
||||
### 5.3 密码错误次数统计
|
||||
|
||||
- `SysPasswordService` 继续使用 `PWD_ERR_CNT_KEY + username`
|
||||
- 每次密码校验失败,错误次数自增并重置锁定窗口 TTL
|
||||
- 达到最大错误次数时维持现有异常行为
|
||||
- 登录成功后清理错误次数缓存
|
||||
|
||||
### 5.4 防重提交
|
||||
|
||||
- `SameUrlDataInterceptor` 继续使用 `REPEAT_SUBMIT_KEY + 请求地址 + token/header`
|
||||
- 缓存值仍为请求参数快照和时间戳
|
||||
- TTL 继续使用注解 `interval` 指定的毫秒值
|
||||
- 比较逻辑不变,仍按“参数一致且时间窗口内重复提交”拦截
|
||||
|
||||
### 5.5 限流
|
||||
|
||||
- `RateLimiterAspect` 删除对 `RedisTemplate` 和 Lua 脚本的依赖
|
||||
- 改为调用 `RedisCache.increment(key, timeout, unit)` 实现固定窗口内计数
|
||||
- 当计数值超过注解配置的阈值时,继续抛出当前业务异常
|
||||
|
||||
限流范围明确为当前单实例应用内,不做跨实例聚合。
|
||||
|
||||
### 5.6 系统参数缓存
|
||||
|
||||
- `SysConfigServiceImpl` 保持现有 key 规则 `SYS_CONFIG_KEY + configKey`
|
||||
- 读取优先查本地缓存,未命中时查库并回填缓存
|
||||
- 新增、修改、删除、刷新操作保持现有清理逻辑
|
||||
|
||||
### 5.7 数据字典缓存
|
||||
|
||||
- `DictUtils` 保持现有 key 规则 `SYS_DICT_KEY + dictType`
|
||||
- 字典写入、读取、删除、批量清理逻辑不变
|
||||
|
||||
### 5.8 在线用户
|
||||
|
||||
- `SysUserOnlineController` 继续通过 `LOGIN_TOKEN_KEY*` 扫描缓存 key
|
||||
- 缓存中的 `LoginUser` 继续转换为在线用户列表
|
||||
- 强退用户仍通过删除指定 token key 实现
|
||||
|
||||
在线用户监控的可见范围定义为“当前单实例内未过期的登录 token”。
|
||||
|
||||
### 5.9 缓存监控
|
||||
|
||||
保留 `/monitor/cache` 相关接口和前端页面,但数据源改为本地缓存统计:
|
||||
|
||||
- 缓存类型:`IN_MEMORY`
|
||||
- 运行模式:`single-instance`
|
||||
- 总键数
|
||||
- 命中次数
|
||||
- 未命中次数
|
||||
- 过期清理次数
|
||||
- 写入次数
|
||||
|
||||
同时保留以下能力:
|
||||
|
||||
- 按缓存分类查看 key
|
||||
- 查看缓存值
|
||||
- 按分类清理缓存
|
||||
- 按 key 清理缓存
|
||||
- 清空全部缓存
|
||||
|
||||
## 6. 前端适配设计
|
||||
|
||||
本次前端改造仅针对缓存监控页,不扩展到其它模块。
|
||||
|
||||
### 6.1 缓存监控首页
|
||||
|
||||
页面保留原入口,但将 Redis 专属展示改为内存缓存语义:
|
||||
|
||||
- `Redis版本` 改为 `缓存类型`
|
||||
- `运行模式` 显示 `单实例`
|
||||
- `dbSize` 语义改为 `总键数`
|
||||
- 图表改为展示:
|
||||
- 命中次数
|
||||
- 未命中次数
|
||||
- 过期清理次数
|
||||
- 写入次数
|
||||
|
||||
### 6.2 缓存列表页
|
||||
|
||||
- 保持现有接口路径和交互方式
|
||||
- 展示的数据仍按缓存 key 前缀分类
|
||||
- 支持查看值与清理操作
|
||||
|
||||
## 7. 依赖与配置调整
|
||||
|
||||
### 7.1 删除依赖
|
||||
|
||||
从 Maven 依赖中移除:
|
||||
|
||||
- `spring-boot-starter-data-redis`
|
||||
- `commons-pool2`
|
||||
|
||||
### 7.2 删除配置
|
||||
|
||||
从 `ruoyi-admin/src/main/resources/application.yml` 中删除:
|
||||
|
||||
- `spring.redis` 整段配置
|
||||
|
||||
### 7.3 删除基础设施代码
|
||||
|
||||
删除或停用以下 Redis 专属配置类:
|
||||
|
||||
- `RedisConfig`
|
||||
- `FastJson2JsonRedisSerializer`
|
||||
|
||||
## 8. 兼容边界
|
||||
|
||||
以下行为是本次改造后的明确边界:
|
||||
|
||||
- 服务重启后,登录态、验证码、限流计数、在线用户、密码错误次数等内存数据全部丢失
|
||||
- 缓存监控统计为当前进程内累计统计,不代表外部系统状态
|
||||
- 不支持多实例共享缓存
|
||||
|
||||
以上边界与“单实例后端”约束一致,属于本次设计的预期结果。
|
||||
|
||||
## 9. 测试与验收
|
||||
|
||||
### 9.1 后端测试重点
|
||||
|
||||
- 本地缓存对象读写
|
||||
- TTL 过期行为
|
||||
- `keys` 模糊匹配
|
||||
- 删除和批量删除
|
||||
- 原子递增计数
|
||||
- token 登录态写入、读取、刷新、删除
|
||||
- 限流行为
|
||||
- 验证码写入与校验
|
||||
- 在线用户列表与强退
|
||||
- 系统参数缓存与字典缓存读取、刷新
|
||||
- 缓存监控接口返回结构
|
||||
|
||||
### 9.2 前端验收重点
|
||||
|
||||
- 缓存监控页正常打开
|
||||
- 基本信息展示为内存缓存语义
|
||||
- 图表能正确展示内存统计数据
|
||||
- 缓存列表页能查看 key、查看值、清理缓存
|
||||
|
||||
## 10. 实施拆分
|
||||
|
||||
由于本次涉及后端与前端改造,实施阶段需要分别输出两份实施计划文档:
|
||||
|
||||
- 后端实施计划
|
||||
- 前端实施计划
|
||||
|
||||
实施计划将在本设计确认后单独编写并保存到 `doc/` 目录。
|
||||
16
doc/2026-04-15-贷款定价密钥统一为密码传输配置后端实施记录.md
Normal file
16
doc/2026-04-15-贷款定价密钥统一为密码传输配置后端实施记录.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 2026-04-15 贷款定价密钥统一为密码传输配置后端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将贷款定价敏感字段加解密服务统一为只读取 `security.password-transfer.key`。
|
||||
- 删除对 `loan-pricing.sensitive.key` 的依赖,避免出现双配置源。
|
||||
- 调整定向单元测试,校验未配置时抛出的错误信息改为 `security.password-transfer.key 未配置`。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
|
||||
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 执行 `mvn -pl ruoyi-admin -am -DskipTests package` 验证后端整体打包。
|
||||
14
doc/2026-04-15-贷款定价敏感字段密钥配置修复后端实施记录.md
Normal file
14
doc/2026-04-15-贷款定价敏感字段密钥配置修复后端实施记录.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 2026-04-15 贷款定价敏感字段密钥配置修复后端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修复贷款定价首页列表请求返回 `loan-pricing.sensitive.key 未配置` 的问题。
|
||||
- 在 `application.yml` 中新增 `loan-pricing.sensitive.key`,并直接复用现有的 `security.password-transfer.key`,保证贷款定价敏感字段加解密与密码传输使用同一把密钥。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-admin/src/main/resources/application.yml`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 执行 `mvn -pl ruoyi-admin -am -DskipTests package`,验证后端配置修改后可正常完成打包。
|
||||
14
doc/2026-04-15-首页改为利率测算列表前端实施记录.md
Normal file
14
doc/2026-04-15-首页改为利率测算列表前端实施记录.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 2026-04-15 首页改为利率测算列表前端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将前端默认首页路由从 `@/views/index` 调整为 `@/views/loanPricing/workflow/index`,使登录后的首页直接进入利率测算列表。
|
||||
- 为利率测算详情页补充显式命名路由 `LoanPricingWorkflowDetail`,保证首页列表点击“查看”后可以正常进入详情页。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-ui/src/router/index.js`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 使用 `nvm` 切换 Node 版本后执行 `npm run build:prod`,验证前端路由配置可以正常通过生产构建。
|
||||
18
doc/2026-04-15-首页面包屑与贷款定价密钥异常修复实施记录.md
Normal file
18
doc/2026-04-15-首页面包屑与贷款定价密钥异常修复实施记录.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 2026-04-15 首页面包屑与贷款定价密钥异常修复实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修复首页访问 `/index` 时面包屑重复注入首页节点,导致前端出现 `Duplicate keys detected: '/index'` 警告的问题。
|
||||
- 修复贷款定价敏感字段加解密服务只读取 `loan-pricing.sensitive.key`,未在专用配置缺失时回退到 `security.password-transfer.key`,导致首页列表接口返回 `loan-pricing.sensitive.key 未配置` 的问题。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-ui/src/components/Breadcrumb/index.vue`
|
||||
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
|
||||
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
|
||||
- `ruoyi-ui/tests/home-breadcrumb-dedup.test.js`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 已执行 `node ruoyi-ui/tests/home-breadcrumb-dedup.test.js`,结果通过。
|
||||
- 已尝试执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,但当前 `ruoyi-loan-pricing` 模块存在与本次改动无关的编译问题,导致无法完成该条 Maven 定向验证。
|
||||
BIN
doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx
Normal file
BIN
doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx
Normal file
Binary file not shown.
BIN
doc/上虞对公利率测算_上传字段与展示字段 .xlsx
Normal file
BIN
doc/上虞对公利率测算_上传字段与展示字段 .xlsx
Normal file
Binary file not shown.
63
pom.xml
63
pom.xml
@@ -34,8 +34,9 @@
|
||||
<tomcat.version>9.0.112</tomcat.version>
|
||||
<logback.version>1.2.13</logback.version>
|
||||
<spring-security.version>5.7.14</spring-security.version>
|
||||
<spring-framework.version>5.3.39</spring-framework.version>
|
||||
</properties>
|
||||
<spring-framework.version>5.3.39</spring-framework.version>
|
||||
<mybatis-plus.version>3.5.1</mybatis-plus.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
<dependencyManagement>
|
||||
@@ -114,12 +115,18 @@
|
||||
<version>${yauaa.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- pagehelper 分页插件 -->
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
<version>${pagehelper.boot.version}</version>
|
||||
</dependency>
|
||||
<!-- pagehelper 分页插件 -->
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
<version>${pagehelper.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 获取系统信息 -->
|
||||
<dependency>
|
||||
@@ -204,17 +211,24 @@
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 利率定价模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -225,10 +239,11 @@
|
||||
<module>ruoyi-admin</module>
|
||||
<module>ruoyi-framework</module>
|
||||
<module>ruoyi-system</module>
|
||||
<module>ruoyi-quartz</module>
|
||||
<module>ruoyi-generator</module>
|
||||
<module>ruoyi-common</module>
|
||||
</modules>
|
||||
<module>ruoyi-quartz</module>
|
||||
<module>ruoyi-generator</module>
|
||||
<module>ruoyi-common</module>
|
||||
<module>ruoyi-loan-pricing</module>
|
||||
</modules>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<build>
|
||||
@@ -271,4 +286,4 @@
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -55,13 +55,25 @@
|
||||
<artifactId>ruoyi-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-generator</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-generator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 利率定价模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
@@ -93,4 +105,4 @@
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -1,121 +1,148 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.SysCache;
|
||||
|
||||
/**
|
||||
* 缓存监控
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/cache")
|
||||
public class CacheController
|
||||
{
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
||||
{
|
||||
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
|
||||
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
|
||||
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
|
||||
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
|
||||
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
|
||||
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
|
||||
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping()
|
||||
public AjaxResult getInfo() throws Exception
|
||||
{
|
||||
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
|
||||
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
|
||||
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
|
||||
|
||||
Map<String, Object> result = new HashMap<>(3);
|
||||
result.put("info", info);
|
||||
result.put("dbSize", dbSize);
|
||||
|
||||
List<Map<String, String>> pieList = new ArrayList<>();
|
||||
commandStats.stringPropertyNames().forEach(key -> {
|
||||
Map<String, String> data = new HashMap<>(2);
|
||||
String property = commandStats.getProperty(key);
|
||||
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
|
||||
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
|
||||
pieList.add(data);
|
||||
});
|
||||
result.put("commandStats", pieList);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping("/getNames")
|
||||
public AjaxResult cache()
|
||||
{
|
||||
return AjaxResult.success(caches);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping("/getKeys/{cacheName}")
|
||||
public AjaxResult getCacheKeys(@PathVariable String cacheName)
|
||||
{
|
||||
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
|
||||
return AjaxResult.success(new TreeSet<>(cacheKeys));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping("/getValue/{cacheName}/{cacheKey}")
|
||||
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
|
||||
{
|
||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
|
||||
return AjaxResult.success(sysCache);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@DeleteMapping("/clearCacheName/{cacheName}")
|
||||
public AjaxResult clearCacheName(@PathVariable String cacheName)
|
||||
{
|
||||
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
|
||||
redisTemplate.delete(cacheKeys);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@DeleteMapping("/clearCacheKey/{cacheKey}")
|
||||
public AjaxResult clearCacheKey(@PathVariable String cacheKey)
|
||||
{
|
||||
redisTemplate.delete(cacheKey);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@DeleteMapping("/clearCacheAll")
|
||||
public AjaxResult clearCacheAll()
|
||||
{
|
||||
Collection<String> cacheKeys = redisTemplate.keys("*");
|
||||
redisTemplate.delete(cacheKeys);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
}
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStats;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.system.domain.SysCache;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 缓存监控
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/monitor/cache")
|
||||
public class CacheController
|
||||
{
|
||||
private final RedisCache redisCache;
|
||||
|
||||
private final static List<SysCache> caches = new ArrayList<SysCache>();
|
||||
{
|
||||
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
|
||||
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
|
||||
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
|
||||
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
|
||||
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
|
||||
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
|
||||
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
|
||||
}
|
||||
|
||||
public CacheController(RedisCache redisCache)
|
||||
{
|
||||
this.redisCache = redisCache;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping()
|
||||
public AjaxResult getInfo()
|
||||
{
|
||||
InMemoryCacheStats stats = redisCache.getCacheStats();
|
||||
Map<String, Object> info = new LinkedHashMap<String, Object>();
|
||||
info.put("cache_type", stats.getCacheType());
|
||||
info.put("cache_mode", stats.getMode());
|
||||
info.put("key_size", stats.getKeySize());
|
||||
info.put("hit_count", stats.getHitCount());
|
||||
info.put("miss_count", stats.getMissCount());
|
||||
info.put("expired_count", stats.getExpiredCount());
|
||||
info.put("write_count", stats.getWriteCount());
|
||||
|
||||
Map<String, Object> result = new HashMap<String, Object>(3);
|
||||
result.put("info", info);
|
||||
result.put("dbSize", stats.getKeySize());
|
||||
|
||||
List<Map<String, String>> pieList = new ArrayList<Map<String, String>>();
|
||||
pieList.add(statEntry("hit_count", stats.getHitCount()));
|
||||
pieList.add(statEntry("miss_count", stats.getMissCount()));
|
||||
pieList.add(statEntry("expired_count", stats.getExpiredCount()));
|
||||
pieList.add(statEntry("write_count", stats.getWriteCount()));
|
||||
result.put("commandStats", pieList);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping("/getNames")
|
||||
public AjaxResult cache()
|
||||
{
|
||||
return AjaxResult.success(caches);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping("/getKeys/{cacheName}")
|
||||
public AjaxResult getCacheKeys(@PathVariable String cacheName)
|
||||
{
|
||||
Set<String> cacheKeys = new TreeSet<String>(redisCache.keys(cacheName + "*"));
|
||||
return AjaxResult.success(cacheKeys);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@GetMapping("/getValue/{cacheName}/{cacheKey}")
|
||||
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
|
||||
{
|
||||
Object cacheValue = redisCache.getCacheObject(cacheKey);
|
||||
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValueToString(cacheValue));
|
||||
return AjaxResult.success(sysCache);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@DeleteMapping("/clearCacheName/{cacheName}")
|
||||
public AjaxResult clearCacheName(@PathVariable String cacheName)
|
||||
{
|
||||
Collection<String> cacheKeys = redisCache.keys(cacheName + "*");
|
||||
redisCache.deleteObject(cacheKeys);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@DeleteMapping("/clearCacheKey/{cacheKey}")
|
||||
public AjaxResult clearCacheKey(@PathVariable String cacheKey)
|
||||
{
|
||||
redisCache.deleteObject(cacheKey);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
|
||||
@DeleteMapping("/clearCacheAll")
|
||||
public AjaxResult clearCacheAll()
|
||||
{
|
||||
redisCache.clear();
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
private Map<String, String> statEntry(String name, long value)
|
||||
{
|
||||
Map<String, String> data = new HashMap<String, String>(2);
|
||||
data.put("name", name);
|
||||
data.put("value", String.valueOf(value));
|
||||
return data;
|
||||
}
|
||||
|
||||
private String cacheValueToString(Object cacheValue)
|
||||
{
|
||||
if (cacheValue == null)
|
||||
{
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
if (cacheValue instanceof String)
|
||||
{
|
||||
return (String) cacheValue;
|
||||
}
|
||||
return JSON.toJSONString(cacheValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,15 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysMenu;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginBody;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.text.Convert;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.web.service.SysLoginService;
|
||||
import com.ruoyi.framework.web.service.SysPermissionService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.text.Convert;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.framework.web.service.SysLoginService;
|
||||
import com.ruoyi.framework.web.service.SysPermissionService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import com.ruoyi.system.service.ISysMenuService;
|
||||
|
||||
@@ -44,8 +45,11 @@ public class SysLoginController
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private PasswordTransferCryptoService passwordTransferCryptoService;
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
@@ -54,12 +58,13 @@ public class SysLoginController
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public AjaxResult login(@RequestBody LoginBody loginBody)
|
||||
{
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
// 生成令牌
|
||||
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
|
||||
loginBody.getUuid());
|
||||
public AjaxResult login(@RequestBody LoginBody loginBody)
|
||||
{
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
loginBody.setPassword(passwordTransferCryptoService.decrypt(loginBody.getPassword()));
|
||||
// 生成令牌
|
||||
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
|
||||
loginBody.getUuid());
|
||||
ajax.put(Constants.TOKEN, token);
|
||||
return ajax;
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.common.utils.file.MimeTypeUtils;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.common.utils.file.MimeTypeUtils;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
|
||||
/**
|
||||
* 个人信息 业务处理
|
||||
@@ -38,8 +39,11 @@ public class SysProfileController extends BaseController
|
||||
@Autowired
|
||||
private ISysUserService userService;
|
||||
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
private PasswordTransferCryptoService passwordTransferCryptoService;
|
||||
|
||||
/**
|
||||
* 个人信息
|
||||
@@ -90,13 +94,13 @@ public class SysProfileController extends BaseController
|
||||
*/
|
||||
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/updatePwd")
|
||||
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
|
||||
{
|
||||
String oldPassword = params.get("oldPassword");
|
||||
String newPassword = params.get("newPassword");
|
||||
LoginUser loginUser = getLoginUser();
|
||||
Long userId = loginUser.getUserId();
|
||||
SysUser user = userService.selectUserById(userId);
|
||||
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
|
||||
{
|
||||
String oldPassword = passwordTransferCryptoService.decrypt(params.get("oldPassword"));
|
||||
String newPassword = passwordTransferCryptoService.decrypt(params.get("newPassword"));
|
||||
LoginUser loginUser = getLoginUser();
|
||||
Long userId = loginUser.getUserId();
|
||||
SysUser user = userService.selectUserById(userId);
|
||||
String password = user.getPassword();
|
||||
if (!SecurityUtils.matchesPassword(oldPassword, password))
|
||||
{
|
||||
|
||||
@@ -4,12 +4,13 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.model.RegisterBody;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.web.service.SysRegisterService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.model.RegisterBody;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.framework.web.service.SysRegisterService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
|
||||
/**
|
||||
* 注册验证
|
||||
@@ -22,17 +23,21 @@ public class SysRegisterController extends BaseController
|
||||
@Autowired
|
||||
private SysRegisterService registerService;
|
||||
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private PasswordTransferCryptoService passwordTransferCryptoService;
|
||||
|
||||
@PostMapping("/register")
|
||||
public AjaxResult register(@RequestBody RegisterBody user)
|
||||
{
|
||||
if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
|
||||
{
|
||||
return error("当前系统没有开启注册功能!");
|
||||
}
|
||||
String msg = registerService.register(user);
|
||||
return StringUtils.isEmpty(msg) ? success() : error(msg);
|
||||
}
|
||||
}
|
||||
if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
|
||||
{
|
||||
return error("当前系统没有开启注册功能!");
|
||||
}
|
||||
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
|
||||
String msg = registerService.register(user);
|
||||
return StringUtils.isEmpty(msg) ? success() : error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ import com.ruoyi.common.core.domain.entity.SysRole;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.system.service.ISysDeptService;
|
||||
import com.ruoyi.system.service.ISysPostService;
|
||||
import com.ruoyi.system.service.ISysRoleService;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.system.service.ISysDeptService;
|
||||
import com.ruoyi.system.service.ISysPostService;
|
||||
import com.ruoyi.system.service.ISysRoleService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
|
||||
/**
|
||||
@@ -50,8 +51,11 @@ public class SysUserController extends BaseController
|
||||
@Autowired
|
||||
private ISysDeptService deptService;
|
||||
|
||||
@Autowired
|
||||
private ISysPostService postService;
|
||||
@Autowired
|
||||
private ISysPostService postService;
|
||||
|
||||
@Autowired
|
||||
private PasswordTransferCryptoService passwordTransferCryptoService;
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
@@ -134,13 +138,14 @@ public class SysUserController extends BaseController
|
||||
{
|
||||
return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
|
||||
{
|
||||
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
|
||||
}
|
||||
user.setCreateBy(getUsername());
|
||||
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
|
||||
return toAjax(userService.insertUser(user));
|
||||
else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
|
||||
{
|
||||
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
|
||||
}
|
||||
user.setCreateBy(getUsername());
|
||||
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
|
||||
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
|
||||
return toAjax(userService.insertUser(user));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,13 +197,14 @@ public class SysUserController extends BaseController
|
||||
@PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
|
||||
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/resetPwd")
|
||||
public AjaxResult resetPwd(@RequestBody SysUser user)
|
||||
{
|
||||
userService.checkUserAllowed(user);
|
||||
userService.checkUserDataScope(user.getUserId());
|
||||
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
|
||||
user.setUpdateBy(getUsername());
|
||||
return toAjax(userService.resetPwd(user));
|
||||
public AjaxResult resetPwd(@RequestBody SysUser user)
|
||||
{
|
||||
userService.checkUserAllowed(user);
|
||||
userService.checkUserDataScope(user.getUserId());
|
||||
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
|
||||
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
|
||||
user.setUpdateBy(getUsername());
|
||||
return toAjax(userService.resetPwd(user));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为63310
|
||||
port: 63310
|
||||
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:
|
||||
@@ -58,4 +77,7 @@ spring:
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
multi-statement-allow: true
|
||||
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
|
||||
84
ruoyi-admin/src/main/resources/application-pro.yml
Normal file
84
ruoyi-admin/src/main/resources/application-pro.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为63310
|
||||
port: 63310
|
||||
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://64.127.23.7:3306/loan-pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: lrdb
|
||||
password: Synx2024
|
||||
# 从库数据源
|
||||
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
|
||||
model:
|
||||
url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
|
||||
|
||||
|
||||
84
ruoyi-admin/src/main/resources/application-uat.yml
Normal file
84
ruoyi-admin/src/main/resources/application-uat.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为63310
|
||||
port: 63310
|
||||
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://192.168.0.111:40628/loan-pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
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
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
|
||||
|
||||
@@ -13,23 +13,6 @@ ruoyi:
|
||||
# 验证码类型 math 数字计算 char 字符验证
|
||||
captchaType: math
|
||||
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为8080
|
||||
port: 8080
|
||||
servlet:
|
||||
# 应用的访问路径
|
||||
context-path: /
|
||||
tomcat:
|
||||
# tomcat的URI编码
|
||||
uri-encoding: UTF-8
|
||||
# 连接数满后的排队数,默认为100
|
||||
accept-count: 1000
|
||||
threads:
|
||||
# tomcat最大线程数,默认为200
|
||||
max: 800
|
||||
# Tomcat启动初始化的线程数,默认值10
|
||||
min-spare: 100
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
@@ -65,28 +48,6 @@ spring:
|
||||
restart:
|
||||
# 热部署开关
|
||||
enabled: true
|
||||
# redis 配置
|
||||
redis:
|
||||
# 地址
|
||||
host: 116.62.17.81
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# 密码
|
||||
password: Kfcx@1234
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 8
|
||||
# 连接池的最大数据库连接数
|
||||
max-active: 8
|
||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
|
||||
# token配置
|
||||
token:
|
||||
@@ -97,8 +58,8 @@ token:
|
||||
# 令牌有效期(默认30分钟)
|
||||
expireTime: 30
|
||||
|
||||
# MyBatis配置
|
||||
mybatis:
|
||||
# MyBatis-Plus配置
|
||||
mybatis-plus:
|
||||
# 搜索指定包别名
|
||||
typeAliasesPackage: com.ruoyi.**.domain
|
||||
# 配置mapper的扫描,找到所有的mapper.xml映射文件
|
||||
@@ -134,3 +95,7 @@ xss:
|
||||
excludes: /system/notice
|
||||
# 匹配链接
|
||||
urlPatterns: /system/*,/monitor/*,/tool/*
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
key: "1234567890abcdef"
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
class CacheControllerTest
|
||||
{
|
||||
@Test
|
||||
void shouldReturnInMemoryCacheSummary() throws Exception
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CacheController(redisCache)).build();
|
||||
|
||||
mockMvc.perform(get("/monitor/cache"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.info.cache_type").value("IN_MEMORY"))
|
||||
.andExpect(jsonPath("$.data.info.cache_mode").value("single-instance"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClearCacheKeysByPrefix() throws Exception
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
redisCache.setCacheObject("login_tokens:a", "A");
|
||||
redisCache.setCacheObject("login_tokens:b", "B");
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CacheController(redisCache)).build();
|
||||
|
||||
mockMvc.perform(delete("/monitor/cache/clearCacheName/login_tokens:"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/monitor/cache/getKeys/login_tokens:"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.framework.web.service.SysLoginService;
|
||||
|
||||
class SysLoginControllerPasswordTransferTest
|
||||
{
|
||||
@Test
|
||||
void shouldDecryptPasswordBeforeCallingLoginService() throws Exception
|
||||
{
|
||||
SysLoginService loginService = mock(SysLoginService.class);
|
||||
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
|
||||
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("admin123");
|
||||
when(loginService.login("admin", "admin123", "1", "u")).thenReturn("token");
|
||||
|
||||
SysLoginController controller = new SysLoginController();
|
||||
ReflectionTestUtils.setField(controller, "loginService", loginService);
|
||||
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
mockMvc.perform(post("/login")
|
||||
.contentType("application/json")
|
||||
.content("{\"username\":\"admin\",\"password\":\"cipher\",\"code\":\"1\",\"uuid\":\"u\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(passwordTransferCryptoService).decrypt("cipher");
|
||||
verify(loginService).login("admin", "admin123", "1", "u");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
|
||||
class SysProfileControllerPasswordTransferTest
|
||||
{
|
||||
@AfterEach
|
||||
void tearDown()
|
||||
{
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecryptPasswordsBeforeCheckingOldPassword() throws Exception
|
||||
{
|
||||
ISysUserService userService = mock(ISysUserService.class);
|
||||
TokenService tokenService = mock(TokenService.class);
|
||||
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
|
||||
when(passwordTransferCryptoService.decrypt("oldCipher")).thenReturn("oldPlain");
|
||||
when(passwordTransferCryptoService.decrypt("newCipher")).thenReturn("newPlain");
|
||||
when(userService.resetUserPwd(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(1);
|
||||
|
||||
SysUser storedUser = new SysUser();
|
||||
storedUser.setUserId(2L);
|
||||
storedUser.setPassword(SecurityUtils.encryptPassword("oldPlain"));
|
||||
when(userService.selectUserById(2L)).thenReturn(storedUser);
|
||||
|
||||
SysUser currentUser = new SysUser();
|
||||
currentUser.setUserId(2L);
|
||||
currentUser.setUserName("admin");
|
||||
LoginUser loginUser = new LoginUser(2L, 1L, currentUser, Collections.emptySet());
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
|
||||
|
||||
SysProfileController controller = new SysProfileController();
|
||||
ReflectionTestUtils.setField(controller, "userService", userService);
|
||||
ReflectionTestUtils.setField(controller, "tokenService", tokenService);
|
||||
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
mockMvc.perform(put("/system/user/profile/updatePwd")
|
||||
.contentType("application/json")
|
||||
.content("{\"oldPassword\":\"oldCipher\",\"newPassword\":\"newCipher\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(passwordTransferCryptoService).decrypt("oldCipher");
|
||||
verify(passwordTransferCryptoService).decrypt("newCipher");
|
||||
verify(userService).resetUserPwd(org.mockito.ArgumentMatchers.eq(2L), org.mockito.ArgumentMatchers.anyString());
|
||||
verify(tokenService).setLoginUser(loginUser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import com.ruoyi.common.core.domain.model.RegisterBody;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.framework.web.service.SysRegisterService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
|
||||
class SysRegisterControllerPasswordTransferTest
|
||||
{
|
||||
@Test
|
||||
void shouldDecryptPasswordBeforeCallingRegisterService() throws Exception
|
||||
{
|
||||
SysRegisterService registerService = mock(SysRegisterService.class);
|
||||
ISysConfigService configService = mock(ISysConfigService.class);
|
||||
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
|
||||
when(configService.selectConfigByKey("sys.account.registerUser")).thenReturn("true");
|
||||
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("admin123");
|
||||
when(registerService.register(any(RegisterBody.class))).thenReturn("");
|
||||
|
||||
SysRegisterController controller = new SysRegisterController();
|
||||
ReflectionTestUtils.setField(controller, "registerService", registerService);
|
||||
ReflectionTestUtils.setField(controller, "configService", configService);
|
||||
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
mockMvc.perform(post("/register")
|
||||
.contentType("application/json")
|
||||
.content("{\"username\":\"u1\",\"password\":\"cipher\",\"code\":\"1\",\"uuid\":\"u\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(passwordTransferCryptoService).decrypt("cipher");
|
||||
ArgumentCaptor<RegisterBody> captor = ArgumentCaptor.forClass(RegisterBody.class);
|
||||
verify(registerService).register(captor.capture());
|
||||
assertEquals("admin123", captor.getValue().getPassword());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
|
||||
import com.ruoyi.system.service.ISysDeptService;
|
||||
import com.ruoyi.system.service.ISysPostService;
|
||||
import com.ruoyi.system.service.ISysRoleService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
|
||||
class SysUserControllerPasswordTransferTest
|
||||
{
|
||||
@AfterEach
|
||||
void tearDown()
|
||||
{
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecryptPasswordBeforeAddingUser() throws Exception
|
||||
{
|
||||
ISysUserService userService = mock(ISysUserService.class);
|
||||
ISysRoleService roleService = mock(ISysRoleService.class);
|
||||
ISysDeptService deptService = mock(ISysDeptService.class);
|
||||
ISysPostService postService = mock(ISysPostService.class);
|
||||
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
|
||||
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("initPwd");
|
||||
when(userService.checkUserNameUnique(org.mockito.ArgumentMatchers.any(SysUser.class))).thenReturn(true);
|
||||
when(userService.insertUser(org.mockito.ArgumentMatchers.any(SysUser.class))).thenReturn(1);
|
||||
|
||||
setAuthentication();
|
||||
|
||||
SysUserController controller = new SysUserController();
|
||||
ReflectionTestUtils.setField(controller, "userService", userService);
|
||||
ReflectionTestUtils.setField(controller, "roleService", roleService);
|
||||
ReflectionTestUtils.setField(controller, "deptService", deptService);
|
||||
ReflectionTestUtils.setField(controller, "postService", postService);
|
||||
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
mockMvc.perform(post("/system/user")
|
||||
.contentType("application/json")
|
||||
.content("{\"userName\":\"u1\",\"nickName\":\"n1\",\"deptId\":1,\"password\":\"cipher\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(passwordTransferCryptoService).decrypt("cipher");
|
||||
ArgumentCaptor<SysUser> captor = ArgumentCaptor.forClass(SysUser.class);
|
||||
verify(userService).insertUser(captor.capture());
|
||||
assertTrue(SecurityUtils.matchesPassword("initPwd", captor.getValue().getPassword()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecryptPasswordBeforeResettingUserPassword() throws Exception
|
||||
{
|
||||
ISysUserService userService = mock(ISysUserService.class);
|
||||
ISysRoleService roleService = mock(ISysRoleService.class);
|
||||
ISysDeptService deptService = mock(ISysDeptService.class);
|
||||
ISysPostService postService = mock(ISysPostService.class);
|
||||
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
|
||||
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("resetPwd");
|
||||
when(userService.resetPwd(org.mockito.ArgumentMatchers.any(SysUser.class))).thenReturn(1);
|
||||
|
||||
setAuthentication();
|
||||
|
||||
SysUserController controller = new SysUserController();
|
||||
ReflectionTestUtils.setField(controller, "userService", userService);
|
||||
ReflectionTestUtils.setField(controller, "roleService", roleService);
|
||||
ReflectionTestUtils.setField(controller, "deptService", deptService);
|
||||
ReflectionTestUtils.setField(controller, "postService", postService);
|
||||
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
mockMvc.perform(put("/system/user/resetPwd")
|
||||
.contentType("application/json")
|
||||
.content("{\"userId\":2,\"password\":\"cipher\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(passwordTransferCryptoService).decrypt("cipher");
|
||||
ArgumentCaptor<SysUser> captor = ArgumentCaptor.forClass(SysUser.class);
|
||||
verify(userService).resetPwd(captor.capture());
|
||||
assertTrue(SecurityUtils.matchesPassword("resetPwd", captor.getValue().getPassword()));
|
||||
}
|
||||
|
||||
private void setAuthentication()
|
||||
{
|
||||
SysUser currentUser = new SysUser();
|
||||
currentUser.setUserId(1L);
|
||||
currentUser.setUserName("admin");
|
||||
LoginUser loginUser = new LoginUser(1L, 1L, currentUser, Collections.emptySet());
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,16 @@
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- pagehelper 分页插件 -->
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- pagehelper 分页插件 -->
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 自定义验证注解 -->
|
||||
<dependency>
|
||||
@@ -89,30 +94,24 @@
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- redis 缓存操作 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- 解析客户端操作系统、浏览器等 -->
|
||||
<dependency>
|
||||
<groupId>nl.basjes.parse.useragent</groupId>
|
||||
<artifactId>yauaa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- pool 对象池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 解析客户端操作系统、浏览器等 -->
|
||||
<dependency>
|
||||
<groupId>nl.basjes.parse.useragent</groupId>
|
||||
<artifactId>yauaa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- servlet包 -->
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<!-- servlet包 -->
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
29
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java
vendored
Normal file
29
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheEntry.java
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.common.core.cache;
|
||||
|
||||
class InMemoryCacheEntry
|
||||
{
|
||||
private final Object value;
|
||||
|
||||
private final Long expireAtMillis;
|
||||
|
||||
InMemoryCacheEntry(Object value, Long expireAtMillis)
|
||||
{
|
||||
this.value = value;
|
||||
this.expireAtMillis = expireAtMillis;
|
||||
}
|
||||
|
||||
Object getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
Long getExpireAtMillis()
|
||||
{
|
||||
return expireAtMillis;
|
||||
}
|
||||
|
||||
boolean isExpired(long now)
|
||||
{
|
||||
return expireAtMillis != null && expireAtMillis.longValue() <= now;
|
||||
}
|
||||
}
|
||||
65
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java
vendored
Normal file
65
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStats.java
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.ruoyi.common.core.cache;
|
||||
|
||||
public class InMemoryCacheStats
|
||||
{
|
||||
private final String cacheType;
|
||||
|
||||
private final String mode;
|
||||
|
||||
private final long keySize;
|
||||
|
||||
private final long hitCount;
|
||||
|
||||
private final long missCount;
|
||||
|
||||
private final long expiredCount;
|
||||
|
||||
private final long writeCount;
|
||||
|
||||
public InMemoryCacheStats(String cacheType, String mode, long keySize, long hitCount, long missCount,
|
||||
long expiredCount, long writeCount)
|
||||
{
|
||||
this.cacheType = cacheType;
|
||||
this.mode = mode;
|
||||
this.keySize = keySize;
|
||||
this.hitCount = hitCount;
|
||||
this.missCount = missCount;
|
||||
this.expiredCount = expiredCount;
|
||||
this.writeCount = writeCount;
|
||||
}
|
||||
|
||||
public String getCacheType()
|
||||
{
|
||||
return cacheType;
|
||||
}
|
||||
|
||||
public String getMode()
|
||||
{
|
||||
return mode;
|
||||
}
|
||||
|
||||
public long getKeySize()
|
||||
{
|
||||
return keySize;
|
||||
}
|
||||
|
||||
public long getHitCount()
|
||||
{
|
||||
return hitCount;
|
||||
}
|
||||
|
||||
public long getMissCount()
|
||||
{
|
||||
return missCount;
|
||||
}
|
||||
|
||||
public long getExpiredCount()
|
||||
{
|
||||
return expiredCount;
|
||||
}
|
||||
|
||||
public long getWriteCount()
|
||||
{
|
||||
return writeCount;
|
||||
}
|
||||
}
|
||||
290
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java
vendored
Normal file
290
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/InMemoryCacheStore.java
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
package com.ruoyi.common.core.cache;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class InMemoryCacheStore
|
||||
{
|
||||
private final ConcurrentHashMap<String, InMemoryCacheEntry> entries = new ConcurrentHashMap<String, InMemoryCacheEntry>();
|
||||
|
||||
private final AtomicLong hitCount = new AtomicLong();
|
||||
|
||||
private final AtomicLong missCount = new AtomicLong();
|
||||
|
||||
private final AtomicLong expiredCount = new AtomicLong();
|
||||
|
||||
private final AtomicLong writeCount = new AtomicLong();
|
||||
|
||||
public void set(String key, Object value)
|
||||
{
|
||||
putEntry(key, new InMemoryCacheEntry(value, null));
|
||||
}
|
||||
|
||||
public void set(String key, Object value, long timeout, TimeUnit unit)
|
||||
{
|
||||
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
|
||||
putEntry(key, new InMemoryCacheEntry(value, Long.valueOf(expireAtMillis)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(String key)
|
||||
{
|
||||
InMemoryCacheEntry entry = readEntry(key);
|
||||
return entry == null ? null : (T) entry.getValue();
|
||||
}
|
||||
|
||||
public boolean hasKey(String key)
|
||||
{
|
||||
return readEntry(key) != null;
|
||||
}
|
||||
|
||||
public boolean delete(String key)
|
||||
{
|
||||
return entries.remove(key) != null;
|
||||
}
|
||||
|
||||
public boolean delete(Collection<String> keys)
|
||||
{
|
||||
boolean deleted = false;
|
||||
if (keys == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (String key : keys)
|
||||
{
|
||||
deleted = delete(key) || deleted;
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public Set<String> keys(String pattern)
|
||||
{
|
||||
purgeExpiredEntries();
|
||||
Set<String> matchedKeys = new TreeSet<String>();
|
||||
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
|
||||
{
|
||||
if (matches(pattern, entry.getKey()))
|
||||
{
|
||||
matchedKeys.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
return matchedKeys;
|
||||
}
|
||||
|
||||
public boolean expire(String key, long timeout, TimeUnit unit)
|
||||
{
|
||||
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
|
||||
return entries.computeIfPresent(key, (cacheKey, entry) -> entry.isExpired(System.currentTimeMillis())
|
||||
? null
|
||||
: new InMemoryCacheEntry(entry.getValue(), Long.valueOf(expireAtMillis))) != null;
|
||||
}
|
||||
|
||||
public long getExpire(String key)
|
||||
{
|
||||
return getExpire(key, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public long getExpire(String key, TimeUnit unit)
|
||||
{
|
||||
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||
InMemoryCacheEntry entry = readEntry(key);
|
||||
if (entry == null)
|
||||
{
|
||||
return -2L;
|
||||
}
|
||||
if (entry.getExpireAtMillis() == null)
|
||||
{
|
||||
return -1L;
|
||||
}
|
||||
long remainingMillis = Math.max(0L, entry.getExpireAtMillis().longValue() - System.currentTimeMillis());
|
||||
long unitMillis = Math.max(1L, unit.toMillis(1L));
|
||||
return (remainingMillis + unitMillis - 1L) / unitMillis;
|
||||
}
|
||||
|
||||
public long increment(String key, long timeout, TimeUnit unit)
|
||||
{
|
||||
Objects.requireNonNull(unit, "TimeUnit must not be null");
|
||||
AtomicLong result = new AtomicLong();
|
||||
entries.compute(key, (cacheKey, currentEntry) -> {
|
||||
long now = System.currentTimeMillis();
|
||||
boolean missingOrExpired = currentEntry == null || currentEntry.isExpired(now);
|
||||
long nextValue = missingOrExpired ? 1L : toLong(currentEntry.getValue()) + 1L;
|
||||
Long expireAtMillis = missingOrExpired || currentEntry.getExpireAtMillis() == null
|
||||
? Long.valueOf(now + Math.max(0L, unit.toMillis(timeout)))
|
||||
: currentEntry.getExpireAtMillis();
|
||||
if (missingOrExpired && currentEntry != null)
|
||||
{
|
||||
expiredCount.incrementAndGet();
|
||||
}
|
||||
result.set(nextValue);
|
||||
return new InMemoryCacheEntry(Long.valueOf(nextValue), expireAtMillis);
|
||||
});
|
||||
writeCount.incrementAndGet();
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public void clear()
|
||||
{
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
public InMemoryCacheStats snapshot()
|
||||
{
|
||||
purgeExpiredEntries();
|
||||
return new InMemoryCacheStats("IN_MEMORY", "single-instance", entries.size(), hitCount.get(), missCount.get(),
|
||||
expiredCount.get(), writeCount.get());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Map<String, T> getMap(String key)
|
||||
{
|
||||
Map<String, T> value = get(key);
|
||||
return value == null ? null : new HashMap<String, T>(value);
|
||||
}
|
||||
|
||||
public <T> void putMap(String key, Map<String, T> dataMap)
|
||||
{
|
||||
set(key, new HashMap<String, T>(dataMap));
|
||||
}
|
||||
|
||||
public <T> void putMapValue(String key, String mapKey, T value)
|
||||
{
|
||||
long ttl = getExpire(key, TimeUnit.MILLISECONDS);
|
||||
Map<String, T> current = getMap(key);
|
||||
if (current == null)
|
||||
{
|
||||
current = new HashMap<String, T>();
|
||||
}
|
||||
current.put(mapKey, value);
|
||||
setWithOptionalTtl(key, current, ttl);
|
||||
}
|
||||
|
||||
public boolean deleteMapValue(String key, String mapKey)
|
||||
{
|
||||
long ttl = getExpire(key, TimeUnit.MILLISECONDS);
|
||||
Map<String, Object> current = getMap(key);
|
||||
if (current == null || !current.containsKey(mapKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
current.remove(mapKey);
|
||||
setWithOptionalTtl(key, current, ttl);
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Set<T> getSet(String key)
|
||||
{
|
||||
Set<T> value = get(key);
|
||||
return value == null ? null : new HashSet<T>(value);
|
||||
}
|
||||
|
||||
public <T> void putSet(String key, Set<T> dataSet)
|
||||
{
|
||||
set(key, new HashSet<T>(dataSet));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> List<T> getList(String key)
|
||||
{
|
||||
List<T> value = get(key);
|
||||
return value == null ? null : new ArrayList<T>(value);
|
||||
}
|
||||
|
||||
public <T> void putList(String key, List<T> dataList)
|
||||
{
|
||||
set(key, new ArrayList<T>(dataList));
|
||||
}
|
||||
|
||||
private void putEntry(String key, InMemoryCacheEntry entry)
|
||||
{
|
||||
entries.put(key, entry);
|
||||
writeCount.incrementAndGet();
|
||||
}
|
||||
|
||||
private InMemoryCacheEntry readEntry(String key)
|
||||
{
|
||||
InMemoryCacheEntry entry = entries.get(key);
|
||||
if (entry == null)
|
||||
{
|
||||
missCount.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
if (entry.isExpired(System.currentTimeMillis()))
|
||||
{
|
||||
removeExpiredEntry(key, entry);
|
||||
missCount.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
hitCount.incrementAndGet();
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void purgeExpiredEntries()
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
|
||||
{
|
||||
if (entry.getValue().isExpired(now))
|
||||
{
|
||||
removeExpiredEntry(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeExpiredEntry(String key, InMemoryCacheEntry expectedEntry)
|
||||
{
|
||||
if (entries.remove(key, expectedEntry))
|
||||
{
|
||||
expiredCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matches(String pattern, String key)
|
||||
{
|
||||
if ("*".equals(pattern))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (pattern.endsWith("*"))
|
||||
{
|
||||
return key.startsWith(pattern.substring(0, pattern.length() - 1));
|
||||
}
|
||||
return key.equals(pattern);
|
||||
}
|
||||
|
||||
private long toLong(Object value)
|
||||
{
|
||||
if (value instanceof Number)
|
||||
{
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
}
|
||||
|
||||
private void setWithOptionalTtl(String key, Object value, long ttlMillis)
|
||||
{
|
||||
if (ttlMillis > 0L)
|
||||
{
|
||||
set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
else
|
||||
{
|
||||
set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,42 @@
|
||||
package com.ruoyi.common.core.redis;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.BoundSetOperations;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ValueOperations;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* spring redis 工具类
|
||||
*
|
||||
* @author ruoyi
|
||||
**/
|
||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||
@Component
|
||||
public class RedisCache
|
||||
{
|
||||
@Autowired
|
||||
public RedisTemplate redisTemplate;
|
||||
|
||||
/**
|
||||
* 缓存基本的对象,Integer、String、实体类等
|
||||
package com.ruoyi.common.core.redis;
|
||||
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStats;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 本地缓存门面,保留原有 RedisCache 业务入口。
|
||||
*
|
||||
* @author ruoyi
|
||||
**/
|
||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||
@Component
|
||||
public class RedisCache
|
||||
{
|
||||
private final InMemoryCacheStore cacheStore;
|
||||
|
||||
public RedisCache(InMemoryCacheStore cacheStore)
|
||||
{
|
||||
this.cacheStore = cacheStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存基本的对象,Integer、String、实体类等
|
||||
*
|
||||
* @param key 缓存的键值
|
||||
* @param value 缓存的值
|
||||
*/
|
||||
public <T> void setCacheObject(final String key, final T value)
|
||||
{
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
*/
|
||||
public <T> void setCacheObject(final String key, final T value)
|
||||
{
|
||||
cacheStore.set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存基本的对象,Integer、String、实体类等
|
||||
@@ -44,10 +46,10 @@ public class RedisCache
|
||||
* @param timeout 时间
|
||||
* @param timeUnit 时间颗粒度
|
||||
*/
|
||||
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
|
||||
{
|
||||
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
|
||||
}
|
||||
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
|
||||
{
|
||||
cacheStore.set(key, value, timeout.longValue(), timeUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置有效时间
|
||||
@@ -69,10 +71,10 @@ public class RedisCache
|
||||
* @param unit 时间单位
|
||||
* @return true=设置成功;false=设置失败
|
||||
*/
|
||||
public boolean expire(final String key, final long timeout, final TimeUnit unit)
|
||||
{
|
||||
return redisTemplate.expire(key, timeout, unit);
|
||||
}
|
||||
public boolean expire(final String key, final long timeout, final TimeUnit unit)
|
||||
{
|
||||
return cacheStore.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效时间
|
||||
@@ -80,10 +82,10 @@ public class RedisCache
|
||||
* @param key Redis键
|
||||
* @return 有效时间
|
||||
*/
|
||||
public long getExpire(final String key)
|
||||
{
|
||||
return redisTemplate.getExpire(key);
|
||||
}
|
||||
public long getExpire(final String key)
|
||||
{
|
||||
return cacheStore.getExpire(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 key是否存在
|
||||
@@ -91,10 +93,10 @@ public class RedisCache
|
||||
* @param key 键
|
||||
* @return true 存在 false不存在
|
||||
*/
|
||||
public Boolean hasKey(String key)
|
||||
{
|
||||
return redisTemplate.hasKey(key);
|
||||
}
|
||||
public Boolean hasKey(String key)
|
||||
{
|
||||
return cacheStore.hasKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存的基本对象。
|
||||
@@ -102,21 +104,20 @@ public class RedisCache
|
||||
* @param key 缓存键值
|
||||
* @return 缓存键值对应的数据
|
||||
*/
|
||||
public <T> T getCacheObject(final String key)
|
||||
{
|
||||
ValueOperations<String, T> operation = redisTemplate.opsForValue();
|
||||
return operation.get(key);
|
||||
}
|
||||
public <T> T getCacheObject(final String key)
|
||||
{
|
||||
return cacheStore.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单个对象
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
public boolean deleteObject(final String key)
|
||||
{
|
||||
return redisTemplate.delete(key);
|
||||
}
|
||||
public boolean deleteObject(final String key)
|
||||
{
|
||||
return cacheStore.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除集合对象
|
||||
@@ -124,10 +125,19 @@ public class RedisCache
|
||||
* @param collection 多个对象
|
||||
* @return
|
||||
*/
|
||||
public boolean deleteObject(final Collection collection)
|
||||
{
|
||||
return redisTemplate.delete(collection) > 0;
|
||||
}
|
||||
public boolean deleteObject(final Collection collection)
|
||||
{
|
||||
if (collection == null || collection.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
List<String> keys = new ArrayList<String>(collection.size());
|
||||
for (Object item : collection)
|
||||
{
|
||||
keys.add(String.valueOf(item));
|
||||
}
|
||||
return cacheStore.delete(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存List数据
|
||||
@@ -136,11 +146,11 @@ public class RedisCache
|
||||
* @param dataList 待缓存的List数据
|
||||
* @return 缓存的对象
|
||||
*/
|
||||
public <T> long setCacheList(final String key, final List<T> dataList)
|
||||
{
|
||||
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
public <T> long setCacheList(final String key, final List<T> dataList)
|
||||
{
|
||||
cacheStore.putList(key, dataList);
|
||||
return dataList == null ? 0 : dataList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存的list对象
|
||||
@@ -148,10 +158,10 @@ public class RedisCache
|
||||
* @param key 缓存的键值
|
||||
* @return 缓存键值对应的数据
|
||||
*/
|
||||
public <T> List<T> getCacheList(final String key)
|
||||
{
|
||||
return redisTemplate.opsForList().range(key, 0, -1);
|
||||
}
|
||||
public <T> List<T> getCacheList(final String key)
|
||||
{
|
||||
return cacheStore.getList(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存Set
|
||||
@@ -160,16 +170,11 @@ public class RedisCache
|
||||
* @param dataSet 缓存的数据
|
||||
* @return 缓存数据的对象
|
||||
*/
|
||||
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
|
||||
{
|
||||
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
|
||||
Iterator<T> it = dataSet.iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
setOperation.add(it.next());
|
||||
}
|
||||
return setOperation;
|
||||
}
|
||||
public <T> long setCacheSet(final String key, final Set<T> dataSet)
|
||||
{
|
||||
cacheStore.putSet(key, dataSet);
|
||||
return dataSet == null ? 0 : dataSet.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存的set
|
||||
@@ -177,10 +182,10 @@ public class RedisCache
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public <T> Set<T> getCacheSet(final String key)
|
||||
{
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
}
|
||||
public <T> Set<T> getCacheSet(final String key)
|
||||
{
|
||||
return cacheStore.getSet(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存Map
|
||||
@@ -188,12 +193,13 @@ public class RedisCache
|
||||
* @param key
|
||||
* @param dataMap
|
||||
*/
|
||||
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
|
||||
{
|
||||
if (dataMap != null) {
|
||||
redisTemplate.opsForHash().putAll(key, dataMap);
|
||||
}
|
||||
}
|
||||
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
|
||||
{
|
||||
if (dataMap != null)
|
||||
{
|
||||
cacheStore.putMap(key, dataMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存的Map
|
||||
@@ -201,10 +207,10 @@ public class RedisCache
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
public <T> Map<String, T> getCacheMap(final String key)
|
||||
{
|
||||
return redisTemplate.opsForHash().entries(key);
|
||||
}
|
||||
public <T> Map<String, T> getCacheMap(final String key)
|
||||
{
|
||||
return cacheStore.getMap(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 往Hash中存入数据
|
||||
@@ -213,10 +219,10 @@ public class RedisCache
|
||||
* @param hKey Hash键
|
||||
* @param value 值
|
||||
*/
|
||||
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
|
||||
{
|
||||
redisTemplate.opsForHash().put(key, hKey, value);
|
||||
}
|
||||
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
|
||||
{
|
||||
cacheStore.putMapValue(key, hKey, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash中的数据
|
||||
@@ -225,11 +231,11 @@ public class RedisCache
|
||||
* @param hKey Hash键
|
||||
* @return Hash中的对象
|
||||
*/
|
||||
public <T> T getCacheMapValue(final String key, final String hKey)
|
||||
{
|
||||
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
|
||||
return opsForHash.get(key, hKey);
|
||||
}
|
||||
public <T> T getCacheMapValue(final String key, final String hKey)
|
||||
{
|
||||
Map<String, T> map = cacheStore.getMap(key);
|
||||
return map == null ? null : map.get(hKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个Hash中的数据
|
||||
@@ -238,10 +244,20 @@ public class RedisCache
|
||||
* @param hKeys Hash键集合
|
||||
* @return Hash对象集合
|
||||
*/
|
||||
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
|
||||
{
|
||||
return redisTemplate.opsForHash().multiGet(key, hKeys);
|
||||
}
|
||||
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
|
||||
{
|
||||
Map<String, T> map = cacheStore.getMap(key);
|
||||
if (map == null || hKeys == null || hKeys.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<T> values = new ArrayList<T>(hKeys.size());
|
||||
for (Object hKey : hKeys)
|
||||
{
|
||||
values.add(map.get(String.valueOf(hKey)));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash中的某条数据
|
||||
@@ -250,10 +266,10 @@ public class RedisCache
|
||||
* @param hKey Hash键
|
||||
* @return 是否成功
|
||||
*/
|
||||
public boolean deleteCacheMapValue(final String key, final String hKey)
|
||||
{
|
||||
return redisTemplate.opsForHash().delete(key, hKey) > 0;
|
||||
}
|
||||
public boolean deleteCacheMapValue(final String key, final String hKey)
|
||||
{
|
||||
return cacheStore.deleteMapValue(key, hKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存的基本对象列表
|
||||
@@ -261,8 +277,23 @@ public class RedisCache
|
||||
* @param pattern 字符串前缀
|
||||
* @return 对象列表
|
||||
*/
|
||||
public Collection<String> keys(final String pattern)
|
||||
{
|
||||
return redisTemplate.keys(pattern);
|
||||
}
|
||||
}
|
||||
public Collection<String> keys(final String pattern)
|
||||
{
|
||||
return cacheStore.keys(pattern);
|
||||
}
|
||||
|
||||
public long increment(String key, long timeout, TimeUnit unit)
|
||||
{
|
||||
return cacheStore.increment(key, timeout, unit);
|
||||
}
|
||||
|
||||
public InMemoryCacheStats getCacheStats()
|
||||
{
|
||||
return cacheStore.snapshot();
|
||||
}
|
||||
|
||||
public void clear()
|
||||
{
|
||||
cacheStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
@@ -39,15 +40,30 @@ public class DictUtils
|
||||
* @param key 参数键
|
||||
* @return dictDatas 字典数据列表
|
||||
*/
|
||||
public static List<SysDictData> getDictCache(String key)
|
||||
{
|
||||
JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
|
||||
if (StringUtils.isNotNull(arrayCache))
|
||||
{
|
||||
return arrayCache.toList(SysDictData.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public static List<SysDictData> getDictCache(String key)
|
||||
{
|
||||
Object cacheObject = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
|
||||
if (cacheObject instanceof JSONArray)
|
||||
{
|
||||
JSONArray arrayCache = (JSONArray) cacheObject;
|
||||
return arrayCache.toList(SysDictData.class);
|
||||
}
|
||||
if (cacheObject instanceof List<?>)
|
||||
{
|
||||
List<?> listCache = (List<?>) cacheObject;
|
||||
List<SysDictData> dictDatas = new ArrayList<SysDictData>(listCache.size());
|
||||
for (Object item : listCache)
|
||||
{
|
||||
if (!(item instanceof SysDictData))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
dictDatas.add((SysDictData) item);
|
||||
}
|
||||
return dictDatas;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型和字典值获取字典标签
|
||||
|
||||
@@ -10,18 +10,27 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* 通用http发送方法
|
||||
@@ -282,12 +291,38 @@ public class HttpUtils
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrustAnyHostnameVerifier implements HostnameVerifier
|
||||
{
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static class TrustAnyHostnameVerifier implements HostnameVerifier
|
||||
{
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T doPostFormUrlEncoded(String url, Map<String, String> params, HttpHeaders headers, Class<T> responseType)
|
||||
{
|
||||
MultiValueMap<String, String> formParams = new LinkedMultiValueMap<String, String>();
|
||||
if (params != null && !params.isEmpty())
|
||||
{
|
||||
formParams.setAll(params);
|
||||
}
|
||||
|
||||
HttpHeaders requestHeaders = headers == null ? new HttpHeaders() : headers;
|
||||
requestHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
requestHeaders.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(formParams, requestHeaders);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
try
|
||||
{
|
||||
ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, responseType);
|
||||
log.info("POST(form-urlencoded) 请求成功,URL:{},响应结果:{}", url, response.getBody());
|
||||
return response.getBody();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("POST(form-urlencoded) 请求失败,URL:" + url + ",异常信息:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
vendored
Normal file
58
ruoyi-common/src/test/java/com/ruoyi/common/core/cache/InMemoryCacheStoreTest.java
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package com.ruoyi.common.core.cache;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class InMemoryCacheStoreTest
|
||||
{
|
||||
@Test
|
||||
void shouldExpireEntryAfterTtl() throws Exception
|
||||
{
|
||||
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||
store.set("captcha_codes:1", "1234", 20L, TimeUnit.MILLISECONDS);
|
||||
Thread.sleep(40L);
|
||||
assertNull(store.get("captcha_codes:1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPrefixKeysInSortedOrder()
|
||||
{
|
||||
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||
store.set("login_tokens:a", "A");
|
||||
store.set("login_tokens:b", "B");
|
||||
store.set("sys_dict:x", "X");
|
||||
|
||||
Set<String> expected = new HashSet<String>();
|
||||
expected.add("login_tokens:a");
|
||||
expected.add("login_tokens:b");
|
||||
assertEquals(expected, store.keys("login_tokens:*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTrackHitsMissesWritesAndExpiredEntries() throws Exception
|
||||
{
|
||||
InMemoryCacheStore store = new InMemoryCacheStore();
|
||||
store.set("captcha_codes:2", "5678", 20L, TimeUnit.MILLISECONDS);
|
||||
assertTrue(store.hasKey("captcha_codes:2"));
|
||||
assertEquals("5678", store.get("captcha_codes:2"));
|
||||
Thread.sleep(40L);
|
||||
assertNull(store.get("captcha_codes:2"));
|
||||
assertFalse(store.hasKey("captcha_codes:2"));
|
||||
|
||||
InMemoryCacheStats stats = store.snapshot();
|
||||
assertEquals("IN_MEMORY", stats.getCacheType());
|
||||
assertEquals("single-instance", stats.getMode());
|
||||
assertEquals(0, stats.getKeySize());
|
||||
assertEquals(2L, stats.getHitCount());
|
||||
assertEquals(2L, stats.getMissCount());
|
||||
assertEquals(1L, stats.getExpiredCount());
|
||||
assertEquals(1L, stats.getWriteCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.common.core.redis;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RedisCacheTest
|
||||
{
|
||||
@Test
|
||||
void shouldSupportSetGetDeleteAndExpireThroughRedisCacheFacade()
|
||||
{
|
||||
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||
cache.setCacheObject("login_tokens:abc", "payload", 1, TimeUnit.SECONDS);
|
||||
assertEquals("payload", cache.getCacheObject("login_tokens:abc"));
|
||||
assertTrue(cache.hasKey("login_tokens:abc"));
|
||||
assertTrue(cache.deleteObject("login_tokens:abc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportKeysBulkDeleteAndRemainingTtl()
|
||||
{
|
||||
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||
cache.setCacheObject("login_tokens:a", "A", 5, TimeUnit.SECONDS);
|
||||
cache.setCacheObject("login_tokens:b", "B", 5, TimeUnit.SECONDS);
|
||||
cache.setCacheObject("sys_dict:x", "X");
|
||||
|
||||
assertNotNull(cache.getExpire("login_tokens:a"));
|
||||
assertTrue(cache.getExpire("login_tokens:a") > 0L);
|
||||
assertEquals(2, cache.keys("login_tokens:*").size());
|
||||
|
||||
Collection<String> keys = new ArrayList<String>();
|
||||
keys.add("login_tokens:a");
|
||||
keys.add("login_tokens:b");
|
||||
assertTrue(cache.deleteObject(keys));
|
||||
assertFalse(cache.hasKey("login_tokens:a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncrementCounterWithinTtlWindow() throws Exception
|
||||
{
|
||||
RedisCache cache = new RedisCache(new InMemoryCacheStore());
|
||||
assertEquals(1L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
|
||||
assertEquals(2L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
|
||||
Thread.sleep(70L);
|
||||
assertNull(cache.getCacheObject("rate_limit:test"));
|
||||
assertEquals(1L, cache.increment("rate_limit:test", 50L, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class DictUtilsTest
|
||||
{
|
||||
@AfterEach
|
||||
void tearDown()
|
||||
{
|
||||
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnDictListWhenCacheStoresArrayList()
|
||||
{
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerSingleton("redisCache", new RedisCache(new InMemoryCacheStore()));
|
||||
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", beanFactory);
|
||||
|
||||
SysDictData dictData = new SysDictData();
|
||||
dictData.setDictType("sys_normal_disable");
|
||||
dictData.setDictLabel("正常");
|
||||
dictData.setDictValue("0");
|
||||
|
||||
DictUtils.setDictCache("sys_normal_disable", Collections.singletonList(dictData));
|
||||
|
||||
List<SysDictData> dictCache = DictUtils.getDictCache("sys_normal_disable");
|
||||
|
||||
assertNotNull(dictCache);
|
||||
assertEquals(1, dictCache.size());
|
||||
assertEquals("正常", dictCache.get(0).getDictLabel());
|
||||
assertEquals("0", dictCache.get(0).getDictValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnDictListWhenCacheStoresJsonArray()
|
||||
{
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
beanFactory.registerSingleton("redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", beanFactory);
|
||||
|
||||
JSONArray jsonArray = JSONArray.parseArray("[{\"dictType\":\"sys_normal_disable\",\"dictLabel\":\"正常\",\"dictValue\":\"0\"}]");
|
||||
redisCache.setCacheObject(DictUtils.getCacheKey("sys_normal_disable"), jsonArray);
|
||||
|
||||
List<SysDictData> dictCache = DictUtils.getDictCache("sys_normal_disable");
|
||||
|
||||
assertNotNull(dictCache);
|
||||
assertEquals(1, dictCache.size());
|
||||
assertEquals("正常", dictCache.get(0).getDictLabel());
|
||||
assertEquals("0", dictCache.get(0).getDictValue());
|
||||
}
|
||||
}
|
||||
@@ -29,16 +29,16 @@
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里数据库连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>pro.fessional</groupId>
|
||||
<artifactId>kaptcha</artifactId>
|
||||
<!-- 阿里数据库连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>pro.fessional</groupId>
|
||||
<artifactId>kaptcha</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
@@ -53,12 +53,18 @@
|
||||
<artifactId>oshi-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
<!-- 系统模块-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,89 +1,75 @@
|
||||
package com.ruoyi.framework.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.common.annotation.RateLimiter;
|
||||
import com.ruoyi.common.enums.LimitType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.ip.IpUtils;
|
||||
|
||||
/**
|
||||
* 限流处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class RateLimiterAspect
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
|
||||
|
||||
private RedisTemplate<Object, Object> redisTemplate;
|
||||
|
||||
private RedisScript<Long> limitScript;
|
||||
|
||||
@Autowired
|
||||
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
|
||||
{
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setLimitScript(RedisScript<Long> limitScript)
|
||||
{
|
||||
this.limitScript = limitScript;
|
||||
}
|
||||
|
||||
@Before("@annotation(rateLimiter)")
|
||||
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
|
||||
{
|
||||
int time = rateLimiter.time();
|
||||
int count = rateLimiter.count();
|
||||
|
||||
String combineKey = getCombineKey(rateLimiter, point);
|
||||
List<Object> keys = Collections.singletonList(combineKey);
|
||||
try
|
||||
{
|
||||
Long number = redisTemplate.execute(limitScript, keys, count, time);
|
||||
if (StringUtils.isNull(number) || number.intValue() > count)
|
||||
{
|
||||
throw new ServiceException("访问过于频繁,请稍候再试");
|
||||
}
|
||||
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("服务器限流异常,请稍候再试");
|
||||
}
|
||||
}
|
||||
|
||||
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
|
||||
{
|
||||
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
||||
if (rateLimiter.limitType() == LimitType.IP)
|
||||
{
|
||||
stringBuffer.append(IpUtils.getIpAddr()).append("-");
|
||||
}
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Class<?> targetClass = method.getDeclaringClass();
|
||||
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
}
|
||||
package com.ruoyi.framework.aspectj;
|
||||
|
||||
import com.ruoyi.common.annotation.RateLimiter;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.enums.LimitType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.ip.IpUtils;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 限流处理
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class RateLimiterAspect
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
|
||||
|
||||
private final RedisCache redisCache;
|
||||
|
||||
public RateLimiterAspect(RedisCache redisCache)
|
||||
{
|
||||
this.redisCache = redisCache;
|
||||
}
|
||||
|
||||
@Before("@annotation(rateLimiter)")
|
||||
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
|
||||
{
|
||||
int time = rateLimiter.time();
|
||||
int count = rateLimiter.count();
|
||||
|
||||
String combineKey = getCombineKey(rateLimiter, point);
|
||||
try
|
||||
{
|
||||
long number = redisCache.increment(combineKey, time, TimeUnit.SECONDS);
|
||||
if (number > count)
|
||||
{
|
||||
throw new ServiceException("访问过于频繁,请稍候再试");
|
||||
}
|
||||
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number, combineKey);
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("服务器限流异常,请稍候再试");
|
||||
}
|
||||
}
|
||||
|
||||
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
|
||||
{
|
||||
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
||||
if (rateLimiter.limitType() == LimitType.IP)
|
||||
{
|
||||
stringBuffer.append(IpUtils.getIpAddr()).append("-");
|
||||
}
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Class<?> targetClass = method.getDeclaringClass();
|
||||
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.util.TimeZone;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.util.TimeZone;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
|
||||
@@ -23,8 +23,8 @@ public class ApplicationConfig
|
||||
* 时区配置
|
||||
*/
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
|
||||
{
|
||||
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
|
||||
}
|
||||
}
|
||||
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
|
||||
{
|
||||
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONReader;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import com.alibaba.fastjson2.filter.Filter;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
|
||||
/**
|
||||
* Redis使用FastJson序列化
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||
{
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
|
||||
|
||||
private Class<T> clazz;
|
||||
|
||||
public FastJson2JsonRedisSerializer(Class<T> clazz)
|
||||
{
|
||||
super();
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(T t) throws SerializationException
|
||||
{
|
||||
if (t == null)
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(byte[] bytes) throws SerializationException
|
||||
{
|
||||
if (bytes == null || bytes.length <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String str = new String(bytes, DEFAULT_CHARSET);
|
||||
|
||||
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import javax.sql.DataSource;
|
||||
import org.apache.ibatis.io.VFS;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* Mybatis支持*匹配扫描包
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisConfig
|
||||
{
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
|
||||
|
||||
public static String setTypeAliasesPackage(String typeAliasesPackage)
|
||||
{
|
||||
ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
|
||||
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
|
||||
List<String> allResult = new ArrayList<String>();
|
||||
try
|
||||
{
|
||||
for (String aliasesPackage : typeAliasesPackage.split(","))
|
||||
{
|
||||
List<String> result = new ArrayList<String>();
|
||||
aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
|
||||
+ ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
|
||||
Resource[] resources = resolver.getResources(aliasesPackage);
|
||||
if (resources != null && resources.length > 0)
|
||||
{
|
||||
MetadataReader metadataReader = null;
|
||||
for (Resource resource : resources)
|
||||
{
|
||||
if (resource.isReadable())
|
||||
{
|
||||
metadataReader = metadataReaderFactory.getMetadataReader(resource);
|
||||
try
|
||||
{
|
||||
result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
|
||||
}
|
||||
catch (ClassNotFoundException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.size() > 0)
|
||||
{
|
||||
HashSet<String> hashResult = new HashSet<String>(result);
|
||||
allResult.addAll(hashResult);
|
||||
}
|
||||
}
|
||||
if (allResult.size() > 0)
|
||||
{
|
||||
typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
return typeAliasesPackage;
|
||||
}
|
||||
|
||||
public Resource[] resolveMapperLocations(String[] mapperLocations)
|
||||
{
|
||||
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
|
||||
List<Resource> resources = new ArrayList<Resource>();
|
||||
if (mapperLocations != null)
|
||||
{
|
||||
for (String mapperLocation : mapperLocations)
|
||||
{
|
||||
try
|
||||
{
|
||||
Resource[] mappers = resourceResolver.getResources(mapperLocation);
|
||||
resources.addAll(Arrays.asList(mappers));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
return resources.toArray(new Resource[resources.size()]);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
|
||||
{
|
||||
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
|
||||
String mapperLocations = env.getProperty("mybatis.mapperLocations");
|
||||
String configLocation = env.getProperty("mybatis.configLocation");
|
||||
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
|
||||
VFS.addImplClass(SpringBootVFS.class);
|
||||
|
||||
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
|
||||
sessionFactory.setDataSource(dataSource);
|
||||
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
|
||||
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
|
||||
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
|
||||
return sessionFactory.getObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MybatisPlusConfig
|
||||
{
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor()
|
||||
{
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* redis配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisConfig extends CachingConfigurerSupport
|
||||
{
|
||||
@Bean
|
||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
||||
{
|
||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
||||
|
||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(serializer);
|
||||
|
||||
// Hash的key也采用StringRedisSerializer的序列化方式
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> limitScript()
|
||||
{
|
||||
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
|
||||
redisScript.setScriptText(limitScriptText());
|
||||
redisScript.setResultType(Long.class);
|
||||
return redisScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流脚本
|
||||
*/
|
||||
private String limitScriptText()
|
||||
{
|
||||
return "local key = KEYS[1]\n" +
|
||||
"local count = tonumber(ARGV[1])\n" +
|
||||
"local time = tonumber(ARGV[2])\n" +
|
||||
"local current = redis.call('get', key);\n" +
|
||||
"if current and tonumber(current) > count then\n" +
|
||||
" return tonumber(current);\n" +
|
||||
"end\n" +
|
||||
"current = redis.call('incr', key)\n" +
|
||||
"if tonumber(current) == 1 then\n" +
|
||||
" redis.call('expire', key, time)\n" +
|
||||
"end\n" +
|
||||
"return tonumber(current);";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ruoyi.framework.config.handler;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
|
||||
/**
|
||||
* 实体类自动配置创建日期和更新日期
|
||||
*/
|
||||
@Component
|
||||
public class MyMetaHandler implements MetaObjectHandler
|
||||
{
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject)
|
||||
{
|
||||
String auditUser = SecurityUtils.getLoginUser().getUser().getNickName() + '-' + SecurityUtils.getUsername();
|
||||
this.setFieldValByName("createBy", auditUser, metaObject);
|
||||
this.setFieldValByName("createTime", new Date(), metaObject);
|
||||
this.setFieldValByName("updateBy", auditUser, metaObject);
|
||||
this.setFieldValByName("updateTime", new Date(), metaObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject)
|
||||
{
|
||||
String auditUser = SecurityUtils.getLoginUser().getUser().getNickName() + '-' + SecurityUtils.getUsername();
|
||||
this.setFieldValByName("updateBy", auditUser, metaObject);
|
||||
this.setFieldValByName("updateTime", new Date(), metaObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.framework.web.service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
|
||||
@Service
|
||||
public class PasswordTransferCryptoService
|
||||
{
|
||||
@Value("${security.password-transfer.key}")
|
||||
private String key;
|
||||
|
||||
public String decrypt(String cipherText)
|
||||
{
|
||||
try
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"));
|
||||
return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ServiceException("密码解密失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.framework.aspectj;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.ruoyi.common.annotation.RateLimiter;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import java.lang.reflect.Method;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.Signature;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RateLimiterAspectTest
|
||||
{
|
||||
@Test
|
||||
void shouldRejectThirdRequestWithinWindow() throws Throwable
|
||||
{
|
||||
RateLimiterAspect aspect = new RateLimiterAspect(new RedisCache(new InMemoryCacheStore()));
|
||||
Method method = TestRateLimitTarget.class.getDeclaredMethod("limited");
|
||||
JoinPoint joinPoint = mockJoinPoint(method);
|
||||
RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
|
||||
|
||||
assertDoesNotThrow(() -> aspect.doBefore(joinPoint, rateLimiter));
|
||||
assertDoesNotThrow(() -> aspect.doBefore(joinPoint, rateLimiter));
|
||||
assertThrows(ServiceException.class, () -> aspect.doBefore(joinPoint, rateLimiter));
|
||||
}
|
||||
|
||||
private JoinPoint mockJoinPoint(Method method)
|
||||
{
|
||||
MethodSignature signature = mock(MethodSignature.class);
|
||||
when(signature.getMethod()).thenReturn(method);
|
||||
|
||||
JoinPoint joinPoint = mock(JoinPoint.class);
|
||||
when(joinPoint.getSignature()).thenReturn((Signature) signature);
|
||||
return joinPoint;
|
||||
}
|
||||
|
||||
static class TestRateLimitTarget
|
||||
{
|
||||
@RateLimiter(count = 2, time = 60)
|
||||
public void limited()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.framework.web.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.ruoyi.common.annotation.RepeatSubmit;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.framework.interceptor.impl.SameUrlDataInterceptor;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
class TokenServiceLocalCacheTest
|
||||
{
|
||||
@AfterEach
|
||||
void clearRequestContext()
|
||||
{
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreLoginUserWithTokenTtl()
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
TokenService tokenService = new TokenService();
|
||||
ReflectionTestUtils.setField(tokenService, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(tokenService, "header", "Authorization");
|
||||
ReflectionTestUtils.setField(tokenService, "secret", "unit-test-secret");
|
||||
ReflectionTestUtils.setField(tokenService, "expireTime", 30);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRemoteAddr("127.0.0.1");
|
||||
request.addHeader("User-Agent", "Mozilla/5.0");
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||
|
||||
SysUser user = new SysUser();
|
||||
user.setUserName("admin");
|
||||
user.setPassword("password");
|
||||
LoginUser loginUser = new LoginUser();
|
||||
loginUser.setUser(user);
|
||||
|
||||
String jwt = tokenService.createToken(loginUser);
|
||||
|
||||
assertNotNull(jwt);
|
||||
assertNotNull(redisCache.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()));
|
||||
assertTrue(redisCache.getExpire(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()) > 0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteCaptchaAfterValidation()
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
SysLoginService loginService = new SysLoginService();
|
||||
ISysConfigService configService = mock(ISysConfigService.class);
|
||||
when(configService.selectCaptchaEnabled()).thenReturn(true);
|
||||
ReflectionTestUtils.setField(loginService, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(loginService, "configService", configService);
|
||||
|
||||
redisCache.setCacheObject(CacheConstants.CAPTCHA_CODE_KEY + "uuid", "ABCD", 1, TimeUnit.MINUTES);
|
||||
|
||||
loginService.validateCaptcha("admin", "ABCD", "uuid");
|
||||
|
||||
assertFalse(redisCache.hasKey(CacheConstants.CAPTCHA_CODE_KEY + "uuid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportMillisecondRepeatSubmitWindow() throws Exception
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
SameUrlDataInterceptor interceptor = new SameUrlDataInterceptor();
|
||||
ReflectionTestUtils.setField(interceptor, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(interceptor, "header", "Authorization");
|
||||
|
||||
RepeatSubmit repeatSubmit = RepeatSubmitTarget.class.getDeclaredMethod("submit")
|
||||
.getAnnotation(RepeatSubmit.class);
|
||||
|
||||
MockHttpServletRequest firstRequest = createRepeatRequest();
|
||||
assertFalse(interceptor.isRepeatSubmit(firstRequest, repeatSubmit));
|
||||
|
||||
MockHttpServletRequest secondRequest = createRepeatRequest();
|
||||
assertTrue(interceptor.isRepeatSubmit(secondRequest, repeatSubmit));
|
||||
|
||||
Thread.sleep(70L);
|
||||
MockHttpServletRequest thirdRequest = createRepeatRequest();
|
||||
assertFalse(interceptor.isRepeatSubmit(thirdRequest, repeatSubmit));
|
||||
}
|
||||
|
||||
private MockHttpServletRequest createRepeatRequest()
|
||||
{
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRequestURI("/loan/pricing");
|
||||
request.addHeader("Authorization", "token-1");
|
||||
request.addParameter("loanId", "1001");
|
||||
return request;
|
||||
}
|
||||
|
||||
static class RepeatSubmitTarget
|
||||
{
|
||||
@RepeatSubmit(interval = 50)
|
||||
public void submit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
52
ruoyi-loan-pricing/pom.xml
Normal file
52
ruoyi-loan-pricing/pom.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>ruoyi</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>3.9.2</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
|
||||
<description>
|
||||
利率定价模块
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.36</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.ruoyi.loanpricing.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.PageDomain;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.page.TableSupport;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
|
||||
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 利率定价流程Controller
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/loanPricing/workflow")
|
||||
public class LoanPricingWorkflowController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private ILoanPricingWorkflowService loanPricingWorkflowService;
|
||||
|
||||
/**
|
||||
* 发起个人客户利率定价流程
|
||||
*/
|
||||
@Log(title = "个人客户利率定价流程", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/create/personal")
|
||||
public AjaxResult createPersonal(@Validated @RequestBody PersonalLoanPricingCreateDTO dto) {
|
||||
LoanPricingWorkflow result = loanPricingWorkflowService.createPersonalLoanPricing(dto);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起企业客户利率定价流程
|
||||
*/
|
||||
@Log(title = "企业客户利率定价流程", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/create/corporate")
|
||||
public AjaxResult createCorporate(@Validated @RequestBody CorporateLoanPricingCreateDTO dto)
|
||||
{
|
||||
LoanPricingWorkflow result = loanPricingWorkflowService.createCorporateLoanPricing(dto);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询利率定价流程列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
Page<LoanPricingWorkflowListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
|
||||
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(page, loanPricingWorkflow);
|
||||
TableDataInfo rspData = new TableDataInfo();
|
||||
rspData.setCode(200);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(result.getRecords());
|
||||
rspData.setTotal(result.getTotal());
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询利率定价流程详情
|
||||
*/
|
||||
@GetMapping("/{serialNum}")
|
||||
public AjaxResult getInfo(@PathVariable("serialNum") String serialNum)
|
||||
{
|
||||
LoanPricingWorkflowVO workflow = loanPricingWorkflowService.selectLoanPricingBySerialNum(serialNum);
|
||||
if (workflow == null)
|
||||
{
|
||||
return error("记录不存在");
|
||||
}
|
||||
return success(workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设定执行利率
|
||||
*/
|
||||
@Log(title = "利率定价流程", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{serialNum}/executeRate")
|
||||
public AjaxResult setExecuteRate(@PathVariable("serialNum") String serialNum,
|
||||
@RequestBody Map<String, String> request) {
|
||||
String executeRate = request.get("executeRate");
|
||||
boolean success = loanPricingWorkflowService.setExecuteRate(serialNum, executeRate);
|
||||
return success ? success() : error("设定失败");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.loanpricing.controller;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
/**
|
||||
* @Author 吴凯程
|
||||
* @Date 2025/11/10
|
||||
**/
|
||||
@RestController
|
||||
@RequestMapping("/rate/pricing/mock")
|
||||
public class LoanRatePricingMockController extends BaseController {
|
||||
|
||||
@Anonymous
|
||||
@PostMapping("/invokeModel")
|
||||
public AjaxResult invokeModel( ModelInvokeDTO modelInvokeDTO) {
|
||||
ObjectNode jsonNodes;
|
||||
if (modelInvokeDTO.getCustType().equals("个人")) {
|
||||
jsonNodes = loadJsonFromResource("data/retail_output.json");
|
||||
} else {
|
||||
jsonNodes = loadJsonFromResource("data/corp_output.json");
|
||||
}
|
||||
|
||||
return new AjaxResult(10000, "success", jsonNodes);
|
||||
}
|
||||
|
||||
private ObjectNode loadJsonFromResource(String resourcePath){
|
||||
ClassPathResource classPathResource = new ClassPathResource(resourcePath);
|
||||
try (InputStream inputStream = classPathResource.getInputStream();){
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
return objectMapper.readValue(inputStream, ObjectNode.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ruoyi.loanpricing.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
/**
|
||||
* 企业客户利率定价发起DTO
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
public class CorporateLoanPricingCreateDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "客户内码不能为空")
|
||||
private String custIsn;
|
||||
|
||||
private String custType;
|
||||
|
||||
private String custName;
|
||||
|
||||
private String idType;
|
||||
|
||||
private String idNum;
|
||||
|
||||
@NotBlank(message = "还款方式不能为空")
|
||||
@Pattern(regexp = "^(分期|不分期)$", message = "还款方式必须是:分期、不分期之一")
|
||||
private String repayMethod;
|
||||
|
||||
@NotBlank(message = "担保方式不能为空")
|
||||
@Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一")
|
||||
private String guarType;
|
||||
|
||||
@NotBlank(message = "申请金额不能为空")
|
||||
private String applyAmt;
|
||||
|
||||
@NotBlank(message = "借款期限不能为空")
|
||||
@Pattern(regexp = "^[1-6]$", message = "借款期限必须是 1 到 6 年")
|
||||
private String loanTerm;
|
||||
|
||||
private String isGreenLoan;
|
||||
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
@NotBlank(message = "抵质押类型不能为空")
|
||||
@Pattern(regexp = "^(一类|二类|三类|四类)$", message = "抵质押类型必须是:一类、二类、三类、四类之一")
|
||||
private String collType;
|
||||
|
||||
private String collThirdParty;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.ruoyi.loanpricing.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author 吴凯程
|
||||
* @Date 2025/12/23
|
||||
**/
|
||||
@Data
|
||||
public class ModelInvokeDTO {
|
||||
/**
|
||||
* 业务方流水号(必填)
|
||||
* 可使用时间戳,或自定义随机生成
|
||||
*/
|
||||
private String serialNum;
|
||||
|
||||
/**
|
||||
* 机构编码(必填)
|
||||
* 固定值:892000
|
||||
*/
|
||||
private String orgCode;
|
||||
|
||||
/**
|
||||
* 运行模式(必填)
|
||||
* 固定值:1:同步
|
||||
*/
|
||||
private String runType = "1";
|
||||
|
||||
/**
|
||||
* 客户内码(必填)
|
||||
*/
|
||||
private String custIsn;
|
||||
|
||||
/**
|
||||
* 客户类型(必填)
|
||||
* 可选值:个人/企业
|
||||
*/
|
||||
private String custType;
|
||||
|
||||
/**
|
||||
* 担保方式(必填)
|
||||
* 可选值:信用,保证,抵押,质押
|
||||
*/
|
||||
private String guarType;
|
||||
|
||||
/**
|
||||
* 还款方式(必填)
|
||||
* 可选值:分期/不分期
|
||||
*/
|
||||
private String repayMethod;
|
||||
|
||||
/**
|
||||
* 中间业务_个人_快捷支付(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String midPerQuickPay;
|
||||
|
||||
/**
|
||||
* 中间业务_个人_电费代扣(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String midPerEleDdc;
|
||||
|
||||
/**
|
||||
* 中间业务_企业_电费代扣(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String midEntEleDdc;
|
||||
|
||||
/**
|
||||
* 中间业务_企业_水费代扣(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String midEntWaterDdc;
|
||||
|
||||
/**
|
||||
* 申请金额(必填)
|
||||
* 单位:元
|
||||
*/
|
||||
private String applyAmt;
|
||||
|
||||
/**
|
||||
* 贷款期限(必填)
|
||||
* 单位:年
|
||||
*/
|
||||
private String loanTerm;
|
||||
|
||||
/**
|
||||
* 净身企业(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String isCleanEnt;
|
||||
|
||||
/**
|
||||
* 开立基本结算账户(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String hasSettleAcct;
|
||||
|
||||
/**
|
||||
* 制造业企业(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String isManufacturing;
|
||||
|
||||
/**
|
||||
* 省农担担保贷款(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String isAgriGuar;
|
||||
|
||||
/**
|
||||
* 绿色贷款(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String isGreenLoan;
|
||||
|
||||
/**
|
||||
* 贸易和建筑业企业(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
/**
|
||||
* 是否纳税信用等级A级(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String isTaxA;
|
||||
|
||||
/**
|
||||
* 是否县级及以上农业龙头企业(非必填)
|
||||
* 可选值:true/false
|
||||
*/
|
||||
private String isAgriLeading;
|
||||
|
||||
private String isInclusiveFinance;
|
||||
|
||||
/**
|
||||
* 贷款用途(非必填)
|
||||
* 可选值:consumer/business
|
||||
*/
|
||||
private String loanPurpose;
|
||||
|
||||
/**
|
||||
* 是否有经营佐证(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String bizProof;
|
||||
|
||||
/**
|
||||
* 循环功能(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String loanLoop;
|
||||
|
||||
/**
|
||||
* 抵质押类型(非必填)
|
||||
* 可选值:一类/二类/三类/四类
|
||||
*/
|
||||
private String collType;
|
||||
|
||||
/**
|
||||
* 抵质押物是否三方所有(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String collThirdParty;
|
||||
|
||||
// /**
|
||||
// * 贷款利率(必填)
|
||||
// */
|
||||
// private String loanRate;
|
||||
|
||||
/**
|
||||
* 客户名称(非必填)
|
||||
*/
|
||||
private String custName;
|
||||
|
||||
/**
|
||||
* 证件类型(非必填)
|
||||
*/
|
||||
private String idType;
|
||||
|
||||
/**
|
||||
* 证件号码(非必填)
|
||||
*/
|
||||
private String idNum;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.loanpricing.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
/**
|
||||
* 个人客户利率定价发起DTO
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
public class PersonalLoanPricingCreateDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "客户内码不能为空")
|
||||
private String custIsn;
|
||||
|
||||
private String custName;
|
||||
|
||||
private String idType;
|
||||
|
||||
private String idNum;
|
||||
|
||||
@NotBlank(message = "担保方式不能为空")
|
||||
@Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一")
|
||||
private String guarType;
|
||||
|
||||
@NotBlank(message = "申请金额不能为空")
|
||||
private String applyAmt;
|
||||
|
||||
@NotBlank(message = "贷款用途不能为空")
|
||||
@Pattern(regexp = "^(consumer|business)$", message = "贷款用途必须是:consumer、business之一")
|
||||
private String loanPurpose;
|
||||
|
||||
@NotBlank(message = "借款期限不能为空")
|
||||
private String loanTerm;
|
||||
|
||||
private String bizProof;
|
||||
|
||||
private String loanLoop;
|
||||
|
||||
private String collType;
|
||||
|
||||
private String collThirdParty;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.ruoyi.loanpricing.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 利率定价流程对象 loan_pricing_workflow
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@TableName("loan_pricing_workflow")
|
||||
public class LoanPricingWorkflow implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 模型输出ID */
|
||||
private Long modelOutputId;
|
||||
|
||||
/** 业务方流水号 */
|
||||
private String serialNum;
|
||||
|
||||
/** 机构编码 */
|
||||
private String orgCode;
|
||||
|
||||
/** 运行模式: 1-同步 */
|
||||
private String runType;
|
||||
|
||||
/** 客户内码 */
|
||||
@NotBlank(message = "客户内码不能为空")
|
||||
private String custIsn;
|
||||
|
||||
/** 客户类型: 个人/企业 */
|
||||
@NotBlank(message = "客户类型不能为空")
|
||||
private String custType;
|
||||
|
||||
/** 担保方式: 信用/保证/抵押/质押 */
|
||||
@NotBlank(message = "担保方式不能为空")
|
||||
private String guarType;
|
||||
|
||||
/** 还款方式: 分期/不分期 */
|
||||
private String repayMethod;
|
||||
|
||||
/** 中间业务_个人_快捷支付: true/false */
|
||||
private String midPerQuickPay;
|
||||
|
||||
/** 中间业务_个人_电费代扣: true/false */
|
||||
private String midPerEleDdc;
|
||||
|
||||
/** 中间业务_企业_电费代扣: true/false */
|
||||
private String midEntEleDdc;
|
||||
|
||||
/** 中间业务_企业_水费代扣: true/false */
|
||||
private String midEntWaterDdc;
|
||||
|
||||
/** 申请金额(元) */
|
||||
@NotBlank(message = "申请金额不能为空")
|
||||
private String applyAmt;
|
||||
|
||||
/**
|
||||
* 借款期限(年)
|
||||
*/
|
||||
private String loanTerm;
|
||||
|
||||
/** 净身企业: true/false */
|
||||
private String isCleanEnt;
|
||||
|
||||
/** 开立基本结算账户: true/false */
|
||||
private String hasSettleAcct;
|
||||
|
||||
/** 制造业企业: true/false */
|
||||
private String isManufacturing;
|
||||
|
||||
/** 省农担担保贷款: true/false */
|
||||
@JsonIgnore
|
||||
private String isAgriGuar;
|
||||
|
||||
/**
|
||||
* 贸易和建筑业企业标识: true/false
|
||||
*/
|
||||
@JsonIgnore
|
||||
private String isTradeConstruction;
|
||||
|
||||
/**
|
||||
* 贸易和建筑业企业标识: 0/1
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
/**
|
||||
* 绿色贷款: true/false
|
||||
*/
|
||||
private String isGreenLoan;
|
||||
|
||||
/**
|
||||
* 科技型企业: true/false
|
||||
*/
|
||||
@JsonIgnore
|
||||
private String isTechEnt;
|
||||
|
||||
/** 是否纳税信用等级A级: true/false */
|
||||
private String isTaxA;
|
||||
|
||||
/** 是否县级及以上农业龙头企业: true/false */
|
||||
private String isAgriLeading;
|
||||
|
||||
/** 贷款用途: consumer-消费/business-经营 */
|
||||
private String loanPurpose;
|
||||
|
||||
/** 是否有经营佐证: true/false */
|
||||
private String bizProof;
|
||||
|
||||
/** 循环功能: true/false */
|
||||
private String loanLoop;
|
||||
|
||||
/** 抵质押类型: 一类/二类/三类/四类 */
|
||||
private String collType;
|
||||
|
||||
/** 抵质押物是否三方所有: true/false */
|
||||
private String collThirdParty;
|
||||
|
||||
/** 贷款利率 */
|
||||
private String loanRate;
|
||||
|
||||
/**
|
||||
* 执行利率(%)
|
||||
*/
|
||||
private String executeRate;
|
||||
|
||||
/** 客户名称 */
|
||||
private String custName;
|
||||
|
||||
/**
|
||||
* 证件类型
|
||||
*/
|
||||
private String idType;
|
||||
|
||||
/** 证件号码 */
|
||||
private String idNum;
|
||||
|
||||
/** 是否普惠小微借款人: true/false */
|
||||
private String isInclusiveFinance;
|
||||
|
||||
/** 创建者 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.ruoyi.loanpricing.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 贷款定价模型输入参数对象
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
public class ModelCorpOutputFields {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
// 客户内码
|
||||
private String custIsn;
|
||||
// 客户类型
|
||||
private String custType;
|
||||
// 担保方式
|
||||
private String guarType;
|
||||
// 客户名称
|
||||
private String custName;
|
||||
// 证件类型
|
||||
private String idType;
|
||||
// 证件号码
|
||||
private String idNum;
|
||||
// 还款方式
|
||||
@TableField(exist = false)
|
||||
private String repayMethod;
|
||||
// 基准利率
|
||||
private String baseLoanRate;
|
||||
// 我行首贷客户
|
||||
private String isFirstLoan;
|
||||
// 用信天数
|
||||
private String faithDay;
|
||||
// BP_首贷
|
||||
private String bpFirstLoan;
|
||||
// BP_贷龄
|
||||
private String bpAgeLoan;
|
||||
// TOTAL_BP_忠诚度
|
||||
private String totalBpLoyalty;
|
||||
// 存款年日均
|
||||
private String balanceAvg;
|
||||
// 贷款年日均
|
||||
private String loanAvg;
|
||||
// 派生率
|
||||
private String derivationRate;
|
||||
// TOTAL_BP_贡献度
|
||||
private String totalBpContribution;
|
||||
// 中间业务_企业_企业互联
|
||||
private String midEntConnect;
|
||||
// 中间业务_企业_有效价值客户
|
||||
private String midEntEffect;
|
||||
// 中间业务_企业_国际业务
|
||||
private String midEntInter;
|
||||
// 中间业务_企业_承兑
|
||||
private String midEntAccept;
|
||||
// 中间业务_企业_贴现
|
||||
private String midEntDiscount;
|
||||
// 中间业务_企业_电费代扣
|
||||
private String midEntEleDdc;
|
||||
// 中间业务_企业_水费代扣
|
||||
private String midEntWaterDdc;
|
||||
// 中间业务_企业_税务代扣
|
||||
private String midEntTax;
|
||||
// BP_中间业务
|
||||
private String bpMid;
|
||||
// 代发工资户数
|
||||
private String payroll;
|
||||
// 存量贷款余额
|
||||
private String invLoanAmount;
|
||||
// BP_代发工资
|
||||
private String bpPayroll;
|
||||
// 净身企业
|
||||
private String isCleanEnt;
|
||||
// 开立基本结算账户
|
||||
private String hasSettleAcct;
|
||||
// 贸易和建筑业企业
|
||||
@TableField(exist = false)
|
||||
private String isTradeBuildEnt;
|
||||
// 省农担担保贷款
|
||||
@JsonIgnore
|
||||
private String isAgriGuar;
|
||||
// 绿色贷款
|
||||
private String isGreenLoan;
|
||||
// BP_企业客户类别
|
||||
private String bpEntType;
|
||||
// TOTAL_BP_关联度
|
||||
private String totoalBpRelevance;
|
||||
// 贷款期限
|
||||
private String loanTerm;
|
||||
// BP_贷款期限
|
||||
private String bpLoanTerm;
|
||||
// 申请金额
|
||||
private String applyAmt;
|
||||
// BP_贷款额度
|
||||
private String bpLoanAmount;
|
||||
// 抵质押类型
|
||||
private String collType;
|
||||
// 抵质押物是否三方所有
|
||||
private String collThirdParty;
|
||||
// BP_抵押物
|
||||
private String bpCollateral;
|
||||
// 灰名单客户
|
||||
private String greyCust;
|
||||
// 本金逾期
|
||||
private String prinOverdue;
|
||||
// 利息逾期
|
||||
private String interestOverdue;
|
||||
// 信用卡逾期
|
||||
private String cardOverdue;
|
||||
// BP_灰名单与逾期
|
||||
private String bpGreyOverdue;
|
||||
// TOTAL_BP_风险度
|
||||
private String totoalBpRisk;
|
||||
// 浮动BP
|
||||
private String totalBp;
|
||||
// 测算利率
|
||||
private String calculateRate;
|
||||
// 历史利率
|
||||
private String loanRateHistory;
|
||||
// 产品最低利率下限
|
||||
private String minRateProduct;
|
||||
// 平滑幅度
|
||||
private String smoothRange;
|
||||
// 最终测算利率
|
||||
private String finalCalculateRate;
|
||||
// 参考利率
|
||||
private String referenceRate;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.ruoyi.loanpricing.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 贷款定价模型输入参数对象
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
@Data
|
||||
public class ModelRetailOutputFields {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
// 客户内码
|
||||
private String custIsn;
|
||||
|
||||
// 客户类型
|
||||
private String custType;
|
||||
|
||||
// 担保方式
|
||||
private String guarType;
|
||||
|
||||
// 客户名称
|
||||
private String custName;
|
||||
|
||||
// 证件类型
|
||||
private String idType;
|
||||
|
||||
// 证件号码
|
||||
private String idNum;
|
||||
|
||||
// 基准利率
|
||||
private String baseLoanRate;
|
||||
|
||||
// 我行首贷客户
|
||||
private String isFirstLoan;
|
||||
|
||||
// 用信天数
|
||||
private String faithDay;
|
||||
|
||||
// 客户年龄
|
||||
private String custAge;
|
||||
|
||||
// BP_首贷
|
||||
private String bpFirstLoan;
|
||||
|
||||
// BP_贷龄
|
||||
private String bpAgeLoan;
|
||||
|
||||
// BP_年龄
|
||||
private String bpAge;
|
||||
|
||||
// TOTAL_BP_忠诚度
|
||||
private String totalBpLoyalty;
|
||||
|
||||
// 存款年日均
|
||||
private String balanceAvg;
|
||||
|
||||
// 贷款年日均
|
||||
private String loanAvg;
|
||||
|
||||
// 派生率
|
||||
private String derivationRate;
|
||||
|
||||
// TOTAL_BP_贡献度
|
||||
private String totalBpContribution;
|
||||
|
||||
// 中间业务_个人_信用卡
|
||||
private String midPerCard;
|
||||
|
||||
// 中间业务_个人_一码通
|
||||
private String midPerPass;
|
||||
|
||||
// 中间业务_个人_丰收互联
|
||||
private String midPerHarvest;
|
||||
|
||||
// 中间业务_个人_有效客户
|
||||
private String midPerEffect;
|
||||
|
||||
// 中间业务_个人_快捷支付
|
||||
private String midPerQuickPay;
|
||||
|
||||
// 中间业务_个人_电费代扣
|
||||
private String midPerEleDdc;
|
||||
|
||||
// 中间业务_个人_水费代扣
|
||||
private String midPerWaterDdc;
|
||||
|
||||
// 中间业务_个人_华数费代扣
|
||||
private String midPerHuashuDdc;
|
||||
|
||||
// 中间业务_个人_煤气费代扣
|
||||
private String MidPerGasDdc;
|
||||
|
||||
// 中间业务_个人_市民卡
|
||||
private String midPerCitizencard;
|
||||
|
||||
// 中间业务_个人_理财业务
|
||||
private String midPerFinMan;
|
||||
|
||||
// 中间业务_个人_etc
|
||||
private String midPerEtc;
|
||||
|
||||
// BP_中间业务
|
||||
private String bpMid;
|
||||
|
||||
// TOTAL_BP_关联度(注意原字段名拼写错误:totoalBpRelevance,已保留原拼写)
|
||||
private String totoalBpRelevance;
|
||||
|
||||
// 申请金额
|
||||
private String applyAmt;
|
||||
|
||||
// BP_贷款额度
|
||||
private String bpLoanAmount;
|
||||
|
||||
// 贷款用途
|
||||
private String loanPurpose;
|
||||
|
||||
// 是否有经营佐证
|
||||
private String bizProof;
|
||||
|
||||
// BP_贷款用途
|
||||
private String bpLoanUse;
|
||||
|
||||
// 循环功能
|
||||
private String loanLoop;
|
||||
|
||||
// BP_循环功能
|
||||
private String bpLoanLoop;
|
||||
|
||||
// 抵质押类型
|
||||
private String collType;
|
||||
|
||||
// 抵质押物是否三方所有
|
||||
private String collThirdParty;
|
||||
|
||||
// BP_抵押物
|
||||
private String bpCollateral;
|
||||
|
||||
// 灰名单客户
|
||||
private String greyCust;
|
||||
|
||||
// 本金逾期
|
||||
private String prinOverdue;
|
||||
|
||||
// 利息逾期
|
||||
private String interestOverdue;
|
||||
|
||||
// 信用卡逾期
|
||||
private String cardOverdue;
|
||||
|
||||
// BP_灰名单与逾期
|
||||
private String bpGreyOverdue;
|
||||
|
||||
// TOTAL_BP_风险度(注意原字段名拼写错误:totoalBpRisk,已保留原拼写)
|
||||
private String totoalBpRisk;
|
||||
|
||||
// 浮动BP
|
||||
private String totalBp;
|
||||
|
||||
// 测算利率
|
||||
private String calculateRate;
|
||||
|
||||
// 历史利率
|
||||
private String loanRateHistory;
|
||||
|
||||
// 产品最低利率下限
|
||||
private String minRateProduct;
|
||||
|
||||
// 平滑幅度
|
||||
private String smoothRange;
|
||||
|
||||
// 最终测算利率
|
||||
private String finalCalculateRate;
|
||||
|
||||
// 参考利率
|
||||
private String referenceRate;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class LoanPricingWorkflowListVO
|
||||
{
|
||||
private String serialNum;
|
||||
|
||||
private String custName;
|
||||
|
||||
private String custType;
|
||||
|
||||
private String guarType;
|
||||
|
||||
private String applyAmt;
|
||||
|
||||
private String calculateRate;
|
||||
|
||||
private String executeRate;
|
||||
|
||||
private Date createTime;
|
||||
|
||||
private String createBy;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Author: wkc
|
||||
* @CreateTime: 2026-01-21
|
||||
*/
|
||||
@Data
|
||||
public class LoanPricingWorkflowVO {
|
||||
|
||||
private LoanPricingWorkflow loanPricingWorkflow;
|
||||
|
||||
private ModelRetailOutputFields modelRetailOutputFields;
|
||||
|
||||
private ModelCorpOutputFields modelCorpOutputFields;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.loanpricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 利率定价流程Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
public interface LoanPricingWorkflowMapper extends BaseMapper<LoanPricingWorkflow>
|
||||
{
|
||||
IPage<LoanPricingWorkflowListVO> selectWorkflowPageWithRates(Page<?> page,
|
||||
@Param("query") LoanPricingWorkflow query);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.loanpricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
|
||||
/**
|
||||
* 对公贷款定价模型输出字段Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
public interface ModelCorpOutputFieldsMapper extends BaseMapper<ModelCorpOutputFields>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.loanpricing.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
|
||||
/**
|
||||
* 零售贷款定价模型输出字段Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-21
|
||||
*/
|
||||
public interface ModelRetailOutputFieldsMapper extends BaseMapper<ModelRetailOutputFields>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 利率定价流程Service接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
public interface ILoanPricingWorkflowService
|
||||
{
|
||||
/**
|
||||
* 发起个人客户利率定价流程
|
||||
*
|
||||
* @param dto 个人客户发起DTO
|
||||
* @return 结果
|
||||
*/
|
||||
public LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto);
|
||||
|
||||
/**
|
||||
* 发起企业客户利率定价流程
|
||||
*
|
||||
* @param dto 企业客户发起DTO
|
||||
* @return 结果
|
||||
*/
|
||||
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto);
|
||||
|
||||
/**
|
||||
* 查询利率定价流程列表
|
||||
*
|
||||
* @param loanPricingWorkflow 利率定价流程信息
|
||||
* @return 利率定价流程集合
|
||||
*/
|
||||
public List<LoanPricingWorkflow> selectLoanPricingList(LoanPricingWorkflow loanPricingWorkflow);
|
||||
|
||||
/**
|
||||
* 分页查询利率定价流程列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param loanPricingWorkflow 利率定价流程信息
|
||||
* @return 分页结果
|
||||
*/
|
||||
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow);
|
||||
|
||||
/**
|
||||
* 查询利率定价流程详情
|
||||
*
|
||||
* @param serialNum 业务方流水号
|
||||
* @return 利率定价流程
|
||||
*/
|
||||
public LoanPricingWorkflowVO selectLoanPricingBySerialNum(String serialNum);
|
||||
|
||||
/**
|
||||
* 设定执行利率
|
||||
*
|
||||
* @param serialNum 业务方流水号
|
||||
* @param executeRate 执行利率
|
||||
* @return 是否成功
|
||||
*/
|
||||
public boolean setExecuteRate(String serialNum, String executeRate);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* @Author: wkc
|
||||
* @CreateTime: 2026-01-21
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@EnableAsync
|
||||
public class LoanPricingModelService {
|
||||
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
|
||||
@Resource
|
||||
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
|
||||
|
||||
@Resource
|
||||
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
|
||||
|
||||
@Resource
|
||||
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
|
||||
|
||||
@Resource
|
||||
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
|
||||
|
||||
public void invokeModelAsync(Long workflowId) {
|
||||
LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectById(workflowId);
|
||||
if (Objects.isNull(loanPricingWorkflow)){
|
||||
log.error("未找到对应的流程信息,未调用模型服务");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName()));
|
||||
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum()));
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
log.error("贷款定价模型调用前敏感字段解密失败", ex);
|
||||
throw ex;
|
||||
}
|
||||
ModelInvokeDTO modelInvokeDTO = new ModelInvokeDTO();
|
||||
BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO);
|
||||
modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(loanPricingWorkflow.getIsTradeConstruction()));
|
||||
if ("个人".equals(loanPricingWorkflow.getCustType()))
|
||||
{
|
||||
normalizePersonalModelInvokeDTO(modelInvokeDTO);
|
||||
}
|
||||
if ("企业".equals(loanPricingWorkflow.getCustType()))
|
||||
{
|
||||
normalizeCorporateModelInvokeDTO(modelInvokeDTO);
|
||||
}
|
||||
JSONObject response = modelService.invokeModel(modelInvokeDTO);
|
||||
if (loanPricingWorkflow.getCustType().equals("个人")){
|
||||
// 个人模型
|
||||
ModelRetailOutputFields modelRetailOutputFields = JSON.parseObject(response.toJSONString(), ModelRetailOutputFields.class);
|
||||
modelRetailOutputFieldsMapper.insert(modelRetailOutputFields);
|
||||
log.info("个人模型调用成功");
|
||||
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
|
||||
workflowToUpdate.setId(loanPricingWorkflow.getId());
|
||||
workflowToUpdate.setModelOutputId(modelRetailOutputFields.getId());
|
||||
loanPricingWorkflowMapper.updateById(workflowToUpdate);
|
||||
log.info("更新流程信息成功");
|
||||
}else if (loanPricingWorkflow.getCustType().equals("企业")){
|
||||
// 企业模型
|
||||
ModelCorpOutputFields modelCorpOutputFields = JSON.parseObject(response.toJSONString(), ModelCorpOutputFields.class);
|
||||
modelCorpOutputFieldsMapper.insert(modelCorpOutputFields);
|
||||
log.info("企业模型调用成功");
|
||||
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
|
||||
workflowToUpdate.setId(loanPricingWorkflow.getId());
|
||||
workflowToUpdate.setModelOutputId(modelCorpOutputFields.getId());
|
||||
loanPricingWorkflowMapper.updateById(workflowToUpdate);
|
||||
log.info("更新流程信息成功");
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof()));
|
||||
modelInvokeDTO.setLoanLoop(toZeroOne(modelInvokeDTO.getLoanLoop()));
|
||||
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
|
||||
}
|
||||
|
||||
private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
modelInvokeDTO.setIsGreenLoan(toZeroOne(modelInvokeDTO.getIsGreenLoan()));
|
||||
modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(modelInvokeDTO.getIsTradeBuildEnt()));
|
||||
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
|
||||
}
|
||||
|
||||
private String toZeroOne(String value)
|
||||
{
|
||||
if ("true".equals(value) || "1".equals(value))
|
||||
{
|
||||
return "1";
|
||||
}
|
||||
if ("false".equals(value) || "0".equals(value))
|
||||
{
|
||||
return "0";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Service
|
||||
public class LoanPricingSensitiveDisplayService
|
||||
{
|
||||
public String maskCustName(String custName)
|
||||
{
|
||||
if (!StringUtils.hasText(custName))
|
||||
{
|
||||
return custName;
|
||||
}
|
||||
if (custName.contains("公司") && custName.length() > 4)
|
||||
{
|
||||
return custName.substring(0, 2) + repeatMask(custName.length() - 4) + custName.substring(custName.length() - 2);
|
||||
}
|
||||
if (custName.length() == 1)
|
||||
{
|
||||
return custName;
|
||||
}
|
||||
return custName.substring(0, 1) + repeatMask(custName.length() - 1);
|
||||
}
|
||||
|
||||
public String maskIdNum(String idNum)
|
||||
{
|
||||
if (!StringUtils.hasText(idNum))
|
||||
{
|
||||
return idNum;
|
||||
}
|
||||
if (idNum.startsWith("91") && idNum.length() == 18)
|
||||
{
|
||||
return idNum.substring(0, 2) + repeatMask(13) + idNum.substring(idNum.length() - 3);
|
||||
}
|
||||
if (idNum.matches("\\d{17}[\\dXx]"))
|
||||
{
|
||||
return idNum.substring(0, 4) + repeatMask(8) + idNum.substring(idNum.length() - 4);
|
||||
}
|
||||
if (idNum.length() > 5)
|
||||
{
|
||||
return idNum.substring(0, 2) + repeatMask(idNum.length() - 5) + idNum.substring(idNum.length() - 3);
|
||||
}
|
||||
return repeatMask(idNum.length());
|
||||
}
|
||||
|
||||
private String repeatMask(int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
builder.append('*');
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.TypeReference;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.http.HttpUtils;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @Author 吴凯程
|
||||
* @Date 2025/12/11
|
||||
**/
|
||||
@Service
|
||||
@Slf4j
|
||||
@EnableAsync
|
||||
public class ModelService {
|
||||
|
||||
@Value("${model.url}")
|
||||
private String modelUrl;
|
||||
|
||||
|
||||
|
||||
public JSONObject invokeModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
Map<String, String> requestBody = entityToMap(modelInvokeDTO);
|
||||
JSONObject response = HttpUtils.doPostFormUrlEncoded(modelUrl, requestBody, null, JSONObject.class);
|
||||
log.info("------------------->调用模型返回结果:" + JSON.toJSONString(response));
|
||||
if(Objects.nonNull(response) && response.containsKey("code") && response.getInteger("code") == 10000){
|
||||
JSONObject mappingOutputFields = response.getJSONObject("data").getJSONObject("mappingOutputFields");
|
||||
// return JSON.parseObject(mappingOutputFields.toJSONString(), ModelOutputFields.class);
|
||||
return mappingOutputFields;
|
||||
}else{
|
||||
log.error("------------------->调用模型失败,失败原因为:" + response.getString("message"));
|
||||
throw new ServiceException("调用模型失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用FastJSON将实体类转换为Map<String, String>
|
||||
* @param obj 待转换的实体类对象
|
||||
* @return 转换后的Map
|
||||
*/
|
||||
public static Map<String, String> entityToMap(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
// 先转为JSON字符串,再转换为指定类型的Map
|
||||
String jsonStr = JSON.toJSONString(obj);
|
||||
return JSON.parseObject(jsonStr, new TypeReference<Map<String, String>>() {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
@Service
|
||||
public class SensitiveFieldCryptoService
|
||||
{
|
||||
private final String key;
|
||||
|
||||
public SensitiveFieldCryptoService(@Value("${security.password-transfer.key:}") String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String encrypt(String plainText)
|
||||
{
|
||||
validateKey();
|
||||
if (!StringUtils.hasText(plainText))
|
||||
{
|
||||
return plainText;
|
||||
}
|
||||
try
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"));
|
||||
return Base64.getEncoder().encodeToString(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ServiceException("贷款定价敏感字段加密失败");
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(String cipherText)
|
||||
{
|
||||
validateKey();
|
||||
if (!StringUtils.hasText(cipherText))
|
||||
{
|
||||
return cipherText;
|
||||
}
|
||||
try
|
||||
{
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"));
|
||||
return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)), StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ServiceException("贷款定价敏感字段解密失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateKey()
|
||||
{
|
||||
if (!StringUtils.hasText(key))
|
||||
{
|
||||
throw new IllegalStateException("security.password-transfer.key 未配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.ruoyi.loanpricing.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
|
||||
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
|
||||
import com.ruoyi.loanpricing.service.LoanPricingSensitiveDisplayService;
|
||||
import com.ruoyi.loanpricing.service.LoanPricingModelService;
|
||||
import com.ruoyi.loanpricing.service.SensitiveFieldCryptoService;
|
||||
import com.ruoyi.loanpricing.util.LoanPricingConverter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 利率定价流程Service业务层处理
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
@Service
|
||||
public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowService
|
||||
{
|
||||
@Resource
|
||||
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
|
||||
|
||||
@Resource
|
||||
private LoanPricingModelService loanPricingModelService;
|
||||
|
||||
@Resource
|
||||
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
|
||||
|
||||
@Resource
|
||||
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
|
||||
|
||||
@Resource
|
||||
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
|
||||
|
||||
@Resource
|
||||
private LoanPricingSensitiveDisplayService loanPricingSensitiveDisplayService;
|
||||
|
||||
|
||||
/**
|
||||
* 发起利率定价流程
|
||||
*
|
||||
* @param loanPricingWorkflow 利率定价流程信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
// 自动生成业务方流水号(时间戳)
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
|
||||
String serialNum = sdf.format(new Date());
|
||||
loanPricingWorkflow.setSerialNum(serialNum);
|
||||
|
||||
// 设置默认值
|
||||
if (!StringUtils.hasText(loanPricingWorkflow.getOrgCode()))
|
||||
{
|
||||
loanPricingWorkflow.setOrgCode("892000");
|
||||
}
|
||||
if (!StringUtils.hasText(loanPricingWorkflow.getRunType()))
|
||||
{
|
||||
loanPricingWorkflow.setRunType("1");
|
||||
}
|
||||
loanPricingWorkflow.setCustType("企业".equals(loanPricingWorkflow.getCustType()) ? "企业" : loanPricingWorkflow.getCustType());
|
||||
loanPricingWorkflow.setIsTradeBuildEnt(loanPricingWorkflow.getIsTradeConstruction());
|
||||
|
||||
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
|
||||
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
|
||||
loanPricingWorkflowMapper.insert(loanPricingWorkflow);
|
||||
loanPricingModelService.invokeModelAsync(loanPricingWorkflow.getId());
|
||||
|
||||
return loanPricingWorkflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起个人客户利率定价流程
|
||||
*
|
||||
* @param dto 个人客户发起DTO
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto) {
|
||||
LoanPricingWorkflow entity = LoanPricingConverter.toEntity(dto);
|
||||
return createLoanPricing(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起企业客户利率定价流程
|
||||
*
|
||||
* @param dto 企业客户发起DTO
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto) {
|
||||
LoanPricingWorkflow entity = LoanPricingConverter.toEntity(dto);
|
||||
return createLoanPricing(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询利率定价流程列表
|
||||
*
|
||||
* @param loanPricingWorkflow 利率定价流程信息
|
||||
* @return 利率定价流程
|
||||
*/
|
||||
@Override
|
||||
public List<LoanPricingWorkflow> selectLoanPricingList(LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = buildQueryWrapper(loanPricingWorkflow);
|
||||
// 按更新时间倒序
|
||||
wrapper.orderByDesc(LoanPricingWorkflow::getUpdateTime);
|
||||
return loanPricingWorkflowMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询利率定价流程列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param loanPricingWorkflow 利率定价流程信息
|
||||
* @return 利率定价流程
|
||||
*/
|
||||
@Override
|
||||
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, loanPricingWorkflow);
|
||||
pageResult.getRecords().forEach(row -> row.setCustName(
|
||||
loanPricingSensitiveDisplayService.maskCustName(
|
||||
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询利率定价流程详情
|
||||
*
|
||||
* @param serialNum 业务方流水号
|
||||
* @return 利率定价流程
|
||||
*/
|
||||
@Override
|
||||
public LoanPricingWorkflowVO selectLoanPricingBySerialNum(String serialNum)
|
||||
{
|
||||
LoanPricingWorkflowVO loanPricingWorkflowVO = new LoanPricingWorkflowVO();
|
||||
|
||||
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum);
|
||||
LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectOne(wrapper);
|
||||
String plainCustName = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName());
|
||||
String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum());
|
||||
loanPricingWorkflow.setCustName(loanPricingSensitiveDisplayService.maskCustName(plainCustName));
|
||||
loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum));
|
||||
loanPricingWorkflow.setIsTradeBuildEnt(loanPricingWorkflow.getIsTradeConstruction());
|
||||
loanPricingWorkflowVO.setLoanPricingWorkflow(loanPricingWorkflow);
|
||||
|
||||
if (Objects.nonNull(loanPricingWorkflow.getModelOutputId())){
|
||||
if (loanPricingWorkflow.getCustType().equals("个人")){
|
||||
ModelRetailOutputFields modelRetailOutputFields = modelRetailOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
|
||||
if (Objects.nonNull(modelRetailOutputFields))
|
||||
{
|
||||
maskModelRetailOutputBasicInfo(modelRetailOutputFields);
|
||||
loanPricingWorkflow.setLoanRate(modelRetailOutputFields.getFinalCalculateRate());
|
||||
}
|
||||
loanPricingWorkflowVO.setModelRetailOutputFields(modelRetailOutputFields);
|
||||
}
|
||||
if (loanPricingWorkflow.getCustType().equals("企业")){
|
||||
ModelCorpOutputFields modelCorpOutputFields = modelCorpOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
|
||||
if (Objects.nonNull(modelCorpOutputFields))
|
||||
{
|
||||
maskModelCorpOutputBasicInfo(modelCorpOutputFields);
|
||||
modelCorpOutputFields.setRepayMethod(loanPricingWorkflow.getRepayMethod());
|
||||
modelCorpOutputFields.setIsTradeBuildEnt(loanPricingWorkflow.getIsTradeBuildEnt());
|
||||
loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getCalculateRate());
|
||||
}
|
||||
loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return loanPricingWorkflowVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*
|
||||
* @param loanPricingWorkflow 利率定价流程信息
|
||||
* @return LambdaQueryWrapper
|
||||
*/
|
||||
private LambdaQueryWrapper<LoanPricingWorkflow> buildQueryWrapper(LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 按创建者筛选
|
||||
if (StringUtils.hasText(loanPricingWorkflow.getCreateBy()))
|
||||
{
|
||||
wrapper.like(LoanPricingWorkflow::getCreateBy, loanPricingWorkflow.getCreateBy());
|
||||
}
|
||||
|
||||
// 按客户内码模糊查询
|
||||
if (StringUtils.hasText(loanPricingWorkflow.getCustIsn()))
|
||||
{
|
||||
wrapper.like(LoanPricingWorkflow::getCustIsn, loanPricingWorkflow.getCustIsn());
|
||||
}
|
||||
|
||||
// 按机构号筛选
|
||||
if (StringUtils.hasText(loanPricingWorkflow.getOrgCode()))
|
||||
{
|
||||
wrapper.like(LoanPricingWorkflow::getOrgCode, loanPricingWorkflow.getOrgCode());
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void maskModelRetailOutputBasicInfo(ModelRetailOutputFields modelRetailOutputFields)
|
||||
{
|
||||
modelRetailOutputFields.setCustName(
|
||||
loanPricingSensitiveDisplayService.maskCustName(modelRetailOutputFields.getCustName()));
|
||||
modelRetailOutputFields.setIdNum(
|
||||
loanPricingSensitiveDisplayService.maskIdNum(modelRetailOutputFields.getIdNum()));
|
||||
}
|
||||
|
||||
private void maskModelCorpOutputBasicInfo(ModelCorpOutputFields modelCorpOutputFields)
|
||||
{
|
||||
modelCorpOutputFields.setCustName(
|
||||
loanPricingSensitiveDisplayService.maskCustName(modelCorpOutputFields.getCustName()));
|
||||
modelCorpOutputFields.setIdNum(
|
||||
loanPricingSensitiveDisplayService.maskIdNum(modelCorpOutputFields.getIdNum()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设定执行利率
|
||||
*
|
||||
* @param serialNum 业务方流水号
|
||||
* @param executeRate 执行利率
|
||||
* @return 是否成功
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean setExecuteRate(String serialNum, String executeRate) {
|
||||
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum);
|
||||
LoanPricingWorkflow workflow = loanPricingWorkflowMapper.selectOne(wrapper);
|
||||
|
||||
if (workflow == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
workflow.setExecuteRate(executeRate);
|
||||
int result = loanPricingWorkflowMapper.updateById(workflow);
|
||||
return result > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.loanpricing.util;
|
||||
|
||||
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
|
||||
/**
|
||||
* 利率定价转换器
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-19
|
||||
*/
|
||||
public class LoanPricingConverter {
|
||||
|
||||
/**
|
||||
* 个人客户DTO转Entity
|
||||
*
|
||||
* @param dto 个人客户发起DTO
|
||||
* @return 利率定价流程实体
|
||||
*/
|
||||
public static LoanPricingWorkflow toEntity(PersonalLoanPricingCreateDTO dto) {
|
||||
LoanPricingWorkflow entity = new LoanPricingWorkflow();
|
||||
// 映射共同字段
|
||||
entity.setCustIsn(dto.getCustIsn());
|
||||
entity.setCustType("个人");
|
||||
entity.setCustName(dto.getCustName());
|
||||
entity.setIdType(dto.getIdType());
|
||||
entity.setIdNum(dto.getIdNum());
|
||||
entity.setGuarType(dto.getGuarType());
|
||||
entity.setApplyAmt(dto.getApplyAmt());
|
||||
entity.setLoanPurpose(dto.getLoanPurpose());
|
||||
entity.setLoanTerm(dto.getLoanTerm());
|
||||
entity.setCollType(dto.getCollType());
|
||||
entity.setCollThirdParty(dto.getCollThirdParty());
|
||||
// 映射个人特有字段
|
||||
entity.setBizProof(dto.getBizProof());
|
||||
entity.setLoanLoop(dto.getLoanLoop());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业客户DTO转Entity
|
||||
*
|
||||
* @param dto 企业客户发起DTO
|
||||
* @return 利率定价流程实体
|
||||
*/
|
||||
public static LoanPricingWorkflow toEntity(CorporateLoanPricingCreateDTO dto) {
|
||||
LoanPricingWorkflow entity = new LoanPricingWorkflow();
|
||||
// 映射共同字段
|
||||
entity.setCustIsn(dto.getCustIsn());
|
||||
entity.setCustType("企业");
|
||||
entity.setCustName(dto.getCustName());
|
||||
entity.setIdType(dto.getIdType());
|
||||
entity.setIdNum(dto.getIdNum());
|
||||
entity.setRepayMethod(dto.getRepayMethod());
|
||||
entity.setGuarType(dto.getGuarType());
|
||||
entity.setApplyAmt(dto.getApplyAmt());
|
||||
entity.setCollType(dto.getCollType());
|
||||
entity.setCollThirdParty(dto.getCollThirdParty());
|
||||
// 映射企业特有字段
|
||||
entity.setIsGreenLoan(dto.getIsGreenLoan());
|
||||
entity.setIsTradeConstruction(dto.getIsTradeBuildEnt());
|
||||
entity.setIsTradeBuildEnt(dto.getIsTradeBuildEnt());
|
||||
entity.setLoanTerm(dto.getLoanTerm());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user