From fffeaa48a533af6b7cc9799894aa7500bcaaba94 Mon Sep 17 00:00:00 2001 From: zhangjf Date: Mon, 2 Mar 2026 07:31:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor(docs):=E6=B8=85=E7=90=86=E6=97=A7?= =?UTF-8?q?=E7=89=88=E6=96=87=E6=A1=A3=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除过时的中文命名文档文件 -统一使用英文命名的规范文档 -保持文档目录整洁 --- doc/资金服务平台 FundPlatform 功能清单.md | 263 -- doc/资金服务平台 FundPlatform 架构设计文档.md | 4143 ----------------- doc/资金服务平台 FundPlatform 需求文档.md | 794 ---- 3 files changed, 5200 deletions(-) delete mode 100644 doc/资金服务平台 FundPlatform 功能清单.md delete mode 100644 doc/资金服务平台 FundPlatform 架构设计文档.md delete mode 100644 doc/资金服务平台 FundPlatform 需求文档.md diff --git a/doc/资金服务平台 FundPlatform 功能清单.md b/doc/资金服务平台 FundPlatform 功能清单.md deleted file mode 100644 index fec5d4e..0000000 --- a/doc/资金服务平台 FundPlatform 功能清单.md +++ /dev/null @@ -1,263 +0,0 @@ -## 四、功能模块分解 - -### 4.1 系统管理模块 - -#### 4.1.1 用户管理 -- 用户注册/登录/注销 -- 用户信息维护(姓名、手机号、邮箱、部门等) -- 角色权限管理(管理员、财务、项目经理、普通员工) -- 操作日志记录与查询 ✅ - - **实现状态**:✅ 已完成 - - 后端:OperationLogMapper + OperationLogService + OperationLogController - - 前端:operationLog.js + operationLog.vue - - 功能: - * 分页查询(支持模块、操作类型、操作人、时间范围过滤) - * 查询详情(完整展示请求参数、响应结果、错误信息) - * 删除单条日志 - * 批量删除(删除N天前的所有日志,支持7-365天) - * 操作类型:查询/新增/更新/删除/导入/导出/登录/登出 - * 执行时长监控(超时标红>1000ms) - * 状态展示(成功/失败) - -#### 4.1.2 组织架构管理 ✅ -- 部门管理(增删改查、层级关系) ✅ - - **实现状态**:✅ 已完成 - - 后端:DeptMapper + DeptService + DeptController - - 前端:dept.js + dept.vue - - 功能: - * 获取部门树(树形结构展示) - * 获取部门列表(扁平结构) - * 查询部门详情 - * 新增部门(支持顶级部门和子部门) - * 更新部门信息 - * 删除部门(含子部门校验) - * 部门层级管理(自动计算层级) - * 负责人管理(姓名+电话) - * 状态管理(启用/禁用) - * 排序功能 -- 岗位管理(岗位定义、职责描述) ✅ - - **实现状态**:✅ 已完成 - - 后端:PostMapper + PostService + PostController - - 前端:post.js + post.vue - - 功能: - * 分页查询(支持编码、名称、部门、状态过滤) - * 按部门查询岗位列表 - * 获取所有启用的岗位 - * 新增/编辑/删除岗位 - * 状态管理(启用/禁用) - * 岗位职责、岗位要求管理 - * 部门树选择器 -- 人员分配(部门人员配置) ✅ - - **实现状态**:✅ 已完成 - - 后端:UserAssignmentVO + UserAssignmentService + UserAssignmentController - - 前端:userAssignment.js + userAssignment.vue - - 功能: - * 分页查询用户分配列表(支持用户名、姓名、部门、岗位、状态过滤) - * 按部门查询用户列表 - * 分配用户到部门和岗位(支持单独分配) - * 批量分配用户 - * 移除用户分配 - * 仅显示未分配用户筛选 - * 部门树选择器、岗位下拉选择 - * 职位管理 - -#### 4.1.3 文件管理 ✅ -- 文件上传(合同附件、收款凭证、支出凭证) ✅ -- 文件列表管理 ✅ -- 文件预览(图片、PDF) ✅ -- 文件下载 ✅ -- 文件删除 ✅ -- **实现状态**:✅ 已完成 - - 后端:FileRecord + FileRecordMapper + FileService + FileController - - 前端:file.js + file.vue - - 功能: - * 文件上传(支持多类型:图片、PDF、Office、文本) - * 文件验证(类型白名单、大小限制50MB) - * 本地存储(支持扩展至COS/OSS) - * 按日期分目录存储 - * UUID重命名防冲突 - * 业务关联(合同/收款/支出/其他) - * 分页查询、按业务查询 - * 图片/PDF预览 - * 文件下载 - * 物理+逻辑双删除 - -#### 4.1.4 系统配置 -- 基础参数设置(公司信息、币种、日期格式等) - ---- - -### 4.2 客户管理模块 - -#### 4.2.1 客户信息管理 -- 客户档案创建/编辑/删除/禁用 -- 客户分类管理(按行业、规模、等级等) -- 客户联系人管理(联系人信息、联系方式) - - ---- - -### 4.3 项目管理模块 - -#### 4.3.1 项目信息管理 -- 项目创建/编辑/归档/删除 -- 项目基本信息维护(项目名称、编号、负责人、开始/结束日期) -- 项目状态管理(筹备中、进行中、已完成、已归档) - -#### 4.3.2 项目关联管理 ✅ -- 客户关联(项目所属客户) -- 团队成员分配(项目经理、开发人员、财务等) -- **实现状态**:✅ 已完成 - - 后端:ProjectMemberMapper + ProjectMemberService + ProjectMemberController - - 前端:projectMember.js + projectMember.vue - - 功能: - * 按项目查询成员列表 - * 按用户查询项目列表 - * 添加/编辑/移除成员 - * 角色管理(项目经理/开发/测试/财务/普通成员) - * 状态管理(在职/已离开) - * 工作量占比管理 - - ---- - -### 4.4 需求清单管理模块 - -#### 4.4.1 需求工单信息管理 -- 需求工单创建/编辑/删除 -- 需求工单详情维护: - - 需求工单名称 - - 需求描述 - - 所属客户 - - 所属项目 - - 开发工时 - - 交付日期 - - 应收款金额 - - 应收款日期 -- 需求状态管理(待开发、开发中、待交付、已完成) - -#### 4.4.2 应收款管理 -- 应收款金额设置 -- 应收款日期管理 -- 交付日期跟踪与提醒 - - -### 4.5 支出类型管理模块 - -#### 4.5.1 支出分类管理 -- 支出类型创建/编辑/删除 -- 支出类型层级管理(一级分类、二级分类) -- 常见支出类型示例: - - 人力成本(工资、奖金、社保) - - 办公费用(房租、水电、办公用品) - - 差旅费用(交通、住宿、餐饮) - - 采购费用(设备、软件、服务) - - 其他费用 - ---- - -### 4.6 支出管理模块 - -#### 4.6.1 支出申请 -- 支出录入字段: - - 支出金额 - - 支出类型 - - 支出事由 - - 支出日期 - - 所属项目 - - 申请人 - - 附件上传(发票、合同等) - -#### 4.6.3 支出执行 -- 付款操作(确认付款、付款日期) -- 付款凭证管理(付款截图、银行回单) -- 付款状态更新(待付款、已付款、已核销) - -#### 4.6.4 支出状态管理 -- 标记完成(确认支出已完成) -- 作废处理(支出作废、原因记录) -- 退款管理(退款申请、退款记录) - -#### 4.6.5 支出统计分析 ✅ -- 支出明细查询(多条件筛选) ✅ -- 支出趋势分析(月度、季度、年度) ✅ -- **实现状态**:✅ 已完成 - - 后端:DashboardVO + DashboardService + DashboardController - - 前端:dashboard.js + dashboard/index.vue - - 功能: - * 概览数据:项目数、客户数、合同数、需求工单数 - * 收支统计:总收入、总支出、净利润、应收款、逾期金额 - * 本月数据:本月收入/支出、新增项目/客户 - * 趋势图表:收支趋势折线图(最近12个月) - * 分布图表:项目状态分布饼图 - * 分布图表:支出类型分布饼图 - * 分布图表:应收款状态分布饼图 - * ECharts可视化:折线图、环形饼图 - * 响应式设计:窗口大小变化自动重绘 - ---- - -### 4.7 应收款管理模块 - -#### 4.7.1 应收款确认 -- 应收款生成(从需求清单自动生成) -- 应收款金额确认(确认应收金额) -- 应收款日期确认(确认应收日期) - -#### 4.7.2 收款管理 ✅ -- 收款记录录入: - - 实际收款金额 - - 收款日期 - - 收款方式(银行转账、现金、支票等) - - 收款凭证(上传凭证照片) -- 收款方式管理(维护常用收款方式) -- 收款凭证管理(凭证归档、查询) -- **实现状态**:✅ 已完成 - - 后端:ReceiptMapper + ReceiptService + ReceiptController - - 前端:receipt.js + receipt.vue - - 功能:分页查询、新增、编辑、删除、应收款关联 - ---- - -### 4.10 移动端模块 (H5) - -#### 4.10.1 移动端首页 -- 数据概览(今日收支、待收款) -- 快捷入口(快速录入) - -#### 4.10.3 移动查询 -- 收支查询(个人收支、项目收支) -- 项目查询(项目进度、项目收支) -- 客户查询(客户信息、客户往来) - -#### 4.10.4 移动录入 -- 支出录入(快速录入支出申请) -- 收款录入(现场收款记录) - ---- - -## 五、技术架构建议 - -### 5.1 后端架构 - -| 组件 | 技术选型 | 说明 | -| ------------ | ------------------------- | ---------------------------------- | -| **应用框架** | Spring Cloud Alibaba + nacos | Java生态,成熟稳定,适合企业级应用 | -| **数据库** | MySQL 8.0 | 支持事务、ACID,数据持久化 | -| **缓存** | Redis 7.x | 会话管理、热点数据缓存 | -| **文件存储** | 腾讯COS | 文件上传、附件存储 | -| **定时任务** | XXL-JOB | 定时提醒、数据统计 | -| **API文档** | Swagger / Knife4j | 接口文档自动生成 | - -### 5.2 前端架构 - -| 端 | 技术栈 | 说明 | -| ------------ | --------------------------------- | ----------------------------------- | -| **管理后台** | Vue 3 + TypeScript + Element Plus | 响应式设计,组件丰富 | -| **移动端** | Vue 3 + Vite 5 + Vant 4 | 移动端H5响应式应用 | -| **图表库** | ECharts 5.x | 数据可视化、报表展示 | -| **构建工具** | Vite 4.x | 快速构建、热更新 | - -### 5.3 - -**文档结束** diff --git a/doc/资金服务平台 FundPlatform 架构设计文档.md b/doc/资金服务平台 FundPlatform 架构设计文档.md deleted file mode 100644 index 9bb4c7f..0000000 --- a/doc/资金服务平台 FundPlatform 架构设计文档.md +++ /dev/null @@ -1,4143 +0,0 @@ -# 资金服务平台 (FundPlatform) - 架构设计文档 - -> **文档版本**: v1.8 -> **创建日期**: 2026-02-13 -> **更新日期**: 2026-02-23 -> **项目名称**: 资金服务平台 -> **项目代号**: fundplatform - ---- - -## 一、架构设计概述 - -### 1.1 设计目标 - -| 目标 | 描述 | -|------|------| -| **高可用性** | 系统可用性≥99.5%,支持故障自动恢复 | -| **高性能** | 支持≥100人并发,接口响应<3秒 | -| **可扩展性** | 微服务架构,支持水平扩展 | -| **安全性** | 数据加密传输,完善的权限控制 | -| **可维护性** | 模块化设计,代码结构清晰 | -| **多租户** | 支持一库多租户和一库一租户两种模式 | -| **可观测性** | 全链路日志跟踪,支持 Head 日志追踪 | - -### 1.2 架构风格 - -采用 **微服务架构** + **前后端分离** + **多租户架构** 模式: -- 后端:Spring Cloud Alibaba 微服务框架 -- 前端:Vue 3 + Element Plus 管理后台 -- 移动端:Vue 3 + Vite 5 + Vant 4 移动端应用 -- 数据层:MySQL + Redis 缓存(支持多租户隔离) -- 基础设施:Nacos 服务治理、Nginx 负载均衡 -- 可观测性:Head 日志追踪 + 全链路监控 - ---- - -## 二、多租户架构设计 - -### 2.1 多租户架构概述 - -系统支持两种多租户模式,可根据业务需求灵活选择: - -| 模式 | 说明 | 适用场景 | -|------|------|----------| -| **一库多租户** | 所有租户共享一个数据库,通过 `tenant_id` 字段隔离 | 中小租户、数据量较小、成本敏感 | -| **一库一租户** | 每个租户独立数据库,物理隔离 | 大客户、数据量大、安全要求高 | - -### 2.2 一库多租户模式 - -#### 2.2.1 架构设计 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 应用服务层 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ fund-sys │ │ fund-cust │ │ fund-proj │ │ -│ │ (多租户) │ │ (多租户) │ │ (多租户) │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -└─────────┼────────────────┼────────────────┼─────────────────┘ - │ │ │ - └────────────────┴────────────────┘ - │ - ┌───────────────┴───────────────┐ - │ TenantContextHolder │ - │ (线程级租户上下文存储) │ - └───────────────┬───────────────┘ - │ -┌─────────────────────────┼─────────────────────────────────────┐ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ MySQL 数据库 │ │ -│ │ ┌─────────────────────────────────────────────────┐ │ │ -│ │ │ fund_platform 库 │ │ │ -│ │ │ ┌───────────┬───────────┬───────────┬─────────┐ │ │ │ -│ │ │ │ tenant_id │ user_id │ username │ ... │ │ │ │ -│ │ │ ├───────────┼───────────┼───────────┼─────────┤ │ │ │ -│ │ │ │ 1 │ 1 │ admin │ ... │ │ │ │ -│ │ │ │ 1 │ 2 │ user1 │ ... │ │ │ │ -│ │ │ │ 2 │ 1 │ admin │ ... │ │ │ │ -│ │ │ └───────────┴───────────┴───────────┴─────────┘ │ │ │ -│ │ │ 所有表包含 tenant_id 字段用于数据隔离 │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└───────────────────────────────────────────────────────────────┘ -``` - -#### 2.2.2 技术实现 - -**1. 租户识别** - -```java -/** - * 租户上下文持有者 - */ -public class TenantContextHolder { - private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>(); - - public static void setTenantId(Long tenantId) { - CURRENT_TENANT.set(tenantId); - } - - public static Long getTenantId() { - return CURRENT_TENANT.get(); - } - - public static void clear() { - CURRENT_TENANT.remove(); - } -} -``` - -**2. 租户拦截器** - -```java -/** - * 租户拦截器 - 从请求头或JWT中提取租户ID - */ -@Component -public class TenantInterceptor implements HandlerInterceptor { - - @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) { - // 从请求头获取租户ID - String tenantId = request.getHeader("X-Tenant-Id"); - - // 或从JWT Token中解析 - if (StringUtils.isEmpty(tenantId)) { - String token = request.getHeader("Authorization"); - tenantId = JwtUtil.getTenantId(token); - } - - if (StringUtils.isNotEmpty(tenantId)) { - TenantContextHolder.setTenantId(Long.valueOf(tenantId)); - } - - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, - HttpServletResponse response, - Object handler, Exception ex) { - TenantContextHolder.clear(); - } -} -``` - -**3. MyBatis-Plus 租户插件** - -```java -/** - * 多租户 SQL 拦截器 - */ -@Component -public class TenantLineInnerInterceptor implements InnerInterceptor { - - @Override - public void beforeQuery(Executor executor, - MappedStatement ms, - Object parameter, - RowBounds rowBounds, - ResultHandler resultHandler, - BoundSql boundSql) { - // 获取当前租户ID - Long tenantId = TenantContextHolder.getTenantId(); - if (tenantId == null) { - return; - } - - // 获取SQL - String originalSql = boundSql.getSql(); - - // 忽略特定表(如租户表本身) - if (isIgnoreTable(ms.getId())) { - return; - } - - // 添加租户条件 - String tenantSql = addTenantCondition(originalSql, tenantId); - - // 重写SQL - reflectSetField(boundSql, "sql", tenantSql); - } - - private String addTenantCondition(String sql, Long tenantId) { - // 使用 JSqlParser 解析并修改 SQL - try { - Statement statement = CCJSqlParserUtil.parse(sql); - if (statement instanceof Select) { - Select select = (Select) statement; - PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); - - // 添加 WHERE 条件 - EqualsTo equalsTo = new EqualsTo(); - equalsTo.setLeftExpression(new Column("tenant_id")); - equalsTo.setRightExpression(new LongValue(tenantId)); - - Expression where = plainSelect.getWhere(); - if (where == null) { - plainSelect.setWhere(equalsTo); - } else { - AndExpression and = new AndExpression(where, equalsTo); - plainSelect.setWhere(and); - } - - return select.toString(); - } - } catch (JSQLParserException e) { - log.error("SQL解析失败", e); - } - return sql; - } -} -``` - -**4. 实体基类** - -```java -/** - * 多租户实体基类 - */ -@Data -public abstract class TenantBaseEntity extends BaseEntity { - - @TableField(fill = FieldFill.INSERT) - private Long tenantId; -} -``` - -#### 2.2.3 数据隔离策略 - -| 隔离级别 | 实现方式 | 说明 | -|----------|----------|------| -| **行级隔离** | tenant_id 字段过滤 | 默认方式,所有业务表包含 tenant_id | -| **schema隔离** | 不同租户使用不同schema | 可选,适用于数据量大的租户 | -| **缓存隔离** | Redis Key 添加租户前缀 | `tenant:{id}:user:info` | -| **文件隔离** | COS 目录按租户划分 | `/tenant/{id}/files/` | - -### 2.3 一库一租户模式 - -#### 2.3.1 架构设计 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 应用服务层 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ fund-sys │ │ fund-cust │ │ fund-proj │ │ -│ │ (多租户) │ │ (多租户) │ │ (多租户) │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -└─────────┼────────────────┼────────────────┼─────────────────┘ - │ │ │ - └────────────────┴────────────────┘ - │ - ┌───────────────┴───────────────┐ - │ DynamicDataSource │ - │ (动态数据源路由) │ - └───────────────┬───────────────┘ - │ - ┌───────────────┼───────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ tenant_1_db │ │ tenant_2_db │ │ tenant_3_db │ -│ (租户1数据库) │ │ (租户2数据库) │ │ (租户3数据库) │ -│ │ │ │ │ │ -│ • sys_user │ │ • sys_user │ │ • sys_user │ -│ • customer │ │ • customer │ │ • customer │ -│ • project │ │ • project │ │ • project │ -│ • expense │ │ • expense │ │ • expense │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -#### 2.3.2 技术实现 - -**1. 动态数据源配置** - -```java -/** - * 动态数据源上下文 - */ -public class DynamicDataSourceContextHolder { - private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); - - public static void setDataSourceKey(String key) { - CONTEXT_HOLDER.set(key); - } - - public static String getDataSourceKey() { - return CONTEXT_HOLDER.get(); - } - - public static void clear() { - CONTEXT_HOLDER.remove(); - } -} - -/** - * 动态数据源 - */ -public class DynamicDataSource extends AbstractRoutingDataSource { - - @Override - protected Object determineCurrentLookupKey() { - return DynamicDataSourceContextHolder.getDataSourceKey(); - } -} -``` - -**2. 数据源管理器** - -```java -/** - * 租户数据源管理器 - */ -@Component -public class TenantDataSourceManager { - - @Autowired - private DynamicDataSource dynamicDataSource; - - @Autowired - private TenantDataSourceConfigRepository configRepository; - - /** - * 添加租户数据源 - */ - public void addDataSource(Long tenantId) { - TenantDataSourceConfig config = configRepository.findByTenantId(tenantId); - - HikariConfig hikariConfig = new HikariConfig(); - hikariConfig.setDriverClassName(config.getDriverClassName()); - hikariConfig.setJdbcUrl(config.getJdbcUrl()); - hikariConfig.setUsername(config.getUsername()); - hikariConfig.setPassword(config.getPassword()); - - // HikariCP 优化配置 - hikariConfig.setPoolName("TenantPool-" + tenantId); - hikariConfig.setMinimumIdle(5); - hikariConfig.setMaximumPoolSize(20); - hikariConfig.setIdleTimeout(300000); - hikariConfig.setConnectionTimeout(20000); - hikariConfig.setMaxLifetime(1200000); - hikariConfig.setConnectionTestQuery("SELECT 1"); - - // 性能优化配置 - hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); - hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); - hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - hikariConfig.addDataSourceProperty("useServerPrepStmts", "true"); - hikariConfig.addDataSourceProperty("useLocalSessionState", "true"); - hikariConfig.addDataSourceProperty("rewriteBatchedStatements", "true"); - hikariConfig.addDataSourceProperty("cacheResultSetMetadata", "true"); - hikariConfig.addDataSourceProperty("cacheServerConfiguration", "true"); - hikariConfig.addDataSourceProperty("elideSetAutoCommits", "true"); - hikariConfig.addDataSourceProperty("maintainTimeStats", "false"); - - HikariDataSource dataSource = new HikariDataSource(hikariConfig); - - // 添加到动态数据源 - dynamicDataSource.addDataSource("tenant_" + tenantId, dataSource); - } - - /** - * 切换租户数据源 - */ - public void switchDataSource(Long tenantId) { - String key = "tenant_" + tenantId; - - // 检查数据源是否存在 - if (!dynamicDataSource.hasDataSource(key)) { - addDataSource(tenantId); - } - - DynamicDataSourceContextHolder.setDataSourceKey(key); - } -} -``` - -**3. 租户数据源拦截器** - -```java -/** - * 租户数据源切换拦截器 - */ -@Component -public class TenantDataSourceInterceptor implements HandlerInterceptor { - - @Autowired - private TenantDataSourceManager dataSourceManager; - - @Override - public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, - Object handler) { - // 获取租户ID - String tenantId = request.getHeader("X-Tenant-Id"); - - if (StringUtils.isNotEmpty(tenantId)) { - // 切换数据源 - dataSourceManager.switchDataSource(Long.valueOf(tenantId)); - } - - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, - HttpServletResponse response, - Object handler, Exception ex) { - DynamicDataSourceContextHolder.clear(); - } -} -``` - -### 2.4 租户管理 - -#### 2.4.1 租户表设计 - -```sql -CREATE TABLE sys_tenant ( - tenant_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID', - tenant_code VARCHAR(50) NOT NULL UNIQUE COMMENT '租户编码', - tenant_name VARCHAR(100) NOT NULL COMMENT '租户名称', - tenant_type TINYINT DEFAULT 1 COMMENT '租户类型:1-一库多租户,2-一库一租户', - db_config JSON COMMENT '数据库配置(一库一租户模式使用)', - status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用', - expire_time DATETIME COMMENT '过期时间', - created_time DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_code (tenant_code), - INDEX idx_status (status) -) COMMENT='租户表'; -``` - -#### 2.4.2 租户创建流程 - -``` -┌─────────┐ 创建租户 ┌─────────┐ -│ 管理员 │ ───────────────> │ 系统 │ -└─────────┘ └────┬────┘ - │ - ┌─────────────┼─────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌───────────┐ ┌───────────┐ ┌───────────┐ - │ 保存租户 │ │ 初始化数据 │ │ 创建数据源 │ - │ 基本信息 │ │ (菜单/角色)│ │ (一库一租户)│ - └───────────┘ └───────────┘ └───────────┘ -``` - -### 2.5 一库一租户路由方案对比 - -#### 2.5.1 方案对比:DynamicDataSource vs Feign 动态路由 - -| 对比维度 | DynamicDataSource | Feign 动态路由 | -|----------|-------------------|----------------| -| **架构层级** | 数据层 | 服务层 | -| **隔离级别** | 数据源级别 | 服务实例级别 | -| **资源占用** | 高(连接池 × N租户) | 中(服务实例 × N租户) | -| **扩展性** | 差(单服务多数据源) | 优(独立扩容) | -| **故障隔离** | 差(单点故障影响多租户) | 优(租户间互不影响) | -| **跨租户查询** | 困难 | 可通过聚合服务实现 | -| **服务治理** | 简单 | 需要完善的服务发现 | -| **部署复杂度** | 低 | 中 | -| **私有化部署** | 困难 | 简单(整体迁移) | -| **运维成本** | 中 | 高 | - -#### 2.5.2 推荐方案:Feign 动态路由 - -**推荐理由:** -1. **真正的租户隔离**:每个租户拥有独立的服务实例和数据库 -2. **弹性扩容**:单个租户可独立水平扩展 -3. **故障隔离**:单个租户故障不影响其他租户 -4. **私有化友好**:便于大客户私有化部署 -5. **符合云原生**:与 Kubernetes 等容器平台配合良好 - ---- - -### 2.6 Feign 动态路由详细设计 - -#### 2.6.1 架构设计 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Feign 动态路由架构 │ -│ │ -│ ┌─────────┐ │ -│ │ 客户端 │ Header: X-Tenant-Id: tenant_001 │ -│ └────┬────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Gateway (网关) │ │ -│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ -│ │ │ TenantRoutingFilter │ │ │ -│ │ │ • 解析 X-Tenant-Id │ │ │ -│ │ │ • 根据租户ID路由到对应服务组 │ │ │ -│ │ │ • 添加 X-Tenant-Route 标记 │ │ │ -│ │ └─────────────────────────────────────────────────────────────┘ │ │ -│ └────────────────────────────────┬───────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────────┼───────────────────────────┐ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Nacos 服务注册中心 │ │ -│ │ │ │ -│ │ 服务分组设计: │ │ -│ │ • fund-sys:DEFAULT (共享服务组) │ │ -│ │ • fund-sys:TENANT_001 (租户001专属) │ │ -│ │ • fund-sys:TENANT_002 (租户002专属) │ │ -│ │ • fund-cust:TENANT_001 │ │ -│ │ • ... │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────────┼───────────────────────────┐ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 租户001服务组 │ │ 租户002服务组 │ │ 租户003服务组 │ │ -│ │ │ │ │ │ │ │ -│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │ -│ │ │fund-sys │ │ │ │fund-sys │ │ │ │fund-sys │ │ │ -│ │ │:8100 │ │ │ │:8101 │ │ │ │:8102 │ │ │ -│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │ -│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │ -│ │ │fund-cust │ │ │ │fund-cust │ │ │ │fund-cust │ │ │ -│ │ │:8110 │ │ │ │:8111 │ │ │ │:8112 │ │ │ -│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │ -│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │ -│ │ │fund-proj │ │ │ │fund-proj │ │ │ │fund-proj │ │ │ -│ │ │:8120 │ │ │ │:8121 │ │ │ │:8122 │ │ │ -│ │ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ └─────────────────────────┼─────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────────┼───────────────────────────┐ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │tenant_001│ │tenant_002│ │tenant_003│ │ -│ │ _db │ │ _db │ │ _db │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -#### 2.6.2 核心组件实现 - -**1. 租户路由元数据管理** - -```java -/** - * 租户路由元数据 - */ -@Data -@Component -@ConfigurationProperties(prefix = "tenant.routing") -public class TenantRoutingProperties { - - /** 是否启用租户路由 */ - private boolean enabled = true; - - /** 租户ID请求头 */ - private String tenantHeader = "X-Tenant-Id"; - - /** 服务组分隔符 */ - private String groupSeparator = "TENANT_"; - - /** 共享服务列表(不区分租户) */ - private List sharedServices = Arrays.asList("fund-gateway", "fund-report"); - - /** 租户服务配置 */ - private Map tenantConfigs = new HashMap<>(); -} - -/** - * 租户服务配置 - */ -@Data -public class TenantServiceConfig { - /** 租户ID */ - private String tenantId; - - /** 服务实例列表 */ - private Map services = new HashMap<>(); - - /** 数据库配置 */ - private DatabaseConfig database; -} - -/** - * 服务实例配置 - */ -@Data -public class ServiceInstanceConfig { - /** 服务名 */ - private String serviceName; - - /** 端口号 */ - private int port; - - /** 实例数 */ - private int replicas = 1; -} -``` - -**2. Gateway 租户路由过滤器** - -```java -/** - * Gateway 租户路由过滤器 - * 根据 X-Tenant-Id 路由到对应租户的服务组 - */ -@Component -@Order(-50) -public class TenantRoutingGatewayFilter implements GlobalFilter { - - @Autowired - private TenantRoutingProperties routingProperties; - - @Autowired - private NacosServiceManager nacosServiceManager; - - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - ServerHttpRequest request = exchange.getRequest(); - String tenantId = request.getHeaders().getFirst(routingProperties.getTenantHeader()); - - // 未指定租户或共享服务,使用默认路由 - if (StringUtils.isEmpty(tenantId) || isSharedService(exchange)) { - return chain.filter(exchange); - } - - // 构建租户服务组名称 - String tenantGroup = buildTenantGroup(tenantId); - - // 将租户路由信息存入 Exchange 属性 - exchange.getAttributes().put("tenantId", tenantId); - exchange.getAttributes().put("tenantGroup", tenantGroup); - - // 添加到请求头,传递给下游服务 - ServerHttpRequest mutatedRequest = request.mutate() - .header("X-Tenant-Group", tenantGroup) - .header("X-Tenant-Id", tenantId) - .build(); - - log.info("[TenantRouting] Route request to tenant group: {}, URI: {}", - tenantGroup, request.getURI()); - - return chain.filter(exchange.mutate().request(mutatedRequest).build()); - } - - /** - * 构建租户服务组名称 - */ - private String buildTenantGroup(String tenantId) { - return routingProperties.getGroupSeparator() + tenantId.toUpperCase(); - } - - /** - * 判断是否为共享服务 - */ - private boolean isSharedService(ServerWebExchange exchange) { - String path = exchange.getRequest().getURI().getPath(); - return routingProperties.getSharedServices().stream() - .anyMatch(path::contains); - } -} -``` - -**3. 自定义负载均衡器(按租户路由)** - -```java -/** - * 租户感知的负载均衡器 - */ -public class TenantAwareLoadBalancer implements ReactorServiceInstanceLoadBalancer { - - private final String serviceId; - private final ObjectProvider supplierProvider; - - public TenantAwareLoadBalancer(String serviceId, - ObjectProvider supplierProvider) { - this.serviceId = serviceId; - this.supplierProvider = supplierProvider; - } - - @Override - public Mono> choose(Request request) { - // 从请求上下文获取租户ID - String tenantId = getTenantIdFromRequest(request); - String tenantGroup = buildTenantGroup(tenantId); - - ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable(); - - return supplier.get().next() - .map(instances -> filterByTenantGroup(instances, tenantGroup)) - .map(this::getInstanceResponse); - } - - /** - * 根据租户组过滤服务实例 - */ - private List filterByTenantGroup( - List instances, String tenantGroup) { - - // 优先选择租户专属实例 - List tenantInstances = instances.stream() - .filter(inst -> tenantGroup.equals(inst.getMetadata().get("tenant-group"))) - .collect(Collectors.toList()); - - if (!tenantInstances.isEmpty()) { - return tenantInstances; - } - - // 回退到默认实例 - return instances.stream() - .filter(inst -> !inst.getMetadata().containsKey("tenant-group")) - .collect(Collectors.toList()); - } - - private Response getInstanceResponse(List instances) { - if (instances.isEmpty()) { - return new EmptyResponse(); - } - // 轮询选择 - int index = ThreadLocalRandom.current().nextInt(instances.size()); - return new DefaultResponse(instances.get(index)); - } -} - -/** - * 租户负载均衡配置 - */ -@Configuration -public class TenantLoadBalancerConfig { - - @Bean - public ReactorLoadBalancer tenantAwareLoadBalancer( - Environment environment, - LoadBalancerClientFactory loadBalancerClientFactory) { - - String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); - - return new TenantAwareLoadBalancer(name, - loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class)); - } -} -``` - -**4. Feign 调用链拦截器(租户 + 用户信息传递)** - -```java -/** - * Feign 调用链拦截器 - * 在服务间调用时传递租户信息和当前操作用户信息 - */ -@Component -public class FeignChainInterceptor implements RequestInterceptor { - - @Autowired - private TenantRoutingProperties routingProperties; - - private static final String HEADER_UID = "X-Uid"; - private static final String HEADER_UNAME = "X-Uname"; - private static final String HEADER_TENANT_ID = "X-Tenant-Id"; - private static final String HEADER_TENANT_GROUP = "X-Tenant-Group"; - - @Override - public void apply(RequestTemplate template) { - // 1. 传递租户信息 - String tenantId = TenantContextHolder.getTenantId(); - if (StringUtils.isNotEmpty(tenantId)) { - template.header(HEADER_TENANT_ID, tenantId); - - String tenantGroup = buildTenantGroup(tenantId); - template.header(HEADER_TENANT_GROUP, tenantGroup); - } - - // 2. 传递当前操作用户信息(关键) - Long uid = UserContext.getCurrentUserId(); - String uname = UserContext.getCurrentUsername(); - - if (uid != null) { - template.header(HEADER_UID, String.valueOf(uid)); - log.debug("[FeignChain] Add uid to header: {}", uid); - } - - if (StringUtils.isNotEmpty(uname)) { - template.header(HEADER_UNAME, uname); - log.debug("[FeignChain] Add uname to header: {}", uname); - } - - // 3. 传递 TraceId(保持链路追踪) - String traceId = TraceIdUtil.getCurrentTraceId(); - if (StringUtils.isNotEmpty(traceId)) { - template.header(TraceIdUtil.TRACE_ID_HEADER, traceId); - } - } - - private String buildTenantGroup(String tenantId) { - return routingProperties.getGroupSeparator() + tenantId.toUpperCase(); - } -} -``` - -**5. 用户上下文管理(支持调用链传递)** - -```java -/** - * 用户上下文持有者 - * 存储当前登录用户信息,支持跨服务传递 - * 支持同步和异步场景 - */ -public class UserContext { - - // ========== ThreadLocal 存储(同步场景)========== - private static final ThreadLocal CURRENT_USER_ID = new ThreadLocal<>(); - private static final ThreadLocal CURRENT_USERNAME = new ThreadLocal<>(); - private static final ThreadLocal CURRENT_USER = new ThreadLocal<>(); - - // ========== 用户上下文载体(支持异步传递)========== - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class UserContextHolder { - private Long userId; - private String username; - private String tenantId; - private String traceId; - - public static UserContextHolder fromCurrent() { - return new UserContextHolder( - getCurrentUserId(), - getCurrentUsername(), - TenantContextHolder.getTenantId(), - TraceIdUtil.getCurrentTraceId() - ); - } - } - - // ========== 同步场景方法 ========== - - /** - * 设置当前用户(登录时调用) - */ - public static void setCurrentUser(User user) { - if (user != null) { - CURRENT_USER_ID.set(user.getUserId()); - CURRENT_USERNAME.set(user.getUsername()); - CURRENT_USER.set(user); - - // 同时设置到 MDC,用于日志输出 - MDC.put("uid", String.valueOf(user.getUserId())); - MDC.put("uname", user.getUsername()); - } - } - - /** - * 从请求头解析用户(服务间调用时) - */ - public static void setFromHeaders(String uid, String uname) { - if (StringUtils.isNotEmpty(uid)) { - CURRENT_USER_ID.set(Long.valueOf(uid)); - MDC.put("uid", uid); - } - if (StringUtils.isNotEmpty(uname)) { - CURRENT_USERNAME.set(uname); - MDC.put("uname", uname); - } - } - - public static Long getCurrentUserId() { - return CURRENT_USER_ID.get(); - } - - public static String getCurrentUsername() { - return CURRENT_USERNAME.get(); - } - - public static User getCurrentUser() { - return CURRENT_USER.get(); - } - - /** - * 获取当前操作用户(用于审计日志) - */ - public static String getOperator() { - String uname = CURRENT_USERNAME.get(); - return StringUtils.isNotEmpty(uname) ? uname : "system"; - } - - public static void clear() { - CURRENT_USER_ID.remove(); - CURRENT_USERNAME.remove(); - CURRENT_USER.remove(); - MDC.remove("uid"); - MDC.remove("uname"); - } - - // ========== 异步场景支持 ========== - - /** - * 包装 Runnable,传递用户上下文到异步线程 - */ - public static Runnable wrap(Runnable runnable) { - UserContextHolder holder = UserContextHolder.fromCurrent(); - return () -> { - try { - setFromHolder(holder); - runnable.run(); - } finally { - clear(); - } - }; - } - - /** - * 包装 Callable,传递用户上下文到异步线程 - */ - public static Callable wrap(Callable callable) { - UserContextHolder holder = UserContextHolder.fromCurrent(); - return () -> { - try { - setFromHolder(holder); - return callable.call(); - } finally { - clear(); - } - }; - } - - /** - * 包装 Supplier,传递用户上下文到异步线程 - */ - public static Supplier wrap(Supplier supplier) { - UserContextHolder holder = UserContextHolder.fromCurrent(); - return () -> { - try { - setFromHolder(holder); - return supplier.get(); - } finally { - clear(); - } - }; - } - - /** - * 包装 Function,传递用户上下文到异步线程 - */ - public static Function wrap(Function function) { - UserContextHolder holder = UserContextHolder.fromCurrent(); - return (T t) -> { - try { - setFromHolder(holder); - return function.apply(t); - } finally { - clear(); - } - }; - } - - /** - * 从 Holder 设置上下文 - */ - private static void setFromHolder(UserContextHolder holder) { - if (holder != null) { - if (holder.getUserId() != null) { - CURRENT_USER_ID.set(holder.getUserId()); - MDC.put("uid", String.valueOf(holder.getUserId())); - } - if (StringUtils.isNotEmpty(holder.getUsername())) { - CURRENT_USERNAME.set(holder.getUsername()); - MDC.put("uname", holder.getUsername()); - } - if (StringUtils.isNotEmpty(holder.getTenantId())) { - TenantContextHolder.setTenantId(holder.getTenantId()); - MDC.put("tenantId", holder.getTenantId()); - } - if (StringUtils.isNotEmpty(holder.getTraceId())) { - TraceIdUtil.setTraceId(holder.getTraceId()); - } - } - } - - /** - * 创建 CompletableFuture 上下文包装器 - */ - public static CompletableFuture wrapFuture(Supplier> supplier) { - UserContextHolder holder = UserContextHolder.fromCurrent(); - return supplier.get().whenComplete((result, ex) -> { - // 确保上下文清理 - clear(); - }); - } -} -``` - -**5. 异步场景使用示例** - -```java -/** - * 异步服务示例 - */ -@Service -public class AsyncService { - - @Autowired - private ThreadPoolExecutor executor; - - @Autowired - private ProjectMapper projectMapper; - - /** - * 异步处理 - 方式1:使用包装器 - */ - public void asyncProcessWithWrapper(Long projectId) { - // 包装 Runnable,自动传递当前用户上下文 - executor.execute(UserContext.wrap(() -> { - // 在异步线程中也能获取用户信息 - Long uid = UserContext.getCurrentUserId(); - String uname = UserContext.getCurrentUsername(); - - log.info("[Async] 用户 {} 处理项目 {}", uname, projectId); - - // 数据库操作 - Project project = projectMapper.selectById(projectId); - project.setProcessedBy(uid); - projectMapper.updateById(project); - })); - } - - /** - * 异步处理 - 方式2:使用 CompletableFuture - */ - public CompletableFuture asyncQueryProject(Long projectId) { - return CompletableFuture.supplyAsync( - UserContext.wrap(() -> { - // 异步查询时保持用户上下文 - log.info("[Async] 用户 {} 查询项目 {}", - UserContext.getCurrentUsername(), projectId); - - return projectMapper.selectById(projectId); - }), - executor - ); - } - - /** - * 批量异步处理 - */ - public void batchAsyncProcess(List projectIds) { - List> futures = projectIds.stream() - .map(id -> CompletableFuture.runAsync( - UserContext.wrap(() -> processSingleProject(id)), - executor - )) - .collect(Collectors.toList()); - - // 等待所有任务完成 - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - } - - private void processSingleProject(Long projectId) { - // 每个异步线程都有独立的用户上下文 - Long uid = UserContext.getCurrentUserId(); - String uname = UserContext.getCurrentUsername(); - - log.info("[BatchAsync] 用户 {} 处理项目 {}", uname, projectId); - - // 业务处理... - } - - /** - * 链式异步调用 - */ - public CompletableFuture chainAsyncProcess(Long projectId) { - return CompletableFuture.supplyAsync( - UserContext.wrap(() -> fetchProject(projectId)), executor) - .thenApplyAsync(UserContext.wrap(project -> { - // 转换操作,上下文自动传递 - log.info("[Chain] 用户 {} 转换项目 {}", - UserContext.getCurrentUsername(), project.getProjectName()); - return transformProject(project); - }), executor) - .thenComposeAsync(UserContext.wrap(transformed -> - // 异步保存 - CompletableFuture.supplyAsync( - UserContext.wrap(() -> saveProject(transformed)), executor) - ), executor); - } - - private Project fetchProject(Long id) { /* ... */ return null; } - private Project transformProject(Project p) { /* ... */ return p; } - private String saveProject(Project p) { /* ... */ return "saved"; } -} - -/** - * Spring @Async 支持 - */ -@Configuration -@EnableAsync -public class AsyncConfig implements AsyncConfigurer { - - @Override - @Bean(name = "taskExecutor") - public Executor getAsyncExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(50); - executor.setQueueCapacity(200); - executor.setThreadNamePrefix("async-"); - - // 关键:使用 ContextPropagatingTaskDecorator 传递上下文 - executor.setTaskDecorator(new ContextPropagatingTaskDecorator()); - executor.initialize(); - return executor; - } - - @Override - public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { - return new SimpleAsyncUncaughtExceptionHandler(); - } -} - -/** - * 上下文传递装饰器(用于 @Async) - */ -public class ContextPropagatingTaskDecorator implements TaskDecorator { - - @Override - public Runnable decorate(Runnable runnable) { - // 使用 UserContext 的包装方法 - return UserContext.wrap(runnable); - } -} - -/** - * 使用 @Async 注解 - */ -@Service -public class NotificationService { - - /** - * 异步发送通知(自动传递用户上下文) - */ - @Async("taskExecutor") - public void sendNotificationAsync(Long userId, String message) { - // 在异步方法中也能获取当前操作用户 - String operator = UserContext.getCurrentUsername(); - log.info("[Notification] 用户 {} 发送通知给 {}: {}", operator, userId, message); - - // 记录操作日志 - operationLogService.saveLog(new OperationLog() - .setOperation("发送通知") - .setOperatorId(UserContext.getCurrentUserId()) - .setOperatorName(operator) - .setTargetId(userId) - .setContent(message) - ); - - // 发送通知... - } -} -``` - -**6. 响应式编程支持(WebFlux)** - -```java -/** - * Reactor 上下文工具(用于 WebFlux) - */ -public class ReactiveUserContext { - - private static final String USER_CONTEXT_KEY = "UserContext"; - - /** - * 将用户上下文写入 Reactor Context - */ - public static Mono withUserContext(Mono mono, UserContextHolder holder) { - return mono.contextWrite(Context.of(USER_CONTEXT_KEY, holder)); - } - - /** - * 从 Reactor Context 读取用户上下文 - */ - public static Mono getUserContext() { - return Mono.deferContextual(ctx -> { - if (ctx.hasKey(USER_CONTEXT_KEY)) { - return Mono.just(ctx.get(USER_CONTEXT_KEY)); - } - return Mono.empty(); - }); - } - - /** - * 在响应式链中获取用户ID - */ - public static Mono getUserId() { - return getUserContext().map(UserContextHolder::getUserId); - } - - /** - * 包装响应式操作 - */ - public static Mono wrap(Mono mono) { - UserContextHolder holder = UserContextHolder.fromCurrent(); - return withUserContext(mono, holder); - } -} - -/** - * WebFlux 过滤器 - */ -@Component -public class ReactiveContextFilter implements WebFilter { - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - ServerHttpRequest request = exchange.getRequest(); - - // 解析用户信息 - String uid = request.getHeaders().getFirst("X-Uid"); - String uname = request.getHeaders().getFirst("X-Uname"); - String tenantId = request.getHeaders().getFirst("X-Tenant-Id"); - String traceId = request.getHeaders().getFirst("X-Trace-Id"); - - UserContextHolder holder = new UserContextHolder( - StringUtils.isNotEmpty(uid) ? Long.valueOf(uid) : null, - uname, - tenantId, - traceId - ); - - // 写入 Reactor Context - return chain.filter(exchange) - .contextWrite(Context.of("UserContext", holder)); - } -} -``` - -**5. 统一全局上下文管理(GlobalContext)** - -为了统一管理和传递全局参数(tid、uid、uname、traceId 等),设计一个综合的 GlobalContext: - -```java -/** - * 全局上下文持有者 - * 统一管理租户ID、用户ID、用户名、TraceId等全局参数 - * 支持同步和异步场景 - */ -public class GlobalContext { - - // ========== 全局参数键名 ========== - public static final String KEY_TENANT_ID = "tid"; - public static final String KEY_USER_ID = "uid"; - public static final String KEY_USERNAME = "uname"; - public static final String KEY_TRACE_ID = "traceId"; - public static final String KEY_TENANT_GROUP = "tgroup"; - - // ========== ThreadLocal 存储 ========== - private static final ThreadLocal> CONTEXT = ThreadLocal.withInitial(HashMap::new); - - // ========== 全局上下文载体(支持异步传递)========== - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class ContextSnapshot { - private String tenantId; - private Long userId; - private String username; - private String traceId; - private String tenantGroup; - private Map extra; - - public static ContextSnapshot create() { - return ContextSnapshot.builder() - .tenantId(getTenantId()) - .userId(getUserId()) - .username(getUsername()) - .traceId(getTraceId()) - .tenantGroup(getTenantGroup()) - .extra(new HashMap<>(getAll())) - .build(); - } - - public void apply() { - if (tenantId != null) set(KEY_TENANT_ID, tenantId); - if (userId != null) set(KEY_USER_ID, userId); - if (username != null) set(KEY_USERNAME, username); - if (traceId != null) set(KEY_TRACE_ID, traceId); - if (tenantGroup != null) set(KEY_TENANT_GROUP, tenantGroup); - if (extra != null) extra.forEach((k, v) -> { - if (!CONTEXT.get().containsKey(k)) set(k, v); - }); - } - } - - // ========== 基础操作 ========== - @SuppressWarnings("unchecked") - public static T get(String key) { return (T) CONTEXT.get().get(key); } - - public static void set(String key, Object value) { - CONTEXT.get().put(key, value); - if (value != null) MDC.put(key, String.valueOf(value)); - } - - public static void clear() { - CONTEXT.get().clear(); - MDC.clear(); - } - - // ========== 便捷方法 ========== - public static String getTenantId() { return get(KEY_TENANT_ID); } - public static void setTenantId(String tenantId) { set(KEY_TENANT_ID, tenantId); } - - public static Long getUserId() { - Object uid = get(KEY_USER_ID); - return uid instanceof String ? Long.valueOf((String) uid) : (Long) uid; - } - public static void setUserId(Long userId) { set(KEY_USER_ID, userId); } - - public static String getUsername() { return get(KEY_USERNAME); } - public static void setUsername(String username) { set(KEY_USERNAME, username); } - - public static String getTraceId() { return get(KEY_TRACE_ID); } - public static void setTraceId(String traceId) { set(KEY_TRACE_ID, traceId); } - - // ========== 异步场景支持 ========== - public static ContextSnapshot snapshot() { return ContextSnapshot.create(); } - - public static Runnable wrap(Runnable runnable) { - ContextSnapshot snapshot = snapshot(); - return () -> { try { snapshot.apply(); runnable.run(); } finally { clear(); } }; - } - - public static Callable wrap(Callable callable) { - ContextSnapshot snapshot = snapshot(); - return () -> { try { snapshot.apply(); return callable.call(); } finally { clear(); } }; - } - - public static Supplier wrap(Supplier supplier) { - ContextSnapshot snapshot = snapshot(); - return () -> { try { snapshot.apply(); return supplier.get(); } finally { clear(); } }; - } - - public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) { - return CompletableFuture.supplyAsync(wrap(supplier), executor); - } - - public static CompletableFuture runAsync(Runnable runnable, Executor executor) { - return CompletableFuture.runAsync(wrap(runnable), executor); - } -} -``` - -**6. GlobalContext 使用示例** - -```java -// 同步场景 -@Service -public class ProjectService { - public void createProject(ProjectDTO dto) { - String tid = GlobalContext.getTenantId(); - Long uid = GlobalContext.getUserId(); - String uname = GlobalContext.getUsername(); - - log.info("[{}] 租户 {} 用户 {} 创建项目", tid, uname); - - Project project = new Project(); - project.setTenantId(tid); - project.setCreatedBy(uid); - projectMapper.insert(project); - } -} - -// 异步场景 -@Service -public class AsyncService { - @Autowired private ThreadPoolExecutor executor; - - public void asyncProcess(Long projectId) { - executor.execute(GlobalContext.wrap(() -> { - String tid = GlobalContext.getTenantId(); - Long uid = GlobalContext.getUserId(); - String uname = GlobalContext.getUsername(); - - log.info("[Async] 租户 {} 用户 {} 处理项目 {}", tid, uname, projectId); - - Project project = projectMapper.selectById(projectId); - project.setProcessedBy(uid); - projectMapper.updateById(project); - })); - } - - public CompletableFuture asyncQuery(Long projectId) { - return GlobalContext.supplyAsync(() -> { - log.info("[Async] 用户 {} 查询项目 {}", GlobalContext.getUsername(), projectId); - return projectMapper.selectById(projectId); - }, executor); - } -} -``` - -**原 5. 用户上下文管理(已合并到 GlobalContext)** - -**7. 调用链上下文过滤器(接收方)** - -```java -/** - * 调用链上下文过滤器 - * 接收上游服务传递的租户信息和用户信息 - */ -@Component -@Order(-90) // 在租户过滤器之后,业务过滤器之前 -public class ChainContextFilter implements Filter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - - try { - // 1. 解析租户信息 - String tenantId = httpRequest.getHeader("X-Tenant-Id"); - String tenantGroup = httpRequest.getHeader("X-Tenant-Group"); - - if (StringUtils.isNotEmpty(tenantId)) { - TenantContextHolder.setTenantId(tenantId); - TenantContextHolder.setTenantGroup(tenantGroup); - MDC.put("tenantId", tenantId); - } - - // 2. 解析用户信息(来自上游服务) - String uid = httpRequest.getHeader("X-Uid"); - String uname = httpRequest.getHeader("X-Uname"); - - if (StringUtils.isNotEmpty(uid)) { - UserContext.setFromHeaders(uid, uname); - log.debug("[ChainContext] Received from upstream - uid: {}, uname: {}", uid, uname); - } else { - // 如果没有上游传递,尝试从 Shiro 获取(网关直接访问) - Subject subject = SecurityUtils.getSubject(); - if (subject.isAuthenticated()) { - Long userId = (Long) subject.getPrincipal(); - User user = userService.getById(userId); - if (user != null) { - UserContext.setCurrentUser(user); - } - } - } - - // 3. 解析 TraceId - String traceId = httpRequest.getHeader(TraceIdUtil.TRACE_ID_HEADER); - if (StringUtils.isNotEmpty(traceId)) { - TraceIdUtil.setTraceId(traceId); - } - - chain.doFilter(request, response); - } finally { - // 清理所有上下文 - TenantContextHolder.clear(); - UserContext.clear(); - TraceIdUtil.clear(); - } - } -} -``` - -**8. 调用链传递示意图** - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 服务调用链用户信息传递 │ -│ │ -│ 用户请求 │ -│ │ │ -│ ▼ │ -│ ┌─────────────┐ Headers: │ -│ │ Gateway │ Authorization: Bearer xxx │ -│ │ (网关) │ X-Tenant-Id: tenant_001 │ -│ └──────┬──────┘ │ -│ │ │ -│ │ 解析 Token 获取 uid/uname │ -│ │ 存入 UserContext │ -│ ▼ │ -│ ┌─────────────┐ Headers: │ -│ │ fund-sys │ X-Tenant-Id: tenant_001 │ -│ │ (用户服务) │ X-Tenant-Group: TENANT_001 │ -│ │ │ X-Uid: 1001 ◄── 当前操作用户ID │ -│ │ │ X-Uname: admin ◄── 当前操作用户名 │ -│ │ │ X-Trace-Id: abc123 │ -│ └──────┬──────┘ │ -│ │ │ -│ │ Feign 调用 fund-cust 服务 │ -│ │ FeignChainInterceptor 自动添加 Headers │ -│ ▼ │ -│ ┌─────────────┐ Headers: │ -│ │ fund-cust │ X-Tenant-Id: tenant_001 │ -│ │ (客户服务) │ X-Tenant-Group: TENANT_001 │ -│ │ │ X-Uid: 1001 ◄── 透传用户ID │ -│ │ │ X-Uname: admin ◄── 透传用户名 │ -│ │ │ X-Trace-Id: abc123 │ -│ └──────┬──────┘ │ -│ │ │ -│ │ Feign 调用 fund-proj 服务 │ -│ ▼ │ -│ ┌─────────────┐ Headers: │ -│ │ fund-proj │ X-Tenant-Id: tenant_001 │ -│ │ (项目服务) │ X-Tenant-Group: TENANT_001 │ -│ │ │ X-Uid: 1001 ◄── 持续透传 │ -│ │ │ X-Uname: admin ◄── 持续透传 │ -│ └─────────────┘ │ -│ │ -│ 特点: │ -│ • 每个服务都能获取原始操作用户(uid/uname) │ -│ • 用于操作审计、数据权限控制、日志记录 │ -│ • 无需在每个服务重复解析 Token │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -**9. 使用场景示例(包含异步场景)** - -```java -/** - * 项目服务 - 创建项目 - */ -@Service -public class ProjectServiceImpl implements ProjectService { - - @Autowired - private ProjectMapper projectMapper; - - @Autowired - private OperationLogService logService; - - @Override - @Transactional - public void createProject(ProjectCreateDTO dto) { - // 1. 获取当前操作用户(来自调用链传递) - Long operatorId = UserContext.getCurrentUserId(); - String operatorName = UserContext.getCurrentUsername(); - - // 2. 设置项目创建人 - Project project = new Project(); - project.setProjectName(dto.getProjectName()); - project.setCustomerId(dto.getCustomerId()); - project.setCreatedBy(operatorId); // 使用传递过来的 uid - project.setCreatedTime(LocalDateTime.now()); - - projectMapper.insert(project); - - // 3. 记录操作日志(包含操作人信息) - logService.saveLog(new OperationLog() - .setOperation("创建项目") - .setOperatorId(operatorId) - .setOperatorName(operatorName) // 使用传递过来的 uname - .setTargetId(project.getProjectId()) - .setContent("创建项目:" + dto.getProjectName()) - ); - - log.info("[Project] 用户 {} 创建了项目 {}", operatorName, project.getProjectName()); - } -} - -/** - * 操作日志服务 - 记录日志 - */ -@Service -public class OperationLogServiceImpl implements OperationLogService { - - @Autowired - private OperationLogMapper logMapper; - - @Override - public void saveLog(OperationLog log) { - // 自动补充操作人信息(如果未设置) - if (log.getOperatorId() == null) { - log.setOperatorId(UserContext.getCurrentUserId()); - } - if (StringUtils.isEmpty(log.getOperatorName())) { - log.setOperatorName(UserContext.getCurrentUsername()); - } - - // 补充租户信息 - log.setTenantId(TenantContextHolder.getTenantId()); - - logMapper.insert(log); - } -} -``` - -**9. 日志格式(包含 uid/uname)** - -```xml - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{tenantId}] [%X{uid}] [%X{uname}] %-5level %logger{36} - %msg%n - - - -``` - -输出示例: -``` -2026-02-13 14:30:25.123 [http-nio-8120-exec-1] [abc123def456] [tenant_001] [1001] [admin] INFO c.f.p.s.ProjectService - 用户 admin 创建了项目 资金管理系统 -``` - -**原 4. Feign 租户路由拦截器(已合并到 FeignChainInterceptor)** - - -**10. Nacos 服务注册(带租户标记)** - -```java -/** - * Nacos 租户服务注册配置 - */ -@Configuration -public class NacosTenantRegistrationConfig { - - @Value("${tenant.id:}") - private String tenantId; - - @Bean - public NacosRegistrationCustomizer tenantNacosRegistrationCustomizer() { - return registration -> { - if (StringUtils.isNotEmpty(tenantId)) { - // 添加租户标记到服务元数据 - registration.getMetadata().put("tenant-id", tenantId); - registration.getMetadata().put("tenant-group", "TENANT_" + tenantId.toUpperCase()); - - // 修改服务分组 - registration.setGroup("TENANT_" + tenantId.toUpperCase()); - - log.info("[NacosRegistration] Register service with tenant: {}", tenantId); - } - }; - } -} -``` - -**11. 租户上下文管理** - -```java -/** - * 租户上下文持有者 - */ -public class TenantContextHolder { - - private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>(); - private static final ThreadLocal CURRENT_TENANT_GROUP = new ThreadLocal<>(); - - public static void setTenantId(String tenantId) { - CURRENT_TENANT.set(tenantId); - } - - public static String getTenantId() { - return CURRENT_TENANT.get(); - } - - public static void setTenantGroup(String tenantGroup) { - CURRENT_TENANT_GROUP.set(tenantGroup); - } - - public static String getTenantGroup() { - return CURRENT_TENANT_GROUP.get(); - } - - public static void clear() { - CURRENT_TENANT.remove(); - CURRENT_TENANT_GROUP.remove(); - } -} - -/** - * 租户上下文过滤器 - */ -@Component -public class TenantContextFilter implements Filter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - - try { - // 从请求头获取租户信息 - String tenantId = httpRequest.getHeader("X-Tenant-Id"); - String tenantGroup = httpRequest.getHeader("X-Tenant-Group"); - - if (StringUtils.isNotEmpty(tenantId)) { - TenantContextHolder.setTenantId(tenantId); - TenantContextHolder.setTenantGroup(tenantGroup); - - // 设置到 MDC 用于日志 - MDC.put("tenantId", tenantId); - } - - chain.doFilter(request, response); - } finally { - TenantContextHolder.clear(); - MDC.remove("tenantId"); - } - } -} -``` - -#### 2.6.3 配置示例(包含调用链配置) - -```yaml -# application-tenant.yml -spring: - cloud: - nacos: - discovery: - # 动态服务组,由代码设置 - group: ${TENANT_GROUP:DEFAULT} - metadata: - tenant-id: ${TENANT_ID:} - tenant-group: ${TENANT_GROUP:DEFAULT} - loadbalancer: - configurations: tenant-aware - -tenant: - routing: - enabled: true - tenant-header: X-Tenant-Id - group-separator: TENANT_ - # 共享服务(所有租户共用) - shared-services: - - fund-gateway - - fund-report - - fund-file - # 租户配置 - tenant-configs: - tenant_001: - tenant-id: tenant_001 - services: - fund-sys: - service-name: fund-sys - port: 8100 - replicas: 2 - fund-cust: - service-name: fund-cust - port: 8110 - replicas: 1 - database: - url: jdbc:mysql://localhost:3306/tenant_001_db - username: tenant_001 - password: xxx - tenant_002: - tenant-id: tenant_002 - services: - fund-sys: - service-name: fund-sys - port: 8101 - replicas: 1 - fund-cust: - service-name: fund-cust - port: 8111 - replicas: 1 - database: - url: jdbc:mysql://localhost:3306/tenant_002_db - username: tenant_002 - password: xxx -``` - -#### 2.6.4 调用链配置示例 - -``` -```yaml -# application.yml - 调用链配置 -spring: - cloud: - openfeign: - client: - config: - default: - # Feign 拦截器配置 - requestInterceptors: - - com.fundplatform.common.feign.FeignChainInterceptor - # 连接超时 - connectTimeout: 5000 - # 读取超时 - readTimeout: 10000 - -# 调用链上下文过滤器配置 -chain: - context: - # 是否启用调用链传递 - enabled: true - # 传递的 Headers - propagate-headers: - - X-Tenant-Id - - X-Tenant-Group - - X-Uid - - X-Uname - - X-Trace-Id -``` - -#### 2.6.5 部署架构 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Kubernetes 部署示例 │ -│ │ -│ apiVersion: apps/v1 │ -│ kind: Deployment │ -│ metadata: │ -│ name: fund-sys-tenant-001 │ -│ labels: │ -│ app: fund-sys │ -│ tenant: tenant-001 │ -│ spec: │ -│ replicas: 2 │ -│ selector: │ -│ matchLabels: │ -│ app: fund-sys │ -│ tenant: tenant-001 │ -│ template: │ -│ metadata: │ -│ labels: │ -│ app: fund-sys │ -│ tenant: tenant-001 │ -│ spec: │ -│ containers: │ -│ - name: fund-sys │ -│ image: fundplatform/fund-sys:1.0.0 │ -│ env: │ -│ - name: TENANT_ID │ -│ value: "tenant_001" │ -│ - name: TENANT_GROUP │ -│ value: "TENANT_001" │ -│ - name: DB_URL │ -│ value: "jdbc:mysql://mysql:3306/tenant_001_db" │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -#### 2.6.5 两种方案最终对比 - -| 场景 | 推荐方案 | 原因 | -|------|----------|------| -| 中小租户 (< 100) | DynamicDataSource | 资源利用率高,运维简单 | -| 大客户/私有化 | **Feign 动态路由** | 物理隔离,独立扩容 | -| 金融/政务行业 | **Feign 动态路由** | 安全合规要求高 | -| 快速原型/MVP | DynamicDataSource | 快速上线,成本低 | -| 长期运营 SaaS | **Feign 动态路由** | 可持续演进,租户自治 | - ---- - -## 三、Head 日志追踪设计 - -### 3.1 概述 - -Head 日志追踪(Header-based Logging)是一种基于请求头的全链路日志追踪方案,通过在每个请求中传递唯一的 Trace ID,实现跨服务的调用链追踪。 - -### 3.2 架构设计 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Head 日志追踪架构 │ -│ │ -│ ┌─────────┐ │ -│ │ 客户端 │ X-Trace-Id: abc123 │ -│ └────┬────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Gateway (网关) │ │ -│ │ ┌─────────────────────────────────────────────────────────────┐ │ │ -│ │ │ TraceIdFilter │ │ │ -│ │ │ • 生成/接收 TraceId │ │ │ -│ │ │ • 写入 MDC (Mapped Diagnostic Context) │ │ │ -│ │ │ • 传递 TraceId 到下游服务 │ │ │ -│ │ └─────────────────────────────────────────────────────────────┘ │ │ -│ └────────────────────────────────┬───────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────────┼───────────────────────────┐ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │fund-sys │ │fund-cust│ │fund-proj│ │ -│ │ │ │ │ │ │ │ -│ │• 接收 │ │• 接收 │ │• 接收 │ │ -│ │ TraceId│ │ TraceId│ │ TraceId│ │ -│ │• 写入 │ │• 写入 │ │• 写入 │ │ -│ │ MDC │ │ MDC │ │ MDC │ │ -│ │• 记录 │ │• 记录 │ │• 记录 │ │ -│ │ 日志 │ │ 日志 │ │ 日志 │ │ -│ └────┬────┘ └────┬────┘ └────┬────┘ │ -│ │ │ │ │ -│ └─────────────────────────┼─────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ELK / Loki 日志平台 │ │ -│ │ • 日志收集 → 解析 → 存储 → 检索 → 可视化 │ │ -│ │ • 支持 TraceId 全链路查询 │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### 3.3 技术实现 - -#### 3.3.1 TraceId 生成与传递 - -```java -/** - * TraceId 工具类 - */ -public class TraceIdUtil { - - public static final String TRACE_ID_HEADER = "X-Trace-Id"; - public static final String TRACE_ID_MDC_KEY = "traceId"; - public static final String SPAN_ID_MDC_KEY = "spanId"; - - /** - * 生成 TraceId - */ - public static String generateTraceId() { - return UUID.randomUUID().toString().replace("-", "").substring(0, 16); - } - - /** - * 生成 SpanId - */ - public static String generateSpanId() { - return UUID.randomUUID().toString().replace("-", "").substring(0, 8); - } - - /** - * 获取当前 TraceId - */ - public static String getCurrentTraceId() { - return MDC.get(TRACE_ID_MDC_KEY); - } - - /** - * 设置 TraceId 到 MDC - */ - public static void setTraceId(String traceId) { - MDC.put(TRACE_ID_MDC_KEY, traceId); - } - - /** - * 清除 MDC - */ - public static void clear() { - MDC.clear(); - } -} -``` - -#### 3.3.2 Gateway TraceId 过滤器 - -```java -/** - * Gateway TraceId 过滤器 - */ -@Component -@Order(-100) -public class TraceIdGatewayFilter implements GlobalFilter { - - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - ServerHttpRequest request = exchange.getRequest(); - HttpHeaders headers = request.getHeaders(); - - // 获取或生成 TraceId - String traceId = headers.getFirst(TraceIdUtil.TRACE_ID_HEADER); - if (StringUtils.isEmpty(traceId)) { - traceId = TraceIdUtil.generateTraceId(); - } - - // 生成 SpanId - String spanId = TraceIdUtil.generateSpanId(); - - // 添加到 MDC - TraceIdUtil.setTraceId(traceId); - MDC.put(TraceIdUtil.SPAN_ID_MDC_KEY, spanId); - - // 添加到请求头,传递给下游服务 - ServerHttpRequest mutatedRequest = request.mutate() - .header(TraceIdUtil.TRACE_ID_HEADER, traceId) - .header("X-Span-Id", spanId) - .header("X-Parent-Span-Id", MDC.get(TraceIdUtil.SPAN_ID_MDC_KEY)) - .build(); - - // 记录请求日志 - log.info("[Gateway] Request: {} {}, TraceId: {}", - request.getMethod(), - request.getURI(), - traceId); - - final String finalTraceId = traceId; - - return chain.filter(exchange.mutate().request(mutatedRequest).build()) - .doFinally(signalType -> { - // 记录响应日志 - log.info("[Gateway] Response: {}, TraceId: {}", - exchange.getResponse().getStatusCode(), - finalTraceId); - TraceIdUtil.clear(); - }); - } -} -``` - -#### 3.3.3 服务间 TraceId 传递 - -```java -/** - * Feign 请求拦截器 - 传递 TraceId - */ -@Component -public class FeignTraceIdInterceptor implements RequestInterceptor { - - @Override - public void apply(RequestTemplate template) { - String traceId = TraceIdUtil.getCurrentTraceId(); - if (StringUtils.isNotEmpty(traceId)) { - template.header(TraceIdUtil.TRACE_ID_HEADER, traceId); - } - } -} - -/** - * 服务端 TraceId 过滤器 - */ -@Component -public class TraceIdFilter implements Filter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - - // 从请求头获取 TraceId - String traceId = httpRequest.getHeader(TraceIdUtil.TRACE_ID_HEADER); - if (StringUtils.isEmpty(traceId)) { - traceId = TraceIdUtil.generateTraceId(); - } - - // 设置到 MDC - TraceIdUtil.setTraceId(traceId); - MDC.put(TraceIdUtil.SPAN_ID_MDC_KEY, TraceIdUtil.generateSpanId()); - - try { - chain.doFilter(request, response); - } finally { - TraceIdUtil.clear(); - } - } -} -``` - -#### 3.3.4 日志配置 - -```xml - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{36} - %msg%n - - - - - - logs/application.log - - logs/application.%d{yyyy-MM-dd}.%i.log - 30 - 100MB - - - true - traceId - spanId - tenantId - - - - - - - - -``` - -### 3.4 日志格式规范 - -#### 3.4.1 标准日志格式 - -``` -[时间] [线程] [TraceId] [SpanId] [租户ID] [级别] [类名] - [消息] - -示例: -2026-02-13 14:30:25.123 [http-nio-8100-exec-1] [abc123def456] [span789] [tenant_1] INFO c.f.s.c.UserController - 用户登录成功: admin -``` - -#### 3.4.2 JSON 日志格式 - -```json -{ - "@timestamp": "2026-02-13T14:30:25.123+08:00", - "level": "INFO", - "logger_name": "com.fundplatform.sys.controller.UserController", - "message": "用户登录成功: admin", - "thread_name": "http-nio-8100-exec-1", - "traceId": "abc123def456", - "spanId": "span789", - "tenantId": "tenant_1", - "service": "fund-sys", - "host": "192.168.1.100" -} -``` - -### 3.5 日志收集与分析 - -#### 3.5.1 ELK Stack 架构 - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ 应用服务 │────>│ Filebeat │────>│ Logstash │────>│ Elasticsearch│ -│ (日志文件) │ │ (日志收集) │ │ (日志处理) │ │ (日志存储) │ -└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘ - │ - ▼ - ┌─────────────┐ - │ Kibana │ - │ (日志查询) │ - └─────────────┘ -``` - -#### 3.5.2 Kibana 查询示例 - -``` -# 根据 TraceId 查询全链路日志 -traceId: "abc123def456" - -# 根据租户ID查询日志 -tenantId: "tenant_1" - -# 查询特定服务的错误日志 -service: "fund-sys" AND level: "ERROR" - -# 查询特定用户的操作日志 -message: "admin" AND tenantId: "tenant_1" -``` - -### 3.6 调用链追踪增强 - -#### 3.6.1 与 SkyWalking 集成 - -```java -/** - * SkyWalking 插件配置 - */ -@Component -public class SkyWalkingConfig { - - /** - * 自定义 SkyWalking 标签 - */ - @TraceContext - public void addCustomTags() { - // 添加租户ID标签 - String tenantId = TenantContextHolder.getTenantId(); - if (tenantId != null) { - Tags.ofKey("tenant.id").set(tenantId); - } - - // 添加用户ID标签 - String userId = SecurityUtils.getCurrentUserId(); - if (userId != null) { - Tags.ofKey("user.id").set(userId); - } - } -} -``` - -#### 3.6.2 性能指标采集 - -```java -/** - * 接口性能监控 - */ -@Aspect -@Component -public class PerformanceAspect { - - private static final Logger perfLog = LoggerFactory.getLogger("PERFORMANCE"); - - @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)") - public Object around(ProceedingJoinPoint point) throws Throwable { - long start = System.currentTimeMillis(); - String method = point.getSignature().toShortString(); - String traceId = TraceIdUtil.getCurrentTraceId(); - - try { - return point.proceed(); - } finally { - long cost = System.currentTimeMillis() - start; - perfLog.info("[Performance] Method: {}, Cost: {}ms, TraceId: {}", - method, cost, traceId); - } - } -} -``` - -### 3.7 API请求日志 - -#### 3.7.1 概述 - -API请求日志通过AOP(面向切面编程)实现,用于记录所有Controller层接口的请求信息,包括请求地址、请求参数、请求头和返回结果,并输出到独立的日志文件(aop.log),便于问题排查和审计追踪。 - -#### 3.7.2 技术实现 - -**1. Maven依赖** - -```xml - - - org.springframework.boot - spring-boot-starter-aop - - - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - -``` - -**2. AOP切面实现** - -```java -/** - * API请求日志切面 - * 记录请求地址、请求参数、请求header和返回参数 - */ -@Aspect -@Component -public class ApiLogAspect { - - private static final Logger log = LoggerFactory.getLogger("API_LOG"); - private static final ObjectMapper objectMapper = new ObjectMapper(); - - static { - // 注册JavaTimeModule以支持Java 8日期时间类型 - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - } - - /** - * 切点:所有Controller层方法 - */ - @Pointcut("execution(* com.fundplatform..controller..*.*(..))") - public void controllerPointcut() { - } - - /** - * 环绕通知:记录请求和响应信息 - */ - @Around("controllerPointcut()") - public Object around(ProceedingJoinPoint joinPoint) throws Throwable { - long startTime = System.currentTimeMillis(); - - // 获取请求信息 - ServletRequestAttributes attributes = - (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - return joinPoint.proceed(); - } - - HttpServletRequest request = attributes.getRequest(); - - // 构建日志信息 - Map logInfo = new HashMap<>(); - - // 1. 请求地址 - logInfo.put("requestUrl", request.getRequestURL().toString()); - logInfo.put("requestMethod", request.getMethod()); - logInfo.put("requestUri", request.getRequestURI()); - - // 2. 请求参数(URL参数) - Map params = new HashMap<>(); - Enumeration paramNames = request.getParameterNames(); - while (paramNames.hasMoreElements()) { - String paramName = paramNames.nextElement(); - params.put(paramName, request.getParameter(paramName)); - } - logInfo.put("requestParams", params); - - // 3. 请求头(仅记录关键header) - Map headers = new HashMap<>(); - String[] keyHeaders = {"X-User-Id", "X-Tenant-Id", "Content-Type"}; - for (String headerName : keyHeaders) { - String value = request.getHeader(headerName); - if (value != null) { - headers.put(headerName, value); - } - } - logInfo.put("requestHeaders", headers); - - // 4. 请求体参数(POST/PUT等) - Object[] args = joinPoint.getArgs(); - if (args != null && args.length > 0) { - for (Object arg : args) { - if (arg != null && - !(arg instanceof HttpServletRequest) && - !arg.getClass().getName().startsWith("jakarta.servlet")) { - logInfo.put("requestBody", arg); - break; - } - } - } - - // 方法信息 - logInfo.put("className", joinPoint.getTarget().getClass().getName()); - logInfo.put("methodName", joinPoint.getSignature().getName()); - - // 执行方法并记录响应 - Object result = null; - try { - result = joinPoint.proceed(); - logInfo.put("status", "SUCCESS"); - if (result != null) { - logInfo.put("responseType", result.getClass().getSimpleName()); - try { - logInfo.put("responseBody", objectMapper.writeValueAsString(result)); - } catch (Exception e) { - logInfo.put("responseBody", result.toString()); - } - } - } catch (Exception e) { - logInfo.put("status", "FAILED"); - logInfo.put("errorMessage", e.getMessage()); - throw e; - } finally { - long costTime = System.currentTimeMillis() - startTime; - logInfo.put("costTime", costTime + "ms"); - log.info(objectMapper.writeValueAsString(logInfo)); - } - - return result; - } -} -``` - -**3. Logback配置** - -```xml - - - ${LOG_PATH}/${APP_NAME}/aop.log - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] - %msg%n - UTF-8 - - - ${LOG_PATH}/${APP_NAME}/aop-%d{yyyy-MM-dd}.%i.log - - 100MB - - 30 - - - - - - - -``` - -#### 3.7.3 日志格式 - -AOP日志以JSON格式输出,包含以下字段: - -| 字段 | 说明 | 示例 | -|------|------|------| -| requestUrl | 完整请求URL | http://localhost:8100/api/v1/users | -| requestMethod | HTTP方法 | GET/POST/PUT/DELETE | -| requestUri | 请求URI | /api/v1/users | -| requestParams | URL查询参数 | {"page": "1", "size": "10"} | -| requestHeaders | 关键请求头 | {"X-User-Id": "1", "X-Tenant-Id": "1"} | -| requestBody | 请求体参数 | {"username": "admin"} | -| className | 处理类名 | UserController | -| methodName | 处理方法名 | list | -| status | 执行状态 | SUCCESS/FAILED | -| responseBody | 响应结果 | {"code": 200, "data": [...]} | -| errorMessage | 错误信息(失败时) | 用户不存在 | -| costTime | 执行耗时 | 25ms | - -**日志示例:** - -```json -{ - "requestUrl": "http://localhost:8100/api/v1/auth/login", - "requestMethod": "POST", - "requestUri": "/api/v1/auth/login", - "requestParams": {}, - "requestHeaders": { - "Content-Type": "application/json" - }, - "requestBody": { - "username": "admin", - "password": "******" - }, - "className": "com.fundplatform.sys.controller.AuthController", - "methodName": "login", - "status": "SUCCESS", - "responseType": "Result", - "responseBody": "{\"code\":200,\"data\":{\"token\":\"xxx\"}}", - "costTime": "125ms" -} -``` - -#### 3.7.4 日志文件说明 - -| 服务 | 日志路径 | 说明 | -|------|----------|------| -| fund-sys | logs/fund-sys/aop.log | fund-sys服务的API请求日志 | -| fund-cust | logs/fund-cust/aop.log | fund-cust服务的API请求日志 | -| fund-proj | logs/fund-proj/aop.log | fund-proj服务的API请求日志 | - -#### 3.7.5 注意事项 - -1. **敏感信息过滤**:对于密码、token等敏感信息,应在记录前进行脱敏处理 -2. **大对象处理**:对于响应体过大的接口,可考虑截断或跳过响应体记录 -3. **性能影响**:AOP日志会增加少量性能开销,生产环境建议配置合理的日志级别 -4. **文件滚动**:日志文件按日期和大小滚动,最大保留30天 - ---- - -## 四、系统整体架构 - -### 2.1 架构全景图 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ 接入层 (Access Layer) │ -│ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ │ -│ │ Web管理端 │ │ 移动端H5 │ │ -│ │ (Vue3 + Element Plus) │ │ (Vue3 + Vite5 + Vant) │ │ -│ └──────────────┬──────────────────┘ └──────────────┬──────────────────┘ │ -└─────────────────┼─────────────────────────────────────┼─────────────────────┘ - │ │ - └─────────────────────┬───────────────┘ - │ -┌────────────────────────────────────┼────────────────────────────────────────┐ -│ 网关层 (Gateway Layer) │ -│ ┌─────────────────────────────────┴─────────────────────────────────────┐ │ -│ │ Nginx 负载均衡 │ │ -│ │ SSL终止 / 静态资源 / 反向代理 │ │ -│ └─────────────────────────────────┬─────────────────────────────────────┘ │ -│ │ │ -│ ┌─────────────────────────────────┴─────────────────────────────────────┐ │ -│ │ Spring Cloud Gateway │ │ -│ │ 路由转发 / 限流熔断 / 鉴权认证 / 日志记录 │ │ -│ └─────────────────────────────────┬─────────────────────────────────────┘ │ -└────────────────────────────────────┼────────────────────────────────────────┘ - │ -┌────────────────────────────────────┼────────────────────────────────────────┐ -│ 服务层 (Service Layer) │ -│ │ │ -│ ┌─────────────────────────────────┴─────────────────────────────────────┐ │ -│ │ Nacos 服务注册与配置中心 │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ 系统服务 │ │ 客户中心 │ │ 项目中心 │ │ 需求中心 │ │ -│ │ fund-sys │ │ fund-cust │ │ fund-proj │ │ fund-req │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ • 用户管理 │ │ • 客户管理 │ │ • 项目管理 │ │ • 需求工单 │ │ -│ │ • 权限管理 │ │ • 联系人管理 │ │ • 成员管理 │ │ • 应收款管理 │ │ -│ │ • 部门管理 │ │ │ │ │ │ │ │ -│ │ • 日志管理 │ │ │ │ │ │ │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ │ -│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │ -│ │ 支出中心 │ │ 收款中心 │ │ 报表中心 │ │ 文件中心 │ │ -│ │ fund-exp │ │ fund-receipt │ │ fund-report │ │ fund-file │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ • 支出类型 │ │ • 收款记录 │ │ • 收支统计 │ │ • 文件上传 │ │ -│ │ • 支出申请 │ │ • 收款凭证 │ │ • 趋势分析 │ │ • 文件存储 │ │ -│ │ • 付款管理 │ │ • 账期管理 │ │ • 数据导出 │ │ • 文件下载 │ │ -│ │ • 统计分析 │ │ │ │ │ │ │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - │ -┌────────────────────────────────────┼────────────────────────────────────────┐ -│ 数据层 (Data Layer) │ -│ │ │ -│ ┌─────────────────────────────────┴─────────────────────────────────────┐ │ -│ │ Redis 缓存集群 │ │ -│ │ 会话缓存 / 热点数据 / 分布式锁 / 限流计数器 │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ MySQL 8.0 主从集群 │ │ -│ │ 用户库 / 业务库 / 日志库 / 报表库 │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ 腾讯COS 对象存储 │ │ -│ │ 文件存储 / 图片存储 / 凭证附件 │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 服务划分 - -| 服务名 | 服务编码 | 职责 | 端口范围 | -|--------|----------|------|----------| -| 网关服务 | fund-gateway | 路由、鉴权、限流 | 8080 | -| 系统服务 | fund-sys | 用户、权限、部门、日志 | 8100-8109 | -| 客户中心 | fund-cust | 客户、联系人管理 | 8110-8119 | -| 项目中心 | fund-proj | 项目、成员管理 | 8120-8129 | -| 需求中心 | fund-req | 需求工单、应收款 | 8130-8139 | -| 支出中心 | fund-exp | 支出类型、支出管理 | 8140-8149 | -| 收款中心 | fund-receipt | 收款记录、账期管理 | 8150-8159 | -| 报表中心 | fund-report | 统计分析、数据导出 | 8160-8169 | -| 文件中心 | fund-file | 文件上传、存储管理 | 8170-8179 | - -### 2.3 模块通信方式 - -系统采用**分层通信架构**,根据模块职责划分为基础服务和业务服务,采用不同的通信方式: - -#### 2.3.1 基础服务 - Maven 依赖方式 - -**定义**:提供基础能力、不对外暴露 HTTP 接口的模块 - -| 基础服务 | 说明 | 提供能力 | 依赖方式 | -|---------|------|---------|---------| -| **fund-common** | 公共模块 | 基础实体、工具类、统一响应格式 | Maven 依赖 | -| **fund-gateway** | API 网关 | 路由转发、统一鉴权、限流熔断 | 独立部署 | - -**特点**: -- ✅ **编译时依赖**:通过 Maven `` 直接引入 -- ✅ **本地调用**:方法级别直接调用,无网络开销 -- ✅ **强类型约束**:编译期类型检查,更安全 -- ✅ **版本统一**:通过父 POM 统一管理版本 - -**示例**: -```xml - - - com.fundplatform - fund-common - ${project.version} - -``` - -#### 2.3.2 业务服务 - OpenFeign 通信方式 - -**定义**:提供 HTTP API 接口、独立部署的业务模块 - -| 业务服务 | 说明 | 提供 API | 通信方式 | -|---------|------|---------|---------| -| **fund-sys** | 系统服务 | 用户、角色、权限、部门 API | OpenFeign | -| **fund-cust** | 客户中心 | 客户、联系人管理 API | OpenFeign | -| **fund-proj** | 项目中心 | 项目、成员管理 API | OpenFeign | -| **fund-req** | 需求中心 | 需求工单、应收款 API | OpenFeign | -| **fund-exp** | 支出中心 | 支出类型、支出管理 API | OpenFeign | -| **fund-receipt** | 收款中心 | 收款记录、账期管理 API | OpenFeign | -| **fund-report** | 报表中心 | 统计分析、数据导出 API | OpenFeign | -| **fund-file** | 文件中心 | 文件上传、存储管理 API | OpenFeign | - -**特点**: -- ✅ **松耦合**:服务间独立部署、独立升级 -- ✅ **声明式调用**:通过接口注解定义,自动生成实现 -- ✅ **负载均衡**:集成 LoadBalancer,自动分发请求 -- ✅ **服务发现**:通过 Nacos 动态获取服务地址 -- ✅ **熔断降级**:集成 Sentinel,提供容错能力 -- ✅ **链路追踪**:自动传递 TraceId、租户ID、用户信息 - -**示例**: -```java -/** - * 客户服务 Feign 客户端 - */ -@FeignClient( - name = "fund-cust", // 服务名(Nacos注册名) - path = "/api/v1/customer", // 接口路径前缀 - fallbackFactory = CustomerFallbackFactory.class // 降级处理 -) -public interface CustomerFeignClient { - - /** - * 根据客户ID查询客户信息 - */ - @GetMapping("/{customerId}") - Result getCustomerById(@PathVariable Long customerId); - - /** - * 批量查询客户信息 - */ - @PostMapping("/batch") - Result> getCustomersByIds(@RequestBody List customerIds); -} -``` - -**调用示例**: -```java -@Service -@RequiredArgsConstructor -public class ProjectService { - - // 注入 Feign 客户端 - private final CustomerFeignClient customerFeignClient; - - public ProjectDetail getProjectDetail(Long projectId) { - // 1. 查询项目基本信息 - Project project = projectMapper.selectById(projectId); - - // 2. 通过 Feign 调用客户服务获取客户信息 - Result result = customerFeignClient.getCustomerById(project.getCustomerId()); - Customer customer = result.getData(); - - // 3. 组装返回 - return ProjectDetail.builder() - .project(project) - .customer(customer) - .build(); - } -} -``` - -#### 2.3.3 通信链路追踪 - -所有 Feign 调用自动传递以下 Header 信息: - -| Header 名称 | 说明 | 来源 | 用途 | -|------------|------|------|------| -| `X-Tenant-Id` | 租户ID | TenantContextHolder | 多租户数据隔离 | -| `X-Uid` | 用户ID | 当前登录用户 | 操作日志记录 | -| `X-Uname` | 用户名 | 当前登录用户 | 操作日志记录 | -| `X-Trace-Id` | 链路追踪ID | MDC.get("traceId") | 全链路日志追踪 | - -**实现机制**: -```java -/** - * Feign 调用链拦截器 - * 自动添加租户、用户、链路追踪信息到请求头 - */ -@Component -public class FeignChainInterceptor implements RequestInterceptor { - - @Override - public void apply(RequestTemplate template) { - // 1. 传递租户ID - Long tenantId = TenantContextHolder.getTenantId(); - if (tenantId != null) { - template.header("X-Tenant-Id", String.valueOf(tenantId)); - } - - // 2. 传递用户信息 - Long uid = GlobalContext.getUid(); - String uname = GlobalContext.getUname(); - if (uid != null) { - template.header("X-Uid", String.valueOf(uid)); - template.header("X-Uname", uname); - } - - // 3. 传递链路追踪ID - String traceId = MDC.get("traceId"); - if (traceId != null) { - template.header("X-Trace-Id", traceId); - } - } -} -``` - -#### 2.3.4 通信架构图 - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ 前端应用 │ -│ (管理后台 + 移动端) │ -└────────────────────────────┬────────────────────────────────────────┘ - │ HTTP/HTTPS - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ fund-gateway (API网关) │ -│ 路由、鉴权、限流、熔断 │ -└──┬──────────────┬──────────────┬──────────────┬─────────────────────┘ - │ │ │ │ - │ HTTP │ HTTP │ HTTP │ HTTP - ▼ ▼ ▼ ▼ -┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ -│fund-sys│ │fund-cust│ │fund-proj│ │fund-exp│ -│系统服务│ │客户中心│ │项目中心│ │支出中心│ -└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ - │ │ │ │ - │ Maven依赖 │ OpenFeign ◄──┤ OpenFeign ◄──┤ - ▼ ▼ ▼ ▼ -┌──────────────────────────────────────────────────┐ -│ fund-common (公共模块) │ -│ (Result、BaseEntity、工具类等) │ -└──────────────────────────────────────────────────┘ -``` - -#### 2.3.5 通信方式选择标准 - -| 场景 | 推荐方式 | 理由 | -|------|---------|------| -| 使用公共实体类 | Maven 依赖 | 编译期检查,无运行时开销 | -| 使用工具类方法 | Maven 依赖 | 直接调用,性能最优 | -| 跨服务调用 API | OpenFeign | 服务解耦,支持独立部署 | -| 查询其他服务数据 | OpenFeign | 通过 HTTP API,标准化通信 | -| 调用第三方服务 | OpenFeign | 统一的调用方式和错误处理 | - -**反模式**(不推荐): -- ❌ 业务服务间通过 Maven 依赖互相引用(导致强耦合) -- ❌ 直接通过数据库访问其他服务的表(破坏服务边界) -- ❌ 使用 RestTemplate 而非 OpenFeign(缺少服务发现和负载均衡) - -#### 2.3.6 参数对象(DTO)管理策略 - -OpenFeign 调用中涉及的参数对象(请求 DTO / 响应 DTO),采用 **“通用参数独立模块 + 领域参数分模块管理”** 的组合策略: - -##### 2.3.6.1 通用参数对象(独立模块管理) - -**定义**:跨多个业务域都会用到的通用结构,由基础公共模块统一提供。 - -| 对象类别 | 示例 | 所在模块 | -|----------|------|----------| -| 统一返回结果 | `Result`、`PageResult` | `fund-common` | -| 分页/排序参数 | `PageRequest`、`SortRequest`(预留) | `fund-common` | -| 公共基础实体 | `BaseEntity`(含 `tenant_id`、审计字段) | `fund-common` | - -**约定**: -- 任何服务如果需要统一返回格式或分页能力,优先复用 `fund-common` 中的通用对象; -- 通用对象由架构组维护,变更需评估对所有服务的影响。 - -##### 2.3.6.2 领域参数对象(分模块管理) - -**定义**:只属于某个业务域的请求 / 响应对象,例如: -- 客户查询条件:`CustomerQueryDTO`(fund-cust) -- 项目创建请求:`ProjectCreateRequest`(fund-proj) -- 收款记录筛选条件:`ReceiptFilterDTO`(fund-receipt) - -**管理策略**: -- **分模块管理**: - - 每个提供 API 的服务,在**自身模块内部**定义领域参数对象; - - 参数对象的 Java 类不在多个业务模块之间通过 Maven 互相依赖,避免形成隐性耦合; - - 服务之间通过 **JSON 结构契约** 进行解耦,具体字段以 API 文档为准。 -- **消费方处理方式**: - - 服务消费方可以: - - 直接使用与提供方字段结构一致的本地 VO/DTO,并通过 JSON 序列化/反序列化完成转换;或 - - 在消费方内部定义更贴近本地业务的 DTO,并在 Service 层做一次字段映射。 - -**示例**(领域请求 DTO 定义在提供方模块): -```java -// fund-cust 模块内部 -@Data -public class CustomerQueryDTO { - private String keyword; - private Integer status; - private LocalDate startDate; - private LocalDate endDate; -} - -@RestController -@RequestMapping("/api/v1/customer") -public class CustomerController { - - @PostMapping("/search") - public Result> search(@RequestBody CustomerQueryDTO query) { - // ... 省略具体实现 - } -} -``` - -**消费方调用示例**: -```java -// fund-proj 模块内部定义本地请求对象 -@Data -public class CustomerSearchRequest { - private String keyword; -} - -@FeignClient(name = "fund-cust", path = "/api/v1/customer") -public interface CustomerFeignClient { - - @PostMapping("/search") - Result> search(@RequestBody CustomerQueryDTO query); -} - -@Service -@RequiredArgsConstructor -public class ProjectService { - - private final CustomerFeignClient customerFeignClient; - - public List findCustomers(CustomerSearchRequest request) { - CustomerQueryDTO dto = new CustomerQueryDTO(); - dto.setKeyword(request.getKeyword()); - return customerFeignClient.search(dto).getData(); - } -} -``` - -> **结论**: -> - 通用参数对象放在 `fund-common` 独立模块统一管理; -> - 具体业务领域的参数对象采用**分模块管理**,定义在各自服务内部; -> - 业务服务间通过 OpenFeign + JSON 契约解耦,不共享彼此的领域实体类。 - ---- - -## 三、技术架构 - -### 3.1 技术栈选型 - -#### 3.1.1 后端技术栈 - -| 层级 | 技术选型 | 版本 | 用途 | -|------|----------|------|------| -| **基础框架** | Spring Boot | 3.2.x | 应用基础框架 | -| **微服务框架** | Spring Cloud Alibaba | 2022.x | 微服务治理 | -| **服务注册** | Nacos | 3.0.0 | 服务注册与配置中心 | -| **服务网关** | Spring Cloud Gateway | 4.x | API网关 | -| **负载均衡** | Spring Cloud LoadBalancer | 4.x | 客户端负载均衡 | -| **服务调用** | OpenFeign | 4.x | 声明式HTTP客户端 | -| **服务容错** | Sentinel | 1.8.x | 限流、熔断、降级 | -| **ORM框架** | MyBatis-Plus | 3.5.x | 数据访问层 | -| **数据库连接池** | HikariCP | 5.x | 高性能连接池 | -| **缓存框架** | Spring Data Redis | 3.x | Redis操作 | -| **安全框架** | Apache Shiro | 2.x | 认证授权、会话管理 | -| **JWT令牌** | jjwt | 0.12.x | Token生成与验证 | -| **API文档** | Knife4j | 4.x | Swagger增强 | -| **对象存储** | 腾讯云COS SDK | 5.x | 文件存储 | -| **定时任务** | XXL-JOB | 2.4.x | 分布式任务调度 | -| **日志框架** | Logback + SLF4J | - | 日志记录 | -| **工具类库** | Hutool | 5.8.x | 常用工具 | - -#### 3.1.2 前端技术栈 - -| 端 | 技术选型 | 版本 | 用途 | -|----|----------|------|------| -| **管理后台** | Vue | 3.4.x | 前端框架 | -| | TypeScript | 5.x | 类型安全 | -| | Element Plus | 2.5.x | UI组件库 | -| | Pinia | 2.x | 状态管理 | -| | Vue Router | 4.x | 路由管理 | -| | Axios | 1.x | HTTP客户端 | -| | ECharts | 5.x | 图表库 | -| | Vite | 5.x | 构建工具 | -| **移动端** | Vue | 3.4.x | 前端框架 | -| | Vite | 5.x | 构建工具 | -| | TypeScript | 5.x | 类型安全 | -| | Vant | 4.x | 移动端UI库 | -| | Pinia | 2.x | 状态管理 | -| | Vue Router | 4.x | 路由管理 | -| | Axios | 1.x | HTTP客户端 | - -### 3.2 架构分层 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 表现层 (Presentation) │ -│ Controller / DTO / VO / 参数校验 / 异常处理 │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ 业务层 (Business) │ -│ Service / 业务逻辑 / 事务管理 / 权限校验 │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ 数据访问层 (Data Access) │ -│ Mapper / DAO / MyBatis-Plus / 数据库操作 │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ 基础设施层 (Infrastructure) │ -│ Redis / MySQL / COS / MQ / 缓存 / 消息队列 │ -└─────────────────────────────────────────────────────────────┘ -``` - -**层次职责与依赖方向**: -- 表现层:负责请求接收、参数校验、调用应用服务、统一返回封装;不直接包含业务规则。 -- 应用服务层(App Service):以“用例”为中心,负责业务流程编排、事务边界控制、跨服务调用(OpenFeign)等;依赖领域服务和数据访问层。 -- 领域服务层(Domain Service):承载核心领域规则和计算逻辑,不直接依赖基础设施(数据库、缓存等),尽量保持纯业务逻辑。 -- 数据访问层:由 `XxxDataService` + Mapper + Entity 组成,仅负责数据查询、持久化和简单数据转换。 -- 基础设施层:对外部资源(数据库、缓存、消息中间件、对象存储等)的适配封装。 - -**依赖规范**: -- 依赖方向自上而下:Controller → AppService → DomainService → DataService/Mapper → 基础设施层; -- 下层禁止依赖上层(例如 DataService 不得调用 Controller 或 AppService); -- 业务模块之间通过 OpenFeign 进行调用,不通过 Maven 直接依赖其他模块的 DataService/Entity。 - ---- - -## 四、数据架构 - -### 4.1 数据库设计 - -#### 4.1.1 数据库划分 - -| 数据库 | 用途 | 主要表 | -|--------|------|--------| -| fund_sys | 系统数据 | sys_user, sys_dept, sys_role, sys_menu, sys_log | -| fund_biz | 业务数据 | customer, project, requirement, expense, receivable, receipt | -| fund_file | 文件数据 | file_record, file_chunk | - -#### 4.1.2 分库分表策略 - -| 表名 | 策略 | 分片键 | 说明 | -|------|------|--------|------| -| sys_operation_log | 按时间分表 | created_time | 每月一张表 | -| expense | 按时间分表 | expense_date | 每季度一张表 | -| receipt | 按时间分表 | receipt_date | 每季度一张表 | - -### 4.2 缓存设计 - -#### 4.2.1 缓存策略 - -| 缓存类型 | Key前缀 | 过期时间 | 说明 | -|----------|---------|----------|------| -| 用户会话 | `session:` | 30分钟 | JWT Token 黑名单 | -| 用户信息 | `user:info:` | 1小时 | 用户基本信息缓存 | -| 字典数据 | `dict:` | 24小时 | 系统字典数据 | -| 热点数据 | `hot:` | 10分钟 | 频繁访问数据 | -| 限流计数 | `limit:` | 1分钟 | API限流计数 | -| 分布式锁 | `lock:` | 30秒 | 并发控制 | - -#### 4.2.2 缓存更新策略 - -``` -┌─────────────┐ 查询 ┌─────────────┐ -│ 业务请求 │ ───────────> │ 查询缓存 │ -└─────────────┘ └──────┬──────┘ - │ - ┌───────────────┼───────────────┐ - │ 命中 │ 未命中 │ - ▼ ▼ │ - ┌─────────────┐ ┌─────────────┐ │ - │ 返回缓存 │ │ 查询数据库 │ │ - │ 数据 │ │ │ │ - └─────────────┘ └──────┬──────┘ │ - │ │ - ▼ │ - ┌─────────────┐ │ - │ 写入缓存 │<─────┘ - │ 设置过期 │ - └─────────────┘ -``` - ---- - -## 五、安全架构 - -### 5.1 认证授权(Apache Shiro) - -#### 5.1.1 Shiro 架构设计 - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Shiro 认证授权架构 │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Subject(主体) │ │ -│ │ 封装了当前用户的登录状态、权限信息 │ │ -│ └────────────────────────────────┬────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ SecurityManager(安全管理器) │ │ -│ │ 核心组件,管理所有 Subject │ │ -│ └──────────────┬─────────────────┼─────────────────┬───────────────────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ Authenticator │ │ Authorizer │ │ SessionManager │ │ -│ │ (认证器) │ │ (授权器) │ │ (会话管理器) │ │ -│ │ │ │ │ │ │ │ -│ │ • 登录认证 │ │ • 角色校验 │ │ • 会话创建 │ │ -│ │ • 多Realm认证 │ │ • 权限校验 │ │ • 会话存储 │ │ -│ │ • rememberMe │ │ • 注解权限 │ │ • 会话超时 │ │ -│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ Realm │ │ SessionDAO │ │ CacheManager │ │ -│ │ (领域) │ │ (会话持久化) │ │ (缓存管理) │ │ -│ │ │ │ │ │ │ │ -│ │ • 用户数据查询 │ │ • Redis存储 │ │ • 认证缓存 │ │ -│ │ • 密码比对 │ │ • 分布式会话 │ │ • 授权缓存 │ │ -│ │ • 权限数据加载 │ │ │ │ • 会话缓存 │ │ -│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -#### 5.1.2 Shiro + JWT 认证流程 - -``` -┌─────────┐ ┌─────────┐ -│ 客户端 │ │ 服务端 │ -└────┬────┘ └────┬────┘ - │ │ - │ 1. 登录请求 (username/password) │ - │ ──────────────────────────────────────> │ - │ │ - │ ┌─────────┴─────────┐ - │ │ Shiro Realm 认证 │ - │ │ • 查询用户数据 │ - │ │ • 密码比对(Bcrypt)│ - │ │ • 加载用户权限 │ - │ └─────────┬─────────┘ - │ │ - │ 2. 生成 JWT Token │ - │ {uid, uname, tenantId, roles, perm} │ - │ <────────────────────────────────────── │ - │ │ - │ 3. 业务请求 (Header: Authorization) │ - │ Header: X-Uid: 1001 │ - │ Header: X-Uname: admin │ - │ ──────────────────────────────────────> │ - │ │ - │ ┌─────────┴─────────┐ - │ │ JWT Filter 验证 │ - │ │ • Token合法性 │ - │ │ • Token过期检查 │ - │ │ • 刷新Token(可选) │ - │ └─────────┬─────────┘ - │ │ - │ ┌─────────┴─────────┐ - │ │ Shiro 授权检查 │ - │ │ • @RequiresRoles │ - │ │ • @RequiresPerms │ - │ └─────────┬─────────┘ - │ │ - │ 4. 返回业务数据 │ - │ <────────────────────────────────────── │ -``` - -#### 5.1.3 Shiro 核心配置 - -``` -┌─────────┐ ┌─────────┐ -│ 客户端 │ │ 服务端 │ -└────┬────┘ └────┬────┘ - │ │ - │ 1. 登录请求 (username/password) │ - │ ──────────────────────────────────────> │ - │ │ - │ ┌─────────┴─────────┐ - │ │ 验证用户名密码 │ - │ │ 生成JWT Token │ - │ └─────────┬─────────┘ - │ │ - │ 2. 返回 Token │ - │ <────────────────────────────────────── │ - │ │ - │ 3. 业务请求 (Header: Authorization) │ - │ ──────────────────────────────────────> │ - │ ┌─────────┴─────────┐ - │ │ 验证Token有效性 │ - │ │ 解析用户信息 │ - │ │ 权限校验 │ - │ └─────────┬─────────┘ - │ │ - │ 4. 返回业务数据 │ - │ <────────────────────────────────────── │ -``` - -#### 5.1.3 Shiro 核心实现 - -**1. Shiro 配置类** - -```java -/** - * Shiro 配置类 - */ -@Configuration -public class ShiroConfig { - - /** - * SecurityManager 配置 - */ - @Bean - public DefaultWebSecurityManager securityManager( - JwtRealm jwtRealm, - SessionManager sessionManager, - CacheManager cacheManager) { - - DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); - - // 配置 Realm - securityManager.setRealm(jwtRealm); - - // 配置会话管理器 - securityManager.setSessionManager(sessionManager); - - // 配置缓存管理器 - securityManager.setCacheManager(cacheManager); - - // 配置记住我 - securityManager.setRememberMeManager(rememberMeManager()); - - return securityManager; - } - - /** - * Shiro 过滤器链配置 - */ - @Bean - public ShiroFilterFactoryBean shiroFilterFactoryBean( - DefaultWebSecurityManager securityManager) { - - ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean(); - filterFactoryBean.setSecurityManager(securityManager); - - // 自定义过滤器 - Map filters = new HashMap<>(); - filters.put("jwt", new JwtAuthenticationFilter()); - filterFactoryBean.setFilters(filters); - - // 过滤器链规则 - Map filterChainDefinitionMap = new LinkedHashMap<>(); - filterChainDefinitionMap.put("/api/auth/login", "anon"); - filterChainDefinitionMap.put("/api/auth/logout", "anon"); - filterChainDefinitionMap.put("/swagger-ui/**", "anon"); - filterChainDefinitionMap.put("/v3/api-docs/**", "anon"); - filterChainDefinitionMap.put("/**", "jwt"); - - filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); - - return filterFactoryBean; - } - - /** - * 会话管理器(使用 Redis 实现分布式会话) - */ - @Bean - public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { - DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); - - // 使用 Redis 存储会话 - sessionManager.setSessionDAO(redisSessionDAO); - - // 会话超时时间(30分钟) - sessionManager.setGlobalSessionTimeout(30 * 60 * 1000); - - // 删除无效会话 - sessionManager.setDeleteInvalidSessions(true); - - // 会话验证间隔 - sessionManager.setSessionValidationInterval(10 * 60 * 1000); - - return sessionManager; - } - - /** - * Redis 会话 DAO - */ - @Bean - public RedisSessionDAO redisSessionDAO(RedisTemplate redisTemplate) { - RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); - redisSessionDAO.setRedisTemplate(redisTemplate); - redisSessionDAO.setKeyPrefix("shiro:session:"); - redisSessionDAO.setExpire(30 * 60); - return redisSessionDAO; - } - - /** - * 缓存管理器(Redis) - */ - @Bean - public CacheManager cacheManager(RedisTemplate redisTemplate) { - RedisCacheManager cacheManager = new RedisCacheManager(); - cacheManager.setRedisTemplate(redisTemplate); - cacheManager.setKeyPrefix("shiro:cache:"); - return cacheManager; - } - - /** - * 启用 Shiro 注解 - */ - @Bean - public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { - DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator(); - proxyCreator.setProxyTargetClass(true); - return proxyCreator; - } - - @Bean - public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( - DefaultWebSecurityManager securityManager) { - AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); - advisor.setSecurityManager(securityManager); - return advisor; - } -} -``` - -**2. JWT Realm 实现** - -```java -/** - * JWT Realm - 认证和授权 - */ -@Component -public class JwtRealm extends AuthorizingRealm { - - @Autowired - private JwtUtil jwtUtil; - - @Autowired - private UserService userService; - - @Autowired - private PermissionService permissionService; - - /** - * 支持 JWT Token - */ - @Override - public boolean supports(AuthenticationToken token) { - return token instanceof JwtToken; - } - - /** - * 授权(验证权限时调用) - */ - @Override - protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { - // 获取用户ID - Long userId = (Long) principals.getPrimaryPrincipal(); - - SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); - - // 查询用户角色 - Set roles = permissionService.getUserRoles(userId); - authorizationInfo.setRoles(roles); - - // 查询用户权限 - Set permissions = permissionService.getUserPermissions(userId); - authorizationInfo.setStringPermissions(permissions); - - return authorizationInfo; - } - - /** - * 认证(登录时调用) - */ - @Override - protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) - throws AuthenticationException { - - JwtToken jwtToken = (JwtToken) token; - String jwt = jwtToken.getToken(); - - // 验证 JWT - if (!jwtUtil.validateToken(jwt)) { - throw new AuthenticationException("Token 无效或已过期"); - } - - // 解析 JWT 获取用户ID - Long userId = jwtUtil.getUserId(jwt); - String username = jwtUtil.getUsername(jwt); - - // 查询用户信息 - User user = userService.getById(userId); - if (user == null) { - throw new UnknownAccountException("用户不存在"); - } - - // 检查用户状态 - if (user.getStatus() == 0) { - throw new LockedAccountException("账号已被禁用"); - } - - // 将用户信息存入上下文(供后续使用) - UserContext.setCurrentUser(user); - - // 返回认证信息 - return new SimpleAuthenticationInfo( - userId, // 主体(用户ID) - jwt, // 凭证(JWT) - getName() // Realm 名称 - ); - } -} - -/** - * JWT Token 封装 - */ -public class JwtToken implements AuthenticationToken { - - private final String token; - - public JwtToken(String token) { - this.token = token; - } - - @Override - public Object getPrincipal() { - return token; - } - - @Override - public Object getCredentials() { - return token; - } - - public String getToken() { - return token; - } -} -``` - -**3. JWT 过滤器** - -```java -/** - * JWT 认证过滤器 - */ -public class JwtAuthenticationFilter extends BasicHttpAuthenticationFilter { - - @Override - protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { - HttpServletRequest httpRequest = (HttpServletRequest) request; - String authorization = httpRequest.getHeader("Authorization"); - return authorization != null && authorization.startsWith("Bearer "); - } - - @Override - protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { - HttpServletRequest httpRequest = (HttpServletRequest) request; - String authorization = httpRequest.getHeader("Authorization"); - String token = authorization.substring(7); - - JwtToken jwtToken = new JwtToken(token); - - try { - // 提交给 Realm 进行认证 - getSubject(request, response).login(jwtToken); - return true; - } catch (AuthenticationException e) { - // 认证失败 - HttpServletResponse httpResponse = (HttpServletResponse) response; - httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - httpResponse.setContentType("application/json;charset=UTF-8"); - httpResponse.getWriter().write("{\"code\":401,\"message\":\"认证失败\"}"); - return false; - } - } - - @Override - protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { - if (isLoginAttempt(request, response)) { - try { - return executeLogin(request, response); - } catch (Exception e) { - return false; - } - } - return false; - } - - @Override - protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { - HttpServletResponse httpResponse = (HttpServletResponse) response; - httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - httpResponse.setContentType("application/json;charset=UTF-8"); - httpResponse.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}"); - return false; - } -} -``` - -**4. 权限注解使用** - -```java -/** - * 用户控制器 - */ -@RestController -@RequestMapping("/api/user") -public class UserController { - - /** - * 需要登录 - */ - @RequiresAuthentication - @GetMapping("/info") - public Result getUserInfo() { - Long userId = UserContext.getCurrentUserId(); - return Result.success(userService.getUserInfo(userId)); - } - - /** - * 需要 admin 角色 - */ - @RequiresRoles("admin") - @GetMapping("/list") - public Result> listUsers(PageParam pageParam) { - return Result.success(userService.listUsers(pageParam)); - } - - /** - * 需要 user:create 权限 - */ - @RequiresPermissions("user:create") - @PostMapping - public Result createUser(@RequestBody @Valid UserCreateDTO dto) { - userService.createUser(dto); - return Result.success(); - } - - /** - * 需要多个权限(逻辑与) - */ - @RequiresPermissions({"user:update", "user:assignRole"}) - @PutMapping("/{userId}/role") - public Result assignRole(@PathVariable Long userId, @RequestBody List roleIds) { - userService.assignRoles(userId, roleIds); - return Result.success(); - } -} -``` - -#### 5.1.4 权限模型 - -采用 **RBAC** (Role-Based Access Control) 模型: - -``` -用户 (User) ──N:M──> 角色 (Role) ──N:M──> 权限 (Permission) - │ - └─N:M──> 菜单/按钮 (Menu) -``` - -### 5.2 数据安全 - -| 安全措施 | 实现方式 | -|----------|----------| -| 传输加密 | HTTPS/TLS 1.3 | -| 密码加密 | MD5(前端加密后传输,后端验证) | -| 敏感数据 | AES-256 加密存储 | -| SQL注入 | MyBatis预编译语句 | -| XSS攻击 | 前端转义 + 后端过滤 | -| CSRF攻击 | Token验证 + SameSite Cookie | - ---- - -## 六、部署架构 - -### 6.1 前端部署路径设计 - -前端项目采用 **Nginx 子路径部署** 模式,通过统一的 Nginx 入口提供静态资源服务。 - -#### 6.1.1 部署路径规划 - -| 前端项目 | 部署路径 | 访问地址 | 说明 | -|----------|----------|----------|------| -| fund-admin | `/fadmin/` | `http://host/fadmin/` | 管理后台 | -| fund-mobile | `/fmobile/` | `http://host/fmobile/` | 移动端H5 | -| API网关 | `/fund/` | `http://host/fund/` | 后端API统一前缀 | - -#### 6.1.2 Nginx 配置示例 - -```nginx -server { - listen 80; - server_name localhost; - - # 管理后台前端 (部署路径: /fadmin/) - location /fadmin/ { - alias /opt/fundplatform/web/admin/; - try_files $uri $uri/ /fadmin/index.html; - } - - # 移动端H5 (部署路径: /fmobile/) - location /fmobile/ { - alias /opt/fundplatform/web/mobile/; - try_files $uri $uri/ /fmobile/index.html; - } - - # API代理 (网关前缀: /fund) - location /fund/ { - proxy_pass http://127.0.0.1:8000/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -#### 6.1.3 前端构建配置 - -前端项目需配置 `VITE_BASE` 环境变量以支持子路径部署: - -**fund-admin/.env.production**: -```properties -VITE_BASE=/fadmin/ -VITE_API_BASE_URL=/fund -``` - -**fund-mobile/.env.production**: -```properties -VITE_BASE=/fmobile/ -VITE_API_BASE_URL=/fund -``` - -**vite.config.ts** 配置动态 base 路径: -```typescript -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd()) - const base = env.VITE_BASE || '/' - return { - base, - // ...其他配置 - } -}) -``` - -**Vue Router** 配置动态 base 路径: -```typescript -const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes -}) -``` - -#### 6.1.4 API请求路径规范 - -前端所有API请求统一使用网关前缀 `/fund`,由Gateway转发到后端服务: - -``` -前端请求 Gateway转发 后端服务 -/fund/auth/login -> /api/v1/auth/login -> fund-sys -/fund/customer/page -> /api/v1/customer/page -> fund-cust -/fund/project/page -> /api/v1/project/page -> fund-proj -/fund/exp/expense -> /api/v1/exp/expense -> fund-exp -/fund/receipt/receivable -> /api/v1/receipt/receivable -> fund-receipt -``` - -#### 6.1.5 API集中管理规范 - -前端项目必须采用**独立目录或文件集中管理**对后台API的请求,便于调整和优化。 - -**目录结构示例**: - -``` -fund-mobile/src/ -└── api/ - ├── index.ts # API集中定义入口,统一导出 - ├── request.ts # Axios实例配置、拦截器 - └── modules/ # 按业务模块拆分(可选) - ├── auth.ts # 用户认证API - ├── customer.ts # 客户管理API - └── expense.ts # 支出管理API -``` - -**api/index.ts 示例**: - -```typescript -import request from './request' - -// ===================== 用户认证 ===================== -export function login(data: { username: string; password: string }) { - return request.post('/auth/login', data) -} - -export function getUserInfo() { - return request.get('/auth/info') -} - -// ===================== 支出管理 ===================== -export function createExpense(data: any) { - return request.post('/exp/expense', data) -} - -export function getExpenseTypeTree() { - return request.get('/exp/expense-type/tree') -} - -// ===================== 应收款管理 ===================== -export function getReceivableList(params: { pageNum: number; pageSize: number }) { - return request.get('/receipt/receivable/page', { params }) -} -``` - -**Vue组件调用示例**: - -```typescript -// 正确:使用集中定义的API函数 -import { getReceivableList, createExpense } from '@/api' - -const res = await getReceivableList({ pageNum: 1, pageSize: 10 }) - -// 错误:直接在组件中硬编码URL -import request from '@/api/request' -const res = await request.get('/receipt/api/v1/receipt/receivable/page') -``` - -**规范要求**: - -1. **禁止硬编码**:Vue组件中禁止直接使用 `request.get('/xxx/xxx')` 形式调用API -2. **统一入口**:所有API函数从 `@/api` 统一导出,按模块分组 -3. **路径简化**:API函数内部使用简化路径(如 `/auth/login`),由 request.ts 的 baseURL 统一添加前缀 -4. **便于维护**:后端接口变更时,只需修改 api/index.ts 一处即可 - -### 6.3 生产环境部署 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 负载均衡层 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Nginx (主) + Nginx (备) │ │ -│ │ Keepalived VIP: 192.168.1.10 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────────┐ -│ 应用服务层 │ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Gateway-1 │ │ Gateway-2 │ │ Gateway-3 │ │ -│ │ :8080 │ │ :8080 │ │ :8080 │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ -│ ┌──────┴────────────────┴────────────────┴──────┐ │ -│ │ Nacos 集群 (3节点) │ │ -│ │ 8848 / 9848 / 7848 │ │ -│ └───────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │fund-sys │ │fund-cust │ │fund-proj │ │fund-req │ │ -│ │ x 2 │ │ x 2 │ │ x 2 │ │ x 2 │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │fund-exp │ │fund-rec │ │fund-rpt │ │fund-file │ │ -│ │ x 2 │ │ x 2 │ │ x 2 │ │ x 2 │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────────┐ -│ 数据存储层 │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Redis 集群 (6节点,3主3从) │ │ -│ │ 6379 / 6380 / 6381 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ MySQL 主从集群 │ │ -│ │ Master: 3306 ──> Slave: 3306 (x2) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 6.4 容器化部署 - -#### 6.4.1 Docker Compose 配置 - -```yaml -version: '3.8' -services: - # Nacos - nacos: - image: nacos/nacos-server:v3.0.0 - ports: - - "8848:8848" - - "9848:9848" - environment: - - MODE=standalone - - # Redis - redis: - image: redis:8.0.2 - ports: - - "6379:6379" - command: redis-server --requirepass zjf@123456 - - # MySQL - mysql: - image: mysql:8.0 - ports: - - "3306:3306" - environment: - - MYSQL_ROOT_PASSWORD=zjf@123456 - volumes: - - ./sql:/docker-entrypoint-initdb.d - - # Gateway - gateway: - image: fundplatform/fund-gateway:1.0.0 - ports: - - "8080:8080" - environment: - - NACOS_SERVER=nacos:8848 - - # Services - fund-sys: - image: fundplatform/fund-sys:1.0.0 - deploy: - replicas: 2 - environment: - - NACOS_SERVER=nacos:8848 - - DB_HOST=mysql - - REDIS_HOST=redis -``` - -### 6.3 单机部署配置 - -#### 6.3.1 配置文件架构 - -项目采用**统一配置 + 个性化配置**分离架构: - -``` -fundplatform/ -├── scripts/ -│ └── env.properties # 统一配置(所有服务共用) -├── fund-sys/src/main/resources/ -│ └── service.properties # 个性化配置(每服务独立) -├── fund-gateway/src/main/resources/ -│ └── service.properties -└── ...其他服务 -``` - -| 配置文件 | 位置 | 用途 | 打包后 | -|----------|------|------|--------| -| `env.properties` | scripts/ | 统一配置(所有服务共用) | conf/env.properties | -| `service.properties` | 各服务 src/main/resources/ | 个性化配置(每服务独立) | conf/service.properties | - -**加载顺序**: 先加载 `env.properties`,后加载 `service.properties`(个性化覆盖统一) - -#### 6.3.2 统一配置 (env.properties) - -```properties -# ============================================ -# 环境变量配置文件(所有服务共用) -# ============================================ - -# Nacos配置 -NACOS_SERVER_ADDR=localhost:8848 -NACOS_NAMESPACE=fund-platform -NACOS_GROUP=DEFAULT_GROUP -NACOS_USERNAME=nacos -NACOS_PASSWORD=nacos - -# Redis配置 -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD=zjf@123456 -REDIS_DATABASE=0 -REDIS_TIMEOUT=10000 -REDIS_POOL_MAX_ACTIVE=8 -REDIS_POOL_MAX_WAIT=-1 -REDIS_POOL_MAX_IDLE=8 -REDIS_POOL_MIN_IDLE=0 - -# Hikari连接池配置 -HIKARI_MINIMUM_IDLE=5 -HIKARI_CONNECTION_TIMEOUT=30000 - -# Sentinel配置(Gateway使用) -SENTINEL_DASHBOARD=localhost:8080 -SENTINEL_PORT=8719 - -# 网关限流配置 -GATEWAY_RATE_LIMIT_REPLENISH_RATE=100 -GATEWAY_RATE_LIMIT_BURST_CAPACITY=200 - -# 日志配置 -LOG_PATH=/datacfs/applogs -LOG_LEVEL_ROOT=INFO -LOG_LEVEL_APP=DEBUG -LOG_PATTERN=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n - -# 多租户路由配置 -TENANT_ROUTING_ENABLED=true -TENANT_HEADER=X-Tenant-Id -DEFAULT_TENANT_ID=1 -``` - -#### 6.3.3 个性化配置 (service.properties) - -```properties -# ============================================ -# 服务个性化配置 -# 此文件随服务打包,每个服务独立配置 -# ============================================ - -# 服务名称 -APP_NAME=fund-sys - -# 实例名称(多租户场景使用,默认与APP_NAME相同) -# 多租户示例:fund-sys-shared, fund-sys-vip001 -INSTANCE_NAME=${APP_NAME} - -# 租户标识(多租户场景使用,用于Nacos元数据路由) -# 空值=共享实例,有值=VIP专属实例 -TENANT_ID= - -# 服务端口(可覆盖application.yml中的配置) -# SERVER_PORT=8100 -``` - -#### 6.3.4 打包目录结构 - -使用 Maven Assembly 打包后的目录结构: - -``` -fund-sys/ # 服务根目录 -├── bin/ # 脚本目录 -│ ├── start.sh # 启动脚本 -│ ├── stop.sh # 停止脚本 -│ ├── restart.sh # 重启脚本 -│ └── status.sh # 状态查看脚本 -├── lib/ # 依赖JAR目录 -│ └── fund-sys.jar # 服务JAR包 -└── conf/ # 配置文件目录 - ├── env.properties # 统一配置 - ├── service.properties # 个性化配置 - ├── application.yml # 主配置 - ├── bootstrap.yml # 启动配置 - └── logback-spring.xml # 日志配置 -``` - -#### 6.3.5 多租户部署配置 - -**共享实例配置**: -```properties -# service.properties -APP_NAME=fund-sys -INSTANCE_NAME=fund-sys -TENANT_ID= # 空值表示共享实例 -``` - -**VIP专属实例配置**: -```properties -# service.properties -APP_NAME=fund-sys -INSTANCE_NAME=fund-sys-vip001 # 实例名称带租户标识 -TENANT_ID=vip001 # 租户ID用于Nacos路由 -``` - -**多实例部署目录**: -``` -/opt/fundplatform/deploy/ -├── fund-sys/ # 共享实例 -│ └── conf/ -│ └── service.properties # INSTANCE_NAME=fund-sys, TENANT_ID= -├── fund-sys-vip001/ # VIP实例1 -│ └── conf/ -│ └── service.properties # INSTANCE_NAME=fund-sys-vip001, TENANT_ID=vip001 -└── fund-sys-vip002/ # VIP实例2 - └── ... -``` - -#### 6.3.6 日志配置集中化 - -日志管理统一由 `logback-spring.xml` 控制,从环境变量读取配置: - -```xml - - - - - - - - -``` - -**注意**: `application.yml` 中不再配置 `logging` 节点,完全由 `logback-spring.xml` 管理。 - -#### 6.3.7 脚本加载逻辑 - -启动脚本统一使用 `load_properties` 函数加载配置: - -```bash -# 加载函数:读取properties文件 -load_properties() { - local file="$1" - if [ -f "$file" ]; then - while IFS='=' read -r key value; do - [[ "$key" =~ ^#.*$ ]] && continue - [[ -z "$key" ]] && continue - key=$(echo "$key" | xargs) - value=$(echo "$value" | xargs) - export "$key=$value" - done < "$file" - fi -} - -# 1. 加载统一配置 -load_properties "${APP_HOME}/conf/env.properties" - -# 2. 加载个性化配置(覆盖统一配置) -load_properties "${APP_HOME}/conf/service.properties" -``` - ---- - -## 七、监控与运维 - -### 7.1 监控体系 - -| 监控类型 | 工具 | 监控内容 | -|----------|------|----------| -| **应用监控** | Prometheus + Grafana | JVM、接口响应、错误率 | -| **链路追踪** | SkyWalking | 调用链、性能瓶颈 | -| **日志收集** | ELK Stack | 业务日志、错误日志 | -| **告警通知** | AlertManager | 钉钉/邮件/短信告警 | - -### 7.2 运维脚本 - -```bash -# 服务启动脚本 -#!/bin/bash -SERVICES=("gateway" "sys" "cust" "proj" "req" "exp" "receipt" "report" "file") -for service in "${SERVICES[@]}"; do - docker-compose up -d fund-${service} - sleep 5 -done - -# 健康检查脚本 -#!/bin/bash -curl -f http://localhost:8080/actuator/health || exit 1 -``` - ---- - -## 八、开发规范 - -### 8.0 开发规则总览 - -| 规则类别 | 内容摘要 | 详细章节 | -|----------|----------|----------| -| 分层架构 | 表现层 / 应用服务层 / 领域服务层 / 数据访问层 / 基础设施层,单向依赖、各司其职 | 3.2 架构分层 | -| 模块通信 | 基础服务走 Maven 依赖,业务服务之间通过 OpenFeign + DTO/JSON 契约解耦 | 2.3 模块通信方式 | -| API 设计 | 契约优先设计,统一使用 DTO/VO,对外响应统一 Result 结构 | 8.2 API 设计规范 | -| 数据访问 | MyBatis-Plus 的 ServiceImpl 只做数据访问,命名为 XxxDataService,不做业务 | 8.3 MyBatis-Plus 使用规范 | -| Controller | 只处理数据接收/校验、调用业务 Service、封装返回结果,不写业务逻辑 | 8.4 Controller 与请求参数校验规范 | -| 参数校验 | 使用 @Valid/@Validated + @NotBlank/@NotNull 等进行 Bean Validation,全局异常统一返回 | 8.4 Controller 与请求参数校验规范 | -| 事务边界 | 事务由应用服务层控制,DataService/Mapper 不主动开事务,跨服务一致性优先事件驱动 | 8.5 事务与测试规范 | -| 测试策略 | DomainService 做单元测试,AppService 做少量集成测试,Controller 做契约测试 | 8.5 事务与测试规范 | - -> **说明**:本章节仅作为开发规则的索引入口,具体细则以对应章节为准。 - -### 8.1 代码结构 - -``` -fund-sys/ -├── src/ -│ ├── main/ -│ │ ├── java/ -│ │ │ └── com/fundplatform/sys/ -│ │ │ ├── SysApplication.java -│ │ │ ├── config/ # 配置类 -│ │ │ ├── controller/ # 控制器层(表现层) -│ │ │ ├── service/ # 业务服务层(AppService/DomainService) -│ │ │ ├── data/ # MyBatis-Plus 数据访问层 -│ │ │ │ ├── entity/ # 实体类(Entity) -│ │ │ │ ├── mapper/ # Mapper 接口 -│ │ │ │ └── service/ # XxxDataService(继承 ServiceImpl) -│ │ │ ├── dto/ # 数据传输对象(DTO) -│ │ │ ├── vo/ # 视图对象(VO) -│ │ │ ├── enums/ # 枚举类 -│ │ │ ├── utils/ # 工具类 -│ │ │ └── handler/ # 处理器 -│ │ └── resources/ -│ │ ├── application.yml -│ │ ├── application-dev.yml -│ │ ├── application-prod.yml -│ │ └── mapper/ # XML 映射文件 -│ └── test/ -├── Dockerfile -├── pom.xml -└── README.md -``` - -### 8.2 API 设计规范 - -| 规范项 | 要求 | -|--------|------| -| **URL** | /api/{service}/{version}/{resource}/{action} | -| **HTTP方法** | GET(查)、POST(增)、PUT(改)、DELETE(删) | -| **响应格式** | { "code": 200, "message": "", "data": {} } | -| **分页** | pageNum, pageSize, total, list | -| **版本** | v1, v2 在URL中体现 | - -**设计原则**: -- 接口设计采用 **契约优先(Contract-First)**,优先在文档/注释中定义好路径、入参 DTO、出参 VO,再落地实现; -- 对外暴露的数据结构统一使用 DTO/VO,不直接复用持久化实体 Entity,避免前后端强耦合; -- DTO 负责接口语义表达,Entity 负责数据库持久化,两者通过组装/转换解耦; -- 所有对外接口应结合 8.4 小节的参数校验规范,保证输入合法性。 - -### 8.3 MyBatis-Plus 使用规范 - -1. **职责边界** - - MyBatis-Plus 的 `ServiceImpl` / `IService` 实现类 **只负责数据访问、数据封装和转换**,不承载业务流程、业务校验、权限控制等逻辑; - - 所有业务逻辑必须放在**业务服务层**(如 `XxxService` / `XxxDomainService`),通过组合调用 `XxxDataService`/Mapper 完成。 - -2. **命名规范:区分数据服务与业务服务** - - 基于 MyBatis-Plus 的数据访问服务类,统一命名为:`XxxDataService` 或 `XxxDataServiceImpl`,例如:`UserDataService`; - - 业务服务类命名为:`XxxService` / `XxxDomainService` / `XxxAppService`,例如:`UserService`; - - 禁止出现既继承 `ServiceImpl` 又以 `XxxService` 命名、同时承载业务逻辑的“万能 Service”。 - -3. **日志规范:MyBatis Service 不使用 @Slf4j** - - 任何继承 `ServiceImpl` 或实现 `IService` 的数据访问服务类,**禁止使用 Lombok 的 `@Slf4j` 注解**; - - 如需记录业务日志,应在上层业务服务类中使用日志记录,或在数据服务中手动声明 `private static final Logger logger = LoggerFactory.getLogger(XxxDataService.class);` 且避免字段名 `log`,防止与 MyBatis 内部 `Log log` 冲突; - - 避免出现 `log.info(...)` 这类调用 MyBatis `Log` 接口的错误用法。 - -4. **包结构规范:数据访问层独立归档** - - 将 MyBatis-Plus 相关的 `Entity`、`Mapper`、`ServiceImpl` 等统一放在**独立的 data 包**中,例如: - - `com.fundplatform.sys.data.entity` - - `com.fundplatform.sys.data.mapper` - - `com.fundplatform.sys.data.service` - - 业务服务层使用 `com.fundplatform.sys.service` / `com.fundplatform.sys.domain` 等包名,与数据访问层在物理结构上清晰分离; - - Controller 只依赖业务服务层,不直接依赖 `ServiceImpl` 或 Mapper,避免 MyBatis 数据服务与业务 Service 在包名和职责上产生混淆。 - -### 8.4 Controller 与请求参数校验规范 - -1. **Controller 职责** - - 仅负责: - - 接收请求数据(包含 Header、Path、Query、Body); - - 调用**业务服务层**(`XxxService` / `XxxDomainService`); - - 对业务层返回结果进行封装和格式转换(如封装为统一 `Result`); - - 明确禁止在 Controller 中编写业务规则、事务控制、跨聚合业务编排等复杂逻辑; - - Controller 不直接依赖 MyBatis 的 `ServiceImpl` / Mapper,只依赖业务服务接口。 - -2. **请求参数校验(Bean Validation)** - - 强制启用 Spring Validation,对外暴露的接口必须进行参数校验: - - 使用 `@Valid` 或 `@Validated` 标注方法参数或类; - - 对必填字段使用 `@NotBlank`(字符串)、`@NotNull`(对象)、`@NotEmpty`(集合/数组)等注解; - - 对数值范围使用 `@Min`、`@Max`、`@Positive`、`@PositiveOrZero` 等注解; - - 对字符串长度使用 `@Size`; - - 推荐在 DTO 层(如 `LoginRequestDTO`、`CustomerCreateDTO`)上声明校验规则,而不是在 Entity 上; - - 统一使用全局异常处理(如 `@RestControllerAdvice`)拦截 `MethodArgumentNotValidException` / `ConstraintViolationException`,返回统一错误结构。 - -3. **示例:登录接口参数校验** - -```java -@Data -public class LoginRequestDTO { - - @NotBlank(message = "用户名不能为空") - private String username; - - @NotBlank(message = "密码不能为空") - private String password; -} - -@RestController -@RequestMapping("/api/v1/auth") -@RequiredArgsConstructor -public class AuthController { - - private final AuthService authService; - - @PostMapping("/login") - public Result login(@Valid @RequestBody LoginRequestDTO request) { - LoginVO vo = authService.login(request); - return Result.success(vo); - } -} -``` - -4. **示例:全局参数校验异常处理** - -```java -@RestControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(MethodArgumentNotValidException.class) - public Result handleValidationException(MethodArgumentNotValidException ex) { - String message = ex.getBindingResult().getFieldErrors().stream() - .findFirst() - .map(FieldError::getDefaultMessage) - .orElse("请求参数不合法"); - return Result.error(400, message); - } -} -``` - ---- - -### 8.5 事务与测试规范 - -1. **事务边界** - - 事务主要由应用服务层(App Service)控制,在应用服务方法上使用 `@Transactional`,统一管理一个用例内的多次数据访问; - - Controller 不直接开启事务,数据访问层(DataService/Mapper)不主动声明事务,只参与由上层开启的事务; - - 跨服务的一致性场景(如应收款 + 收款 + 统计)优先通过事件驱动或异步补偿等方式设计,避免在多个服务间使用分布式强一致事务作为默认方案。 - -2. **可测试性** - - 领域服务(Domain Service)尽量保持无状态、无基础设施依赖,方便通过单元测试验证核心业务规则(金额计算、状态流转、风控规则等); - - 应用服务层可通过少量集成测试(SpringBootTest)验证典型用例的调用链和事务行为; - - Controller 层以契约测试为主,重点验证请求/响应结构是否符合 API 设计规范以及参数校验是否生效。 - ---- - -## 九、附录 - -### 9.1 端口分配表 - -| 服务 | 端口 | 说明 | -|------|------|------| -| Nginx | 80/443 | 负载均衡 | -| Gateway | 8080 | API网关 | -| Nacos | 8848/9848 | 注册配置中心 | -| Redis | 6379 | 缓存服务 | -| MySQL | 3306 | 数据库 | -| XXL-JOB | 8081 | 任务调度中心 | - -### 9.2 文档修订记录 - -| 版本 | 修订日期 | 修订内容 | 修订人 | -|------|----------|----------|--------| -| v1.0 | 2026-02-13 | 初始版本 | zhangjf | -| v1.1 | 2026-02-13 | 补充多租户架构(一库多租户/一库一租户)和 Head 日志追踪设计 | zhangjf | -| v1.2 | 2026-02-13 | 补充 Shiro 认证框架、服务调用链 uid/uname 传递设计 | zhangjf | -| v1.3 | 2026-02-13 | 补充 HikariCP 连接池、支持异步场景的 UserContext 封装 | zhangjf | -| v1.4 | 2026-02-13 | 补充统一全局上下文 GlobalContext,统筹 tid/uid/uname 获取和异步传递 | zhangjf | -| v1.5 | 2026-02-13 | 补充模块通信与 OpenFeign 参数对象管理策略、分层架构职责说明、MyBatis-Plus 使用规范、Controller 与参数校验规范、事务与测试规范及开发规则总览 | zhangjf | -| v1.6 | 2026-02-13 | 补充单机部署配置:配置文件分离架构(env.properties+service.properties)、打包目录结构、多租户部署配置、日志配置集中化、脚本加载逻辑 | zhangjf | -| v1.7 | 2026-02-23 | 新增6.1前端部署路径设计:Nginx子路径部署、部署路径规划、Nginx配置示例、前端构建配置、API请求路径规范 | zhangjf | -| v1.8 | 2026-02-23 | 新增6.1.5 API集中管理规范:独立目录管理、禁止硬编码、统一入口、路径简化、便于维护 | zhangjf | - ---- - -**文档结束** diff --git a/doc/资金服务平台 FundPlatform 需求文档.md b/doc/资金服务平台 FundPlatform 需求文档.md deleted file mode 100644 index ae484f5..0000000 --- a/doc/资金服务平台 FundPlatform 需求文档.md +++ /dev/null @@ -1,794 +0,0 @@ -# 资金服务平台 (FundPlatform) - 需求文档 - -> **文档版本**: v1.0 -> **创建日期**: 2026-02-13 -> **项目名称**: 资金服务平台 -> **项目代号**: fundplatform - ---- - -## 一、项目概述 - -### 1.1 项目背景 - -资金服务平台旨在建立一套完整的公司项目资金管理解决方案,实现对应收账款和项目支出的全流程跟踪与管理,提升财务管理效率和资金周转率。 - -### 1.2 整体目标 - -| 目标类别 | 具体描述 | -| ---------------- | ------------------------------------------------------ | -| **应收账款管理** | 对公司项目的应收账款进行跟踪、确认、收款记录和账期管理 | -| **项目支出管理** | 对公司项目支出进行申请、审批、执行和核销的全流程管理 | -| **数据可视化** | 提供多维度的财务报表和统计分析,辅助决策 | -| **移动办公** | 支持管理后台和移动端H5双端访问,提升办公效率 | - ---- - -## 二、业务需求 - -### 2.1 核心业务流程 - -#### 2.1.1 应收账款管理流程 - -```mermaid -flowchart TD - A[需求工单创建] --> B[交付跟踪] - B --> C[应收款确认] - C --> D[收款记录] - D --> E[账期管理] - E --> F[回款统计] -``` - -#### 2.1.2 支出管理流程 - -```mermaid -flowchart TD - A[支出申请] --> B[付款执行] - B --> C[付款状态更新] - C --> D[标记完成/作废] -``` - -### 2.2 功能需求 - -| 功能类别 | 功能描述 | -| ------------ | -------------------------------------------- | -| **状态管理** | 应收款状态(待收款、部分收款、已收款、逾期) | -| **收款记录** | 实际收款金额、收款日期、收款方式、收款凭证 | -| **付款管理** | 付款操作、付款凭证、付款状态跟踪 | -| **财务报表** | 收支汇总、现金流量、支出趋势分析 | -| **数据导出** | Excel导出、自定义报表 | -| **审计追踪** | 操作日志、数据变更记录 | - -### 2.3 非功能性需求 - -| 需求类别 | 具体要求 | -| ---------- | --------------------------------------------------- | -| **安全性** | 用户权限控制、数据加密传输、操作审计日志 | -| **性能** | 支持≥100人并发访问、列表加载<2秒、查询响应<3秒 | -| **可用性** | 响应式设计、移动端适配、操作流程简化 | -| **可靠性** | 数据每日备份、故障恢复时间<30分钟、系统可用性≥99.5% | -| **兼容性** | 支持主流浏览器(Chrome、Firefox、Safari、Edge) | - ---- - -## 三、功能模块详细需求 - -### 3.1 系统管理模块 - -#### 3.1.1 用户管理 -- **用户注册/登录/注销** - - 支持用户名密码登录 - - 支持登录状态保持 - - 支持安全退出 - -- **用户信息维护** - - 姓名、手机号、邮箱、部门等信息维护 - - 头像上传 - - 密码修改 - -- **角色权限管理** - - 管理员:系统所有权限 - - 财务:支出审批、收款管理、财务报表 - - 项目经理:项目相关操作、支出申请 - - 普通员工:支出申请、个人数据查看 - -- **操作日志记录与查询** - - 记录用户操作行为 - - 支持按时间、用户、操作类型查询 - -#### 3.1.2 组织架构管理 -- **部门管理** - - 部门创建、编辑、删除 - - 部门层级关系维护 - - 部门负责人设置 - -- **岗位管理** - - 岗位定义 - - 岗位职责描述 - -- **人员分配** - - 部门人员配置 - - 人员调动记录 - -#### 3.1.3 系统配置 -- **基础参数设置** - - 公司信息配置 - - 币种设置 - - 日期格式设置 - ---- - -### 3.2 客户管理模块 - -#### 3.2.1 客户信息管理 -- **客户档案管理** - - 客户创建、编辑、删除、禁用 - - 客户编码自动生成 - - 客户名称、简称、类型(企业/个人) - -- **客户分类管理** - - 按行业分类 - - 按规模分类(小型、中型、大型) - - 按等级分类(A/B/C/D/normal) - -- **客户联系人管理** - - 联系人信息维护(姓名、职位、电话、邮箱等) - - 主要联系人标记 - - 联系人排序 - ---- - -### 3.3 项目管理模块 - -#### 3.3.1 项目信息管理 -- **项目基础管理** - - 项目创建、编辑、归档、删除 - - 项目编号自动生成 - - 项目名称、简称维护 - -- **项目基本信息** - - 项目负责人设置 - - 项目开始/结束日期 - - 项目预算金额 - - 合同金额 - -- **项目状态管理** - - 筹备中 - - 进行中 - - 已完成 - - 已归档 - - 已取消 - -#### 3.3.2 项目关联管理 -- **客户关联** - - 项目所属客户绑定 - - 客户项目列表 - -- **团队成员分配** - - 项目经理分配 - - 开发人员分配 - - 财务人员分配 - - 工作量占比设置 - ---- - -### 3.4 需求清单管理模块 - -#### 3.4.1 需求工单信息管理 -- **需求工单基础管理** - - 需求工单创建、编辑、删除 - - 需求工单编号自动生成 - -- **需求工单详情** - - 需求工单名称 - - 需求描述 - - 所属客户(关联客户管理) - - 所属项目(关联项目管理) - - 开发工时(预估/实际) - - 交付日期 - - 应收款金额 - - 应收款日期 - -- **需求状态管理** - - 待开发 - - 开发中 - - 待交付 - - 已完成 - -#### 3.4.2 应收款管理 -- **应收款设置** - - 应收款金额设置 - - 应收款日期管理 - - 付款条款设置 - -- **交付日期跟踪** - - 交付日期提醒 - - 逾期预警 - ---- - -### 3.5 支出类型管理模块 - -#### 3.5.1 支出分类管理 -- **支出类型基础管理** - - 支出类型创建、编辑、删除 - - 支出类型编码设置 - -- **支出类型层级** - - 一级分类 - - 二级分类 - - 层级关系维护 - -- **常见支出类型** - - 人力成本(工资、奖金、社保) - - 办公费用(房租、水电、办公用品) - - 差旅费用(交通、住宿、餐饮) - - 采购费用(设备、软件、服务) - - 其他费用 - ---- - -### 3.6 支出管理模块 - -#### 3.6.1 支出申请 -- **支出录入** - - 支出金额 - - 支出类型(关联支出类型管理) - - 支出事由 - - 支出日期 - - 所属项目(关联项目管理) - - 申请人(自动填充当前用户) - - 附件上传(发票、合同等) - -#### 3.6.2 支出执行 -- **付款操作** - - 确认付款 - - 付款日期记录 - - 付款账户选择 - -- **付款凭证管理** - - 付款截图上传 - - 银行回单上传 - - 凭证查看、下载 - -- **付款状态更新** - - 待付款 - - 已付款 - - 已核销 - -#### 3.6.3 支出状态管理 -- **标记完成** - - 确认支出已完成 - - 完成时间记录 - -- **作废处理** - - 支出作废申请 - - 作废原因记录 - - 作废审批(可选) - -- **退款管理** - - 退款申请 - - 退款金额记录 - - 退款凭证管理 - -#### 3.6.4 支出统计分析 -- **支出明细查询** - - 多条件筛选(时间、类型、项目、申请人等) - - 分页展示 - - 排序功能 - -- **支出趋势分析** - - 月度支出趋势 - - 季度支出统计 - - 年度支出汇总 - - 图表展示(柱状图、折线图) - ---- - -### 3.7 应收款管理模块 - -#### 3.7.1 应收款确认 -- **应收款生成** - - 从需求清单自动生成应收款 - - 应收款编号自动生成 - -- **应收款确认** - - 应收金额确认 - - 应收日期确认 - - 付款截止日期设置 - -#### 3.7.2 收款管理 -- **收款记录录入** - - 实际收款金额 - - 收款日期 - - 收款方式(银行转账、现金、支票、其他) - - 收款凭证上传(银行回单等) - - 付款方信息 - -- **收款方式管理** - - 常用收款方式维护 - - 收款账户管理 - -- **收款凭证管理** - - 凭证归档 - - 凭证查询 - - 凭证下载 - -- **应收款状态更新** - - 待收款 - - 部分收款 - - 已收款 - - 逾期 - ---- - -### 3.8 移动端模块 (H5) - -#### 3.8.1 移动端首页 -- **数据概览** - - 今日收支 - - 待收款金额 - - 待付款金额 - -- **快捷入口** - - 快速录入支出 - - 快速录入收款 - -#### 3.8.2 移动查询 -- **收支查询** - - 个人收支查询 - - 项目收支查询 - - 时间范围筛选 - -- **项目查询** - - 项目列表 - - 项目进度查看 - - 项目收支详情 - -- **客户查询** - - 客户信息查看 - - 客户往来记录 - -#### 3.8.3 移动录入 -- **支出录入** - - 快速录入支出申请 - - 拍照上传发票 - - 选择支出类型 - -- **收款录入** - - 现场收款记录 - - 收款凭证拍照 - - 选择收款方式 - ---- - -## 四、核心数据模型 - -### 4.1 系统管理模块 - -#### 4.1.1 用户表 (sys_user) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| --------------- | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| user_id | BIGINT | - | ✅ | 自增 | 用户ID(主键) | -| username | VARCHAR | 50 | ✅ | - | 用户名(登录账号) | -| password | VARCHAR | 100 | ✅ | - | 密码(加密存储) | -| real_name | VARCHAR | 50 | ✅ | - | 真实姓名 | -| gender | TINYINT | 1 | ❌ | 0 | 性别(0-未知,1-男,2-女) | -| phone | VARCHAR | 20 | ❌ | - | 手机号码 | -| email | VARCHAR | 100 | ❌ | - | 邮箱地址 | -| dept_id | BIGINT | - | ✅ | - | 所属部门ID | -| position | VARCHAR | 50 | ❌ | - | 岗位/职位 | -| role | VARCHAR | 20 | ✅ | user | 角色(admin-管理员,finance-财务,pm-项目经理,user-普通用户) | -| avatar | VARCHAR | 255 | ❌ | - | 头像URL | -| status | TINYINT | 1 | ✅ | 1 | 状态(0-禁用,1-启用) | -| last_login_time | DATETIME | - | ❌ | - | 最后登录时间 | -| last_login_ip | VARCHAR | 50 | ❌ | - | 最后登录IP | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -#### 4.1.2 部门表 (sys_dept) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------ | -------- | --------- | ---- | ----------------- | ------------------------------ | -| dept_id | BIGINT | - | ✅ | 自增 | 部门ID(主键) | -| dept_name | VARCHAR | 100 | ✅ | - | 部门名称 | -| parent_id | BIGINT | - | ❌ | 0 | 父部门ID(0表示顶级部门) | -| dept_code | VARCHAR | 50 | ❌ | - | 部门编码 | -| dept_level | INT | - | ✅ | 1 | 部门层级 | -| sort_order | INT | - | ✅ | 0 | 排序顺序 | -| leader | VARCHAR | 50 | ❌ | - | 部门负责人 | -| leader_phone | VARCHAR | 20 | ❌ | - | 负责人电话 | -| status | TINYINT | 1 | ✅ | 1 | 状态(0-禁用,1-启用) | -| remark | VARCHAR | 500 | ❌ | - | 备注说明 | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -#### 4.1.3 操作日志表 (sys_operation_log) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| -------------- | -------- | --------- | ---- | ----------------- | ----------------------------------- | -| log_id | BIGINT | - | ✅ | 自增 | 日志ID(主键) | -| user_id | BIGINT | - | ✅ | - | 操作人ID | -| username | VARCHAR | 50 | ✅ | - | 操作人用户名 | -| operation | VARCHAR | 100 | ✅ | - | 操作描述 | -| method | VARCHAR | 200 | ✅ | - | 操作方法 | -| params | TEXT | - | ❌ | - | 操作参数(JSON格式) | -| ip | VARCHAR | 50 | ✅ | - | 操作IP | -| user_agent | VARCHAR | 500 | ❌ | - | 用户代理 | -| operation_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 操作时间 | -| cost_time | BIGINT | - | ✅ | 0 | 耗时(毫秒) | -| result | VARCHAR | 20 | ✅ | success | 操作结果(success-成功,fail-失败) | -| error_msg | TEXT | - | ❌ | - | 错误信息 | - -### 4.2 客户管理模块 - -#### 4.2.1 客户表 (customer) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------------ | -------- | --------- | ---- | ----------------- | ----------------------------------------------- | -| customer_id | BIGINT | - | ✅ | 自增 | 客户ID(主键) | -| customer_code | VARCHAR | 50 | ✅ | - | 客户编码(唯一) | -| customer_name | VARCHAR | 200 | ✅ | - | 客户名称 | -| customer_short | VARCHAR | 100 | ❌ | - | 客户简称 | -| customer_type | VARCHAR | 20 | ✅ | enterprise | 客户类型(enterprise-企业,individual-个人) | -| industry | VARCHAR | 50 | ❌ | - | 所属行业 | -| scale | VARCHAR | 20 | ❌ | - | 企业规模(small-小型,medium-中型,large-大型) | -| level | VARCHAR | 20 | ✅ | normal | 客户等级(A/B/C/D/normal) | -| tax_no | VARCHAR | 50 | ❌ | - | 纳税人识别号 | -| legal_person | VARCHAR | 50 | ❌ | - | 法定代表人 | -| address | VARCHAR | 500 | ❌ | - | 公司地址 | -| phone | VARCHAR | 20 | ❌ | - | 联系电话 | -| email | VARCHAR | 100 | ❌ | - | 邮箱地址 | -| status | TINYINT | 1 | ✅ | 1 | 状态(0-禁用,1-启用) | -| remark | VARCHAR | 500 | ❌ | - | 备注说明 | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -#### 4.2.2 客户联系人表 (customer_contact) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------ | -------- | --------- | ---- | ----------------- | ------------------------------ | -| contact_id | BIGINT | - | ✅ | 自增 | 联系人ID(主键) | -| customer_id | BIGINT | - | ✅ | - | 客户ID(外键) | -| contact_name | VARCHAR | 50 | ✅ | - | 联系人姓名 | -| position | VARCHAR | 50 | ❌ | - | 职位/职务 | -| department | VARCHAR | 100 | ❌ | - | 所属部门 | -| phone | VARCHAR | 20 | ❌ | - | 联系电话 | -| mobile | VARCHAR | 20 | ❌ | - | 手机号码 | -| email | VARCHAR | 100 | ❌ | - | 邮箱地址 | -| is_primary | TINYINT | 1 | ✅ | 0 | 是否主要联系人(0-否,1-是) | -| sort_order | INT | - | ✅ | 0 | 排序顺序 | -| status | TINYINT | 1 | ✅ | 1 | 状态(0-禁用,1-启用) | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -### 4.3 项目管理模块 - -#### 4.3.1 项目表 (project) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------------ | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| project_id | BIGINT | - | ✅ | 自增 | 项目ID(主键) | -| project_code | VARCHAR | 50 | ✅ | - | 项目编号(唯一) | -| project_name | VARCHAR | 200 | ✅ | - | 项目名称 | -| project_short | VARCHAR | 100 | ❌ | - | 项目简称 | -| customer_id | BIGINT | - | ✅ | - | 客户ID(外键) | -| project_type | VARCHAR | 20 | ✅ | development | 项目类型(development-开发,maintenance-维护,consulting-咨询) | -| project_manager_id | BIGINT | - | ✅ | - | 项目经理ID | -| start_date | DATE | - | ✅ | - | 项目开始日期 | -| end_date | DATE | - | ❌ | - | 项目结束日期 | -| budget_amount | DECIMAL | 15,2 | ❌ | 0.00 | 项目预算金额 | -| contract_amount | DECIMAL | 15,2 | ❌ | 0.00 | 合同金额 | -| status | VARCHAR | 20 | ✅ | preparing | 项目状态(preparing-筹备中,ongoing-进行中,completed-已完成,archived-已归档,cancelled-已取消) | -| progress | INT | - | ✅ | 0 | 项目进度(0-100) | -| description | TEXT | - | ❌ | - | 项目描述 | -| remark | VARCHAR | 500 | ❌ | - | 备注说明 | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -#### 4.3.2 项目成员表 (project_member) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------ | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| member_id | BIGINT | - | ✅ | 自增 | 成员关系ID(主键) | -| project_id | BIGINT | - | ✅ | - | 项目ID(外键) | -| user_id | BIGINT | - | ✅ | - | 用户ID(外键) | -| role | VARCHAR | 50 | ✅ | member | 项目角色(pm-项目经理,dev-开发,finance-财务,member-普通成员) | -| join_date | DATE | - | ✅ | - | 加入日期 | -| leave_date | DATE | - | ❌ | - | 离开日期 | -| workload | DECIMAL | 5,2 | ❌ | 0.00 | 工作量占比(0-100) | -| status | TINYINT | 1 | ✅ | 1 | 状态(0-已离开,1-在职) | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -### 4.4 需求清单管理模块 - -#### 4.4.1 需求工单表 (requirement) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ----------------- | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| requirement_id | BIGINT | - | ✅ | 自增 | 需求ID(主键) | -| requirement_code | VARCHAR | 50 | ✅ | - | 需求编号(唯一) | -| requirement_name | VARCHAR | 200 | ✅ | - | 需求名称 | -| description | TEXT | - | ❌ | - | 需求描述 | -| project_id | BIGINT | - | ✅ | - | 项目ID(外键) | -| customer_id | BIGINT | - | ✅ | - | 客户ID(外键) | -| priority | VARCHAR | 20 | ✅ | normal | 优先级(high-高,normal-中,low-低) | -| estimated_hours | DECIMAL | 8,2 | ❌ | 0.00 | 预估开发工时(小时) | -| actual_hours | DECIMAL | 8,2 | ❌ | 0.00 | 实际开发工时(小时) | -| planned_start | DATE | - | ❌ | - | 计划开始日期 | -| planned_end | DATE | - | ❌ | - | 计划结束日期 | -| actual_start | DATE | - | ❌ | - | 实际开始日期 | -| actual_end | DATE | - | ❌ | - | 实际结束日期 | -| delivery_date | DATE | - | ✅ | - | 交付日期 | -| receivable_amount | DECIMAL | 15,2 | ✅ | 0.00 | 应收款金额 | -| receivable_date | DATE | - | ✅ | - | 应收款日期 | -| status | VARCHAR | 20 | ✅ | pending | 状态(pending-待开发,developing-开发中,delivered-已交付,completed-已完成) | -| progress | INT | - | ✅ | 0 | 开发进度(0-100) | -| attachment_url | VARCHAR | 500 | ❌ | - | 附件URL(需求文档等) | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -### 4.5 支出类型管理模块 - -#### 4.5.1 支出类型表 (expense_type) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------ | -------- | --------- | ---- | ----------------- | ------------------------------ | -| type_id | BIGINT | - | ✅ | 自增 | 支出类型ID(主键) | -| type_code | VARCHAR | 50 | ❌ | - | 支出类型编码 | -| type_name | VARCHAR | 100 | ✅ | - | 支出类型名称 | -| parent_id | BIGINT | - | ❌ | 0 | 父类型ID(0表示一级类型) | -| type_level | INT | - | ✅ | 1 | 类型层级 | -| sort_order | INT | - | ✅ | 0 | 排序顺序 | -| description | VARCHAR | 200 | ❌ | - | 类型描述 | -| status | TINYINT | 1 | ✅ | 1 | 状态(0-禁用,1-启用) | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -### 4.6 支出管理模块 - -#### 4.6.1 支出表 (expense) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| --------------- | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| expense_id | BIGINT | - | ✅ | 自增 | 支出ID(主键) | -| expense_code | VARCHAR | 50 | ✅ | - | 支出编号(唯一) | -| expense_type_id | BIGINT | - | ✅ | - | 支出类型ID(外键) | -| expense_amount | DECIMAL | 15,2 | ✅ | 0.00 | 支出金额 | -| expense_date | DATE | - | ✅ | - | 支出日期 | -| expense_reason | TEXT | - | ✅ | - | 支出事由 | -| project_id | BIGINT | - | ❌ | - | 所属项目ID(外键,可为空) | -| applicant_id | BIGINT | - | ✅ | - | 申请人ID | -| department_id | BIGINT | - | ✅ | - | 申请部门ID | -| payment_method | VARCHAR | 20 | ✅ | transfer | 付款方式(transfer-转账,cash-现金,check-支票,other-其他) | -| payment_account | VARCHAR | 100 | ❌ | - | 付款账户 | -| attachment_url | VARCHAR | 500 | ❌ | - | 附件URL(发票、合同等) | -| status | VARCHAR | 20 | ✅ | pending | 状态(pending-待付款,paid-已付款,completed-已完成,cancelled-已作废) | -| payment_date | DATE | - | ❌ | - | 付款日期 | -| payment_voucher | VARCHAR | 255 | ❌ | - | 付款凭证(银行回单等) | -| remark | VARCHAR | 500 | ❌ | - | 备注说明 | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -### 4.7 应收款管理模块 - -#### 4.7.1 应收款表 (receivable) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ------------------- | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| receivable_id | BIGINT | - | ✅ | 自增 | 应收款ID(主键) | -| receivable_code | VARCHAR | 50 | ✅ | - | 应收款编号(唯一) | -| requirement_id | BIGINT | - | ✅ | - | 需求ID(外键) | -| project_id | BIGINT | - | ✅ | - | 项目ID(外键) | -| customer_id | BIGINT | - | ✅ | - | 客户ID(外键) | -| receivable_amount | DECIMAL | 15,2 | ✅ | 0.00 | 应收款金额 | -| receivable_date | DATE | - | ✅ | - | 应收款日期 | -| payment_due_date | DATE | - | ✅ | - | 付款截止日期 | -| payment_method | VARCHAR | 20 | ❌ | - | 付款方式(transfer-转账,cash-现金,check-支票,other-其他) | -| bank_account | VARCHAR | 100 | ❌ | - | 收款账户 | -| status | VARCHAR | 20 | ✅ | pending | 状态(pending-待收款,partial-部分收款,received-已收款,overdue-逾期) | -| received_amount | DECIMAL | 15,2 | ✅ | 0.00 | 已收款金额 | -| unpaid_amount | DECIMAL | 15,2 | ✅ | 0.00 | 未收款金额 | -| overdue_days | INT | - | ✅ | 0 | 逾期天数 | -| remark | VARCHAR | 500 | ❌ | - | 备注说明 | -| created_by | BIGINT | - | ✅ | - | 创建人ID | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | -| updated_by | BIGINT | - | ✅ | - | 更新人ID | -| updated_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 更新时间 | -| deleted | TINYINT | 1 | ✅ | 0 | 逻辑删除(0-未删除,1-已删除) | - -#### 4.7.2 收款记录表 (receipt) - -| 字段名 | 类型 | 长度/精度 | 必填 | 默认值 | 说明 | -| ----------------- | -------- | --------- | ---- | ----------------- | ------------------------------------------------------------ | -| receipt_id | BIGINT | - | ✅ | 自增 | 收款记录ID(主键) | -| receipt_code | VARCHAR | 50 | ✅ | - | 收款编号(唯一) | -| receivable_id | BIGINT | - | ✅ | - | 应收款ID(外键) | -| receipt_amount | DECIMAL | 15,2 | ✅ | 0.00 | 收款金额 | -| receipt_date | DATE | - | ✅ | - | 收款日期 | -| receipt_method | VARCHAR | 20 | ✅ | transfer | 收款方式(transfer-转账,cash-现金,check-支票,other-其他) | -| receipt_account | VARCHAR | 100 | ❌ | - | 收款账户 | -| payer_name | VARCHAR | 100 | ❌ | - | 付款方名称 | -| receipt_voucher | VARCHAR | 255 | ❌ | - | 收款凭证URL(银行回单等) | -| operator_id | BIGINT | - | ✅ | - | 操作人ID | -| remark | VARCHAR | 200 | ❌ | - | 备注说明 | -| created_time | DATETIME | - | ✅ | CURRENT_TIMESTAMP | 创建时间 | - ---- - -## 五、技术架构 - -### 5.1 后端架构 - -| 组件 | 技术选型 | 说明 | -| ------------ | ------------------------- | ---------------------------------- | -| **应用框架** | Spring Cloud Alibaba + Nacos | Java生态,成熟稳定,适合企业级应用 | -| **数据库** | MySQL 8.0 | 支持事务、ACID,数据持久化 | -| **缓存** | Redis 7.x | 会话管理、热点数据缓存 | -| **文件存储** | 腾讯COS | 文件上传、附件存储 | -| **定时任务** | XXL-JOB | 定时提醒、数据统计 | -| **API文档** | Swagger / Knife4j | 接口文档自动生成 | - -### 5.2 前端架构 - -| 端 | 技术栈 | 说明 | -| ------------ | --------------------------------- | ----------------------------------- | -| **管理后台** | Vue 3 + TypeScript + Element Plus | 响应式设计,组件丰富 | -| **移动端** | Vue 3 + Vite 5 + Vant 4 | 移动端H5响应式应用 | -| **图表库** | ECharts 5.x | 数据可视化、报表展示 | -| **构建工具** | Vite 4.x | 快速构建、热更新 | - -### 5.3 部署架构 - -``` -┌─────────────────────────────────────────────────────────┐ -│ 负载均衡层 │ -│ Nginx + Keepalived │ -└─────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────┐ -│ 应用服务层 │ -│ Docker + Docker Compose / Kubernetes │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ 应用实例1 │ │ 应用实例2 │ │ 应用实例3 │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────┐ -│ 数据服务层 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ MySQL │ │ Redis │ │ Nacos │ │ -│ │ 主从复制 │ │ 缓存集群 │ │ 服务注册 │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────┐ -│ 存储服务层 │ -│ 腾讯COS │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 六、项目实施规划 - -### 6.1 版本规划 - -#### 第一阶段:MVP版本(预计6-8周) - -**核心功能:** -- 基础用户管理和权限控制 -- 客户、项目管理 -- 需求工单管理 -- 支出类型和支出管理 -- 应收款基础跟踪 -- 简单报表统计 - -**交付物:** -- 管理后台基础功能 -- 数据库设计与实现 -- 基础API接口 - ---- - -#### 第二阶段:功能完善(预计4-6周) - -**新增功能:** -- 收款管理功能(收款记录、收款凭证) -- 账期管理与逾期提醒 -- 移动端开发(Vue3 + Vant) -- 提醒预警机制 - -**交付物:** -- 完整的收款管理 -- 移动端H5应用 -- 消息提醒功能 - ---- - -#### 第三阶段:高级功能(预计4-6周) - -**新增功能:** -- 高级统计分析(多维度报表) -- 数据可视化(图表展示) -- 数据导出功能(Excel/PDF) -- 操作日志与审计追踪 -- 系统配置与参数管理 - -**交付物:** -- 完整的报表系统 -- 数据导出功能 -- 系统管理后台 - ---- - -### 6.2 人员配置建议 - -| 角色 | 人数 | 职责 | -| ---------- | --------- | ---------------------------- | -| 项目经理 | 1 | 项目管理、需求沟通、进度控制 | -| 后端开发 | 2 | 后端API开发、数据库设计 | -| 前端开发 | 2 | 管理后台开发、移动端开发 | -| 测试工程师 | 1 | 功能测试、性能测试、Bug修复 | -| 运维工程师 | 1(兼职) | 环境部署、服务器维护 | - ---- - -## 七、附录 - -### 7.1 术语表 - -| 术语 | 说明 | -| -------- | -------------------------------------------- | -| 应收款 | 公司因销售商品、提供服务等应向客户收取的款项 | -| 支出 | 公司为开展业务活动所发生的各项费用 | -| 账期 | 从交易发生到付款/收款的时间期限 | -| 回款率 | 实际收款金额与应收金额的比率 | -| 预算 | 对未来一定时期内的收支计划 | - -### 7.2 参考资料 - -- 《企业会计准则》 -- 《软件需求规格说明书编写指南》 -- 《RESTful API设计规范》 - ---- - -## 八、文档修订记录 - -| 版本 | 修订日期 | 修订内容 | 修订人 | -| ---- | ---------- | ------------ | -------- | -| v1.0 | 2026-02-13 | 根据功能清单生成需求文档 | zhangjf | - ---- - -**文档结束**