diff --git a/doc/资金服务平台FundPlatform功能清单.md b/doc/资金服务平台FundPlatform功能清单.md new file mode 100644 index 0000000..fec5d4e --- /dev/null +++ b/doc/资金服务平台FundPlatform功能清单.md @@ -0,0 +1,263 @@ +## 四、功能模块分解 + +### 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 new file mode 100644 index 0000000..9bb4c7f --- /dev/null +++ b/doc/资金服务平台FundPlatform架构设计文档.md @@ -0,0 +1,4143 @@ +# 资金服务平台 (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 new file mode 100644 index 0000000..ae484f5 --- /dev/null +++ b/doc/资金服务平台FundPlatform需求文档.md @@ -0,0 +1,794 @@ +# 资金服务平台 (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 | + +--- + +**文档结束**