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:
zhangjf 2026-02-17 09:19:14 +08:00
parent efd1810e11
commit 32abc57338
16 changed files with 2765 additions and 4 deletions

4
.env
View File

@ -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

View 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
View 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;
}
}
}

View 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

View 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

View 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
}
}

View File

@ -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'
})
}

View 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 }
})
}

View 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 }
})
}

View 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>

View 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>

View 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

View 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
View 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
View 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 接口正常
---
**注意**: 本规则清单将持续更新,每次遇到问题都会添加到"常见陷阱清单"中。