Compare commits
No commits in common. "480c052ff1944b45ddc40e7294061ecf3f6a6eb8" and "2765f3f26563895e48d99045ade37cc83034bcc4" have entirely different histories.
480c052ff1
...
2765f3f265
@ -1,50 +0,0 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# Node modules
|
||||
**/node_modules
|
||||
|
||||
# Build output
|
||||
**/dist
|
||||
**/target
|
||||
|
||||
# Logs
|
||||
**/logs
|
||||
*.log
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
doc/
|
||||
|
||||
# Docker
|
||||
docker/
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
|
||||
# Test files
|
||||
**/test
|
||||
**/__tests__
|
||||
**/*.test.ts
|
||||
**/*.test.js
|
||||
**/*.spec.ts
|
||||
**/*.spec.js
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Qoder
|
||||
.qoder/
|
||||
266
deploy.sh
266
deploy.sh
@ -1,266 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 资金服务平台 - Docker 部署脚本
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 打印带颜色的消息
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
show_help() {
|
||||
echo "资金服务平台 Docker 部署脚本"
|
||||
echo ""
|
||||
echo "用法: $0 <命令> [选项]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " start 启动所有服务"
|
||||
echo " stop 停止所有服务"
|
||||
echo " restart 重启所有服务"
|
||||
echo " build 构建所有镜像"
|
||||
echo " rebuild 重新构建所有镜像(不使用缓存)"
|
||||
echo " logs 查看服务日志"
|
||||
echo " status 查看服务状态"
|
||||
echo " clean 清理未使用的镜像和容器"
|
||||
echo " init 初始化环境(首次部署)"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " --service <name> 指定服务名称"
|
||||
echo " --no-cache 构建时不使用缓存"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 start # 启动所有服务"
|
||||
echo " $0 build --no-cache # 重新构建所有镜像"
|
||||
echo " $0 logs --service fund-sys # 查看系统服务日志"
|
||||
}
|
||||
|
||||
# 检查 Docker 环境
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
log_error "Docker 未运行,请先启动 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
log_error "Docker Compose 未安装"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Docker 环境检查通过"
|
||||
}
|
||||
|
||||
# 使用 docker-compose 命令(兼容 v1 和 v2)
|
||||
compose_cmd() {
|
||||
if docker compose version &> /dev/null; then
|
||||
docker compose "$@"
|
||||
else
|
||||
docker-compose "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# 初始化环境
|
||||
init_env() {
|
||||
log_info "初始化部署环境..."
|
||||
|
||||
# 复制环境配置
|
||||
if [ ! -f .env ]; then
|
||||
cp docker/.env .env
|
||||
log_info "已创建 .env 文件"
|
||||
else
|
||||
log_warn ".env 文件已存在,跳过"
|
||||
fi
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p docker/grafana/dashboards
|
||||
mkdir -p docker/grafana/provisioning/datasources
|
||||
mkdir -p docker/grafana/provisioning/dashboards
|
||||
mkdir -p docker/prometheus/rules
|
||||
|
||||
# 设置权限
|
||||
chmod +x deploy.sh 2>/dev/null || true
|
||||
|
||||
log_info "环境初始化完成"
|
||||
}
|
||||
|
||||
# 构建镜像
|
||||
build_images() {
|
||||
local no_cache=""
|
||||
local service=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--no-cache)
|
||||
no_cache="--no-cache"
|
||||
shift
|
||||
;;
|
||||
--service)
|
||||
service="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log_info "开始构建 Docker 镜像..."
|
||||
|
||||
if [ -n "$service" ]; then
|
||||
compose_cmd build $no_cache "$service"
|
||||
else
|
||||
compose_cmd build $no_cache
|
||||
fi
|
||||
|
||||
log_info "镜像构建完成"
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start_services() {
|
||||
log_info "启动服务..."
|
||||
compose_cmd up -d
|
||||
|
||||
log_info "等待服务启动..."
|
||||
sleep 10
|
||||
|
||||
log_info "服务状态:"
|
||||
compose_cmd ps
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
stop_services() {
|
||||
log_info "停止服务..."
|
||||
compose_cmd down
|
||||
log_info "服务已停止"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart_services() {
|
||||
stop_services
|
||||
sleep 3
|
||||
start_services
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
view_logs() {
|
||||
local service=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--service)
|
||||
service="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$service" ]; then
|
||||
compose_cmd logs -f "$service"
|
||||
else
|
||||
compose_cmd logs -f
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
view_status() {
|
||||
log_info "服务状态:"
|
||||
compose_cmd ps
|
||||
|
||||
echo ""
|
||||
log_info "健康检查:"
|
||||
compose_cmd ps --format "table {{.Name}}\t{{.Status}}"
|
||||
}
|
||||
|
||||
# 清理
|
||||
clean_up() {
|
||||
log_info "清理未使用的资源..."
|
||||
|
||||
# 停止并删除所有容器
|
||||
compose_cmd down -v --remove-orphans
|
||||
|
||||
# 删除悬空镜像
|
||||
docker image prune -f
|
||||
|
||||
# 删除未使用的网络
|
||||
docker network prune -f
|
||||
|
||||
log_info "清理完成"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_docker
|
||||
|
||||
case $1 in
|
||||
start)
|
||||
shift
|
||||
start_services "$@"
|
||||
;;
|
||||
stop)
|
||||
shift
|
||||
stop_services "$@"
|
||||
;;
|
||||
restart)
|
||||
shift
|
||||
restart_services "$@"
|
||||
;;
|
||||
build)
|
||||
shift
|
||||
build_images "$@"
|
||||
;;
|
||||
rebuild)
|
||||
shift
|
||||
build_images --no-cache "$@"
|
||||
;;
|
||||
logs)
|
||||
shift
|
||||
view_logs "$@"
|
||||
;;
|
||||
status)
|
||||
view_status
|
||||
;;
|
||||
clean)
|
||||
clean_up
|
||||
;;
|
||||
init)
|
||||
init_env
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "未知命令: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -219,7 +219,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 4.10 移动端模块 (H5)
|
||||
### 4.10 移动端模块 (UniApp)
|
||||
|
||||
#### 4.10.1 移动端首页
|
||||
- 数据概览(今日收支、待收款)
|
||||
@ -254,7 +254,7 @@
|
||||
| 端 | 技术栈 | 说明 |
|
||||
| ------------ | --------------------------------- | ----------------------------------- |
|
||||
| **管理后台** | Vue 3 + TypeScript + Element Plus | 响应式设计,组件丰富 |
|
||||
| **移动端** | Vue 3 + Vite 5 + Vant 4 | 移动端H5响应式应用 |
|
||||
| **移动端** | UniApp + Vue 3 + uView UI | 一套代码多端发布(H5、小程序、App) |
|
||||
| **图表库** | ECharts 5.x | 数据可视化、报表展示 |
|
||||
| **构建工具** | Vite 4.x | 快速构建、热更新 |
|
||||
|
||||
|
||||
@ -25,8 +25,7 @@
|
||||
|
||||
采用 **微服务架构** + **前后端分离** + **多租户架构** 模式:
|
||||
- 后端:Spring Cloud Alibaba 微服务框架
|
||||
- 前端:Vue 3 + Element Plus 管理后台
|
||||
- 移动端:Vue 3 + Vite 5 + Vant 4 移动端应用
|
||||
- 前端:Vue 3 + UniApp 多端应用
|
||||
- 数据层:MySQL + Redis 缓存(支持多租户隔离)
|
||||
- 基础设施:Nacos 服务治理、Nginx 负载均衡
|
||||
- 可观测性:Head 日志追踪 + 全链路监控
|
||||
@ -2449,14 +2448,14 @@ AOP日志以JSON格式输出,包含以下字段:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 接入层 (Access Layer) │
|
||||
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Web管理端 │ │ 移动端H5 │ │
|
||||
│ │ (Vue3 + Element Plus) │ │ (Vue3 + Vite5 + Vant) │ │
|
||||
│ └──────────────┬──────────────────┘ └──────────────┬──────────────────┘ │
|
||||
└─────────────────┼─────────────────────────────────────┼─────────────────────┘
|
||||
│ │
|
||||
└─────────────────────┬───────────────┘
|
||||
│
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Web端 │ │ 移动端 │ │ 小程序 │ │ H5页面 │ │
|
||||
│ │ (Vue3) │ │ (UniApp) │ │ (UniApp) │ │ (UniApp) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
└─────────┼────────────────┼────────────────┼────────────────┼───────────────┘
|
||||
│ │ │ │
|
||||
└────────────────┴────────────────┴────────────────┘
|
||||
│
|
||||
┌────────────────────────────────────┼────────────────────────────────────────┐
|
||||
│ 网关层 (Gateway Layer) │
|
||||
│ ┌─────────────────────────────────┴─────────────────────────────────────┐ │
|
||||
@ -2858,13 +2857,9 @@ public class ProjectService {
|
||||
| | Axios | 1.x | HTTP客户端 |
|
||||
| | ECharts | 5.x | 图表库 |
|
||||
| | Vite | 5.x | 构建工具 |
|
||||
| **移动端** | Vue | 3.4.x | 前端框架 |
|
||||
| | Vite | 5.x | 构建工具 |
|
||||
| | TypeScript | 5.x | 类型安全 |
|
||||
| | Vant | 4.x | 移动端UI库 |
|
||||
| | Pinia | 2.x | 状态管理 |
|
||||
| | Vue Router | 4.x | 路由管理 |
|
||||
| | Axios | 1.x | HTTP客户端 |
|
||||
| **移动端** | UniApp | 3.x | 跨端框架 |
|
||||
| | Vue | 3.x | 前端框架 |
|
||||
| | uView UI | 2.x | 移动端UI库 |
|
||||
|
||||
### 3.2 架构分层
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
| **应收账款管理** | 对公司项目的应收账款进行跟踪、确认、收款记录和账期管理 |
|
||||
| **项目支出管理** | 对公司项目支出进行申请、审批、执行和核销的全流程管理 |
|
||||
| **数据可视化** | 提供多维度的财务报表和统计分析,辅助决策 |
|
||||
| **移动办公** | 支持管理后台和移动端H5双端访问,提升办公效率 |
|
||||
| **移动办公** | 支持管理后台和手机端(UniApp)双端访问,提升办公效率 |
|
||||
|
||||
---
|
||||
|
||||
@ -322,7 +322,7 @@ flowchart TD
|
||||
|
||||
---
|
||||
|
||||
### 3.8 移动端模块 (H5)
|
||||
### 3.8 移动端模块 (UniApp)
|
||||
|
||||
#### 3.8.1 移动端首页
|
||||
- **数据概览**
|
||||
@ -663,7 +663,7 @@ flowchart TD
|
||||
| 端 | 技术栈 | 说明 |
|
||||
| ------------ | --------------------------------- | ----------------------------------- |
|
||||
| **管理后台** | Vue 3 + TypeScript + Element Plus | 响应式设计,组件丰富 |
|
||||
| **移动端** | Vue 3 + Vite 5 + Vant 4 | 移动端H5响应式应用 |
|
||||
| **移动端** | UniApp + Vue 3 + uView UI | 一套代码多端发布(H5、小程序、App) |
|
||||
| **图表库** | ECharts 5.x | 数据可视化、报表展示 |
|
||||
| **构建工具** | Vite 4.x | 快速构建、热更新 |
|
||||
|
||||
@ -725,12 +725,12 @@ flowchart TD
|
||||
**新增功能:**
|
||||
- 收款管理功能(收款记录、收款凭证)
|
||||
- 账期管理与逾期提醒
|
||||
- 移动端开发(Vue3 + Vant)
|
||||
- 移动端开发(UniApp)
|
||||
- 提醒预警机制
|
||||
|
||||
**交付物:**
|
||||
- 完整的收款管理
|
||||
- 移动端H5应用
|
||||
- 移动端应用(H5/小程序)
|
||||
- 消息提醒功能
|
||||
|
||||
---
|
||||
|
||||
@ -184,11 +184,9 @@ services:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
JAVA_OPTS: -Xms256m -Xmx512m
|
||||
# 租户 ID - 共享实例(空值,所有租户可用)
|
||||
# TENANT_ID: ""
|
||||
|
||||
# 租户 ID - 多租户专属实例(逗号分隔,服务多个租户)
|
||||
TENANT_ID: "VIP_001,VIP_002,VIP_003"
|
||||
# 租户元数据 - 共享实例(无特定租户组,所有租户可用)
|
||||
TENANT_ID: "1"
|
||||
TENANT_GROUP: ""
|
||||
ports:
|
||||
- "8100:8100"
|
||||
depends_on:
|
||||
@ -230,8 +228,9 @@ services:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
JAVA_OPTS: -Xms256m -Xmx512m
|
||||
# 租户 ID - 单租户专属实例
|
||||
TENANT_ID: "VIP_004"
|
||||
# 租户元数据 - VIP_001 专属实例
|
||||
TENANT_ID: "1001"
|
||||
TENANT_GROUP: "TENANT_VIP_001"
|
||||
ports:
|
||||
- "8101:8101"
|
||||
depends_on:
|
||||
@ -273,8 +272,9 @@ services:
|
||||
# REDIS_HOST: redis
|
||||
# REDIS_PORT: 6379
|
||||
# JAVA_OPTS: -Xms256m -Xmx512m
|
||||
# # 租户 ID - VIP_002 专属实例
|
||||
# TENANT_ID: "VIP_002"
|
||||
# # 租户元数据 - VIP_002 专属实例
|
||||
# TENANT_ID: "1002"
|
||||
# TENANT_GROUP: "TENANT_VIP_002"
|
||||
# ports:
|
||||
# - "8102:8102"
|
||||
# depends_on:
|
||||
@ -510,54 +510,6 @@ services:
|
||||
networks:
|
||||
- fund-network
|
||||
|
||||
# ==================== 前端服务 ====================
|
||||
|
||||
# 管理后台前端
|
||||
fund-admin:
|
||||
build:
|
||||
context: ./fund-admin
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VITE_API_BASE_URL: http://localhost:8000
|
||||
container_name: fund-admin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- fund-network
|
||||
|
||||
# 移动端H5
|
||||
fund-mobile:
|
||||
build:
|
||||
context: ./fund-mobile
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VITE_API_BASE_URL: http://localhost:8000
|
||||
container_name: fund-mobile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "81:80"
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- fund-network
|
||||
|
||||
# ==================== 网络配置 ====================
|
||||
networks:
|
||||
fund-network:
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
dist
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test
|
||||
**/*.test.ts
|
||||
**/*.spec.ts
|
||||
@ -1,61 +0,0 @@
|
||||
# 资金服务平台管理后台 - Dockerfile
|
||||
# 多阶段构建:Node构建 + Nginx运行
|
||||
|
||||
# ==================== 构建阶段 ====================
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 设置npm镜像(加速下载)
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 复制package文件(利用缓存)
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建参数:API网关地址
|
||||
ARG VITE_API_BASE_URL=http://localhost:8000
|
||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||
|
||||
# 构建生产版本
|
||||
RUN npm run build
|
||||
|
||||
# ==================== 运行阶段 ====================
|
||||
FROM nginx:alpine
|
||||
|
||||
# 安装必要工具
|
||||
RUN apk add --no-cache curl tzdata && \
|
||||
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone && \
|
||||
apk del tzdata
|
||||
|
||||
# 删除默认配置
|
||||
RUN rm -rf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制nginx配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 创建非root用户
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
||||
chown -R nginx:nginx /var/cache/nginx && \
|
||||
chown -R nginx:nginx /var/log/nginx && \
|
||||
touch /var/run/nginx.pid && \
|
||||
chown -R nginx:nginx /var/run/nginx.pid
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost/ || exit 1
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@ -1,54 +0,0 @@
|
||||
# 资金服务平台管理后台 Nginx 配置
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
|
||||
gzip_vary on;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# API代理到网关
|
||||
location /api/ {
|
||||
proxy_pass http://fund-gateway:8000/;
|
||||
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;
|
||||
|
||||
# 超时配置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 文件上传大小限制
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
# Vue Router History模式支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 错误页面
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@ -62,39 +62,3 @@ export function rejectExpense(id: number, comment: string) {
|
||||
export function confirmPayExpense(id: number, payChannel: string, payVoucher?: string) {
|
||||
return request.put(`/exp/api/v1/exp/expense/${id}/confirm-pay?payChannel=${payChannel}&payVoucher=${payVoucher || ''}`)
|
||||
}
|
||||
|
||||
// 导出支出明细
|
||||
export function exportExpense(params?: { title?: string; expenseType?: number; approvalStatus?: number; payStatus?: number }) {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || ''
|
||||
const token = localStorage.getItem('token')
|
||||
const tenantId = localStorage.getItem('tenantId') || '1'
|
||||
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.title) queryParams.append('title', params.title)
|
||||
if (params?.expenseType) queryParams.append('expenseType', String(params.expenseType))
|
||||
if (params?.approvalStatus !== undefined) queryParams.append('approvalStatus', String(params.approvalStatus))
|
||||
if (params?.payStatus !== undefined) queryParams.append('payStatus', String(params.payStatus))
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const url = `${baseUrl}/exp/api/v1/exp/expense/export${queryString ? '?' + queryString : ''}`
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Tenant-Id': tenantId
|
||||
}
|
||||
}).then(response => {
|
||||
if (!response.ok) throw new Error('导出失败')
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition')
|
||||
let filename = '支出明细.xlsx'
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename\*=utf-8''(.+)/i)
|
||||
if (match && match[1]) {
|
||||
filename = decodeURIComponent(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return response.blob().then(blob => ({ blob, filename }))
|
||||
})
|
||||
}
|
||||
|
||||
@ -49,39 +49,3 @@ export function getReceiptById(id: number) {
|
||||
export function createReceipt(data: any) {
|
||||
return request.post('/receipt/api/v1/receipt/receipt', data)
|
||||
}
|
||||
|
||||
// 导出应收款明细
|
||||
export function exportReceivable(params?: { projectId?: number; customerId?: number; status?: string; confirmStatus?: number }) {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || ''
|
||||
const token = localStorage.getItem('token')
|
||||
const tenantId = localStorage.getItem('tenantId') || '1'
|
||||
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.projectId) queryParams.append('projectId', String(params.projectId))
|
||||
if (params?.customerId) queryParams.append('customerId', String(params.customerId))
|
||||
if (params?.status) queryParams.append('status', params.status)
|
||||
if (params?.confirmStatus !== undefined) queryParams.append('confirmStatus', String(params.confirmStatus))
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const url = `${baseUrl}/receipt/api/v1/receipt/receivable/export${queryString ? '?' + queryString : ''}`
|
||||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Tenant-Id': tenantId
|
||||
}
|
||||
}).then(response => {
|
||||
if (!response.ok) throw new Error('导出失败')
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition')
|
||||
let filename = '应收款明细.xlsx'
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename\*=utf-8''(.+)/i)
|
||||
if (match && match[1]) {
|
||||
filename = decodeURIComponent(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return response.blob().then(blob => ({ blob, filename }))
|
||||
})
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@
|
||||
<el-card shadow="never" style="margin-top: 10px">
|
||||
<div style="margin-bottom: 15px">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增支出</el-button>
|
||||
<el-button type="success" :icon="Download" @click="handleExport" :loading="exporting">导出Excel</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||
@ -263,7 +262,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document, Download } from '@element-plus/icons-vue'
|
||||
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getExpenseList,
|
||||
createExpense,
|
||||
@ -271,8 +270,7 @@ import {
|
||||
deleteExpense,
|
||||
submitExpense,
|
||||
approveExpense,
|
||||
rejectExpense,
|
||||
exportExpense
|
||||
rejectExpense
|
||||
} from '@/api/expense'
|
||||
import { getExpenseTypeTree } from '@/api/expense'
|
||||
import { getProjectList } from '@/api/project'
|
||||
@ -287,7 +285,6 @@ const fileList = ref<any[]>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const expenseTypeList = ref<any[]>([])
|
||||
@ -561,34 +558,6 @@ const resetForm = () => {
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const handleExport = async () => {
|
||||
exporting.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (queryParams.title) params.title = queryParams.title
|
||||
if (queryParams.expenseTypeId) params.expenseType = queryParams.expenseTypeId
|
||||
if (queryParams.approvalStatus) params.approvalStatus = parseInt(queryParams.approvalStatus)
|
||||
if (queryParams.payStatus) params.payStatus = parseInt(queryParams.payStatus)
|
||||
|
||||
const { blob, filename } = await exportExpense(params)
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
ElMessage.error('导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchExpenseTypes()
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
<div style="margin-bottom: 15px">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增应收款</el-button>
|
||||
<el-button type="success" :icon="Money">批量收款</el-button>
|
||||
<el-button type="success" :icon="Download" @click="handleExport" :loading="exporting">导出Excel</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||
@ -263,15 +262,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Edit, Delete, View, Money, Download } from '@element-plus/icons-vue'
|
||||
import { Search, Refresh, Plus, Edit, Delete, View, Money } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getReceivableList,
|
||||
createReceivable,
|
||||
updateReceivable,
|
||||
deleteReceivable,
|
||||
recordReceipt,
|
||||
getReceiptRecords,
|
||||
exportReceivable
|
||||
getReceiptRecords
|
||||
} from '@/api/receivable'
|
||||
import { getProjectList } from '@/api/project'
|
||||
import { getCustomerList } from '@/api/customer'
|
||||
@ -279,7 +277,6 @@ import { getCustomerList } from '@/api/customer'
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const receiptLoading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const projectList = ref<any[]>([])
|
||||
@ -503,31 +500,6 @@ const resetForm = () => {
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
const handleExport = async () => {
|
||||
exporting.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (queryParams.receiptStatus) params.status = queryParams.receiptStatus
|
||||
|
||||
const { blob, filename } = await exportReceivable(params)
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
ElMessage.error('导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchProjects()
|
||||
|
||||
@ -3,32 +3,24 @@ package com.fundplatform.common.config;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 多租户路由配置属性
|
||||
*
|
||||
* <p>租户路由基于 Nacos 服务实例的 metadata.tenant-id 元数据进行匹配</p>
|
||||
* <p>支持从 YAML 配置文件读取租户路由配置</p>
|
||||
*
|
||||
* <h3>工作原理:</h3>
|
||||
* <pre>
|
||||
* 1. 服务实例注册到 Nacos 时,在 metadata 中声明 tenant-id
|
||||
* - 共享实例: tenant-id 为空或不存在
|
||||
* - VIP 实例: tenant-id = "VIP_001"
|
||||
*
|
||||
* 2. 负载均衡器根据请求中的 tenantId 匹配实例
|
||||
* - 找到匹配的 tenant-id → VIP 专属实例
|
||||
* - 找不到 → 回退到共享实例
|
||||
* </pre>
|
||||
*
|
||||
* <h3>配置示例:</h3>
|
||||
* <p>配置示例:</p>
|
||||
* <pre>
|
||||
* tenant:
|
||||
* routing:
|
||||
* enabled: true
|
||||
* tenant-header: X-Tenant-Id
|
||||
* fallback-to-shared: true
|
||||
* default-tenant-id: 1
|
||||
* services:
|
||||
* fund-sys:
|
||||
* vip-tenants:
|
||||
* - TENANT_VIP_001
|
||||
* fallback-to-shared: true
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@ -41,6 +33,12 @@ public class TenantRoutingProperties {
|
||||
/** 租户 ID 请求头 */
|
||||
private String tenantHeader = "X-Tenant-Id";
|
||||
|
||||
/** 租户组请求头 */
|
||||
private String tenantGroupHeader = "X-Tenant-Group";
|
||||
|
||||
/** 服务组分隔符 */
|
||||
private String groupSeparator = "TENANT_";
|
||||
|
||||
/** 默认租户 ID(当未指定时使用) */
|
||||
private String defaultTenantId = "1";
|
||||
|
||||
@ -51,8 +49,13 @@ public class TenantRoutingProperties {
|
||||
"fund-file"
|
||||
);
|
||||
|
||||
/** 是否回退到共享实例(当找不到租户专属实例时) */
|
||||
private boolean fallbackToShared = true;
|
||||
/** 租户服务配置映射 */
|
||||
private Map<String, TenantServiceConfig> services = new HashMap<>();
|
||||
|
||||
/** 租户服务配置(旧版兼容) */
|
||||
private Map<String, TenantServiceConfig> tenantConfigs = new HashMap<>();
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@ -70,6 +73,22 @@ public class TenantRoutingProperties {
|
||||
this.tenantHeader = tenantHeader;
|
||||
}
|
||||
|
||||
public String getTenantGroupHeader() {
|
||||
return tenantGroupHeader;
|
||||
}
|
||||
|
||||
public void setTenantGroupHeader(String tenantGroupHeader) {
|
||||
this.tenantGroupHeader = tenantGroupHeader;
|
||||
}
|
||||
|
||||
public String getGroupSeparator() {
|
||||
return groupSeparator;
|
||||
}
|
||||
|
||||
public void setGroupSeparator(String groupSeparator) {
|
||||
this.groupSeparator = groupSeparator;
|
||||
}
|
||||
|
||||
public String getDefaultTenantId() {
|
||||
return defaultTenantId;
|
||||
}
|
||||
@ -86,18 +105,233 @@ public class TenantRoutingProperties {
|
||||
this.sharedServices = sharedServices;
|
||||
}
|
||||
|
||||
public boolean isFallbackToShared() {
|
||||
return fallbackToShared;
|
||||
public Map<String, TenantServiceConfig> getServices() {
|
||||
return services;
|
||||
}
|
||||
|
||||
public void setFallbackToShared(boolean fallbackToShared) {
|
||||
this.fallbackToShared = fallbackToShared;
|
||||
public void setServices(Map<String, TenantServiceConfig> services) {
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
public Map<String, TenantServiceConfig> getTenantConfigs() {
|
||||
return tenantConfigs;
|
||||
}
|
||||
|
||||
public void setTenantConfigs(Map<String, TenantServiceConfig> tenantConfigs) {
|
||||
this.tenantConfigs = tenantConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建租户组名称
|
||||
*/
|
||||
public String buildTenantGroup(String tenantId) {
|
||||
if (tenantId == null || tenantId.isEmpty()) {
|
||||
return "DEFAULT";
|
||||
}
|
||||
return getGroupSeparator() + tenantId.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为共享服务
|
||||
*/
|
||||
public boolean isSharedService(String serviceName) {
|
||||
return sharedServices != null && sharedServices.contains(serviceName);
|
||||
return sharedServices.contains(serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务的 VIP 租户列表
|
||||
*/
|
||||
public List<String> getVipTenants(String serviceName) {
|
||||
TenantServiceConfig config = services.get(serviceName);
|
||||
if (config != null && config.getVipTenants() != null) {
|
||||
return config.getVipTenants();
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断租户是否为某个服务的 VIP 租户
|
||||
*/
|
||||
public boolean isVipTenant(String serviceName, String tenantGroup) {
|
||||
if (tenantGroup == null || tenantGroup.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
List<String> vipTenants = getVipTenants(serviceName);
|
||||
return vipTenants.contains(tenantGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断服务是否启用了共享实例回退
|
||||
*/
|
||||
public boolean isFallbackToShared(String serviceName) {
|
||||
TenantServiceConfig config = services.get(serviceName);
|
||||
if (config != null) {
|
||||
return config.isFallbackToShared();
|
||||
}
|
||||
return true; // 默认启用回退
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户服务配置
|
||||
*/
|
||||
public static class TenantServiceConfig {
|
||||
/** VIP 租户列表(优先路由到专属实例) */
|
||||
private List<String> vipTenants = new ArrayList<>();
|
||||
|
||||
/** 是否回退到共享实例 */
|
||||
private boolean fallbackToShared = true;
|
||||
|
||||
/** 租户 ID */
|
||||
private String tenantId;
|
||||
|
||||
/** 服务实例配置 */
|
||||
private Map<String, ServiceInstanceConfig> instances = new HashMap<>();
|
||||
|
||||
/** 数据库配置(一库一租户模式) */
|
||||
private DatabaseConfig database;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public List<String> getVipTenants() {
|
||||
return vipTenants;
|
||||
}
|
||||
|
||||
public void setVipTenants(List<String> vipTenants) {
|
||||
this.vipTenants = vipTenants != null ? vipTenants : new ArrayList<>();
|
||||
}
|
||||
|
||||
public boolean isFallbackToShared() {
|
||||
return fallbackToShared;
|
||||
}
|
||||
|
||||
public void setFallbackToShared(boolean fallbackToShared) {
|
||||
this.fallbackToShared = fallbackToShared;
|
||||
}
|
||||
|
||||
public String getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
public Map<String, ServiceInstanceConfig> getInstances() {
|
||||
return instances;
|
||||
}
|
||||
|
||||
public void setInstances(Map<String, ServiceInstanceConfig> instances) {
|
||||
this.instances = instances;
|
||||
}
|
||||
|
||||
public DatabaseConfig getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
public void setDatabase(DatabaseConfig database) {
|
||||
this.database = database;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务实例配置
|
||||
*/
|
||||
public static class ServiceInstanceConfig {
|
||||
/** 服务名 */
|
||||
private String serviceName;
|
||||
|
||||
/** 端口号 */
|
||||
private int port = 8080;
|
||||
|
||||
/** 实例数(用于负载均衡) */
|
||||
private int replicas = 1;
|
||||
|
||||
/** 权重(用于加权负载均衡) */
|
||||
private int weight = 1;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getServiceName() {
|
||||
return serviceName;
|
||||
}
|
||||
|
||||
public void setServiceName(String serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public int getReplicas() {
|
||||
return replicas;
|
||||
}
|
||||
|
||||
public void setReplicas(int replicas) {
|
||||
this.replicas = replicas;
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public void setWeight(int weight) {
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库配置(用于一库一租户)
|
||||
*/
|
||||
public static class DatabaseConfig {
|
||||
/** JDBC URL */
|
||||
private String url;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 密码 */
|
||||
private String password;
|
||||
|
||||
/** 驱动类名 */
|
||||
private String driverClassName = "com.mysql.cj.jdbc.Driver";
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getDriverClassName() {
|
||||
return driverClassName;
|
||||
}
|
||||
|
||||
public void setDriverClassName(String driverClassName) {
|
||||
this.driverClassName = driverClassName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,8 +14,6 @@ import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBal
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
@ -26,22 +24,17 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* <p>根据租户 ID 进行服务实例路由,支持:</p>
|
||||
* <ul>
|
||||
* <li>租户专属实例优先(metadata.tenant-id 匹配)</li>
|
||||
* <li>共享实例回退(metadata.tenant-id 为空)</li>
|
||||
* <li>随机负载均衡</li>
|
||||
* <li>租户专属实例优先</li>
|
||||
* <li>共享实例回退</li>
|
||||
* <li>轮询负载均衡</li>
|
||||
* <li>混合模式(VIP 租户专属 + 普通租户共享)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>路由规则:</h3>
|
||||
* <h3>使用场景</h3>
|
||||
* <pre>
|
||||
* 1. 查找 metadata.tenant-id 包含请求 tenantId 的实例 → 专属实例
|
||||
* 2. 找不到 → 回退到共享实例(metadata.tenant-id 为空)
|
||||
* </pre>
|
||||
*
|
||||
* <h3>实例配置:</h3>
|
||||
* <pre>
|
||||
* 共享实例: metadata.tenant-id = "" (空)
|
||||
* 单租户专属: metadata.tenant-id = "VIP_001"
|
||||
* 多租户专属: metadata.tenant-id = "VIP_001,VIP_002,VIP_003"
|
||||
* 混合模式部署:
|
||||
* - VIP 客户:独立部署服务实例(带 tenant-group 标签)
|
||||
* - 普通客户:共享服务实例(无 tenant-group 标签)
|
||||
* </pre>
|
||||
*/
|
||||
public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer {
|
||||
@ -68,8 +61,9 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
return getDefaultResponse();
|
||||
}
|
||||
|
||||
// 从请求上下文获取租户 ID
|
||||
// 从请求上下文获取租户信息
|
||||
String tenantId = getTenantIdFromRequest(request);
|
||||
String tenantGroup = buildTenantGroup(tenantId);
|
||||
|
||||
if (supplierProvider == null) {
|
||||
logger.warn("[TenantLB] ServiceInstanceListSupplier 未提供");
|
||||
@ -83,7 +77,7 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
}
|
||||
|
||||
return supplier.get().next()
|
||||
.map(instances -> filterByTenantId(instances, tenantId))
|
||||
.map(instances -> filterByTenantGroup(instances, tenantGroup))
|
||||
.map(this::getInstanceResponse);
|
||||
}
|
||||
|
||||
@ -129,22 +123,34 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户 ID 过滤服务实例
|
||||
* 构建租户组名称
|
||||
*/
|
||||
String buildTenantGroup(String tenantId) {
|
||||
if (routingProperties != null) {
|
||||
return routingProperties.buildTenantGroup(tenantId);
|
||||
}
|
||||
// 默认逻辑
|
||||
if (tenantId == null || tenantId.isEmpty()) {
|
||||
return "DEFAULT";
|
||||
}
|
||||
return "TENANT_" + tenantId.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据租户组过滤服务实例
|
||||
*
|
||||
* <p>路由策略:</p>
|
||||
* <ol>
|
||||
* <li>优先选择租户专属实例(metadata.tenant-id 包含请求的 tenantId)</li>
|
||||
* <li>回退到共享实例(metadata.tenant-id 为空或不存在)</li>
|
||||
* <li>优先选择租户专属实例(metadata.tenant-group 匹配)</li>
|
||||
* <li>回退到共享实例(无 tenant-group 标签)</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>tenant-id 支持逗号分隔的多个租户 ID</p>
|
||||
*/
|
||||
List<ServiceInstance> filterByTenantId(List<ServiceInstance> instances, String tenantId) {
|
||||
List<ServiceInstance> filterByTenantGroup(List<ServiceInstance> instances, String tenantGroup) {
|
||||
if (instances == null || instances.isEmpty()) {
|
||||
return instances;
|
||||
}
|
||||
|
||||
logger.debug("[TenantLB] 租户 ID:{},候选实例数:{}", tenantId, instances.size());
|
||||
logger.debug("[TenantLB] 租户组:{},候选实例数:{}", tenantGroup, instances.size());
|
||||
|
||||
// 检查是否为共享服务(不需要租户路由)
|
||||
if (routingProperties != null && routingProperties.isSharedService(serviceId)) {
|
||||
@ -153,38 +159,40 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
}
|
||||
|
||||
// 优先选择租户专属实例
|
||||
if (tenantId != null && !tenantId.isEmpty()) {
|
||||
List<ServiceInstance> tenantInstances = instances.stream()
|
||||
.filter(inst -> {
|
||||
List<String> allowedTenants = parseTenantIds(inst);
|
||||
if (allowedTenants.contains(tenantId)) {
|
||||
logger.debug("[TenantLB] 匹配租户专属实例:{}:{} (允许租户: {})",
|
||||
inst.getHost(), inst.getPort(), allowedTenants);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!tenantInstances.isEmpty()) {
|
||||
logger.info("[TenantLB] 找到 {} 个租户专属实例,租户 ID:{}", tenantInstances.size(), tenantId);
|
||||
return tenantInstances;
|
||||
}
|
||||
List<ServiceInstance> tenantInstances = instances.stream()
|
||||
.filter(inst -> {
|
||||
Map<String, String> metadata = inst.getMetadata();
|
||||
if (metadata == null) return false;
|
||||
String instanceGroup = metadata.get("tenant-group");
|
||||
boolean match = tenantGroup.equals(instanceGroup);
|
||||
if (match) {
|
||||
logger.debug("[TenantLB] 匹配租户实例:{}:{}", inst.getHost(), inst.getPort());
|
||||
}
|
||||
return match;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!tenantInstances.isEmpty()) {
|
||||
logger.info("[TenantLB] 找到 {} 个租户专属实例,租户组:{}", tenantInstances.size(), tenantGroup);
|
||||
return tenantInstances;
|
||||
}
|
||||
|
||||
// 检查是否启用回退到共享实例
|
||||
boolean fallbackEnabled = routingProperties == null || routingProperties.isFallbackToShared();
|
||||
boolean fallbackEnabled = true;
|
||||
if (routingProperties != null) {
|
||||
fallbackEnabled = routingProperties.isFallbackToShared(serviceId);
|
||||
}
|
||||
|
||||
if (!fallbackEnabled) {
|
||||
logger.warn("[TenantLB] 服务 {} 未启用共享实例回退,返回空列表", serviceId);
|
||||
return Collections.emptyList();
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 回退到共享实例(metadata.tenant-id 为空或不存在)
|
||||
// 回退到共享实例(无 tenant-group 标签)
|
||||
List<ServiceInstance> sharedInstances = instances.stream()
|
||||
.filter(inst -> {
|
||||
List<String> allowedTenants = parseTenantIds(inst);
|
||||
return allowedTenants.isEmpty(); // 空列表表示共享实例
|
||||
Map<String, String> metadata = inst.getMetadata();
|
||||
return metadata == null || !metadata.containsKey("tenant-group");
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@ -192,30 +200,6 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
return sharedInstances;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析实例元数据中的租户 ID 列表
|
||||
*
|
||||
* @param inst 服务实例
|
||||
* @return 租户 ID 列表,空列表表示共享实例
|
||||
*/
|
||||
private List<String> parseTenantIds(ServiceInstance inst) {
|
||||
Map<String, String> metadata = inst.getMetadata();
|
||||
if (metadata == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String tenantIdStr = metadata.get("tenant-id");
|
||||
if (tenantIdStr == null || tenantIdStr.trim().isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 支持逗号分隔的多个租户 ID
|
||||
return Arrays.stream(tenantIdStr.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从实例列表中选择一个(随机)
|
||||
*/
|
||||
@ -225,9 +209,10 @@ public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalanc
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
// 随机选择
|
||||
int index = ThreadLocalRandom.current().nextInt(instances.size());
|
||||
ServiceInstance chosen = instances.get(index);
|
||||
logger.info("[TenantLB] 选择实例:{}:{}", chosen.getHost(), chosen.getPort());
|
||||
logger.info("[TenantLB] 选择实例:{}:{} (index={})", chosen.getHost(), chosen.getPort(), index);
|
||||
return new DefaultResponse(chosen);
|
||||
}
|
||||
|
||||
|
||||
@ -8,27 +8,19 @@ import org.springframework.cloud.client.serviceregistry.Registration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Nacos 服务注册元数据配置
|
||||
*
|
||||
* <p>服务启动时自动注册租户 ID 到 Nacos,支持租户感知的负载均衡</p>
|
||||
* <p>服务启动时自动注册租户标签到 Nacos,支持租户感知的负载均衡</p>
|
||||
*
|
||||
* <h3>租户 ID 配置规则:</h3>
|
||||
* <pre>
|
||||
* 共享实例: tenant-id 为空(或不配置)→ 所有租户都可使用
|
||||
* VIP实例: tenant-id = "VIP_001" → 仅该租户可用
|
||||
* </pre>
|
||||
*
|
||||
* <h3>配置方式:</h3>
|
||||
* <pre>
|
||||
* # 方式1: 环境变量(Docker/K8s 推荐)
|
||||
* TENANT_ID=VIP_001
|
||||
*
|
||||
* # 方式2: 配置文件
|
||||
* spring.cloud.nacos.discovery.metadata.tenant-id=VIP_001
|
||||
* </pre>
|
||||
* <p>支持两种配置方式:</p>
|
||||
* <ul>
|
||||
* <li>环境变量:TENANT_ID, TENANT_GROUP(Docker 环境推荐)</li>
|
||||
* <li>配置文件:spring.cloud.nacos.discovery.metadata.tenant-id/tenant-group</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(name = "tenant.routing.enabled", havingValue = "true", matchIfMissing = true)
|
||||
@ -40,38 +32,74 @@ public class NacosMetadataConfig {
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 租户 ID,优先级:环境变量 > 配置文件
|
||||
* 为空表示共享实例,供所有租户使用
|
||||
* 租户 ID,优先级:环境变量 > 配置文件 > 默认值
|
||||
*/
|
||||
@Value("${TENANT_ID:${spring.cloud.nacos.discovery.metadata.tenant-id:}}")
|
||||
@Value("${TENANT_ID:${spring.cloud.nacos.discovery.metadata.tenant-id:1}}")
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* 租户组,优先级:环境变量 > 配置文件 > 自动生成
|
||||
* 为空表示共享实例,供所有租户使用
|
||||
*/
|
||||
@Value("${TENANT_GROUP:${spring.cloud.nacos.discovery.metadata.tenant-group:}}")
|
||||
private String tenantGroup;
|
||||
|
||||
/**
|
||||
* Nacos Registration Bean,用于动态添加元数据
|
||||
*/
|
||||
private final Registration registration;
|
||||
|
||||
public NacosMetadataConfig(Registration registration) {
|
||||
this.registration = registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Nacos 元数据
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
logger.info("[Nacos Metadata] 应用名:{}", applicationName);
|
||||
logger.info("[Nacos Metadata] 租户 ID: {}, 租户组:{}", tenantId, tenantGroup);
|
||||
|
||||
// 动态添加租户元数据到服务注册信息
|
||||
if (registration != null && registration.getMetadata() != null) {
|
||||
Map<String, String> metadata = registration.getMetadata();
|
||||
|
||||
// 添加租户 ID 元数据
|
||||
// 添加租户 ID
|
||||
if (tenantId != null && !tenantId.isEmpty()) {
|
||||
metadata.put("tenant-id", tenantId);
|
||||
logger.info("[Nacos] {} 注册为 VIP 专属实例,租户 ID:{}", applicationName, tenantId);
|
||||
} else {
|
||||
logger.info("[Nacos] {} 注册为共享实例,供所有租户使用", applicationName);
|
||||
}
|
||||
|
||||
// 添加租户组(VIP 专属实例才有值,共享实例为空)
|
||||
if (tenantGroup != null && !tenantGroup.isEmpty()) {
|
||||
metadata.put("tenant-group", tenantGroup);
|
||||
logger.info("[Nacos Metadata] 注册为 VIP 专属实例,租户组:{}", tenantGroup);
|
||||
} else {
|
||||
logger.info("[Nacos Metadata] 注册为共享实例,供所有租户使用");
|
||||
}
|
||||
|
||||
logger.info("[Nacos Metadata] 服务元数据:{}", metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户 ID
|
||||
*/
|
||||
public String getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户组
|
||||
*/
|
||||
public String getTenantGroup() {
|
||||
return tenantGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 VIP 专属实例
|
||||
*/
|
||||
public boolean isVipInstance() {
|
||||
return tenantId != null && !tenantId.isEmpty();
|
||||
return tenantGroup != null && !tenantGroup.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,13 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.Request;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestData;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 租户负载均衡器测试
|
||||
@ -22,138 +25,71 @@ class TenantAwareLoadBalancerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleTenantInstance() {
|
||||
// 单租户专属实例
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_002")),
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Collections.emptyMap()) // 共享实例
|
||||
);
|
||||
void testBuildTenantGroup() {
|
||||
// 测试租户组名称构建
|
||||
String group1 = invokeBuildTenantGroup(loadBalancer, "1");
|
||||
assertEquals("TENANT_1", group1);
|
||||
|
||||
// VIP_001 匹配专属实例
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
|
||||
assertEquals(1, vip1Instances.size());
|
||||
assertEquals("VIP_001", vip1Instances.get(0).getMetadata().get("tenant-id"));
|
||||
String group2 = invokeBuildTenantGroup(loadBalancer, "tenant_001");
|
||||
assertEquals("TENANT_TENANT_001", group2);
|
||||
|
||||
// VIP_002 匹配专属实例
|
||||
List<ServiceInstance> vip2Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_002");
|
||||
assertEquals(1, vip2Instances.size());
|
||||
String group3 = invokeBuildTenantGroup(loadBalancer, null);
|
||||
assertEquals("DEFAULT", group3);
|
||||
|
||||
// 未知租户回退到共享实例
|
||||
List<ServiceInstance> unknownInstances = invokeFilterByTenantId(loadBalancer, instances, "VIP_999");
|
||||
assertEquals(1, unknownInstances.size());
|
||||
assertFalse(unknownInstances.get(0).getMetadata().containsKey("tenant-id"));
|
||||
String group4 = invokeBuildTenantGroup(loadBalancer, "");
|
||||
assertEquals("DEFAULT", group4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiTenantInstance() {
|
||||
// 多租户专属实例(一个实例服务多个租户)
|
||||
void testFilterByTenantGroup() {
|
||||
// 创建测试实例
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
// 实例1:服务 VIP_001, VIP_002, VIP_003
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001,VIP_002,VIP_003")),
|
||||
// 实例2:服务 VIP_004, VIP_005
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_004,VIP_005")),
|
||||
// 实例3:共享实例
|
||||
createInstance("fund-sys", "192.168.1.10", 8110, Collections.emptyMap())
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-group", "TENANT_1")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-group", "TENANT_1")),
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-group", "TENANT_2")),
|
||||
createInstance("fund-sys", "192.168.1.4", 8103, Collections.emptyMap()) // 共享实例
|
||||
);
|
||||
|
||||
// VIP_001 匹配实例1
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
|
||||
assertEquals(1, vip1Instances.size());
|
||||
assertEquals("8100", String.valueOf(vip1Instances.get(0).getPort()));
|
||||
// 测试租户 1 过滤
|
||||
List<ServiceInstance> tenant1Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_1");
|
||||
assertEquals(2, tenant1Instances.size());
|
||||
assertTrue(tenant1Instances.stream().allMatch(i -> "TENANT_1".equals(i.getMetadata().get("tenant-group"))));
|
||||
|
||||
// VIP_003 也匹配实例1
|
||||
List<ServiceInstance> vip3Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_003");
|
||||
assertEquals(1, vip3Instances.size());
|
||||
assertEquals("8100", String.valueOf(vip3Instances.get(0).getPort()));
|
||||
// 测试租户 2 过滤
|
||||
List<ServiceInstance> tenant2Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_2");
|
||||
assertEquals(1, tenant2Instances.size());
|
||||
|
||||
// VIP_004 匹配实例2
|
||||
List<ServiceInstance> vip4Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_004");
|
||||
assertEquals(1, vip4Instances.size());
|
||||
assertEquals("8101", String.valueOf(vip4Instances.get(0).getPort()));
|
||||
|
||||
// VIP_005 匹配实例2
|
||||
List<ServiceInstance> vip5Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_005");
|
||||
assertEquals(1, vip5Instances.size());
|
||||
assertEquals("8101", String.valueOf(vip5Instances.get(0).getPort()));
|
||||
|
||||
// 未知租户回退到共享实例
|
||||
List<ServiceInstance> unknownInstances = invokeFilterByTenantId(loadBalancer, instances, "VIP_999");
|
||||
assertEquals(1, unknownInstances.size());
|
||||
assertEquals("8110", String.valueOf(unknownInstances.get(0).getPort()));
|
||||
// 测试未知租户(回退到共享实例)
|
||||
List<ServiceInstance> unknownTenantInstances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_UNKNOWN");
|
||||
assertEquals(1, unknownTenantInstances.size());
|
||||
assertFalse(unknownTenantInstances.get(0).getMetadata().containsKey("tenant-group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedMode() {
|
||||
// 混合模式:单租户实例 + 多租户实例 + 共享实例
|
||||
// 混合模式:VIP 客户有专属实例,普通客户使用共享实例
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
// 单租户专属实例
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
// 多租户专属实例(VIP_002, VIP_003)
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-id", "VIP_002,VIP_003")),
|
||||
// 共享实例
|
||||
createInstance("fund-sys", "192.168.1.10", 8110, Collections.emptyMap())
|
||||
// VIP 租户 001 的专属实例
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-group", "TENANT_VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Map.of("tenant-group", "TENANT_VIP_001")),
|
||||
// VIP 租户 002 的专属实例
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-group", "TENANT_VIP_002")),
|
||||
// 共享实例(普通租户使用)
|
||||
createInstance("fund-sys", "192.168.1.10", 8110, Collections.emptyMap()),
|
||||
createInstance("fund-sys", "192.168.1.11", 8111, Collections.emptyMap())
|
||||
);
|
||||
|
||||
// VIP_001 走单租户实例
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_001");
|
||||
assertEquals(1, vip1Instances.size());
|
||||
assertEquals(8100, vip1Instances.get(0).getPort());
|
||||
// VIP 租户 001 应该路由到专属实例
|
||||
List<ServiceInstance> vip1Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_VIP_001");
|
||||
assertEquals(2, vip1Instances.size());
|
||||
|
||||
// VIP_002 走多租户实例
|
||||
List<ServiceInstance> vip2Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_002");
|
||||
// VIP 租户 002 应该路由到专属实例
|
||||
List<ServiceInstance> vip2Instances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_VIP_002");
|
||||
assertEquals(1, vip2Instances.size());
|
||||
assertEquals(8101, vip2Instances.get(0).getPort());
|
||||
|
||||
// VIP_003 也走多租户实例
|
||||
List<ServiceInstance> vip3Instances = invokeFilterByTenantId(loadBalancer, instances, "VIP_003");
|
||||
assertEquals(1, vip3Instances.size());
|
||||
assertEquals(8101, vip3Instances.get(0).getPort());
|
||||
|
||||
// 普通租户走共享实例
|
||||
List<ServiceInstance> normalInstances = invokeFilterByTenantId(loadBalancer, instances, "NORMAL_001");
|
||||
assertEquals(1, normalInstances.size());
|
||||
assertEquals(8110, normalInstances.get(0).getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSharedInstanceFallback() {
|
||||
// 测试回退到共享实例
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Collections.emptyMap()),
|
||||
createInstance("fund-sys", "192.168.1.3", 8102, Map.of("tenant-id", "")) // 空字符串也是共享
|
||||
);
|
||||
|
||||
// 未知租户应该找到 2 个共享实例
|
||||
List<ServiceInstance> sharedInstances = invokeFilterByTenantId(loadBalancer, instances, "UNKNOWN");
|
||||
assertEquals(2, sharedInstances.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullTenantId() {
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001")),
|
||||
createInstance("fund-sys", "192.168.1.2", 8101, Collections.emptyMap())
|
||||
);
|
||||
|
||||
// null 租户 ID 应该回退到共享实例
|
||||
List<ServiceInstance> result = invokeFilterByTenantId(loadBalancer, instances, null);
|
||||
assertEquals(1, result.size());
|
||||
assertFalse(result.get(0).getMetadata().containsKey("tenant-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTenantIdWithSpaces() {
|
||||
// 测试带空格的租户 ID 配置
|
||||
List<ServiceInstance> instances = Arrays.asList(
|
||||
createInstance("fund-sys", "192.168.1.1", 8100, Map.of("tenant-id", "VIP_001, VIP_002 , VIP_003"))
|
||||
);
|
||||
|
||||
// 应该能正确匹配(去除空格)
|
||||
assertEquals(1, invokeFilterByTenantId(loadBalancer, instances, "VIP_001").size());
|
||||
assertEquals(1, invokeFilterByTenantId(loadBalancer, instances, "VIP_002").size());
|
||||
assertEquals(1, invokeFilterByTenantId(loadBalancer, instances, "VIP_003").size());
|
||||
// 普通租户应该路由到共享实例
|
||||
List<ServiceInstance> normalInstances = invokeFilterByTenantGroup(loadBalancer, instances, "TENANT_NORMAL");
|
||||
assertEquals(2, normalInstances.size());
|
||||
}
|
||||
|
||||
// 辅助方法:创建服务实例
|
||||
@ -168,13 +104,24 @@ class TenantAwareLoadBalancerTest {
|
||||
);
|
||||
}
|
||||
|
||||
// 辅助方法:调用私有方法 filterByTenantId
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<ServiceInstance> invokeFilterByTenantId(TenantAwareLoadBalancer lb, List<ServiceInstance> instances, String tenantId) {
|
||||
// 辅助方法:调用私有方法 buildTenantGroup
|
||||
private String invokeBuildTenantGroup(TenantAwareLoadBalancer lb, String tenantId) {
|
||||
try {
|
||||
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("filterByTenantId", List.class, String.class);
|
||||
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("buildTenantGroup", String.class);
|
||||
method.setAccessible(true);
|
||||
return (List<ServiceInstance>) method.invoke(lb, instances, tenantId);
|
||||
return (String) method.invoke(lb, tenantId);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法:调用私有方法 filterByTenantGroup
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<ServiceInstance> invokeFilterByTenantGroup(TenantAwareLoadBalancer lb, List<ServiceInstance> instances, String tenantGroup) {
|
||||
try {
|
||||
var method = TenantAwareLoadBalancer.class.getDeclaredMethod("filterByTenantGroup", List.class, String.class);
|
||||
method.setAccessible(true);
|
||||
return (List<ServiceInstance>) method.invoke(lb, instances, tenantGroup);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.505 s -- in com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
|
||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.567 s -- in com.fundplatform.common.loadbalancer.TenantAwareLoadBalancerTest
|
||||
|
||||
Binary file not shown.
@ -1,51 +0,0 @@
|
||||
# Docker 环境配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8200}
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: fund-platform
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:fund_platform}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:root123}
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
validation-timeout: 5000
|
||||
leak-detection-threshold: 60000
|
||||
pool-name: FundCustHikariPool
|
||||
connection-init-sql: SELECT 1
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.cust: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
@ -13,10 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
# tenant-id: 空值=共享实例,单值=单租户专属,多值(逗号分隔)=多租户专属
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -41,17 +37,14 @@ mybatis-plus:
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# Feign配置
|
||||
feign:
|
||||
fund-sys:
|
||||
url: http://localhost:8100
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.cust: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
|
||||
@ -13,10 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
# tenant-id: 空值=共享实例,单值=单租户专属,多值(逗号分隔)=多租户专属
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -41,17 +37,14 @@ mybatis-plus:
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# Feign配置
|
||||
feign:
|
||||
fund-sys:
|
||||
url: http://localhost:8100
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.cust: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
|
||||
@ -2,18 +2,12 @@ package com.fundplatform.exp.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.common.util.ExcelUtil;
|
||||
import com.fundplatform.exp.dto.ExpenseExcel;
|
||||
import com.fundplatform.exp.dto.FundExpenseDTO;
|
||||
import com.fundplatform.exp.service.FundExpenseService;
|
||||
import com.fundplatform.exp.vo.FundExpenseVO;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 支出管理Controller
|
||||
*
|
||||
@ -170,32 +164,4 @@ public class FundExpenseController {
|
||||
public Result<java.util.List<java.util.Map<String, Object>>> getTypeDistribution() {
|
||||
return Result.success(expenseService.getTypeDistribution());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出支出明细Excel
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public void exportExcel(
|
||||
@RequestParam(required = false) String title,
|
||||
@RequestParam(required = false) Long expenseType,
|
||||
@RequestParam(required = false) Integer payStatus,
|
||||
@RequestParam(required = false) Integer approvalStatus,
|
||||
HttpServletResponse response) {
|
||||
List<FundExpenseVO> list = expenseService.listExpenses(title, expenseType, payStatus, approvalStatus);
|
||||
List<ExpenseExcel> excelData = list.stream().map(vo -> {
|
||||
ExpenseExcel excel = new ExpenseExcel();
|
||||
excel.setExpenseNo(vo.getExpenseNo());
|
||||
excel.setTitle(vo.getTitle());
|
||||
excel.setExpenseTypeName(vo.getExpenseTypeName());
|
||||
excel.setAmount(vo.getAmount());
|
||||
excel.setExpenseDate(vo.getExpenseDate());
|
||||
excel.setPayeeName(vo.getPayeeName());
|
||||
excel.setApprovalStatus(vo.getApprovalStatus());
|
||||
excel.setPayStatus(vo.getPayStatus());
|
||||
excel.setPurpose(vo.getPurpose());
|
||||
excel.setCreatedTime(vo.getCreatedTime());
|
||||
return excel;
|
||||
}).collect(Collectors.toList());
|
||||
ExcelUtil.exportExcel(excelData, "支出明细", "支出明细", ExpenseExcel.class, response, "支出明细.xlsx");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
package com.fundplatform.exp.dto;
|
||||
|
||||
import cn.afterturn.easypoi.excel.annotation.Excel;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 支出明细Excel导出实体
|
||||
*/
|
||||
public class ExpenseExcel {
|
||||
|
||||
@Excel(name = "支出编号", width = 15)
|
||||
private String expenseNo;
|
||||
|
||||
@Excel(name = "支出标题", width = 25)
|
||||
private String title;
|
||||
|
||||
@Excel(name = "支出类型", width = 12)
|
||||
private String expenseTypeName;
|
||||
|
||||
@Excel(name = "支出金额", width = 12, type = 10)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Excel(name = "支出日期", width = 12, format = "yyyy-MM-dd")
|
||||
private LocalDateTime expenseDate;
|
||||
|
||||
@Excel(name = "收款人", width = 12)
|
||||
private String payeeName;
|
||||
|
||||
@Excel(name = "审批状态", width = 10, replace = {"草稿_0", "待审批_1", "已通过_2", "已拒绝_3", "已撤回_4"})
|
||||
private Integer approvalStatus;
|
||||
|
||||
@Excel(name = "支付状态", width = 10, replace = {"未支付_0", "已支付_1", "支付失败_2"})
|
||||
private Integer payStatus;
|
||||
|
||||
@Excel(name = "用途说明", width = 30)
|
||||
private String purpose;
|
||||
|
||||
@Excel(name = "创建时间", width = 18, format = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
public String getExpenseNo() {
|
||||
return expenseNo;
|
||||
}
|
||||
|
||||
public void setExpenseNo(String expenseNo) {
|
||||
this.expenseNo = expenseNo;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getExpenseTypeName() {
|
||||
return expenseTypeName;
|
||||
}
|
||||
|
||||
public void setExpenseTypeName(String expenseTypeName) {
|
||||
this.expenseTypeName = expenseTypeName;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public LocalDateTime getExpenseDate() {
|
||||
return expenseDate;
|
||||
}
|
||||
|
||||
public void setExpenseDate(LocalDateTime expenseDate) {
|
||||
this.expenseDate = expenseDate;
|
||||
}
|
||||
|
||||
public String getPayeeName() {
|
||||
return payeeName;
|
||||
}
|
||||
|
||||
public void setPayeeName(String payeeName) {
|
||||
this.payeeName = payeeName;
|
||||
}
|
||||
|
||||
public Integer getApprovalStatus() {
|
||||
return approvalStatus;
|
||||
}
|
||||
|
||||
public void setApprovalStatus(Integer approvalStatus) {
|
||||
this.approvalStatus = approvalStatus;
|
||||
}
|
||||
|
||||
public Integer getPayStatus() {
|
||||
return payStatus;
|
||||
}
|
||||
|
||||
public void setPayStatus(Integer payStatus) {
|
||||
this.payStatus = payStatus;
|
||||
}
|
||||
|
||||
public String getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
public void setPurpose(String purpose) {
|
||||
this.purpose = purpose;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedTime() {
|
||||
return createdTime;
|
||||
}
|
||||
|
||||
public void setCreatedTime(LocalDateTime createdTime) {
|
||||
this.createdTime = createdTime;
|
||||
}
|
||||
}
|
||||
@ -14,11 +14,6 @@ public interface FundExpenseService {
|
||||
|
||||
Page<FundExpenseVO> pageExpenses(int pageNum, int pageSize, String title, Long expenseType, Integer payStatus, Integer approvalStatus);
|
||||
|
||||
/**
|
||||
* 查询支出列表(不分页,用于导出)
|
||||
*/
|
||||
java.util.List<FundExpenseVO> listExpenses(String title, Long expenseType, Integer payStatus, Integer approvalStatus);
|
||||
|
||||
boolean deleteExpense(Long id);
|
||||
|
||||
/**
|
||||
|
||||
@ -135,19 +135,6 @@ public class FundExpenseServiceImpl implements FundExpenseService {
|
||||
return voPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FundExpenseVO> listExpenses(String title, Long expenseType, Integer payStatus, Integer approvalStatus) {
|
||||
LambdaQueryWrapper<FundExpense> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(FundExpense::getDeleted, 0);
|
||||
if (StringUtils.hasText(title)) wrapper.like(FundExpense::getTitle, title);
|
||||
if (expenseType != null) wrapper.eq(FundExpense::getExpenseType, expenseType);
|
||||
if (payStatus != null) wrapper.eq(FundExpense::getPayStatus, payStatus);
|
||||
if (approvalStatus != null) wrapper.eq(FundExpense::getApprovalStatus, approvalStatus);
|
||||
wrapper.orderByDesc(FundExpense::getCreatedTime);
|
||||
List<FundExpense> list = expenseDataService.list(wrapper);
|
||||
return list.stream().map(this::convertToVO).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteExpense(Long id) {
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
# Docker 环境配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8500}
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: fund-platform
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:fund_platform}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:root123}
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
validation-timeout: 5000
|
||||
leak-detection-threshold: 60000
|
||||
pool-name: FundExpHikariPool
|
||||
connection-init-sql: SELECT 1
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.exp: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
@ -13,9 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -43,13 +40,3 @@ mybatis-plus:
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.exp: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
@ -13,9 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -43,13 +40,3 @@ mybatis-plus:
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.exp: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -12,7 +12,6 @@
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/ExpApplication.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/controller/ExpenseTypeController.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/vo/FundExpenseVO.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/dto/ExpenseExcel.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/controller/HealthController.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/vo/ExpenseTypeVO.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-exp/src/main/java/com/fundplatform/exp/dto/ExpenseTypeDTO.java
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
# Docker 环境配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8800}
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: fund-platform
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:fund_platform}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:root123}
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
validation-timeout: 5000
|
||||
leak-detection-threshold: 60000
|
||||
pool-name: FundFileHikariPool
|
||||
connection-init-sql: SELECT 1
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
storage:
|
||||
type: ${FILE_STORAGE_TYPE:local}
|
||||
local:
|
||||
path: ${FILE_STORAGE_PATH:/data/files}
|
||||
minio:
|
||||
endpoint: ${MINIO_ENDPOINT:http://minio:9000}
|
||||
access-key: ${MINIO_ACCESS_KEY:minioadmin}
|
||||
secret-key: ${MINIO_SECRET_KEY:minioadmin}
|
||||
bucket: ${MINIO_BUCKET:fund-files}
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.file: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
@ -5,6 +5,12 @@ spring:
|
||||
application:
|
||||
name: fund-file
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/fund_file?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: zjf@123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
@ -13,13 +19,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 共享服务,无需租户路由
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/fund_file?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: ${DB_PASSWORD:zjf@123456}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
@ -41,14 +40,3 @@ file:
|
||||
upload:
|
||||
path: ./uploads
|
||||
max-size: 52428800
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.file: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 共享服务,禁用租户路由
|
||||
tenant:
|
||||
routing:
|
||||
enabled: false
|
||||
|
||||
@ -5,6 +5,12 @@ spring:
|
||||
application:
|
||||
name: fund-file
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/fund_file?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: zjf@123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
@ -13,13 +19,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 共享服务,无需租户路由
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/fund_file?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: ${DB_PASSWORD:zjf@123456}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
# 文件上传配置
|
||||
servlet:
|
||||
@ -41,14 +40,3 @@ file:
|
||||
upload:
|
||||
path: ./uploads
|
||||
max-size: 52428800
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.file: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 共享服务,禁用租户路由
|
||||
tenant:
|
||||
routing:
|
||||
enabled: false
|
||||
|
||||
@ -1,114 +1,166 @@
|
||||
# Docker 环境配置
|
||||
# Docker 环境配置 - Gateway
|
||||
server:
|
||||
port: ${SERVER_PORT:8000}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: fund-gateway
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:docker}
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: fund-platform
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
|
||||
sentinel:
|
||||
transport:
|
||||
dashboard: ${SENTINEL_DASHBOARD:}:8080
|
||||
port: 8719
|
||||
eager: true
|
||||
enabled: true
|
||||
metadata:
|
||||
service-type: gateway
|
||||
|
||||
gateway:
|
||||
default-filters:
|
||||
- name: RequestRateLimiter
|
||||
args:
|
||||
redis-rate-limiter.replenishRate: 100
|
||||
redis-rate-limiter.burstCapacity: 200
|
||||
key-resolver: "#{@ipKeyResolver}"
|
||||
|
||||
discovery:
|
||||
locator:
|
||||
enabled: true
|
||||
lower-case-service-id: true
|
||||
# 全局跨域配置
|
||||
globalcors:
|
||||
cors-configurations:
|
||||
'[/**]':
|
||||
allowedOriginPatterns: "*"
|
||||
allowedMethods: "*"
|
||||
allowedHeaders: "*"
|
||||
allowCredentials: true
|
||||
maxAge: 3600
|
||||
|
||||
allowed-origins: "*"
|
||||
allowed-methods: "*"
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
routes:
|
||||
- id: fund-sys
|
||||
uri: lb://fund-sys
|
||||
predicates:
|
||||
- Path=/sys/**
|
||||
- Path=/api/sys/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
# 租户感知负载均衡(自动添加 tenant-group 请求头)
|
||||
- id: fund-cust
|
||||
uri: lb://fund-cust
|
||||
predicates:
|
||||
- Path=/cust/**
|
||||
- Path=/api/cust/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: fund-proj
|
||||
uri: lb://fund-proj
|
||||
predicates:
|
||||
- Path=/proj/**
|
||||
- Path=/api/proj/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: fund-req
|
||||
uri: lb://fund-req
|
||||
predicates:
|
||||
- Path=/req/**
|
||||
- Path=/api/req/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: fund-exp
|
||||
uri: lb://fund-exp
|
||||
predicates:
|
||||
- Path=/exp/**
|
||||
- Path=/api/exp/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: fund-receipt
|
||||
uri: lb://fund-receipt
|
||||
predicates:
|
||||
- Path=/receipt/**
|
||||
- Path=/api/receipt/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: fund-report
|
||||
uri: lb://fund-report
|
||||
predicates:
|
||||
- Path=/report/**
|
||||
- Path=/api/report/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: fund-file
|
||||
uri: lb://fund-file
|
||||
predicates:
|
||||
- Path=/file/**
|
||||
- Path=/api/file/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:redis}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: 1
|
||||
# JWT 配置
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:YourSecretKeyForJWTTokenGenerationMustBeAtLeast256BitsLong}
|
||||
expiration: ${JWT_EXPIRATION:86400000}
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.gateway: INFO
|
||||
com.fundplatform.common: INFO
|
||||
# ==================== 多租户混合模式配置 ====================
|
||||
tenant:
|
||||
routing:
|
||||
# 启用租户感知负载均衡
|
||||
enabled: true
|
||||
# 默认租户ID
|
||||
default-tenant-id: 1
|
||||
# 租户服务配置(定义每个服务的VIP租户)
|
||||
services:
|
||||
fund-sys:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
- TENANT_VIP_002
|
||||
fallback-to-shared: true
|
||||
fund-cust:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
fund-proj:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
fund-req:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-exp:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-receipt:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-report:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-file:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
|
||||
# Actuator 监控端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
health:
|
||||
livenessstate:
|
||||
enabled: true
|
||||
readinessstate:
|
||||
enabled: true
|
||||
metrics:
|
||||
tags:
|
||||
application: ${spring.application.name}
|
||||
service-type: gateway
|
||||
distribution:
|
||||
percentiles-histogram:
|
||||
http.server.requests: true
|
||||
percentiles:
|
||||
http.server.requests: 0.5,0.95,0.99
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.fundplatform: DEBUG
|
||||
# 多租户负载均衡日志
|
||||
com.fundplatform.common.loadbalancer: DEBUG
|
||||
com.fundplatform.gateway.filter: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{tenantId}] %-5level %logger{36} - %msg%n"
|
||||
|
||||
@ -128,16 +128,29 @@ logging:
|
||||
org.springframework.cloud.gateway: DEBUG
|
||||
com.fundplatform.common.loadbalancer: DEBUG
|
||||
|
||||
# 多租户路由配置(Gateway 全局配置)
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
tenant-group-header: X-Tenant-Group
|
||||
group-separator: TENANT_
|
||||
default-tenant-id: "1"
|
||||
# 共享服务列表(不需要租户路由的服务)
|
||||
shared-services:
|
||||
- fund-gateway
|
||||
- fund-report
|
||||
- fund-file
|
||||
# 默认回退策略
|
||||
fallback-to-shared: true
|
||||
services:
|
||||
fund-sys:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
- TENANT_VIP_002
|
||||
fallback-to-shared: true
|
||||
fund-cust:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
fund-proj:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
|
||||
166
fund-gateway/target/classes/application-docker.yml
Normal file
166
fund-gateway/target/classes/application-docker.yml
Normal file
@ -0,0 +1,166 @@
|
||||
# Docker 环境配置 - Gateway
|
||||
server:
|
||||
port: ${SERVER_PORT:8000}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: fund-gateway
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:docker}
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
discovery:
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
metadata:
|
||||
service-type: gateway
|
||||
|
||||
gateway:
|
||||
discovery:
|
||||
locator:
|
||||
enabled: true
|
||||
lower-case-service-id: true
|
||||
# 全局跨域配置
|
||||
globalcors:
|
||||
cors-configurations:
|
||||
'[/**]':
|
||||
allowed-origins: "*"
|
||||
allowed-methods: "*"
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
routes:
|
||||
- id: fund-sys
|
||||
uri: lb://fund-sys
|
||||
predicates:
|
||||
- Path=/api/sys/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
# 租户感知负载均衡(自动添加 tenant-group 请求头)
|
||||
- id: fund-cust
|
||||
uri: lb://fund-cust
|
||||
predicates:
|
||||
- Path=/api/cust/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: fund-proj
|
||||
uri: lb://fund-proj
|
||||
predicates:
|
||||
- Path=/api/proj/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: fund-req
|
||||
uri: lb://fund-req
|
||||
predicates:
|
||||
- Path=/api/req/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: fund-exp
|
||||
uri: lb://fund-exp
|
||||
predicates:
|
||||
- Path=/api/exp/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: fund-receipt
|
||||
uri: lb://fund-receipt
|
||||
predicates:
|
||||
- Path=/api/receipt/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: fund-report
|
||||
uri: lb://fund-report
|
||||
predicates:
|
||||
- Path=/api/report/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: fund-file
|
||||
uri: lb://fund-file
|
||||
predicates:
|
||||
- Path=/api/file/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
# JWT 配置
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:YourSecretKeyForJWTTokenGenerationMustBeAtLeast256BitsLong}
|
||||
expiration: ${JWT_EXPIRATION:86400000}
|
||||
|
||||
# ==================== 多租户混合模式配置 ====================
|
||||
tenant:
|
||||
routing:
|
||||
# 启用租户感知负载均衡
|
||||
enabled: true
|
||||
# 默认租户ID
|
||||
default-tenant-id: 1
|
||||
# 租户服务配置(定义每个服务的VIP租户)
|
||||
services:
|
||||
fund-sys:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
- TENANT_VIP_002
|
||||
fallback-to-shared: true
|
||||
fund-cust:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
fund-proj:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
fund-req:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-exp:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-receipt:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-report:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
fund-file:
|
||||
vip-tenants: []
|
||||
fallback-to-shared: true
|
||||
|
||||
# Actuator 监控端点配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
health:
|
||||
livenessstate:
|
||||
enabled: true
|
||||
readinessstate:
|
||||
enabled: true
|
||||
metrics:
|
||||
tags:
|
||||
application: ${spring.application.name}
|
||||
service-type: gateway
|
||||
distribution:
|
||||
percentiles-histogram:
|
||||
http.server.requests: true
|
||||
percentiles:
|
||||
http.server.requests: 0.5,0.95,0.99
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.fundplatform: DEBUG
|
||||
# 多租户负载均衡日志
|
||||
com.fundplatform.common.loadbalancer: DEBUG
|
||||
com.fundplatform.gateway.filter: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{tenantId}] %-5level %logger{36} - %msg%n"
|
||||
@ -128,16 +128,29 @@ logging:
|
||||
org.springframework.cloud.gateway: DEBUG
|
||||
com.fundplatform.common.loadbalancer: DEBUG
|
||||
|
||||
# 多租户路由配置(Gateway 全局配置)
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
tenant-group-header: X-Tenant-Group
|
||||
group-separator: TENANT_
|
||||
default-tenant-id: "1"
|
||||
# 共享服务列表(不需要租户路由的服务)
|
||||
shared-services:
|
||||
- fund-gateway
|
||||
- fund-report
|
||||
- fund-file
|
||||
# 默认回退策略
|
||||
fallback-to-shared: true
|
||||
services:
|
||||
fund-sys:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
- TENANT_VIP_002
|
||||
fallback-to-shared: true
|
||||
fund-cust:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
fund-proj:
|
||||
vip-tenants:
|
||||
- TENANT_VIP_001
|
||||
fallback-to-shared: true
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
dist
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test
|
||||
**/*.test.ts
|
||||
**/*.spec.ts
|
||||
@ -1,61 +0,0 @@
|
||||
# 资金服务平台移动端 - Dockerfile
|
||||
# 多阶段构建:Node构建 + Nginx运行
|
||||
|
||||
# ==================== 构建阶段 ====================
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 设置npm镜像(加速下载)
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 复制package文件(利用缓存)
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建参数:API网关地址
|
||||
ARG VITE_API_BASE_URL=http://localhost:8000
|
||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||
|
||||
# 构建生产版本
|
||||
RUN npm run build
|
||||
|
||||
# ==================== 运行阶段 ====================
|
||||
FROM nginx:alpine
|
||||
|
||||
# 安装必要工具
|
||||
RUN apk add --no-cache curl tzdata && \
|
||||
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone && \
|
||||
apk del tzdata
|
||||
|
||||
# 删除默认配置
|
||||
RUN rm -rf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制nginx配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 创建非root用户
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
||||
chown -R nginx:nginx /var/cache/nginx && \
|
||||
chown -R nginx:nginx /var/log/nginx && \
|
||||
touch /var/run/nginx.pid && \
|
||||
chown -R nginx:nginx /var/run/nginx.pid
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost/ || exit 1
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@ -1,54 +0,0 @@
|
||||
# 资金服务平台移动端 Nginx 配置
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
|
||||
gzip_vary on;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# API代理到网关
|
||||
location /api/ {
|
||||
proxy_pass http://fund-gateway:8000/;
|
||||
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;
|
||||
|
||||
# 超时配置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 文件上传大小限制
|
||||
client_max_body_size 100m;
|
||||
}
|
||||
|
||||
# Vue Router History模式支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 错误页面
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
import request from './request'
|
||||
|
||||
// 用户认证
|
||||
export function login(data: { username: string; password: string }) {
|
||||
return request.post('/sys/api/v1/auth/login', data)
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return request.get('/sys/api/v1/auth/info')
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request.post('/sys/api/v1/auth/logout')
|
||||
}
|
||||
|
||||
// 项目管理
|
||||
export function getProjectList(params?: { pageNum: number; pageSize: number; projectName?: string }) {
|
||||
return request.get('/proj/api/v1/project/page', { params })
|
||||
}
|
||||
|
||||
export function getProjectById(id: number) {
|
||||
return request.get(`/proj/api/v1/project/${id}`)
|
||||
}
|
||||
|
||||
// 客户管理
|
||||
export function getCustomerList(params?: { pageNum: number; pageSize: number; customerName?: string }) {
|
||||
return request.get('/cust/api/v1/customer/page', { params })
|
||||
}
|
||||
|
||||
// 支出管理
|
||||
export function createExpense(data: any) {
|
||||
return request.post('/exp/api/v1/exp/expense', data)
|
||||
}
|
||||
|
||||
export function getExpenseList(params: { pageNum: number; pageSize: number }) {
|
||||
return request.get('/exp/api/v1/exp/expense/page', { params })
|
||||
}
|
||||
|
||||
// 应收款管理
|
||||
export function getReceivableList(params: { pageNum: number; pageSize: number; status?: string }) {
|
||||
return request.get('/receipt/api/v1/receipt/receivable/page', { params })
|
||||
}
|
||||
|
||||
export function getUpcomingDueList(daysWithin: number = 7) {
|
||||
return request.get(`/receipt/api/v1/receipt/receivable/upcoming-due?daysWithin=${daysWithin}`)
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
export function getTodayIncome() {
|
||||
return request.get('/receipt/api/v1/receipt/receivable/stats/today-income')
|
||||
}
|
||||
|
||||
export function getTodayExpense() {
|
||||
return request.get('/exp/api/v1/exp/expense/stats/today-expense')
|
||||
}
|
||||
|
||||
export function getUnpaidAmount() {
|
||||
return request.get('/receipt/api/v1/receipt/receivable/stats/unpaid-amount')
|
||||
}
|
||||
|
||||
export function getOverdueCount() {
|
||||
return request.get('/receipt/api/v1/receipt/receivable/stats/overdue-count')
|
||||
}
|
||||
|
||||
// 支出类型
|
||||
export function getExpenseTypeTree() {
|
||||
return request.get('/exp/api/v1/exp/expense-type/tree')
|
||||
}
|
||||
@ -21,18 +21,6 @@ const router = createRouter({
|
||||
component: () => import('@/views/receivable/List.vue'),
|
||||
meta: { title: '应收款列表', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/project',
|
||||
name: 'ProjectList',
|
||||
component: () => import('@/views/project/List.vue'),
|
||||
meta: { title: '项目列表', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/customer',
|
||||
name: 'CustomerList',
|
||||
component: () => import('@/views/customer/List.vue'),
|
||||
meta: { title: '客户列表', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/my',
|
||||
name: 'My',
|
||||
|
||||
@ -61,18 +61,6 @@
|
||||
</div>
|
||||
<div class="quick-text">应收款</div>
|
||||
</div>
|
||||
<div class="quick-item" @click="$router.push('/project')">
|
||||
<div class="quick-icon">
|
||||
<van-icon name="todo-list-o" />
|
||||
</div>
|
||||
<div class="quick-text">项目</div>
|
||||
</div>
|
||||
<div class="quick-item" @click="$router.push('/customer')">
|
||||
<div class="quick-icon">
|
||||
<van-icon name="friends-o" />
|
||||
</div>
|
||||
<div class="quick-text">客户</div>
|
||||
</div>
|
||||
<div class="quick-item" @click="$router.push('/my')">
|
||||
<div class="quick-icon">
|
||||
<van-icon name="user-o" />
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="page customer-list">
|
||||
<van-nav-bar title="客户列表" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<van-search v-model="searchText" placeholder="搜索客户名称" @search="handleSearch" />
|
||||
</div>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div class="customer-card" v-for="item in list" :key="item.customerId">
|
||||
<div class="customer-header">
|
||||
<div class="customer-avatar">{{ item.customerName?.charAt(0) || 'C' }}</div>
|
||||
<div class="customer-info">
|
||||
<div class="customer-name">{{ item.customerName }}</div>
|
||||
<div class="customer-short">{{ item.customerShort || '-' }}</div>
|
||||
</div>
|
||||
<van-tag :type="getLevelType(item.level)">{{ getLevelText(item.level) }}</van-tag>
|
||||
</div>
|
||||
<div class="customer-detail">
|
||||
<div class="detail-item" v-if="item.phone">
|
||||
<van-icon name="phone-o" />
|
||||
<span>{{ item.phone }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="item.industry">
|
||||
<van-icon name="cluster-o" />
|
||||
<span>{{ item.industry }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getCustomerList } from '@/api'
|
||||
|
||||
const searchText = ref('')
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const getLevelType = (level: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
|
||||
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
|
||||
'A': 'primary',
|
||||
'B': 'success',
|
||||
'C': 'warning',
|
||||
'D': 'danger'
|
||||
}
|
||||
return map[level] || 'default'
|
||||
}
|
||||
|
||||
const getLevelText = (level: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'A': 'A类',
|
||||
'B': 'B类',
|
||||
'C': 'C类',
|
||||
'D': 'D类'
|
||||
}
|
||||
return map[level] || '普通'
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res: any = await getCustomerList({
|
||||
pageNum: pageNum.value,
|
||||
pageSize,
|
||||
customerName: searchText.value || undefined
|
||||
})
|
||||
const records = res.data?.records || []
|
||||
if (pageNum.value === 1) {
|
||||
list.value = records
|
||||
} else {
|
||||
list.value.push(...records)
|
||||
}
|
||||
finished.value = records.length < pageSize
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
finished.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
pageNum.value++
|
||||
loadData()
|
||||
}
|
||||
|
||||
const onRefresh = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.customer-list {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.customer-card {
|
||||
background: #fff;
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.customer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.customer-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #007AFF, #5AC8FA);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.customer-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.customer-short {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.customer-detail {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@ -1,205 +0,0 @@
|
||||
<template>
|
||||
<div class="page project-list">
|
||||
<van-nav-bar title="项目列表" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<van-search v-model="searchText" placeholder="搜索项目名称" @search="handleSearch" />
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div class="project-card" v-for="item in list" :key="item.projectId" @click="goDetail(item)">
|
||||
<div class="project-header">
|
||||
<span class="project-name">{{ item.projectName }}</span>
|
||||
<van-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</van-tag>
|
||||
</div>
|
||||
<div class="project-info">
|
||||
<div class="info-item">
|
||||
<van-icon name="user-o" />
|
||||
<span>{{ item.customerName || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<van-icon name="clock-o" />
|
||||
<span>{{ item.startDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-amount">
|
||||
<div class="amount-item">
|
||||
<span class="label">合同金额</span>
|
||||
<span class="value">{{ formatMoney(item.contractAmount) }}</span>
|
||||
</div>
|
||||
<div class="amount-item">
|
||||
<span class="label">预算金额</span>
|
||||
<span class="value">{{ formatMoney(item.budgetAmount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getProjectList } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const searchText = ref('')
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const list = ref<any[]>([])
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const getStatusType = (status: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
|
||||
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
|
||||
'ongoing': 'primary',
|
||||
'completed': 'success',
|
||||
'preparing': 'warning',
|
||||
'archived': 'default',
|
||||
'cancelled': 'danger'
|
||||
}
|
||||
return map[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'ongoing': '进行中',
|
||||
'completed': '已完成',
|
||||
'preparing': '筹备中',
|
||||
'archived': '已归档',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const formatMoney = (value: number) => {
|
||||
if (!value) return '¥0'
|
||||
return '¥' + value.toLocaleString()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res: any = await getProjectList({
|
||||
pageNum: pageNum.value,
|
||||
pageSize,
|
||||
projectName: searchText.value || undefined
|
||||
})
|
||||
const records = res.data?.records || []
|
||||
if (pageNum.value === 1) {
|
||||
list.value = records
|
||||
} else {
|
||||
list.value.push(...records)
|
||||
}
|
||||
finished.value = records.length < pageSize
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
finished.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
pageNum.value++
|
||||
loadData()
|
||||
}
|
||||
|
||||
const onRefresh = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
loadData()
|
||||
}
|
||||
|
||||
const goDetail = (item: any) => {
|
||||
router.push(`/project/${item.projectId}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-list {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: #fff;
|
||||
margin: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.project-amount {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.amount-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.amount-item .label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.amount-item .value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@ -1,51 +0,0 @@
|
||||
# Docker 环境配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8300}
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: fund-platform
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:fund_platform}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:root123}
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
validation-timeout: 5000
|
||||
leak-detection-threshold: 60000
|
||||
pool-name: FundProjHikariPool
|
||||
connection-init-sql: SELECT 1
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.proj: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
@ -13,9 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -46,11 +43,3 @@ logging:
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
|
||||
@ -13,9 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -46,11 +43,3 @@ logging:
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
|
||||
@ -3,11 +3,9 @@ package com.fundplatform.receipt;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
@EnableScheduling
|
||||
public class ReceiptApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@ -2,19 +2,15 @@ package com.fundplatform.receipt.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.common.util.ExcelUtil;
|
||||
import com.fundplatform.receipt.dto.ReceivableDTO;
|
||||
import com.fundplatform.receipt.dto.ReceivableExcel;
|
||||
import com.fundplatform.receipt.service.ReceivableService;
|
||||
import com.fundplatform.receipt.vo.FundReceiptVO;
|
||||
import com.fundplatform.receipt.vo.ReceivableVO;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 应收款管理Controller
|
||||
@ -150,43 +146,4 @@ public class ReceivableController {
|
||||
public Result<Integer> getOverdueCount() {
|
||||
return Result.success(receivableService.getOverdueCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取即将到期的应收款列表
|
||||
* @param daysWithin 未来多少天内到期(默认7天)
|
||||
*/
|
||||
@GetMapping("/upcoming-due")
|
||||
public Result<List<ReceivableVO>> getUpcomingDue(
|
||||
@RequestParam(defaultValue = "7") int daysWithin) {
|
||||
return Result.success(receivableService.getUpcomingDueList(daysWithin));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出应收款明细Excel
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public void exportExcel(
|
||||
@RequestParam(required = false) Long projectId,
|
||||
@RequestParam(required = false) Long customerId,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) Integer confirmStatus,
|
||||
HttpServletResponse response) {
|
||||
List<ReceivableVO> list = receivableService.listReceivables(projectId, customerId, status, confirmStatus);
|
||||
List<ReceivableExcel> excelData = list.stream().map(vo -> {
|
||||
ReceivableExcel excel = new ReceivableExcel();
|
||||
excel.setReceivableCode(vo.getReceivableCode());
|
||||
excel.setProjectName(vo.getProjectName());
|
||||
excel.setCustomerName(vo.getCustomerName());
|
||||
excel.setTotalAmount(vo.getReceivableAmount());
|
||||
excel.setReceivedAmount(vo.getReceivedAmount());
|
||||
excel.setRemainingAmount(vo.getUnpaidAmount());
|
||||
excel.setDueDate(vo.getPaymentDueDate());
|
||||
excel.setReceiptStatus(vo.getStatus());
|
||||
excel.setConfirmStatus(vo.getConfirmStatus());
|
||||
excel.setRemark(vo.getRemark());
|
||||
excel.setCreatedTime(vo.getCreatedTime());
|
||||
return excel;
|
||||
}).collect(Collectors.toList());
|
||||
ExcelUtil.exportExcel(excelData, "应收款明细", "应收款明细", ReceivableExcel.class, response, "应收款明细.xlsx");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
package com.fundplatform.receipt.dto;
|
||||
|
||||
import cn.afterturn.easypoi.excel.annotation.Excel;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 应收款明细Excel导出实体
|
||||
*/
|
||||
public class ReceivableExcel {
|
||||
|
||||
@Excel(name = "应收款编号", width = 15)
|
||||
private String receivableCode;
|
||||
|
||||
@Excel(name = "项目名称", width = 20)
|
||||
private String projectName;
|
||||
|
||||
@Excel(name = "客户名称", width = 15)
|
||||
private String customerName;
|
||||
|
||||
@Excel(name = "应收金额", width = 12, type = 10)
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Excel(name = "已收金额", width = 12, type = 10)
|
||||
private BigDecimal receivedAmount;
|
||||
|
||||
@Excel(name = "未收金额", width = 12, type = 10)
|
||||
private BigDecimal remainingAmount;
|
||||
|
||||
@Excel(name = "应收日期", width = 12, format = "yyyy-MM-dd")
|
||||
private LocalDate dueDate;
|
||||
|
||||
@Excel(name = "收款状态", width = 10, replace = {"未收款_UNPAID", "部分收款_PARTIAL", "已收款_PAID", "已逾期_OVERDUE"})
|
||||
private String receiptStatus;
|
||||
|
||||
@Excel(name = "确认状态", width = 10, replace = {"待确认_0", "已确认_1"})
|
||||
private Integer confirmStatus;
|
||||
|
||||
@Excel(name = "备注", width = 25)
|
||||
private String remark;
|
||||
|
||||
@Excel(name = "创建时间", width = 18, format = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
public String getReceivableCode() {
|
||||
return receivableCode;
|
||||
}
|
||||
|
||||
public void setReceivableCode(String receivableCode) {
|
||||
this.receivableCode = receivableCode;
|
||||
}
|
||||
|
||||
public String getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public void setProjectName(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public void setCustomerName(String customerName) {
|
||||
this.customerName = customerName;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getReceivedAmount() {
|
||||
return receivedAmount;
|
||||
}
|
||||
|
||||
public void setReceivedAmount(BigDecimal receivedAmount) {
|
||||
this.receivedAmount = receivedAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getRemainingAmount() {
|
||||
return remainingAmount;
|
||||
}
|
||||
|
||||
public void setRemainingAmount(BigDecimal remainingAmount) {
|
||||
this.remainingAmount = remainingAmount;
|
||||
}
|
||||
|
||||
public LocalDate getDueDate() {
|
||||
return dueDate;
|
||||
}
|
||||
|
||||
public void setDueDate(LocalDate dueDate) {
|
||||
this.dueDate = dueDate;
|
||||
}
|
||||
|
||||
public String getReceiptStatus() {
|
||||
return receiptStatus;
|
||||
}
|
||||
|
||||
public void setReceiptStatus(String receiptStatus) {
|
||||
this.receiptStatus = receiptStatus;
|
||||
}
|
||||
|
||||
public Integer getConfirmStatus() {
|
||||
return confirmStatus;
|
||||
}
|
||||
|
||||
public void setConfirmStatus(Integer confirmStatus) {
|
||||
this.confirmStatus = confirmStatus;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedTime() {
|
||||
return createdTime;
|
||||
}
|
||||
|
||||
public void setCreatedTime(LocalDateTime createdTime) {
|
||||
this.createdTime = createdTime;
|
||||
}
|
||||
}
|
||||
@ -33,11 +33,6 @@ public interface ReceivableService {
|
||||
*/
|
||||
Page<ReceivableVO> pageReceivables(int pageNum, int pageSize, Long projectId, Long customerId, String status, Integer confirmStatus);
|
||||
|
||||
/**
|
||||
* 查询应收款列表(不分页,用于导出)
|
||||
*/
|
||||
List<ReceivableVO> listReceivables(Long projectId, Long customerId, String status, Integer confirmStatus);
|
||||
|
||||
/**
|
||||
* 确认应收款
|
||||
*/
|
||||
@ -84,10 +79,4 @@ public interface ReceivableService {
|
||||
* 获取逾期应收款数量
|
||||
*/
|
||||
Integer getOverdueCount();
|
||||
|
||||
/**
|
||||
* 获取即将到期的应收款列表
|
||||
* @param daysWithin 未来多少天内到期
|
||||
*/
|
||||
List<ReceivableVO> getUpcomingDueList(int daysWithin);
|
||||
}
|
||||
|
||||
@ -137,19 +137,6 @@ public class ReceivableServiceImpl implements ReceivableService {
|
||||
return voPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReceivableVO> listReceivables(Long projectId, Long customerId, String status, Integer confirmStatus) {
|
||||
LambdaQueryWrapper<Receivable> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Receivable::getDeleted, 0);
|
||||
if (projectId != null) wrapper.eq(Receivable::getProjectId, projectId);
|
||||
if (customerId != null) wrapper.eq(Receivable::getCustomerId, customerId);
|
||||
if (status != null && !status.isEmpty()) wrapper.eq(Receivable::getStatus, status);
|
||||
if (confirmStatus != null) wrapper.eq(Receivable::getConfirmStatus, confirmStatus);
|
||||
wrapper.orderByDesc(Receivable::getCreatedTime);
|
||||
List<Receivable> list = receivableDataService.list(wrapper);
|
||||
return list.stream().map(this::convertToVO).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean confirmReceivable(Long id, Long confirmBy) {
|
||||
@ -403,21 +390,4 @@ public class ReceivableServiceImpl implements ReceivableService {
|
||||
.eq(Receivable::getStatus, STATUS_OVERDUE);
|
||||
return (int) receivableDataService.count(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReceivableVO> getUpcomingDueList(int daysWithin) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate endDate = today.plusDays(daysWithin);
|
||||
|
||||
LambdaQueryWrapper<Receivable> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Receivable::getDeleted, 0)
|
||||
.eq(Receivable::getConfirmStatus, CONFIRM_CONFIRMED)
|
||||
.ne(Receivable::getStatus, STATUS_RECEIVED)
|
||||
.isNotNull(Receivable::getPaymentDueDate)
|
||||
.between(Receivable::getPaymentDueDate, today, endDate)
|
||||
.orderByAsc(Receivable::getPaymentDueDate);
|
||||
|
||||
List<Receivable> list = receivableDataService.list(wrapper);
|
||||
return list.stream().map(this::convertToVO).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
package com.fundplatform.receipt.task;
|
||||
|
||||
import com.fundplatform.receipt.service.ReceivableService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 应收款定时任务
|
||||
* 执行逾期状态更新和提醒
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "fund.schedule.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class ReceivableScheduledTask {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ReceivableScheduledTask.class);
|
||||
|
||||
private final ReceivableService receivableService;
|
||||
|
||||
public ReceivableScheduledTask(ReceivableService receivableService) {
|
||||
this.receivableService = receivableService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天凌晨1点执行逾期状态更新
|
||||
* cron表达式: 秒 分 时 日 月 周
|
||||
*/
|
||||
@Scheduled(cron = "0 0 1 * * ?")
|
||||
public void updateOverdueStatus() {
|
||||
log.info("开始执行逾期状态更新定时任务...");
|
||||
try {
|
||||
receivableService.updateOverdueStatus();
|
||||
log.info("逾期状态更新定时任务执行完成");
|
||||
} catch (Exception e) {
|
||||
log.error("逾期状态更新定时任务执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天上午9点执行逾期提醒
|
||||
* 发送逾期预警通知
|
||||
*/
|
||||
@Scheduled(cron = "0 0 9 * * ?")
|
||||
public void sendOverdueReminder() {
|
||||
log.info("开始执行逾期提醒定时任务...");
|
||||
try {
|
||||
// TODO: 集成消息通知服务后实现
|
||||
// Integer overdueCount = receivableService.getOverdueCount();
|
||||
// notificationService.sendOverdueAlert(overdueCount);
|
||||
log.info("逾期提醒定时任务执行完成(待集成消息通知服务)");
|
||||
} catch (Exception e) {
|
||||
log.error("逾期提醒定时任务执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每周一上午10点执行账期预警
|
||||
* 提醒即将到期的应收款
|
||||
*/
|
||||
@Scheduled(cron = "0 0 10 ? * MON")
|
||||
public void sendDueDateWarning() {
|
||||
log.info("开始执行账期预警定时任务...");
|
||||
try {
|
||||
// TODO: 集成消息通知服务后实现
|
||||
// List<ReceivableVO> upcomingDue = receivableService.getUpcomingDueList(7);
|
||||
// notificationService.sendDueDateWarning(upcomingDue);
|
||||
log.info("账期预警定时任务执行完成(待集成消息通知服务)");
|
||||
} catch (Exception e) {
|
||||
log.error("账期预警定时任务执行失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
# Docker 环境配置
|
||||
server:
|
||||
port: ${SERVER_PORT:8600}
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:nacos:8848}
|
||||
namespace: fund-platform
|
||||
group: DEFAULT_GROUP
|
||||
username: ${NACOS_USERNAME:nacos}
|
||||
password: ${NACOS_PASSWORD:nacos}
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:fund_platform}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||
username: ${MYSQL_USER:root}
|
||||
password: ${MYSQL_PASSWORD:root123}
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
validation-timeout: 5000
|
||||
leak-detection-threshold: 60000
|
||||
pool-name: FundReceiptHikariPool
|
||||
connection-init-sql: SELECT 1
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.receipt: INFO
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
# 定时任务配置
|
||||
fund:
|
||||
schedule:
|
||||
enabled: ${SCHEDULE_ENABLED:true}
|
||||
@ -13,9 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -43,24 +40,3 @@ mybatis-plus:
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.receipt: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
# 定时任务配置
|
||||
fund:
|
||||
schedule:
|
||||
enabled: true
|
||||
# 逾期状态更新:每天凌晨1点
|
||||
overdue-update-cron: "0 0 1 * * ?"
|
||||
# 逾期提醒:每天上午9点
|
||||
overdue-reminder-cron: "0 0 9 * * ?"
|
||||
# 账期预警:每周一上午10点
|
||||
due-date-warning-cron: "0 0 10 ? * MON"
|
||||
|
||||
@ -13,9 +13,6 @@ spring:
|
||||
group: DEFAULT_GROUP
|
||||
username: nacos
|
||||
password: nacos
|
||||
# 租户路由元数据
|
||||
metadata:
|
||||
tenant-id: ${TENANT_ID:}
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -43,24 +40,3 @@ mybatis-plus:
|
||||
logging:
|
||||
level:
|
||||
com.fundplatform.receipt: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 多租户路由配置
|
||||
tenant:
|
||||
routing:
|
||||
enabled: true
|
||||
tenant-header: X-Tenant-Id
|
||||
default-tenant-id: "1"
|
||||
fallback-to-shared: true
|
||||
|
||||
# 定时任务配置
|
||||
fund:
|
||||
schedule:
|
||||
enabled: true
|
||||
# 逾期状态更新:每天凌晨1点
|
||||
overdue-update-cron: "0 0 1 * * ?"
|
||||
# 逾期提醒:每天上午9点
|
||||
overdue-reminder-cron: "0 0 9 * * ?"
|
||||
# 账期预警:每周一上午10点
|
||||
due-date-warning-cron: "0 0 10 ? * MON"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -9,9 +9,11 @@ com/fundplatform/receipt/data/service/FundReceiptDataService.class
|
||||
com/fundplatform/receipt/controller/HealthController.class
|
||||
com/fundplatform/receipt/data/service/ReceivableDataService.class
|
||||
com/fundplatform/receipt/data/mapper/FundReceiptMapper.class
|
||||
com/fundplatform/receipt/service/impl/ReceivableServiceImpl.class
|
||||
com/fundplatform/receipt/data/entity/FundReceipt.class
|
||||
com/fundplatform/receipt/ReceiptApplication.class
|
||||
com/fundplatform/receipt/service/FundReceiptService.class
|
||||
com/fundplatform/receipt/aop/ApiLogAspect.class
|
||||
com/fundplatform/receipt/dto/ReceivableDTO.class
|
||||
com/fundplatform/receipt/controller/ReceivableController.class
|
||||
com/fundplatform/receipt/service/ReceivableService.class
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/data/mapper/FundReceiptMapper.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/aop/ApiLogAspect.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/data/mapper/ReceivableMapper.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/task/ReceivableScheduledTask.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/ReceiptApplication.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/data/service/FundReceiptDataService.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/dto/ReceivableDTO.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/dto/ReceivableExcel.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/data/entity/FundReceipt.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/service/ReceivableService.java
|
||||
/home/along/MyCode/wanjiabuluo/fundplatform/fund-receipt/src/main/java/com/fundplatform/receipt/vo/ReceivableVO.java
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user