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 配置
|
||||
INSTALL_MODE=service
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USERNAME=root
|
||||
@ -10,11 +11,14 @@ MYSQL_PASSWORD=zjf@123456
|
||||
MYSQL_AUTH_PLUGIN=caching_sha2_password
|
||||
|
||||
# Redis 配置
|
||||
INSTALL_MODE=service
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=zjf@123456
|
||||
|
||||
# Nacos 配置
|
||||
INSTALL_PATH=/home/along/MyApp/nacos
|
||||
INSTALL_MODE=local
|
||||
NACOS_HOST=localhost
|
||||
NACOS_PORT=8848
|
||||
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) => {
|
||||
return request({
|
||||
url: '/auth/login',
|
||||
url: '/sys/api/v1/auth/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@ -10,7 +10,7 @@ export const login = (data) => {
|
||||
|
||||
export const refreshToken = (refreshToken) => {
|
||||
return request({
|
||||
url: '/auth/refresh',
|
||||
url: '/sys/api/v1/auth/refresh',
|
||||
method: 'post',
|
||||
data: { refreshToken }
|
||||
})
|
||||
@ -18,7 +18,7 @@ export const refreshToken = (refreshToken) => {
|
||||
|
||||
export const logout = () => {
|
||||
return request({
|
||||
url: '/auth/logout',
|
||||
url: '/sys/api/v1/auth/logout',
|
||||
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