feat: 添加开发规则清单和完善前后端配置
主要变更: 1. 开发规范文档 - 新增《开发规则清单.md》,涵盖技术栈、Maven配置、Lombok规范等 - 记录 Lombok 在 Java 21 + Spring Boot 3 中的已知问题 - 建立代码生成和开发流程规范 2. 前端功能增强 - 新增系统配置管理页面(sysConfig.vue) - 新增数据字典管理页面(sysDict.vue) - 新增财务收据管理页面(receipt.vue) - 更新登录认证 API 配置 3. Docker 部署配置 - 新增应用容器配置(docker-compose.yml) - 新增 Nginx 反向代理配置 - 新增 ELK 日志收集配置(Elasticsearch + Logstash + Filebeat) 4. 性能测试工具 - 新增 JMeter 测试计划(fundplatform-test-plan.jmx) - 新增性能测试执行脚本 5. 环境配置更新 - 更新 .env 环境变量配置 - 同步 fundplatform 子模块最新提交
This commit is contained in:
parent
efd1810e11
commit
32abc57338
4
.env
4
.env
@ -3,6 +3,7 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# MySQL 配置
|
# MySQL 配置
|
||||||
|
INSTALL_MODE=service
|
||||||
MYSQL_HOST=localhost
|
MYSQL_HOST=localhost
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
MYSQL_USERNAME=root
|
MYSQL_USERNAME=root
|
||||||
@ -10,11 +11,14 @@ MYSQL_PASSWORD=zjf@123456
|
|||||||
MYSQL_AUTH_PLUGIN=caching_sha2_password
|
MYSQL_AUTH_PLUGIN=caching_sha2_password
|
||||||
|
|
||||||
# Redis 配置
|
# Redis 配置
|
||||||
|
INSTALL_MODE=service
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=zjf@123456
|
REDIS_PASSWORD=zjf@123456
|
||||||
|
|
||||||
# Nacos 配置
|
# Nacos 配置
|
||||||
|
INSTALL_PATH=/home/along/MyApp/nacos
|
||||||
|
INSTALL_MODE=local
|
||||||
NACOS_HOST=localhost
|
NACOS_HOST=localhost
|
||||||
NACOS_PORT=8848
|
NACOS_PORT=8848
|
||||||
NACOS_USERNAME=nacos
|
NACOS_USERNAME=nacos
|
||||||
|
|||||||
183
docker/app/docker-compose.yml
Normal file
183
docker/app/docker-compose.yml
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# MySQL数据库
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: fund-mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root123456
|
||||||
|
MYSQL_DATABASE: fundplatform
|
||||||
|
MYSQL_USER: funduser
|
||||||
|
MYSQL_PASSWORD: fund123456
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
- ./init:/docker-entrypoint-initdb.d
|
||||||
|
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Redis缓存
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: fund-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
|
||||||
|
command: redis-server /usr/local/etc/redis/redis.conf --requirepass zjf@123456
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Nacos服务注册中心
|
||||||
|
nacos:
|
||||||
|
image: nacos/nacos-server:v2.3.0
|
||||||
|
container_name: fund-nacos
|
||||||
|
environment:
|
||||||
|
MODE: standalone
|
||||||
|
SPRING_DATASOURCE_PLATFORM: mysql
|
||||||
|
MYSQL_SERVICE_HOST: mysql
|
||||||
|
MYSQL_SERVICE_DB_NAME: nacos
|
||||||
|
MYSQL_SERVICE_PORT: 3306
|
||||||
|
MYSQL_SERVICE_USER: root
|
||||||
|
MYSQL_SERVICE_PASSWORD: root123456
|
||||||
|
NACOS_AUTH_ENABLE: true
|
||||||
|
NACOS_CORE_AUTH_SERVER_IDENTITY_KEY: fundplatform
|
||||||
|
NACOS_CORE_AUTH_SERVER_IDENTITY_VALUE: fundplatform123
|
||||||
|
NACOS_CORE_AUTH_DEFAULT_TOKEN_SECRET_KEY: VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
|
||||||
|
JVM_XMS: 512m
|
||||||
|
JVM_XMX: 512m
|
||||||
|
ports:
|
||||||
|
- "8848:8848"
|
||||||
|
- "9848:9848"
|
||||||
|
volumes:
|
||||||
|
- nacos-data:/home/nacos/data
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# fund-sys服务
|
||||||
|
fund-sys:
|
||||||
|
build:
|
||||||
|
context: ../../fundplatform
|
||||||
|
dockerfile: fund-sys/Dockerfile
|
||||||
|
container_name: fund-sys
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: docker
|
||||||
|
NACOS_SERVER_ADDR: nacos:8848
|
||||||
|
MYSQL_HOST: mysql
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PASSWORD: zjf@123456
|
||||||
|
JVM_OPTS: -Xms512m -Xmx1024m
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./logs/fund-sys:/app/logs
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
nacos:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# fund-cust服务
|
||||||
|
fund-cust:
|
||||||
|
build:
|
||||||
|
context: ../../fundplatform
|
||||||
|
dockerfile: fund-cust/Dockerfile
|
||||||
|
container_name: fund-cust
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: docker
|
||||||
|
NACOS_SERVER_ADDR: nacos:8848
|
||||||
|
MYSQL_HOST: mysql
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PASSWORD: zjf@123456
|
||||||
|
JVM_OPTS: -Xms512m -Xmx1024m
|
||||||
|
ports:
|
||||||
|
- "8082:8082"
|
||||||
|
volumes:
|
||||||
|
- ./logs/fund-cust:/app/logs
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
nacos:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# fund-proj服务
|
||||||
|
fund-proj:
|
||||||
|
build:
|
||||||
|
context: ../../fundplatform
|
||||||
|
dockerfile: fund-proj/Dockerfile
|
||||||
|
container_name: fund-proj
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: docker
|
||||||
|
NACOS_SERVER_ADDR: nacos:8848
|
||||||
|
MYSQL_HOST: mysql
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PASSWORD: zjf@123456
|
||||||
|
JVM_OPTS: -Xms512m -Xmx1024m
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
volumes:
|
||||||
|
- ./logs/fund-proj:/app/logs
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
nacos:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Nginx网关
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: fund-nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./nginx/html:/usr/share/nginx/html
|
||||||
|
networks:
|
||||||
|
- fund-network
|
||||||
|
depends_on:
|
||||||
|
- fund-sys
|
||||||
|
- fund-cust
|
||||||
|
- fund-proj
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
redis-data:
|
||||||
|
nacos-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
fund-network:
|
||||||
|
driver: bridge
|
||||||
119
docker/app/nginx/nginx.conf
Normal file
119
docker/app/nginx/nginx.conf
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# 日志格式
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||||
|
'rt=$request_time uct="$upstream_connect_time" '
|
||||||
|
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
# 性能优化
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# Gzip压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||||
|
|
||||||
|
# 上游服务器
|
||||||
|
upstream fund_sys {
|
||||||
|
server fund-sys:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream fund_cust {
|
||||||
|
server fund-cust:8082;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream fund_proj {
|
||||||
|
server fund-proj:8081;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP服务器
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# 前端静态资源
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API代理 - 系统服务
|
||||||
|
location /api/v1/sys/ {
|
||||||
|
proxy_pass http://fund_sys/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API代理 - 客户中心
|
||||||
|
location /api/v1/cust/ {
|
||||||
|
proxy_pass http://fund_cust/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API代理 - 项目管理
|
||||||
|
location /api/v1/proj/ {
|
||||||
|
proxy_pass http://fund_proj/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API代理 - 通用接口(转发到sys服务)
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://fund_sys/;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Swagger文档
|
||||||
|
location /swagger-ui/ {
|
||||||
|
proxy_pass http://fund_sys/swagger-ui/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Actuator健康检查
|
||||||
|
location /actuator/ {
|
||||||
|
proxy_pass http://fund_sys/actuator/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误页面
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
docker/elk/docker-compose.yml
Normal file
81
docker/elk/docker-compose.yml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Elasticsearch
|
||||||
|
elasticsearch:
|
||||||
|
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
|
||||||
|
container_name: fund-elasticsearch
|
||||||
|
environment:
|
||||||
|
- discovery.type=single-node
|
||||||
|
- xpack.security.enabled=false
|
||||||
|
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||||
|
- cluster.routing.allocation.disk.threshold_enabled=false
|
||||||
|
ports:
|
||||||
|
- "9200:9200"
|
||||||
|
volumes:
|
||||||
|
- elasticsearch-data:/usr/share/elasticsearch/data
|
||||||
|
networks:
|
||||||
|
- elk-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9200/_cluster/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Logstash
|
||||||
|
logstash:
|
||||||
|
image: docker.elastic.co/logstash/logstash:8.11.0
|
||||||
|
container_name: fund-logstash
|
||||||
|
environment:
|
||||||
|
- "LS_JAVA_OPTS=-Xms256m -Xmx256m"
|
||||||
|
ports:
|
||||||
|
- "5044:5044"
|
||||||
|
- "9600:9600"
|
||||||
|
volumes:
|
||||||
|
- ./logstash/config:/usr/share/logstash/config
|
||||||
|
- ./logstash/pipeline:/usr/share/logstash/pipeline
|
||||||
|
- /home/along/MyCode/wanjiabuluo/fundplatform/logs:/var/log/fundplatform:ro
|
||||||
|
networks:
|
||||||
|
- elk-network
|
||||||
|
depends_on:
|
||||||
|
elasticsearch:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# Kibana
|
||||||
|
kibana:
|
||||||
|
image: docker.elastic.co/kibana/kibana:8.11.0
|
||||||
|
container_name: fund-kibana
|
||||||
|
environment:
|
||||||
|
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
|
||||||
|
- xpack.security.enabled=false
|
||||||
|
ports:
|
||||||
|
- "5601:5601"
|
||||||
|
networks:
|
||||||
|
- elk-network
|
||||||
|
depends_on:
|
||||||
|
elasticsearch:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# Filebeat
|
||||||
|
filebeat:
|
||||||
|
image: docker.elastic.co/beats/filebeat:8.11.0
|
||||||
|
container_name: fund-filebeat
|
||||||
|
user: root
|
||||||
|
volumes:
|
||||||
|
- ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
|
||||||
|
- /home/along/MyCode/wanjiabuluo/fundplatform/logs:/var/log/fundplatform:ro
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- elk-network
|
||||||
|
depends_on:
|
||||||
|
logstash:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
elasticsearch-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
elk-network:
|
||||||
|
driver: bridge
|
||||||
54
docker/elk/filebeat/filebeat.yml
Normal file
54
docker/elk/filebeat/filebeat.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
filebeat.inputs:
|
||||||
|
- type: log
|
||||||
|
enabled: true
|
||||||
|
paths:
|
||||||
|
- /var/log/fundplatform/fund-sys-json.log
|
||||||
|
- /var/log/fundplatform/fund-proj-json.log
|
||||||
|
fields:
|
||||||
|
log_source: fundplatform
|
||||||
|
service_type: application
|
||||||
|
fields_under_root: true
|
||||||
|
multiline.pattern: '^\{'
|
||||||
|
multiline.negate: true
|
||||||
|
multiline.match: after
|
||||||
|
json.keys_under_root: true
|
||||||
|
json.add_error_key: true
|
||||||
|
|
||||||
|
- type: log
|
||||||
|
enabled: true
|
||||||
|
paths:
|
||||||
|
- /var/log/fundplatform/fund-sys-error.log
|
||||||
|
fields:
|
||||||
|
log_source: fundplatform
|
||||||
|
service_type: error
|
||||||
|
log_level: error
|
||||||
|
fields_under_root: true
|
||||||
|
|
||||||
|
# 容器日志收集
|
||||||
|
- type: container
|
||||||
|
paths:
|
||||||
|
- '/var/lib/docker/containers/*/*.log'
|
||||||
|
processors:
|
||||||
|
- add_docker_metadata:
|
||||||
|
host: "unix:///var/run/docker.sock"
|
||||||
|
|
||||||
|
# 输出到Logstash
|
||||||
|
output.logstash:
|
||||||
|
hosts: ["logstash:5044"]
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# 处理器
|
||||||
|
processors:
|
||||||
|
- add_host_metadata:
|
||||||
|
when.not.contains.tags: forwarded
|
||||||
|
- add_cloud_metadata: ~
|
||||||
|
- add_docker_metadata: ~
|
||||||
|
|
||||||
|
# 日志级别
|
||||||
|
logging.level: info
|
||||||
|
logging.to_files: true
|
||||||
|
logging.files:
|
||||||
|
path: /var/log/filebeat
|
||||||
|
name: filebeat
|
||||||
|
keepfiles: 7
|
||||||
|
permissions: 0644
|
||||||
68
docker/elk/logstash/pipeline/logstash.conf
Normal file
68
docker/elk/logstash/pipeline/logstash.conf
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
input {
|
||||||
|
beats {
|
||||||
|
port => 5044
|
||||||
|
}
|
||||||
|
|
||||||
|
file {
|
||||||
|
path => "/var/log/fundplatform/fund-sys-json.log"
|
||||||
|
start_position => "beginning"
|
||||||
|
sincedb_path => "/dev/null"
|
||||||
|
codec => "json"
|
||||||
|
type => "application"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter {
|
||||||
|
if [type] == "application" {
|
||||||
|
# 解析时间戳
|
||||||
|
date {
|
||||||
|
match => [ "timestamp", "yyyy-MM-dd HH:mm:ss.SSS" ]
|
||||||
|
target => "@timestamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加环境标签
|
||||||
|
mutate {
|
||||||
|
add_field => {
|
||||||
|
"environment" => "production"
|
||||||
|
"log_source" => "fundplatform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取日志级别
|
||||||
|
if [level] {
|
||||||
|
mutate {
|
||||||
|
add_field => { "log_level" => "%{level}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误日志特殊处理
|
||||||
|
if [level] == "ERROR" {
|
||||||
|
mutate {
|
||||||
|
add_tag => [ "error_log" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
if [type] == "application" {
|
||||||
|
elasticsearch {
|
||||||
|
hosts => ["elasticsearch:9200"]
|
||||||
|
index => "fundplatform-logs-%{+YYYY.MM.dd}"
|
||||||
|
template_name => "fundplatform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误日志单独索引
|
||||||
|
if "error_log" in [tags] {
|
||||||
|
elasticsearch {
|
||||||
|
hosts => ["elasticsearch:9200"]
|
||||||
|
index => "fundplatform-errors-%{+YYYY.MM.dd}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同时输出到控制台(调试)
|
||||||
|
stdout {
|
||||||
|
codec => rubydebug
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import request from '../utils/request'
|
|||||||
|
|
||||||
export const login = (data) => {
|
export const login = (data) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/auth/login',
|
url: '/sys/api/v1/auth/login',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
@ -10,7 +10,7 @@ export const login = (data) => {
|
|||||||
|
|
||||||
export const refreshToken = (refreshToken) => {
|
export const refreshToken = (refreshToken) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/auth/refresh',
|
url: '/sys/api/v1/auth/refresh',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { refreshToken }
|
data: { refreshToken }
|
||||||
})
|
})
|
||||||
@ -18,7 +18,7 @@ export const refreshToken = (refreshToken) => {
|
|||||||
|
|
||||||
export const logout = () => {
|
export const logout = () => {
|
||||||
return request({
|
return request({
|
||||||
url: '/auth/logout',
|
url: '/sys/api/v1/auth/logout',
|
||||||
method: 'post'
|
method: 'post'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
63
fund-admin/src/api/sysConfig.js
Normal file
63
fund-admin/src/api/sysConfig.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 系统配置管理API
|
||||||
|
|
||||||
|
export function getConfigPage(params) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/config/page',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigById(configId) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/config/${configId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigValue(configKey) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/config/value/${configKey}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigByGroup(configGroup) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/config/group/${configGroup}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/config',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateConfig(configId, data) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/config/${configId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteConfig(configId) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/config/${configId}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchDeleteConfig(configIds) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/config/batch',
|
||||||
|
method: 'delete',
|
||||||
|
params: { configIds }
|
||||||
|
})
|
||||||
|
}
|
||||||
78
fund-admin/src/api/sysDict.js
Normal file
78
fund-admin/src/api/sysDict.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 数据字典管理API
|
||||||
|
|
||||||
|
export function getDictPage(params) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/dict/page',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDictById(dictId) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/dict/${dictId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDictByType(dictType) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/dict/type/${dictType}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllDictTypes() {
|
||||||
|
return request({
|
||||||
|
url: '/sys/dict/types',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDictMap(dictType) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/dict/map/${dictType}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDictLabel(dictType, dictCode) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/dict/label',
|
||||||
|
method: 'get',
|
||||||
|
params: { dictType, dictCode }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDict(data) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/dict',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDict(dictId, data) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/dict/${dictId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDict(dictId) {
|
||||||
|
return request({
|
||||||
|
url: `/sys/dict/${dictId}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function batchDeleteDict(dictIds) {
|
||||||
|
return request({
|
||||||
|
url: '/sys/dict/batch',
|
||||||
|
method: 'delete',
|
||||||
|
params: { dictIds }
|
||||||
|
})
|
||||||
|
}
|
||||||
391
fund-admin/src/views/finance/receipt.vue
Normal file
391
fund-admin/src/views/finance/receipt.vue
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
<template>
|
||||||
|
<div class="receipt-container">
|
||||||
|
<el-card>
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-form :inline="true" :model="searchForm">
|
||||||
|
<el-form-item label="收款编号">
|
||||||
|
<el-input v-model="searchForm.receiptCode" placeholder="请输入编号" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="应收款">
|
||||||
|
<el-select v-model="searchForm.receivableId" placeholder="请选择应收款" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="receivable in receivableOptions"
|
||||||
|
:key="receivable.receivableId"
|
||||||
|
:label="receivable.receivableCode"
|
||||||
|
:value="receivable.receivableId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<el-row style="margin-bottom: 15px;">
|
||||||
|
<el-button type="primary" @click="handleAdd">新增收款记录</el-button>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table :data="tableData" border v-loading="loading">
|
||||||
|
<el-table-column prop="receiptCode" label="收款编号" width="150" />
|
||||||
|
<el-table-column prop="receivableId" label="应收款" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getReceivableName(row.receivableId) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="receiptAmount" label="收款金额" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
¥{{ row.receiptAmount }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="receiptDate" label="收款日期" width="120" />
|
||||||
|
<el-table-column prop="receiptMethod" label="收款方式" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getReceiptMethodText(row.receiptMethod) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="payerName" label="付款方" width="150" />
|
||||||
|
<el-table-column prop="receiptAccount" label="收款账户" width="180" />
|
||||||
|
<el-table-column prop="receiptVoucher" label="收款凭证" width="120" />
|
||||||
|
<el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="page.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="fetchData"
|
||||||
|
@current-change="fetchData"
|
||||||
|
style="margin-top: 20px; justify-content: flex-end;"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="form.receiptId ? '编辑收款记录' : '新增收款记录'"
|
||||||
|
width="700px"
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="收款编号" prop="receiptCode">
|
||||||
|
<el-input v-model="form.receiptCode" placeholder="请输入编号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="应收款" prop="receivableId">
|
||||||
|
<el-select v-model="form.receivableId" placeholder="请选择应收款" style="width: 100%;" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="receivable in receivableOptions"
|
||||||
|
:key="receivable.receivableId"
|
||||||
|
:label="receivable.receivableCode"
|
||||||
|
:value="receivable.receivableId"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="收款金额" prop="receiptAmount">
|
||||||
|
<el-input-number v-model="form.receiptAmount" :min="0" :precision="2" style="width: 100%;" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="收款日期" prop="receiptDate">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.receiptDate"
|
||||||
|
type="date"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="收款方式" prop="receiptMethod">
|
||||||
|
<el-select v-model="form.receiptMethod" placeholder="请选择收款方式" style="width: 100%;">
|
||||||
|
<el-option label="转账" value="transfer" />
|
||||||
|
<el-option label="现金" value="cash" />
|
||||||
|
<el-option label="支票" value="check" />
|
||||||
|
<el-option label="其他" value="other" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="收款账户" prop="receiptAccount">
|
||||||
|
<el-input v-model="form.receiptAccount" placeholder="请输入收款账户" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="付款方名称" prop="payerName">
|
||||||
|
<el-input v-model="form.payerName" placeholder="请输入付款方名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="收款凭证" prop="receiptVoucher">
|
||||||
|
<el-input v-model="form.receiptVoucher" placeholder="请输入凭证编号或URL" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="备注说明" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入备注说明"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
getReceiptList,
|
||||||
|
createReceipt,
|
||||||
|
updateReceipt,
|
||||||
|
deleteReceipt
|
||||||
|
} from '../../api/receipt'
|
||||||
|
import { getReceivableList } from '../../api/receivable'
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
receiptCode: '',
|
||||||
|
receivableId: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const tableData = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const page = reactive({
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 应收款选项
|
||||||
|
const receivableOptions = ref([])
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
receiptId: null,
|
||||||
|
receiptCode: '',
|
||||||
|
receivableId: null,
|
||||||
|
receiptAmount: 0,
|
||||||
|
receiptDate: null,
|
||||||
|
receiptMethod: 'transfer',
|
||||||
|
receiptAccount: '',
|
||||||
|
payerName: '',
|
||||||
|
receiptVoucher: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules = {
|
||||||
|
receiptCode: [
|
||||||
|
{ required: true, message: '请输入收款编号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
receivableId: [
|
||||||
|
{ required: true, message: '请选择应收款', trigger: 'change' }
|
||||||
|
],
|
||||||
|
receiptAmount: [
|
||||||
|
{ required: true, message: '请输入收款金额', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载应收款列表
|
||||||
|
const loadReceivables = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getReceivableList({ current: 1, size: 1000 })
|
||||||
|
receivableOptions.value = res.records || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载应收款列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取应收款名称
|
||||||
|
const getReceivableName = (receivableId) => {
|
||||||
|
const receivable = receivableOptions.value.find(r => r.receivableId === receivableId)
|
||||||
|
return receivable ? receivable.receivableCode : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取收款方式文本
|
||||||
|
const getReceiptMethodText = (method) => {
|
||||||
|
const methodMap = {
|
||||||
|
'transfer': '转账',
|
||||||
|
'cash': '现金',
|
||||||
|
'check': '支票',
|
||||||
|
'other': '其他'
|
||||||
|
}
|
||||||
|
return methodMap[method] || method
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
current: page.current,
|
||||||
|
size: page.size,
|
||||||
|
receiptCode: searchForm.receiptCode || undefined,
|
||||||
|
receivableId: searchForm.receivableId || undefined
|
||||||
|
}
|
||||||
|
const res = await getReceiptList(params)
|
||||||
|
tableData.value = res.records
|
||||||
|
page.total = res.total
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载数据失败:', error)
|
||||||
|
ElMessage.error(error.message || '加载数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
page.current = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.receiptCode = ''
|
||||||
|
searchForm.receivableId = null
|
||||||
|
page.current = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
const handleAdd = () => {
|
||||||
|
Object.assign(form, {
|
||||||
|
receiptId: null,
|
||||||
|
receiptCode: '',
|
||||||
|
receivableId: null,
|
||||||
|
receiptAmount: 0,
|
||||||
|
receiptDate: null,
|
||||||
|
receiptMethod: 'transfer',
|
||||||
|
receiptAccount: '',
|
||||||
|
payerName: '',
|
||||||
|
receiptVoucher: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
Object.assign(form, {
|
||||||
|
receiptId: row.receiptId,
|
||||||
|
receiptCode: row.receiptCode,
|
||||||
|
receivableId: row.receivableId,
|
||||||
|
receiptAmount: row.receiptAmount,
|
||||||
|
receiptDate: row.receiptDate,
|
||||||
|
receiptMethod: row.receiptMethod,
|
||||||
|
receiptAccount: row.receiptAccount,
|
||||||
|
payerName: row.payerName,
|
||||||
|
receiptVoucher: row.receiptVoucher,
|
||||||
|
remark: row.remark
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.warning('请检查表单填写是否完整')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (form.receiptId) {
|
||||||
|
await updateReceipt(form.receiptId, form)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createReceipt(form)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error)
|
||||||
|
ElMessage.error(error.message || '操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该收款记录吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
await deleteReceipt(row.receiptId)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
|
||||||
|
// 如果是最后一条且不是第一页,返回上一页
|
||||||
|
if (tableData.value.length === 1 && page.current > 1) {
|
||||||
|
page.current--
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
ElMessage.error(error.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadReceivables()
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.receipt-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
404
fund-admin/src/views/system/sysConfig.vue
Normal file
404
fund-admin/src/views/system/sysConfig.vue
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="配置键">
|
||||||
|
<el-input v-model="searchForm.configKey" placeholder="请输入配置键" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置名称">
|
||||||
|
<el-input v-model="searchForm.configName" placeholder="请输入配置名称" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置分组">
|
||||||
|
<el-select v-model="searchForm.configGroup" placeholder="请选择分组" clearable>
|
||||||
|
<el-option label="系统设置" value="system" />
|
||||||
|
<el-option label="邮件设置" value="email" />
|
||||||
|
<el-option label="短信设置" value="sms" />
|
||||||
|
<el-option label="文件设置" value="file" />
|
||||||
|
<el-option label="安全设置" value="security" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><Refresh /></el-icon>重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<el-card class="table-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>系统配置列表</span>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>新增配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column prop="configId" label="ID" width="80" />
|
||||||
|
<el-table-column prop="configKey" label="配置键" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="configName" label="配置名称" min-width="150" />
|
||||||
|
<el-table-column prop="configValue" label="配置值" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="configType" label="类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getTypeTagType(row.configType)">
|
||||||
|
{{ getTypeLabel(row.configType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="configGroup" label="分组" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ getGroupLabel(row.configGroup) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="isSystem" label="系统内置" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isSystem === 1 ? 'danger' : 'info'">
|
||||||
|
{{ row.isSystem === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch
|
||||||
|
v-model="row.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
@change="handleStatusChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">
|
||||||
|
<el-icon><Edit /></el-icon>编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="600px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="配置键" prop="configKey">
|
||||||
|
<el-input v-model="formData.configKey" placeholder="请输入配置键" :disabled="isEdit" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置名称" prop="configName">
|
||||||
|
<el-input v-model="formData.configName" placeholder="请输入配置名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置值" prop="configValue">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.configValue"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入配置值"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置类型" prop="configType">
|
||||||
|
<el-select v-model="formData.configType" placeholder="请选择配置类型" style="width: 100%">
|
||||||
|
<el-option label="字符串" value="string" />
|
||||||
|
<el-option label="整数" value="int" />
|
||||||
|
<el-option label="布尔值" value="boolean" />
|
||||||
|
<el-option label="JSON对象" value="json" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置分组" prop="configGroup">
|
||||||
|
<el-select v-model="formData.configGroup" placeholder="请选择配置分组" style="width: 100%">
|
||||||
|
<el-option label="系统设置" value="system" />
|
||||||
|
<el-option label="邮件设置" value="email" />
|
||||||
|
<el-option label="短信设置" value="sms" />
|
||||||
|
<el-option label="文件设置" value="file" />
|
||||||
|
<el-option label="安全设置" value="security" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="系统内置">
|
||||||
|
<el-radio-group v-model="formData.isSystem">
|
||||||
|
<el-radio :label="0">否</el-radio>
|
||||||
|
<el-radio :label="1">是</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getConfigPage,
|
||||||
|
createConfig,
|
||||||
|
updateConfig,
|
||||||
|
deleteConfig
|
||||||
|
} from '@/api/sysConfig'
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
configKey: '',
|
||||||
|
configName: '',
|
||||||
|
configGroup: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const selectedRows = ref([])
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const formData = reactive({
|
||||||
|
configId: null,
|
||||||
|
configKey: '',
|
||||||
|
configName: '',
|
||||||
|
configValue: '',
|
||||||
|
configType: 'string',
|
||||||
|
configGroup: 'system',
|
||||||
|
isSystem: 0,
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
configKey: [{ required: true, message: '请输入配置键', trigger: 'blur' }],
|
||||||
|
configName: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||||
|
configValue: [{ required: true, message: '请输入配置值', trigger: 'blur' }],
|
||||||
|
configType: [{ required: true, message: '请选择配置类型', trigger: 'change' }],
|
||||||
|
configGroup: [{ required: true, message: '请选择配置分组', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型标签
|
||||||
|
const getTypeTagType = (type) => {
|
||||||
|
const map = {
|
||||||
|
string: '',
|
||||||
|
int: 'success',
|
||||||
|
boolean: 'warning',
|
||||||
|
json: 'info'
|
||||||
|
}
|
||||||
|
return map[type] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeLabel = (type) => {
|
||||||
|
const map = {
|
||||||
|
string: '字符串',
|
||||||
|
int: '整数',
|
||||||
|
boolean: '布尔值',
|
||||||
|
json: 'JSON'
|
||||||
|
}
|
||||||
|
return map[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGroupLabel = (group) => {
|
||||||
|
const map = {
|
||||||
|
system: '系统设置',
|
||||||
|
email: '邮件设置',
|
||||||
|
sms: '短信设置',
|
||||||
|
file: '文件设置',
|
||||||
|
security: '安全设置'
|
||||||
|
}
|
||||||
|
return map[group] || group
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getConfigPage({
|
||||||
|
current: pagination.current,
|
||||||
|
size: pagination.size,
|
||||||
|
...searchForm
|
||||||
|
})
|
||||||
|
tableData.value = res.data.records
|
||||||
|
pagination.total = res.data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.configKey = ''
|
||||||
|
searchForm.configName = ''
|
||||||
|
searchForm.configGroup = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择变化
|
||||||
|
const handleSelectionChange = (rows) => {
|
||||||
|
selectedRows.value = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (current) => {
|
||||||
|
pagination.current = current
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '新增配置'
|
||||||
|
Object.assign(formData, {
|
||||||
|
configId: null,
|
||||||
|
configKey: '',
|
||||||
|
configName: '',
|
||||||
|
configValue: '',
|
||||||
|
configType: 'string',
|
||||||
|
configGroup: 'system',
|
||||||
|
isSystem: 0,
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = '编辑配置'
|
||||||
|
Object.assign(formData, row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm('确认删除该配置吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await deleteConfig(row.configId)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态变更
|
||||||
|
const handleStatusChange = async (row) => {
|
||||||
|
try {
|
||||||
|
await updateConfig(row.configId, { status: row.status })
|
||||||
|
ElMessage.success('状态更新成功')
|
||||||
|
} catch (error) {
|
||||||
|
row.status = row.status === 1 ? 0 : 1
|
||||||
|
console.error('状态更新失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await updateConfig(formData.configId, formData)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createConfig(formData)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
393
fund-admin/src/views/system/sysDict.vue
Normal file
393
fund-admin/src/views/system/sysDict.vue
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 搜索区域 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="字典类型">
|
||||||
|
<el-select v-model="searchForm.dictType" placeholder="请选择字典类型" clearable style="width: 200px">
|
||||||
|
<el-option
|
||||||
|
v-for="type in dictTypeList"
|
||||||
|
:key="type"
|
||||||
|
:label="type"
|
||||||
|
:value="type"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字典标签">
|
||||||
|
<el-input v-model="searchForm.dictLabel" placeholder="请输入字典标签" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><Refresh /></el-icon>重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<el-card class="table-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>数据字典列表</span>
|
||||||
|
<div>
|
||||||
|
<el-button type="success" @click="handleRefreshCache" style="margin-right: 10px">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新缓存
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon>新增字典
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 表格 -->
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="tableData"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column prop="dictId" label="ID" width="80" />
|
||||||
|
<el-table-column prop="dictType" label="字典类型" min-width="150" />
|
||||||
|
<el-table-column prop="dictTypeName" label="类型名称" min-width="150" />
|
||||||
|
<el-table-column prop="dictCode" label="字典编码" width="120" />
|
||||||
|
<el-table-column prop="dictLabel" label="字典标签" min-width="150" />
|
||||||
|
<el-table-column prop="dictValue" label="字典值" min-width="120" />
|
||||||
|
<el-table-column prop="sortOrder" label="排序" width="80" />
|
||||||
|
<el-table-column prop="isDefault" label="默认" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isDefault === 1 ? 'success' : 'info'">
|
||||||
|
{{ row.isDefault === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch
|
||||||
|
v-model="row.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
@change="handleStatusChange(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEdit(row)">
|
||||||
|
<el-icon><Edit /></el-icon>编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDelete(row)">
|
||||||
|
<el-icon><Delete /></el-icon>删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="600px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="100px"
|
||||||
|
>
|
||||||
|
<el-form-item label="字典类型" prop="dictType">
|
||||||
|
<el-input v-model="formData.dictType" placeholder="请输入字典类型编码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型名称" prop="dictTypeName">
|
||||||
|
<el-input v-model="formData.dictTypeName" placeholder="请输入字典类型名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字典编码" prop="dictCode">
|
||||||
|
<el-input v-model="formData.dictCode" placeholder="请输入字典编码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字典标签" prop="dictLabel">
|
||||||
|
<el-input v-model="formData.dictLabel" placeholder="请输入字典标签" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字典值" prop="dictValue">
|
||||||
|
<el-input v-model="formData.dictValue" placeholder="请输入字典值" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序号" prop="sortOrder">
|
||||||
|
<el-input-number v-model="formData.sortOrder" :min="0" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否默认">
|
||||||
|
<el-radio-group v-model="formData.isDefault">
|
||||||
|
<el-radio :label="0">否</el-radio>
|
||||||
|
<el-radio :label="1">是</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="样式类名">
|
||||||
|
<el-input v-model="formData.cssClass" placeholder="请输入CSS样式类名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="回显样式">
|
||||||
|
<el-select v-model="formData.listClass" placeholder="请选择回显样式" style="width: 100%">
|
||||||
|
<el-option label="默认" value="" />
|
||||||
|
<el-option label="主要" value="primary" />
|
||||||
|
<el-option label="成功" value="success" />
|
||||||
|
<el-option label="警告" value="warning" />
|
||||||
|
<el-option label="危险" value="danger" />
|
||||||
|
<el-option label="信息" value="info" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="请输入备注"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getDictPage,
|
||||||
|
getAllDictTypes,
|
||||||
|
createDict,
|
||||||
|
updateDict,
|
||||||
|
deleteDict
|
||||||
|
} from '@/api/sysDict'
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
dictType: '',
|
||||||
|
dictLabel: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 字典类型列表
|
||||||
|
const dictTypeList = ref([])
|
||||||
|
|
||||||
|
// 表格数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const selectedRows = ref([])
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const formData = reactive({
|
||||||
|
dictId: null,
|
||||||
|
dictType: '',
|
||||||
|
dictTypeName: '',
|
||||||
|
dictCode: '',
|
||||||
|
dictLabel: '',
|
||||||
|
dictValue: '',
|
||||||
|
sortOrder: 0,
|
||||||
|
isDefault: 0,
|
||||||
|
cssClass: '',
|
||||||
|
listClass: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
dictType: [{ required: true, message: '请输入字典类型', trigger: 'blur' }],
|
||||||
|
dictTypeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
|
||||||
|
dictCode: [{ required: true, message: '请输入字典编码', trigger: 'blur' }],
|
||||||
|
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||||
|
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载字典类型
|
||||||
|
const loadDictTypes = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getAllDictTypes()
|
||||||
|
dictTypeList.value = res.data || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载字典类型失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getDictPage({
|
||||||
|
current: pagination.current,
|
||||||
|
size: pagination.size,
|
||||||
|
...searchForm
|
||||||
|
})
|
||||||
|
tableData.value = res.data.records
|
||||||
|
pagination.total = res.data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载字典列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.current = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.dictType = ''
|
||||||
|
searchForm.dictLabel = ''
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择变化
|
||||||
|
const handleSelectionChange = (rows) => {
|
||||||
|
selectedRows.value = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (current) => {
|
||||||
|
pagination.current = current
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新缓存
|
||||||
|
const handleRefreshCache = () => {
|
||||||
|
loadDictTypes()
|
||||||
|
loadData()
|
||||||
|
ElMessage.success('缓存刷新成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '新增字典'
|
||||||
|
Object.assign(formData, {
|
||||||
|
dictId: null,
|
||||||
|
dictType: '',
|
||||||
|
dictTypeName: '',
|
||||||
|
dictCode: '',
|
||||||
|
dictLabel: '',
|
||||||
|
dictValue: '',
|
||||||
|
sortOrder: 0,
|
||||||
|
isDefault: 0,
|
||||||
|
cssClass: '',
|
||||||
|
listClass: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = '编辑字典'
|
||||||
|
Object.assign(formData, row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm('确认删除该字典吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await deleteDict(row.dictId)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态变更
|
||||||
|
const handleStatusChange = async (row) => {
|
||||||
|
try {
|
||||||
|
await updateDict(row.dictId, { status: row.status })
|
||||||
|
ElMessage.success('状态更新成功')
|
||||||
|
} catch (error) {
|
||||||
|
row.status = row.status === 1 ? 0 : 1
|
||||||
|
console.error('状态更新失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await updateDict(formData.dictId, formData)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createDict(formData)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadData()
|
||||||
|
loadDictTypes()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDictTypes()
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit c4dcf332ba4422a31de94bef9ecd7b72168df888
|
Subproject commit 85528d3f3bcefbcea1a237175ee93b18f7b8feb2
|
||||||
349
jmeter/fundplatform-test-plan.jmx
Normal file
349
jmeter/fundplatform-test-plan.jmx
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.2">
|
||||||
|
<hashTree>
|
||||||
|
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="资金服务平台性能测试" enabled="true">
|
||||||
|
<stringProp name="TestPlan.comments"></stringProp>
|
||||||
|
<boolProp name="TestPlan.functional_mode">false</boolProp>
|
||||||
|
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
|
||||||
|
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
|
||||||
|
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="BASE_URL" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">BASE_URL</stringProp>
|
||||||
|
<stringProp name="Argument.value">localhost</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="PORT" elementType="Argument">
|
||||||
|
<stringProp name="Argument.name">PORT</stringProp>
|
||||||
|
<stringProp name="Argument.value">8080</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="TestPlan.user_define_classpath"></stringProp>
|
||||||
|
</TestPlan>
|
||||||
|
<hashTree>
|
||||||
|
<!-- 线程组:登录接口压测 -->
|
||||||
|
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="登录接口压测" enabled="true">
|
||||||
|
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||||
|
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
|
||||||
|
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||||
|
<stringProp name="LoopController.loops">10</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="ThreadGroup.num_threads">50</stringProp>
|
||||||
|
<stringProp name="ThreadGroup.ramp_time">10</stringProp>
|
||||||
|
<boolProp name="ThreadGroup.scheduler">false</boolProp>
|
||||||
|
<stringProp name="ThreadGroup.duration"></stringProp>
|
||||||
|
<stringProp name="ThreadGroup.delay"></stringProp>
|
||||||
|
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||||
|
</ThreadGroup>
|
||||||
|
<hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="用户登录" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="username" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">admin</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
<stringProp name="Argument.name">username</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="password" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">admin123</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
<stringProp name="Argument.name">password</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">${PORT}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/api/v1/auth/login</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Content-Type</stringProp>
|
||||||
|
<stringProp name="Header.value">application/json</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="提取Token" enabled="true">
|
||||||
|
<stringProp name="JSONPostProcessor.referenceNames">token</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.token</stringProp>
|
||||||
|
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
|
||||||
|
</JSONPostProcessor>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树" enabled="true">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename"></stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="聚合报告" enabled="true">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename">results/login_result.csv</stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
|
||||||
|
<!-- 线程组:客户列表查询压测 -->
|
||||||
|
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="客户列表查询压测" enabled="true">
|
||||||
|
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||||
|
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
|
||||||
|
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||||
|
<stringProp name="LoopController.loops">20</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="ThreadGroup.num_threads">100</stringProp>
|
||||||
|
<stringProp name="ThreadGroup.ramp_time">20</stringProp>
|
||||||
|
<boolProp name="ThreadGroup.scheduler">false</boolProp>
|
||||||
|
<stringProp name="ThreadGroup.duration"></stringProp>
|
||||||
|
<stringProp name="ThreadGroup.delay"></stringProp>
|
||||||
|
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||||
|
</ThreadGroup>
|
||||||
|
<hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="查询客户列表" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments">
|
||||||
|
<elementProp name="current" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">1</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
<stringProp name="Argument.name">current</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<elementProp name="size" elementType="HTTPArgument">
|
||||||
|
<boolProp name="HTTPArgument.always_encode">false</boolProp>
|
||||||
|
<stringProp name="Argument.value">10</stringProp>
|
||||||
|
<stringProp name="Argument.metadata">=</stringProp>
|
||||||
|
<boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||||
|
<stringProp name="Argument.name">size</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">8082</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/api/v1/customer/list</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${token}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="聚合报告" enabled="true">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename">results/customer_result.csv</stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
|
||||||
|
<!-- 线程组:仪表盘统计压测 -->
|
||||||
|
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="仪表盘统计压测" enabled="true">
|
||||||
|
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||||
|
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
|
||||||
|
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||||
|
<stringProp name="LoopController.loops">50</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="ThreadGroup.num_threads">200</stringProp>
|
||||||
|
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
|
||||||
|
<boolProp name="ThreadGroup.scheduler">false</boolProp>
|
||||||
|
<stringProp name="ThreadGroup.duration"></stringProp>
|
||||||
|
<stringProp name="ThreadGroup.delay"></stringProp>
|
||||||
|
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||||
|
</ThreadGroup>
|
||||||
|
<hashTree>
|
||||||
|
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="获取仪表盘数据" enabled="true">
|
||||||
|
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义的变量" enabled="true">
|
||||||
|
<collectionProp name="Arguments.arguments"/>
|
||||||
|
</elementProp>
|
||||||
|
<stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.port">8081</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.protocol">http</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.path">/api/v1/dashboard</stringProp>
|
||||||
|
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||||
|
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||||
|
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
|
||||||
|
<stringProp name="HTTPSampler.embedded_url_re"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
|
||||||
|
<stringProp name="HTTPSampler.response_timeout"></stringProp>
|
||||||
|
</HTTPSamplerProxy>
|
||||||
|
<hashTree>
|
||||||
|
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器" enabled="true">
|
||||||
|
<collectionProp name="HeaderManager.headers">
|
||||||
|
<elementProp name="" elementType="Header">
|
||||||
|
<stringProp name="Header.name">Authorization</stringProp>
|
||||||
|
<stringProp name="Header.value">Bearer ${token}</stringProp>
|
||||||
|
</elementProp>
|
||||||
|
</collectionProp>
|
||||||
|
</HeaderManager>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="聚合报告" enabled="true">
|
||||||
|
<boolProp name="ResultCollector.error_logging">false</boolProp>
|
||||||
|
<objProp>
|
||||||
|
<name>saveConfig</name>
|
||||||
|
<value class="SampleSaveConfiguration">
|
||||||
|
<time>true</time>
|
||||||
|
<latency>true</latency>
|
||||||
|
<timestamp>true</timestamp>
|
||||||
|
<success>true</success>
|
||||||
|
<label>true</label>
|
||||||
|
<code>true</code>
|
||||||
|
<message>true</message>
|
||||||
|
<threadName>true</threadName>
|
||||||
|
<dataType>true</dataType>
|
||||||
|
<encoding>false</encoding>
|
||||||
|
<assertions>true</assertions>
|
||||||
|
<subresults>true</subresults>
|
||||||
|
<responseData>false</responseData>
|
||||||
|
<samplerData>false</samplerData>
|
||||||
|
<xml>false</xml>
|
||||||
|
<fieldNames>true</fieldNames>
|
||||||
|
<responseHeaders>false</responseHeaders>
|
||||||
|
<requestHeaders>false</requestHeaders>
|
||||||
|
<responseDataOnError>false</responseDataOnError>
|
||||||
|
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
|
||||||
|
<assertionsResultsToSave>0</assertionsResultsToSave>
|
||||||
|
<bytes>true</bytes>
|
||||||
|
<sentBytes>true</sentBytes>
|
||||||
|
<url>true</url>
|
||||||
|
<threadCounts>true</threadCounts>
|
||||||
|
<idleTime>true</idleTime>
|
||||||
|
<connectTime>true</connectTime>
|
||||||
|
</value>
|
||||||
|
</objProp>
|
||||||
|
<stringProp name="filename">results/dashboard_result.csv</stringProp>
|
||||||
|
</ResultCollector>
|
||||||
|
<hashTree/>
|
||||||
|
</hashTree>
|
||||||
|
</hashTree>
|
||||||
|
</hashTree>
|
||||||
|
</jmeterTestPlan>
|
||||||
145
jmeter/run-performance-test.sh
Executable file
145
jmeter/run-performance-test.sh
Executable file
@ -0,0 +1,145 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 资金服务平台性能测试脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " 资金服务平台 JMeter 性能测试"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_title() {
|
||||||
|
echo -e "${BLUE}[TEST]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查JMeter
|
||||||
|
if ! command -v jmeter &> /dev/null; then
|
||||||
|
print_error "JMeter 未安装,请先安装 JMeter"
|
||||||
|
echo "下载地址: https://jmeter.apache.org/download_jmeter.cgi"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建结果目录
|
||||||
|
mkdir -p results reports
|
||||||
|
|
||||||
|
# 解析参数
|
||||||
|
TEST_TYPE=${1:-all}
|
||||||
|
BASE_URL=${2:-localhost}
|
||||||
|
PORT=${3:-8080}
|
||||||
|
|
||||||
|
print_info "测试目标: http://${BASE_URL}:${PORT}"
|
||||||
|
|
||||||
|
# 函数:运行测试
|
||||||
|
run_test() {
|
||||||
|
local test_name=$1
|
||||||
|
local thread_count=$2
|
||||||
|
local duration=$3
|
||||||
|
|
||||||
|
print_title "开始测试: ${test_name}"
|
||||||
|
print_info "并发数: ${thread_count}, 持续时间: ${duration}s"
|
||||||
|
|
||||||
|
jmeter -n -t fundplatform-test-plan.jmx \
|
||||||
|
-Jbase_url=${BASE_URL} \
|
||||||
|
-Jport=${PORT} \
|
||||||
|
-Jthread_count=${thread_count} \
|
||||||
|
-Jduration=${duration} \
|
||||||
|
-l results/${test_name}_$(date +%Y%m%d_%H%M%S).jtl \
|
||||||
|
-e -o reports/${test_name}_$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
print_info "测试完成: ${test_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据测试类型执行
|
||||||
|
case $TEST_TYPE in
|
||||||
|
login)
|
||||||
|
print_title "执行登录接口压测"
|
||||||
|
run_test "login" 50 60
|
||||||
|
;;
|
||||||
|
customer)
|
||||||
|
print_title "执行客户列表查询压测"
|
||||||
|
run_test "customer" 100 120
|
||||||
|
;;
|
||||||
|
dashboard)
|
||||||
|
print_title "执行仪表盘统计压测"
|
||||||
|
run_test "dashboard" 200 180
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
print_title "执行全部性能测试"
|
||||||
|
print_warn "这将执行所有测试场景,耗时较长..."
|
||||||
|
|
||||||
|
# 登录接口压测
|
||||||
|
print_title "场景1: 登录接口压测"
|
||||||
|
print_info "并发50用户,持续60秒"
|
||||||
|
jmeter -n -t fundplatform-test-plan.jmx \
|
||||||
|
-Jbase_url=${BASE_URL} \
|
||||||
|
-Jport=${PORT} \
|
||||||
|
-l results/login_test_$(date +%Y%m%d_%H%M%S).jtl
|
||||||
|
|
||||||
|
# 客户查询压测
|
||||||
|
print_title "场景2: 客户列表查询压测"
|
||||||
|
print_info "并发100用户,持续120秒"
|
||||||
|
# 这里可以添加更多测试场景
|
||||||
|
|
||||||
|
# 仪表盘压测
|
||||||
|
print_title "场景3: 仪表盘统计压测"
|
||||||
|
print_info "并发200用户,持续180秒"
|
||||||
|
|
||||||
|
print_info "所有测试完成"
|
||||||
|
;;
|
||||||
|
report)
|
||||||
|
print_title "生成测试报告"
|
||||||
|
if [ -f "results/latest.jtl" ]; then
|
||||||
|
jmeter -g results/latest.jtl -o reports/latest_report
|
||||||
|
print_info "报告已生成: reports/latest_report/index.html"
|
||||||
|
else
|
||||||
|
print_error "没有找到测试结果文件"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: $0 {login|customer|dashboard|all|report} [base_url] [port]"
|
||||||
|
echo ""
|
||||||
|
echo "测试类型:"
|
||||||
|
echo " login - 登录接口压测 (50并发)"
|
||||||
|
echo " customer - 客户列表查询压测 (100并发)"
|
||||||
|
echo " dashboard - 仪表盘统计压测 (200并发)"
|
||||||
|
echo " all - 执行全部测试"
|
||||||
|
echo " report - 生成测试报告"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 all localhost 8080"
|
||||||
|
echo " $0 login 192.168.1.100 8080"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo " 性能测试执行完成"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "测试结果:"
|
||||||
|
echo " - JTL文件: results/"
|
||||||
|
echo " - HTML报告: reports/"
|
||||||
|
echo ""
|
||||||
|
echo "查看报告:"
|
||||||
|
echo " open reports/*/index.html"
|
||||||
429
开发规则清单.md
Normal file
429
开发规则清单.md
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
# 资金服务平台 - 开发规则清单
|
||||||
|
|
||||||
|
> **创建时间**: 2026-02-17
|
||||||
|
> **用途**: 重新生成代码前的规范和约束
|
||||||
|
> **版本**: v1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 技术栈规范
|
||||||
|
|
||||||
|
### 1.1 核心框架
|
||||||
|
- **Java**: 21
|
||||||
|
- **Spring Boot**: 3.2.0
|
||||||
|
- **Spring Cloud**: 2023.0.0
|
||||||
|
- **认证框架**: Apache Shiro 2.0.0(必须,不使用 Spring Security)
|
||||||
|
- **数据库连接池**: HikariCP
|
||||||
|
- **ORM框架**: MyBatis-Plus 3.5.6
|
||||||
|
|
||||||
|
### 1.2 依赖版本锁定
|
||||||
|
```xml
|
||||||
|
<lombok.version>1.18.30</lombok.version>
|
||||||
|
<shiro.version>2.0.0</shiro.version>
|
||||||
|
<jwt.version>4.4.0</jwt.version>
|
||||||
|
<hutool.version>5.8.23</hutool.version>
|
||||||
|
<mybatis-plus.version>3.5.6</mybatis-plus.version>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 禁用的依赖
|
||||||
|
- ❌ `spring-boot-starter-security`
|
||||||
|
- ❌ Shiro 1.x(不兼容 Spring Boot 3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Maven 多模块配置规范
|
||||||
|
|
||||||
|
### 2.1 父 POM 配置
|
||||||
|
```xml
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>-parameters</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 子模块 POM 配置
|
||||||
|
**每个子模块都必须显式激活 maven-compiler-plugin**:
|
||||||
|
```xml
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>-parameters</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 依赖冲突检查
|
||||||
|
- ⚠️ 避免在 pom.xml 中重复声明同一依赖
|
||||||
|
- ⚠️ 使用 `mvn dependency:tree` 检查依赖冲突
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Lombok 使用规范
|
||||||
|
|
||||||
|
### 3.1 禁止在继承 ServiceImpl 的类上使用 @Slf4j
|
||||||
|
|
||||||
|
**❌ 错误示例**:
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class DeptService extends ServiceImpl<DeptMapper, Dept> {
|
||||||
|
public void saveDept(Dept dept) {
|
||||||
|
log.info("保存部门"); // 编译错误!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 正确示例**:
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class DeptService extends ServiceImpl<DeptMapper, Dept> {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DeptService.class);
|
||||||
|
|
||||||
|
public void saveDept(Dept dept) {
|
||||||
|
logger.info("保存部门");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**: MyBatis-Plus 的 ServiceImpl 基类已定义 `protected Log log`(org.apache.ibatis.logging.Log),与 Lombok @Slf4j 生成的字段冲突。
|
||||||
|
|
||||||
|
### 3.2 实体类 Lombok 注解规范
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("sys_dept")
|
||||||
|
public class Dept extends BaseEntity {
|
||||||
|
// 必须确保 import lombok.Data 和 import lombok.EqualsAndHashCode 存在
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Controller/Component Lombok 注解
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/dept")
|
||||||
|
@RequiredArgsConstructor // 推荐用于依赖注入
|
||||||
|
public class DeptController {
|
||||||
|
private final DeptService deptService;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Shiro 认证框架规范
|
||||||
|
|
||||||
|
### 4.1 核心组件清单
|
||||||
|
1. **JwtToken.java** - 自定义 Token 类
|
||||||
|
2. **JwtRealm.java** - 认证授权域
|
||||||
|
3. **JwtFilter.java** - JWT 过滤器
|
||||||
|
4. **ShiroConfig.java** - Shiro 配置类
|
||||||
|
5. **PasswordEncoderConfig.java** - 密码编码器(使用 BCrypt)
|
||||||
|
|
||||||
|
### 4.2 权限注解规范
|
||||||
|
- ✅ 使用 `@RequiresRoles("ADMIN")`
|
||||||
|
- ✅ 使用 `@RequiresPermissions("system:user:view")`
|
||||||
|
- ❌ 不使用 Spring Security 注解(@PreAuthorize)
|
||||||
|
|
||||||
|
### 4.3 认证流程
|
||||||
|
1. 用户登录 → 查询数据库
|
||||||
|
2. BCrypt 验证密码
|
||||||
|
3. 生成 JWT Token(包含 userId, username, tenantId)
|
||||||
|
4. 每次请求通过 JwtFilter 验证 Token
|
||||||
|
5. JwtRealm 加载用户权限
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 代码编写规范
|
||||||
|
|
||||||
|
### 5.1 分层架构
|
||||||
|
```
|
||||||
|
Controller → Service → Mapper → Database
|
||||||
|
↓ ↓
|
||||||
|
DTO Entity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 包结构规范
|
||||||
|
```
|
||||||
|
com.fundplatform.sys
|
||||||
|
├── controller # 控制器层
|
||||||
|
├── service # 业务逻辑层
|
||||||
|
├── mapper # 数据访问层
|
||||||
|
├── entity # 实体类
|
||||||
|
├── dto # 数据传输对象
|
||||||
|
├── vo # 视图对象
|
||||||
|
├── security # 安全相关(Shiro配置)
|
||||||
|
├── config # 配置类
|
||||||
|
└── util # 工具类
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 命名规范
|
||||||
|
- **Entity**: 单数名词,如 `User.java`、`Dept.java`
|
||||||
|
- **Mapper**: 实体名 + Mapper,如 `UserMapper.java`
|
||||||
|
- **Service**: 实体名 + Service,如 `UserService.java`
|
||||||
|
- **Controller**: 实体名 + Controller,如 `UserController.java`
|
||||||
|
|
||||||
|
### 5.4 日志规范
|
||||||
|
```java
|
||||||
|
// 业务操作日志
|
||||||
|
logger.info("[ServiceName] 业务操作描述: {}, 参数: {}", entity.getName(), param);
|
||||||
|
|
||||||
|
// 错误日志
|
||||||
|
logger.error("[ServiceName] 操作失败: {}", errorMessage, exception);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 数据库规范
|
||||||
|
|
||||||
|
### 6.1 表命名规范
|
||||||
|
- 系统表前缀: `sys_`(如 `sys_user`, `sys_role`)
|
||||||
|
- 业务表前缀: `biz_`(如 `biz_project`, `biz_fund`)
|
||||||
|
|
||||||
|
### 6.2 字段规范
|
||||||
|
- 主键: `主表名_id`(如 `user_id`, `dept_id`)
|
||||||
|
- 租户ID: `tenant_id`(必须字段,默认值 1)
|
||||||
|
- 软删除: `deleted`(0-未删除,1-已删除)
|
||||||
|
- 状态: `status`(1-启用,0-禁用)
|
||||||
|
- 时间: `created_time`, `updated_time`
|
||||||
|
|
||||||
|
### 6.3 BaseEntity 基类
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
public class BaseEntity implements Serializable {
|
||||||
|
private Long tenantId; // 租户ID
|
||||||
|
private Integer deleted; // 删除标记
|
||||||
|
private LocalDateTime createdTime; // 创建时间
|
||||||
|
private Long createdBy; // 创建人
|
||||||
|
private LocalDateTime updatedTime; // 更新时间
|
||||||
|
private Long updatedBy; // 更新人
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 规范
|
||||||
|
|
||||||
|
### 7.1 RESTful 路径规范
|
||||||
|
```
|
||||||
|
GET /api/v1/dept # 查询列表
|
||||||
|
GET /api/v1/dept/{id} # 查询单个
|
||||||
|
POST /api/v1/dept # 创建
|
||||||
|
PUT /api/v1/dept/{id} # 更新
|
||||||
|
DELETE /api/v1/dept/{id} # 删除
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 统一响应格式
|
||||||
|
```java
|
||||||
|
public class Result<T> {
|
||||||
|
private Integer code; // 200-成功,其他-失败
|
||||||
|
private String message;
|
||||||
|
private T data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 认证路径规范
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/login # 登录(不需要认证)
|
||||||
|
POST /api/v1/auth/logout # 登出
|
||||||
|
POST /api/v1/auth/refresh # 刷新Token
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 多租户规范
|
||||||
|
|
||||||
|
### 8.1 租户隔离
|
||||||
|
- 所有业务表必须包含 `tenant_id` 字段
|
||||||
|
- Service 层插入数据时自动设置 `tenant_id`
|
||||||
|
- 查询时自动过滤 `tenant_id`
|
||||||
|
|
||||||
|
### 8.2 默认租户
|
||||||
|
- 系统租户: `tenant_id = 0`
|
||||||
|
- 业务租户: `tenant_id >= 1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 编译和测试规范
|
||||||
|
|
||||||
|
### 9.1 编译命令
|
||||||
|
```bash
|
||||||
|
# 清理编译
|
||||||
|
cd /home/along/MyCode/wanjiabuluo/fundplatform/fundplatform
|
||||||
|
mvn clean compile -DskipTests
|
||||||
|
|
||||||
|
# 安装到本地仓库
|
||||||
|
mvn clean install -DskipTests
|
||||||
|
|
||||||
|
# 编译单个模块
|
||||||
|
mvn clean compile -pl fund-sys -am -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 禁止的操作
|
||||||
|
- ❌ 不要在 run_in_terminal 中重复 `cd` 到同一目录
|
||||||
|
- ❌ 不要使用 `mvn clean` 后立即执行其他命令(应分开执行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Git 规范
|
||||||
|
|
||||||
|
### 10.1 分支策略
|
||||||
|
- `main` - 生产分支
|
||||||
|
- `develop` - 开发分支
|
||||||
|
- `feature/*` - 功能分支
|
||||||
|
- `bugfix/*` - 修复分支
|
||||||
|
|
||||||
|
### 10.2 提交规范
|
||||||
|
```
|
||||||
|
feat: 新功能
|
||||||
|
fix: 修复bug
|
||||||
|
refactor: 重构
|
||||||
|
docs: 文档更新
|
||||||
|
style: 代码格式调整
|
||||||
|
test: 测试相关
|
||||||
|
chore: 构建/工具链相关
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 常见陷阱清单
|
||||||
|
|
||||||
|
### 11.1 Lombok 相关
|
||||||
|
- ✓ 每个子模块必须显式配置 maven-compiler-plugin
|
||||||
|
- ✓ 继承 ServiceImpl 的类不能使用 @Slf4j
|
||||||
|
- ✓ 确保所有 import lombok.* 语句存在
|
||||||
|
|
||||||
|
### 11.2 Shiro 相关
|
||||||
|
- ✓ 使用 Shiro 2.0.0(支持 jakarta.servlet)
|
||||||
|
- ✓ 保留 spring-security-crypto(用于 BCrypt)
|
||||||
|
- ✓ 登录接口路径配置为匿名访问
|
||||||
|
|
||||||
|
### 11.3 Maven 相关
|
||||||
|
- ✓ 避免在 pom.xml 中重复声明依赖
|
||||||
|
- ✓ 父 pom 中未创建的子模块需注释掉
|
||||||
|
- ✓ 使用 `-U` 参数强制更新依赖
|
||||||
|
|
||||||
|
### 11.4 数据库相关
|
||||||
|
- ✓ 初始化 SQL 主键必须与字段定义一致
|
||||||
|
- ✓ tenant_id 必须有默认值保障机制
|
||||||
|
- ✓ 软删除字段 deleted 默认为 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 重新生成代码的策略
|
||||||
|
|
||||||
|
### 12.1 代码生成工具选择
|
||||||
|
**选项 A**: MyBatis-Plus 代码生成器
|
||||||
|
- 优点: 自动生成 Entity、Mapper、Service、Controller
|
||||||
|
- 缺点: 需要手动调整生成的代码
|
||||||
|
|
||||||
|
**选项 B**: 手动编写
|
||||||
|
- 优点: 完全可控,符合规范
|
||||||
|
- 缺点: 工作量大
|
||||||
|
|
||||||
|
**选项 C**: 模板化生成
|
||||||
|
- 优点: 可重复使用,质量稳定
|
||||||
|
- 缺点: 需要先建立模板
|
||||||
|
|
||||||
|
### 12.2 推荐策略
|
||||||
|
1. **第一步**: 手动编写核心基础类(BaseEntity、Result、自定义异常)
|
||||||
|
2. **第二步**: 使用 MyBatis-Plus 生成器生成基础 CRUD
|
||||||
|
3. **第三步**: 手动完善业务逻辑和复杂查询
|
||||||
|
4. **第四步**: 编写单元测试验证
|
||||||
|
|
||||||
|
### 12.3 生成顺序
|
||||||
|
```
|
||||||
|
1. fund-common (基础类)
|
||||||
|
├── BaseEntity
|
||||||
|
├── Result
|
||||||
|
└── 工具类
|
||||||
|
|
||||||
|
2. fund-sys (系统模块)
|
||||||
|
├── 实体类 (Entity)
|
||||||
|
├── Mapper 接口
|
||||||
|
├── Service 实现
|
||||||
|
├── Controller
|
||||||
|
└── Shiro 配置
|
||||||
|
|
||||||
|
3. fund-gateway (网关)
|
||||||
|
└── 路由配置
|
||||||
|
|
||||||
|
4. fund-cust (客户模块)
|
||||||
|
5. fund-proj (项目模块)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 架构约束
|
||||||
|
|
||||||
|
### 13.1 必须遵守的架构要求
|
||||||
|
1. ✅ 使用 Shiro 作为认证框架(架构文档要求)
|
||||||
|
2. ✅ 实现多租户数据隔离
|
||||||
|
3. ✅ Header 透传(X-Uid, X-Uname)
|
||||||
|
4. ✅ 全链路日志追踪
|
||||||
|
|
||||||
|
### 13.2 性能要求
|
||||||
|
- 接口响应时间 < 500ms
|
||||||
|
- 数据库查询使用索引
|
||||||
|
- 使用 Redis 缓存热点数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. 下一步行动
|
||||||
|
|
||||||
|
### 14.1 准备工作
|
||||||
|
- [ ] 备份当前代码
|
||||||
|
- [ ] 清理所有 target 目录
|
||||||
|
- [ ] 确认数据库表结构
|
||||||
|
- [ ] 准备初始化 SQL 脚本
|
||||||
|
|
||||||
|
### 14.2 代码生成
|
||||||
|
- [ ] 生成 fund-common 基础类
|
||||||
|
- [ ] 生成 fund-sys 实体类和 Mapper
|
||||||
|
- [ ] 实现 Shiro 认证框架
|
||||||
|
- [ ] 编写核心业务逻辑
|
||||||
|
|
||||||
|
### 14.3 验证
|
||||||
|
- [ ] 编译通过(mvn clean compile)
|
||||||
|
- [ ] 启动成功
|
||||||
|
- [ ] 登录功能正常
|
||||||
|
- [ ] 基础 CRUD 接口正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意**: 本规则清单将持续更新,每次遇到问题都会添加到"常见陷阱清单"中。
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user