diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index f541eba..d76eac4 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -109,4 +109,6 @@ | 2026-08-13 23:35 | 测试验证 | P5-T10 门禁诊断:修复 WriterGenerationError 吞掉底层 LLM 错误(如 401 详情),补透传测试 | src/genesis/writer/writer_agent.py; tests/test_phase5_writer_agent.py | hy3-free | | 2026-08-13 23:50 | Agent 实现 | 修复 HttpLLMClient 在同步门禁中多次 asyncio.run 复用已关闭事件循环致 Event loop is closed:chat 改为每次调用新建 client(保留 async with 协议)| src/genesis/inference/client.py | hy3-free | | 2026-08-13 23:58 | Agent 实现 | P5-T10 语言对齐:WRITER_PROMPT_TEMPLATE 增加「正文语言须与章节标题一致」约束(模板日文则输出日文)| src/genesis/writer/writer_agent.py; tests/test_phase5_writer_agent.py | hy3-free | -| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash | \ No newline at end of file +| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash || 2026-08-23 | 需求理解 | 用户指出写概要设计书需「要件定義 + 既有系统(设计/代码)做影响调查 → 确定修改范围」;确认场景=追加改修、输入=Java 项目源码;选定既有系统样本 sunOnly/stock-trade-system(股票量化交易系统,无 LICENSE,仅作测试输入保留来源标注,引入 samples/existing-system/,13 模块 278 Java 文件);确认领域不一致 → 新造股票交易域追加改修样本 | docs/superpowers/specs/2026-08-23-impact-mvp.md; samples/existing-system/README.md | deepseek-v4-flash | +| 2026-08-23 | 架构设计 | Impact Agent MVP 设计(brainstorming 逐问确认):门控=用户是否提供 existing_system 路径(流水线级,非章节级);范围确认=与用户确认调查范围(模块/深度);消费模型=影响调查结果是生成概要设计书的主上下文(整本基于影响结果生成,无专用影响章/无 {{section:impact}} 锚点门控);MVP=变更点定位(确定性规则,无 LLM);spec 落盘 docs/superpowers/specs/2026-08-23-impact-mvp.md | docs/superpowers/specs/2026-08-23-impact-mvp.md | deepseek-v4-flash | +| 2026-08-23 | Agent 实现 | Impact Agent MVP 实现(TDD RED→GREEN→全量 351 passed / 99.27%):data_models 新增 ChangeType/ChangeElement/ImpactWarning/ChangeAnalysis/ImpactReport + StructuredSource.impact_report;新增 src/genesis/impact/(code_parser.py Java 解析 @RestController/@Service/@Entity/@TableName、existing_system_explorer.py 组装、impact_agent.py 变更点定位+impact_report_to_dict 序列化);source_aggregator 解除 existing_system=None 硬编码(新增 existing_system_path 门控);writer 集成——GenerationContext.impact_report + to_vars()[impact]、WRITER_PROMPT_TEMPLATE 加「影响调查上下文」、context_builder 透传、WriteOrchestrator.generate 门控自动跑 ImpactAgent + 回填 + 默认 meta(doc_title/version/created_at);新造样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名);门禁(fake 模式)PASS:影响调查 total=16 new=5 modified=8 deleted=3 unchanged=50 warnings=0,概要设计书 13 章产出,影响调查书 JSON 可下载 | src/genesis/data_models.py; src/genesis/impact/code_parser.py; src/genesis/impact/existing_system_explorer.py; src/genesis/impact/impact_agent.py; src/genesis/parsers/source_aggregator.py; src/genesis/writer/models.py; src/genesis/writer/writer_agent.py; src/genesis/writer/context_builder.py; src/genesis/writer/orchestrator.py; tests/test_data_models.py; tests/test_code_parser.py; tests/test_existing_system_explorer.py; tests/test_impact_agent.py; tests/test_source_aggregator.py; tests/test_phase5_models.py; tests/test_phase5_writer_agent.py; tests/test_phase5_context_builder.py; tests/test_phase5_writer_orchestrator.py; samples/要件定義_追加改修_股票.xlsx; samples/existing-system/; docs/superpowers/specs/2026-08-23-impact-mvp.md; _AI_USAGE_LOG.md | deepseek-v4-flash | diff --git a/docs/superpowers/specs/2026-08-23-impact-mvp.md b/docs/superpowers/specs/2026-08-23-impact-mvp.md new file mode 100644 index 0000000..05a03f5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-23-impact-mvp.md @@ -0,0 +1,158 @@ +# Impact Agent MVP 设计(2026-08-23) + +## 背景 + +用户指出:写概要设计书需要「根据要件定义书 + 既有系统的设计与代码做影响调查 → 确定修改范围」。当前 +`source_aggregator.py` 硬编码 `existing_system=None`,无 Impact Agent 实现。本次实现 **MVP 变更点定位 + 影响书**。 + +关键用户决策(brainstorming 确认): +1. 场景 = **追加改修**(需影响调查) +2. 既有系统输入形态 = **Java 项目源码** +3. 既有系统样本 = `samples/existing-system/`(sunOnly/stock-trade-system,股票量化交易系统,无 LICENSE, + 仅作测试输入,保留来源标注) +4. 里程碑范围 = **MVP**:解析既有系统 → 变更点定位 → 影响书(JSON);不做 LLM 要素抽取/关联推理 +5. 样本同域化:新造 `samples/要件定義_追加改修_股票.xlsx`(股票交易域,对齐 sunOnly 真实模块) +6. **门控**:是否提供 `existing_system` 路径决定是否进入影响调查阶段(非章节级) +7. **范围确认**:进入影响调查前与用户确认调查范围(模块/深度) +8. **消费模型**:`ImpactReport` 是生成概要设计书的**主上下文**——整本概要设计书基于影响调查结果生成, + **无专用影响章、无 `{{section:impact}}` 锚点门控**;`ImpactReport` 同时作为可下载独立产物 + +## 架构与数据流 + +``` +要件定義_追加改修_股票.xlsx(新造) samples/existing-system/(sunOnly 源码) + │ 现有 ExcelParser │ 新增 CodeParser(Java 解析) + ▼ ▼ + StructuredSource.tables CodeStructure → ExistingSystemInfo + │ │ + └──────────────┬───────────────────────────┘ + ▼ + ImpactAgent(变更点定位,确定性比对) + - 取 変更区分 + 既存対応 列 + - 与 ExistingSystemInfo 连接比对(存在性校验) + ▼ + ImpactReport(JSON 中间产物,可下载) + ▼ + WriteOrchestrator.generate(impact_report) + - ImpactReport 作为主上下文喂给 WriterAgent + - 整本概要设计书基于影响结果生成 +``` + +## 门控与范围确认 + +- **门控(流水线级)**:`SourceParser.parse(existing_system_path=...)` 提供路径 ⇒ `StructuredSource.existing_system` + 非 None ⇒ 进入影响调查;`WriteOrchestrator.generate` 检测到 `existing_system` 非 None 且未显式传 + `impact_report` 时,自动运行 `ImpactAgent` 生成影响书。 +- **范围确认(human-in-loop)**:MVP 默认范围 = 提供的整个目录树(全模块、Controller/Service/Entity/Endpoint 全层); + 可按参数收窄(`modules`/`depth`)。完整 UI 确认留后续。 +- **门控降级**:CodeParser 失败(非 Java/无源码)⇒ 跳过影响调查并告警,生成照常(无 ImpactReport)。 + +## 变更点定位规则(确定性,无 LLM) + +| 要件情况 | 判定 | +|---|---| +| 新規 + 既存対応空 | `new`(确认新增) | +| 新規 + 既存対応指定 | `conflict` 告警(自相矛盾) | +| 変更 + 既存対応命中且类存在 | `modified`(受影响既有类 = 映射清单) | +| 変更 + 既存対応缺失/类不存在 | `warning`(无法定位修改对象) | +| 削除 + 既存対応命中 | `deleted`(确认删除) | +| 削除 + 既存対応缺失/类不存在 | `warning` | +| 既有类未被任何要件引用 | `unchanged`(summary 计数) | + +`既存対応` 列值 = Controller/Service/Entity 类名(逗号分隔);命中判定为类名出现在 +`ExistingSystemInfo` 三层清单中(不区分大小写,后缀 `Controller`/`Service`/`Entity` 可选)。 + +## 数据模型(data_models.py 新增) + +```python +class ChangeType(Enum): + NEW = "新規"; MODIFIED = "変更"; DELETED = "削除"; UNCHANGED = "不变" + +@dataclass +class ChangeElement: + element_id: str; element_type: str; name: str + change_type: ChangeType + existing_mapping: list[str] # 既存対応 声明值 + impacted_existing: list[str] # 确认命中的既有类 + evidence: str # 既有类 source_uri / 空 + status: str # "ok" | "conflict" | "warning" + +@dataclass +class ImpactWarning: + element_id: str; issue: str + +@dataclass +class ChangeAnalysis: + project_type: str # "enhancement" + new_elements: list[ChangeElement] + modified_elements: list[ChangeElement] + deleted_elements: list[ChangeElement] + unchanged_elements: list[ChangeElement] + warnings: list[ImpactWarning] + +@dataclass +class ImpactReport: + metadata: dict + change_analysis: ChangeAnalysis + summary: dict +``` + +## 组件 + +### `src/genesis/impact/code_parser.py`(新) +- `CodeParser.parse(root_path) -> CodeStructure`:扫描 `**/*.java`,按注解识别: + - `@RestController|@Controller` → ControllerInfo(base_path 取 `@RequestMapping`,endpoints 取方法级映射) + - `@Service` → ServiceInfo(methods) + - `@Entity|@Table(name=...)` → EntityInfo(table_name、fields) + - 方法映射 → EndpointInfo +- 顶层目录 → modules;失败(无 Java/路径无效)→ 抛 `CodeParseError` + +### `src/genesis/impact/existing_system_explorer.py`(新) +- `ExistingSystemExplorer.explore(code: CodeStructure) -> ExistingSystemInfo` + +### `src/genesis/impact/impact_agent.py`(新) +- `ImpactAgent.run(structured_source, scope=None) -> ImpactReport` +- 从 `tables` 提取带 `変更区分`/`既存対応` 列的表行 → 按上表规则分类 → summary + +### `src/genesis/parsers/source_aggregator.py`(改) +- `parse(..., existing_system_path=None)`:提供路径时解析并填充 `existing_system` + +### `src/genesis/writer/`(改) +- `GenerationContext` 新增 `impact_report: object | None = None`;`to_vars()` 增加 `"impact"` 变量 +- `WRITER_PROMPT_TEMPLATE` 增加 `影响调查上下文:\n{{impact}}\n`(在参考资料前) +- `context_builder.build_contexts` 从 `structured_source.impact_report` 透传 +- `WriteOrchestrator.generate(..., impact_report=None)`:门控自动跑 `ImpactAgent`;上下文携带影响书 +- `StructuredSource` 新增 `impact_report: ImpactReport | None = None`(生成后回填,便于 QA/日志) + +## 错误处理 + +- `CodeParseError`(非 Java/无源码/路径无效)→ 门控降级跳过 + 告警 +- `既存対応` 引用不存在 → `warning`,不阻断 +- `新規` 却映射既有 → `conflict` warning + +## 测试策略(TDD,全量 ≥99%) + +- `tests/test_data_models.py`:新增 ImpactReport 模型默认值/构造 +- `tests/test_code_parser.py`:用 `samples/existing-system` 真实文件断言提取(类名/数量/路径) +- `tests/test_existing_system_explorer.py`:组装 ExistingSystemInfo 分层 +- `tests/test_impact_agent.py`:给定 要件定義(新造样本)+ ExistingSystemInfo → 断言分类 +- `tests/test_source_aggregator.py`:`existing_system_path` 加载;无效路径降级/报错 +- `tests/test_phase5_models.py`:更新 `test_generation_context_no_impact_field` → 断言 `impact` 变量存在且为空 +- `tests/test_phase5_writer_agent.py`:断言模板含影响上下文变量 +- `tests/test_phase5_writer_orchestrator.py`:传 impact_report → 断言 `variables["impact"]` 含影响信息 +- 门禁(fake 模式):sunOnly + 新造样本端到端产出 ImpactReport + 概要设计书 + +## 新造样本规格 + +`samples/要件定義_追加改修_股票.xlsx`(股票交易域,对齐 sunOnly 真实模块): +- 機能一覧:F001 止损风控机能(新規,无既存対応) / F002 订单状态查询扩展(変更→OrderController,OrderQueryService) + / F003 实时行情显示扩展(変更→RealtimeMarketDataController) / F004 旧回测机能(削除→StrategyBacktestController) +- 画面一覧(对应画面 + 変更区分)/DB定義(新規 止损设定表/変更 订单表/削除 旧回测表)/IF定義(新規 风控告警连携IF) +- 各表新增 `既存対応` 列(类名或留空)+ `変更区分` 列 +- `改修ポイント` 自由文本(改修概要) + +## 里程碑外(后续) + +- LLM 要素抽取 / 关联推理 / 影响矩阵 +- v1→用户逐条修正→v2 生命周期 UI +- 范围确认 UI / API 化 diff --git a/samples/existing-system/.gitignore b/samples/existing-system/.gitignore new file mode 100644 index 0000000..0c7970f --- /dev/null +++ b/samples/existing-system/.gitignore @@ -0,0 +1,9 @@ +/.idea/ +*.class +*.lst +*.log +.idea/* +*/target/* +*/.idea/* +*.iml +*.ipr \ No newline at end of file diff --git a/samples/existing-system/README.md b/samples/existing-system/README.md new file mode 100644 index 0000000..0d61378 --- /dev/null +++ b/samples/existing-system/README.md @@ -0,0 +1,37 @@ +# 既有系统样本:stock-trade-system(股票量化交易系统) + +## 来源标注 + +- **仓库**: https://github.com/sunOnly/stock-trade-system +- **作者**: sunOnly +- **描述**: 股票量化交易系统(Spring Boot 多模块) +- **许可证**: ⚠️ **仓库未声明 LICENSE**(GitHub 默认保留所有权利) + +## 使用说明(合规声明) + +本目录仅作为本项目(概要设计书自动生成 Agent)的**测试输入样本**使用: + +1. **不进交付物**:本代码不随产品发布、不对外分发、不参与任何形式的再授权。 +2. **仅作影响调查演示**:用于验证「既有系统解析 → 影响调查 → 変更範囲确定 → 概要设计书生成」链路(追加改修场景)。 +3. **来源透明**:保留本 README 与原始 pom.xml/包结构,不篡改作者版权声明。 +4. **许可风险自担**:因上游无 LICENSE,本项目用户需自行确认使用该样本的合规性;正式商用场景建议替换为自有代码或获得作者授权。 + +## 结构(13 个 Spring Boot 模块) + +| 模块 | 职责 | +|------|------| +| trade-application | 应用入口/装配 | +| trade-common | 公共层(dal/entity/util/config/service) | +| trade-gateway | Spring Cloud Gateway 网关 | +| trade-user | 用户/账户域 | +| trade-order | 订单/交易域 | +| trade-risk | 风控域 | +| trade-strategy | 策略域 | +| trade-market-data | 行情数据域 | +| trade-indicator | 指标计算域 | +| trade-notification | 通知域 | +| trade-backtest | 回测域 | +| trade-security | 安全域 | +| trade-mbg | MyBatis 生成器 | + +共 278 个 Java 文件(约 567 KB,已去除 target/ 构建产物与 IDE 配置)。 diff --git a/samples/existing-system/docs/ai.md b/samples/existing-system/docs/ai.md new file mode 100644 index 0000000..e079b50 --- /dev/null +++ b/samples/existing-system/docs/ai.md @@ -0,0 +1,3 @@ +project_rules.md +README.md +plan.md diff --git a/samples/existing-system/docs/plan.md b/samples/existing-system/docs/plan.md new file mode 100644 index 0000000..7a93866 --- /dev/null +++ b/samples/existing-system/docs/plan.md @@ -0,0 +1,116 @@ +# 开发计划 + +## `trade-indicator` 模块开发 + +- # TASK-001 创建 `IndicatorDefinitionDO.java` 和 `IndicatorValueDO.java` 实体类 [已完成] +- # TASK-002 创建 `IndicatorDefinitionMapper.java` 和 `IndicatorValueMapper.java` 数据访问接口 [已完成] +- # TASK-003 创建 `IndicatorDefinitionService.java` 和 `IndicatorValueService.java` 业务逻辑接口 [已完成] +- # TASK-004 创建 `IndicatorDefinitionServiceImpl.java` 和 `IndicatorValueServiceImpl.java` 业务逻辑实现类 [已完成] +- # TASK-005 创建 `IndicatorDefinitionController.java` 和 `IndicatorValueController.java` API 接口 [已完成] +- # TASK-006 创建 `IndicatorDefinitionConvert.java` 和 `IndicatorValueConvert.java` 对象转换工具类 [已完成] +- # TASK-007 创建 `IndicatorJob.java` 定时任务类并启用调度 [已完成] +- # TASK-008 定义 `ErrorCodeConstants.java` 错误码 [已完成] +- # TASK-009 配置 `bootstrap.yml` 和 `logback-spring.xml` [已完成] +- # TASK-010 完善 `pom.xml` 依赖配置 [已完成] +- # TASK-011 编写集成测试 [未开始] +- # TASK-012 功能优化与代码审查 [未开始] + +## `trade-order` 模块开发 + +- # TASK-013 创建 `OrderDO.java`, `OrderItemDO.java`, `OrderLogDO.java` 实体类 [已完成] +- # TASK-014 创建 `OrderMapper.java`, `OrderItemMapper.java`, `OrderLogMapper.java` 数据访问接口 [已完成] +- # TASK-015 创建 `OrderDirectionEnum.java`, `OrderStatusEnum.java`, `OrderTypeEnum.java` 枚举类 [已完成] +- # TASK-016 创建 `OrderService.java`, `OrderItemService.java`, `OrderLogService.java` 业务逻辑接口 [已完成] +- # TASK-017 创建 `OrderServiceImpl.java`, `OrderItemServiceImpl.java`, `OrderLogServiceImpl.java` 业务逻辑实现类 [已完成] +- # TASK-018 创建 `OrderController.java` API 接口 [已完成] +- # TASK-019 创建 `OrderCreateReqVO.java`, `OrderUpdateReqVO.java`, `OrderPageReqVO.java` 请求 VO 类 [已完成] +- # TASK-020 创建 `OrderRespVO.java`, `OrderItemRespVO.java`, `OrderLogRespVO.java` 响应 VO 类 [已完成] +- # TASK-021 创建 `OrderConvert.java`, `OrderItemConvert.java`, `OrderLogConvert.java` 对象转换工具类 [已完成] +- # TASK-022 定义 `ErrorCodeConstants.java` 错误码 [已完成] +- # TASK-023 配置 `bootstrap.yml` 和 `logback-spring.xml` [已完成] +- # TASK-024 完善 `pom.xml` 依赖配置 [已完成] +- # TASK-025 创建 `OrderApplication.java` 启动类 [已完成] +- # TASK-026 编写集成测试 [未开始] +- # TASK-027 功能优化与代码审查 [未开始] + +## `trade-application` 模块开发 + +- # TASK-028 创建基础结构和配置文件 [未开始] +- # TASK-029 实现核心应用逻辑 [未开始] +- # TASK-030 编写集成测试 [未开始] +- # TASK-031 功能优化与代码审查 [未开始] + +## `trade-backtest` 模块开发 + +- # TASK-032 创建回测引擎核心类 [未开始] +- # TASK-033 实现回测数据处理逻辑 [未开始] +- # TASK-034 实现回测结果分析与展示 [未开始] +- # TASK-035 编写集成测试 [未开始] +- # TASK-036 功能优化与代码审查 [未开始] + +## `trade-common` 模块开发 + +- # TASK-CMN-001 `trade-common` 模块基础开发(通用工具类, 通用配置)[进行中] +- # TASK-037 定义通用工具类和常量 [未开始] +- # TASK-038 实现通用配置和基础组件 [未开始] +- # TASK-039 编写单元测试 [未开始] + +## `trade-gateway` 模块开发 + +- # TASK-040 配置网关路由规则 [未开始] +- # TASK-041 实现统一认证和鉴权 [未开始] +- # TASK-042 实现API限流和熔断 [未开始] +- # TASK-043 编写集成测试 [未开始] +- # TASK-044 功能优化与代码审查 [未开始] + +## `trade-market-data` 模块开发 + +- # TASK-MKT-001 `trade-market-data` 模块基础开发(Controller, Service, Mapper, DO)[进行中] +- # TASK-045 实现行情数据采集接口 [未开始] +- # TASK-046 实现行情数据存储逻辑 [未开始] +- # TASK-047 实现行情数据推送机制 [未开始] +- # TASK-048 编写集成测试 [未开始] +- # TASK-049 功能优化与代码审查 [未开始] + +## `trade-mbg` 模块开发 + +- # TASK-050 配置MyBatis Generator [未开始] +- # TASK-051 生成各模块DAO层代码 [未开始] + +## `trade-notification` 模块开发 + +- # TASK-052 实现消息通知服务接口 [未开始] +- # TASK-053 对接邮件、短信等通知渠道 [未开始] +- # TASK-054 编写集成测试 [未开始] +- # TASK-055 功能优化与代码审查 [未开始] + +## `trade-risk` 模块开发 + +- # TASK-056 实现风控规则定义模块 [未开始] +- # TASK-057 实现风控规则执行引擎 [未开始] +- # TASK-058 实现风险预警与处理机制 [未开始] +- # TASK-059 编写集成测试 [未开始] +- # TASK-060 功能优化与代码审查 [未开始] + +## `trade-security` 模块开发 + +- # TASK-061 实现用户认证与授权逻辑 [未开始] +- # TASK-062 实现API接口安全防护 [未开始] +- # TASK-063 编写集成测试 [未开始] +- # TASK-064 功能优化与代码审查 [未开始] + +## `trade-strategy` 模块开发 + +- # TASK-STR-001 `trade-strategy` 模块基础开发(Controller, Service, Mapper, DO)[进行中] +- # TASK-065 实现策略定义与管理模块 [未开始] +- # TASK-066 实现策略回测与实盘交易接口 [未开始] +- # TASK-067 编写集成测试 [未开始] +- # TASK-068 功能优化与代码审查 [未开始] + +## `trade-user` 模块开发 + +- # TASK-USR-001 `trade-user` 模块基础开发(Controller, Service, Mapper, DO)[进行中] +- # TASK-069 创建用户相关实体类、Mapper、Service、Controller [未开始] +- # TASK-070 实现用户注册、登录、信息管理等功能 [未开始] +- # TASK-071 编写集成测试 [未开始] +- # TASK-072 功能优化与代码审查 [未开始] \ No newline at end of file diff --git a/samples/existing-system/pom.xml b/samples/existing-system/pom.xml new file mode 100644 index 0000000..f4e7275 --- /dev/null +++ b/samples/existing-system/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + pom + + + trade-common + trade-user + trade-security + trade-gateway + trade-mbg + trade-strategy + trade-risk + trade-indicator + trade-market-data + trade-backtest + trade-notifyication + trade-order + trade-application + + + 量化交易系统 + 基于Spring Boot的A股超短线交易系统 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.14 + + + + 8 + 8 + 8 + 8.0.33 + UTF-8 + 3.5.3.1 + 1.2.18 + 2.0.32 + 5.8.20 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + mysql + mysql-connector-java + ${mysql.version} + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + + com.alibaba + fastjson + ${fastjson.version} + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-application/pom.xml b/samples/existing-system/trade-application/pom.xml new file mode 100644 index 0000000..bafe4af --- /dev/null +++ b/samples/existing-system/trade-application/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-application + 1.0.0 + trade-application + trade-application + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-amqp + + + + mysql + mysql-connector-java + ${mysql.version} + runtime + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.1 + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + true + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + com.stock + trade-common + 1.0.0 + + + + com.stock + trade-user + 1.0.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/MyBatisGenerator.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/MyBatisGenerator.java new file mode 100644 index 0000000..cafe185 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/MyBatisGenerator.java @@ -0,0 +1,55 @@ +package com.stock.trading; + +import com.baomidou.mybatisplus.generator.AutoGenerator; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.GlobalConfig; +import com.baomidou.mybatisplus.generator.config.PackageConfig; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; + +/** + * MyBatis-Plus代码生成器 + */ +public class MyBatisGenerator { + + public static void main(String[] args) { + // 代码生成器 + AutoGenerator mpg = new AutoGenerator(); + + // 全局配置 + GlobalConfig gc = new GlobalConfig(); + String projectPath = System.getProperty("user.dir"); + gc.setOutputDir(projectPath + "/trade-user/src/main/java"); + gc.setAuthor("macro"); + gc.setOpen(false); + // gc.setSwagger2(true); 实体属性 Swagger2 注解 + mpg.setGlobalConfig(gc); + + // 数据源配置 + DataSourceConfig dsc = new DataSourceConfig(); + dsc.setUrl("jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"); + dsc.setDriverName("com.mysql.cj.jdbc.Driver"); + dsc.setUsername("root"); + dsc.setPassword("password"); + mpg.setDataSource(dsc); + + // 包配置 + PackageConfig pc = new PackageConfig(); + pc.setModuleName("user"); + pc.setParent("com.stock.trading"); + mpg.setPackageInfo(pc); + + // 策略配置 + StrategyConfig strategy = new StrategyConfig(); + strategy.setNaming(NamingStrategy.underline_to_camel); + strategy.setColumnNaming(NamingStrategy.underline_to_camel); + // strategy.setSuperEntityClass("com.stock.trading.common.entity.BaseEntity"); // 设置父类 + strategy.setEntityLombokModel(true); + strategy.setRestControllerStyle(true); + strategy.setInclude("ums_admin"); // 设置需要生成的表名 + strategy.setControllerMappingHyphenStyle(true); + strategy.setTablePrefix(pc.getModuleName() + "_"); + mpg.setStrategy(strategy); + mpg.execute(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/TradeApplication.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/TradeApplication.java new file mode 100644 index 0000000..d73407b --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/TradeApplication.java @@ -0,0 +1,16 @@ +package com.stock.trading; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 交易系统启动类 + */ +@SpringBootApplication +public class TradeApplication { + + public static void main(String[] args) { + SpringApplication.run(TradeApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/MyBatisPlusConfig.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..440df22 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/MyBatisPlusConfig.java @@ -0,0 +1,20 @@ +package com.stock.trading.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MyBatis-Plus配置 + */ +@Configuration +public class MyBatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 + return interceptor; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/RedisConfig.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/RedisConfig.java new file mode 100644 index 0000000..fbed299 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.stock.trading.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置 + */ +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(serializer); + + // HashKey也采用StringRedisSerializer的序列化方式 + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/SecurityConfig.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/SecurityConfig.java new file mode 100644 index 0000000..93eced0 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package com.stock.trading.config; + +import com.stock.trading.user.model.UmsAdmin; +import com.stock.trading.user.service.UmsAdminService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spring Security的配置 + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private UmsAdminService adminService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .authorizeRequests() + .antMatchers("/admin/login", "/admin/register").permitAll() + .anyRequest() + .authenticated(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService()) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public UserDetailsService userDetailsService() { + //获取登录用户信息 + return username -> { + UmsAdmin admin = adminService.getAdminByUsername(username); + if (admin != null) { + List permissionList = new ArrayList<>(); // 假设这里获取用户权限 + return new org.springframework.security.core.userdetails.User(admin.getUsername(), admin.getPassword(), new ArrayList<>()); + } + throw new UsernameNotFoundException("用户名或密码错误"); + }; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/Swagger2Config.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/Swagger2Config.java new file mode 100644 index 0000000..f64ae7c --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/Swagger2Config.java @@ -0,0 +1,50 @@ +package com.stock.trading.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.SecurityScheme; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * Swagger2API文档的配置 + */ +@Configuration +@EnableSwagger2 +public class Swagger2Config { + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.stock.trading.user.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("超短线交易系统") + .description("超短线交易系统") + .contact("macro") + .version("1.0") + .build(); + } + + private List securitySchemes() { + //设置请求头信息 + List result = new ArrayList<>(); + ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header"); + result.add(apiKey); + return result; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/trade/application/TradeApplication.java b/samples/existing-system/trade-application/src/main/java/com/trade/application/TradeApplication.java new file mode 100644 index 0000000..610defc --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/trade/application/TradeApplication.java @@ -0,0 +1,18 @@ +package com.trade.application; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 交易应用服务启动类 + */ +@EnableDiscoveryClient +@SpringBootApplication +public class TradeApplication { + + public static void main(String[] args) { + SpringApplication.run(TradeApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application-dev.yml b/samples/existing-system/trade-application/src/main/resources/application-dev.yml new file mode 100644 index 0000000..3d8b016 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application-dev.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: password \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application-prod.yml b/samples/existing-system/trade-application/src/main/resources/application-prod.yml new file mode 100644 index 0000000..268b2bc --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application-prod.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/stock_trade_prod?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: password \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application-test.yml b/samples/existing-system/trade-application/src/main/resources/application-test.yml new file mode 100644 index 0000000..604344c --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application-test.yml @@ -0,0 +1,11 @@ +server: + port: 8010 +spring: + profiles: + active: test + application: + name: trade-application + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application.yml b/samples/existing-system/trade-application/src/main/resources/application.yml new file mode 100644 index 0000000..d2e698a --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 8080 +spring: + profiles: + active: dev + datasource: + url: jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: wodiXIAO1988 + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + database: 0 + host: localhost + port: 6379 + password: + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest +mybatis-plus: + mapper-locations: classpath*:/mapper/*.xml + global-config: + db-config: + id-type: auto + logic-delete-value: 1 + logic-not-delete-value: 0 + table-underline: true +jwt: + secret: stock-trading-system-secret + expiration: 604800 + tokenHead: Bearer \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/logback-spring.xml b/samples/existing-system/trade-application/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..31f0a9b --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/pom.xml b/samples/existing-system/trade-backtest/pom.xml new file mode 100644 index 0000000..c460b77 --- /dev/null +++ b/samples/existing-system/trade-backtest/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-backtest + jar + + trade-backtest + Strategy backtesting module for the trading system + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.stock + trade-common + ${project.version} + + + + + com.stock + trade-strategy + ${project.version} + + + + + com.stock + trade-market-data + ${project.version} + + + + + com.stock + trade-indicator + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/java/com/trade/backtest/TradeBacktestApplication.java b/samples/existing-system/trade-backtest/src/main/java/com/trade/backtest/TradeBacktestApplication.java new file mode 100644 index 0000000..af76e7b --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/java/com/trade/backtest/TradeBacktestApplication.java @@ -0,0 +1,18 @@ +package com.trade.backtest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 交易回测服务启动类 + */ +@EnableDiscoveryClient +@SpringBootApplication +public class TradeBacktestApplication { + + public static void main(String[] args) { + SpringApplication.run(TradeBacktestApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/application-dev.yml b/samples/existing-system/trade-backtest/src/main/resources/application-dev.yml new file mode 100644 index 0000000..7ddc131 --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/application-dev.yml @@ -0,0 +1,11 @@ +server: + port: 8008 +spring: + profiles: + active: dev + application: + name: trade-backtest + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/application-prod.yml b/samples/existing-system/trade-backtest/src/main/resources/application-prod.yml new file mode 100644 index 0000000..5281fbb --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/application-prod.yml @@ -0,0 +1,11 @@ +server: + port: 8008 +spring: + profiles: + active: prod + application: + name: trade-backtest + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/application-test.yml b/samples/existing-system/trade-backtest/src/main/resources/application-test.yml new file mode 100644 index 0000000..6b78129 --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/application-test.yml @@ -0,0 +1,11 @@ +server: + port: 8008 +spring: + profiles: + active: test + application: + name: trade-backtest + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/logback-spring.xml b/samples/existing-system/trade-backtest/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..720848e --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-common/pom.xml b/samples/existing-system/trade-common/pom.xml new file mode 100644 index 0000000..78c41c0 --- /dev/null +++ b/samples/existing-system/trade-common/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-common + 1.0.0 + trade-common + trade-common + + + + org.springframework.boot + spring-boot-starter + + + + org.projectlombok + lombok + true + + + + + io.swagger.core.v3 + swagger-annotations + 2.2.20 + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + + org.springframework.security + spring-security-core + 5.7.11 + + + + + org.springframework.security + spring-security-config + 5.7.11 + + + + + org.springframework.security + spring-security-web + 5.7.11 + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + 1.4.7 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/common/dal/dataobject/BaseDO.java b/samples/existing-system/trade-common/src/main/java/com/stock/common/dal/dataobject/BaseDO.java new file mode 100644 index 0000000..e131b84 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/common/dal/dataobject/BaseDO.java @@ -0,0 +1,49 @@ +package com.stock.common.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础 DO 类,所有模块的 DO 类都应继承此类 + * + * @author TraeAI + */ +@Data +public abstract class BaseDO implements Serializable { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + /** + * 创建者,例如:userId + * TODO: 考虑从上下文中自动获取创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String creator; + + /** + * 更新者,例如:userId + * TODO: 考虑从上下文中自动获取更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updater; + + /** + * 是否删除 + */ + private Boolean deleted; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/common/pojo/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/stock/common/pojo/CommonResult.java new file mode 100644 index 0000000..e261108 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/common/pojo/CommonResult.java @@ -0,0 +1,46 @@ +package com.stock.common.pojo; + +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import java.io.Serializable; + +/** + * 通用返回结果 + * + * @param 数据泛型 + */ +@Data +@Validated +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示 + */ + private String msg; + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.setCode(200); // 默认成功码为200 + result.setData(data); + result.setMsg("成功"); + return result; + } + + public static CommonResult error(Integer code, String message) { + CommonResult result = new CommonResult<>(); + result.setCode(code); + result.setMsg(message); + return result; + } + + // 可根据需要添加更多静态方法,例如处理特定错误码等 +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/CommonResult.java new file mode 100644 index 0000000..4738c35 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/CommonResult.java @@ -0,0 +1,102 @@ +package com.stock.trading.common.api; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 通用API返回对象 + * @param + */ +@Data +@NoArgsConstructor +public class CommonResult { + private long code; + private String message; + private T data; + + protected CommonResult(long code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(T data) { + return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + * @param message 提示信息 + */ + public static CommonResult success(T data, String message) { + return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + */ + public static CommonResult failed(IErrorCode errorCode) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + * @param message 错误信息 + */ + public static CommonResult failed(IErrorCode errorCode, String message) { + return new CommonResult(errorCode.getCode(), message, null); + } + + /** + * 失败返回结果 + * @param message 提示信息 + */ + public static CommonResult failed(String message) { + return new CommonResult(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败返回结果 + */ + public static CommonResult failed() { + return failed(ResultCode.FAILED); + } + + /** + * 参数验证失败返回结果 + */ + public static CommonResult validateFailed() { + return failed(ResultCode.VALIDATE_FAILED); + } + + /** + * 参数验证失败返回结果 + * @param message 提示信息 + */ + public static CommonResult validateFailed(String message) { + return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); + } + + /** + * 未登录返回结果 + */ + public static CommonResult unauthorized(T data) { + return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); + } + + /** + * 未授权返回结果 + */ + public static CommonResult forbidden(T data) { + return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/IErrorCode.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/IErrorCode.java new file mode 100644 index 0000000..e22d4e3 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/IErrorCode.java @@ -0,0 +1,10 @@ +package com.stock.trading.common.api; + +/** + * 封装API的错误码 + */ +public interface IErrorCode { + long getCode(); + + String getMessage(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/PageResult.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/PageResult.java new file mode 100644 index 0000000..1af54b4 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/PageResult.java @@ -0,0 +1,33 @@ +package com.stock.trading.common.api; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 分页数据封装类 + */ +@Data +@NoArgsConstructor +public class PageResult { + private Long pageNum; + private Long pageSize; + private Long totalPage; + private Long total; + private List list; + + /** + * 将MyBatisPageHelper分页后的list转为分页信息 + */ + public static PageResult restPage(List list) { + PageResult result = new PageResult(); + //PageInfo pageInfo = new PageInfo(list); + //result.setTotalPage(pageInfo.getPages()); + //result.setPageNum(pageInfo.getPageNum()); + //result.setPageSize(pageInfo.getPageSize()); + //result.setTotal(pageInfo.getTotal()); + //result.setList(pageInfo.getList()); + return result; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/ResultCode.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/ResultCode.java new file mode 100644 index 0000000..4da4957 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/ResultCode.java @@ -0,0 +1,27 @@ +package com.stock.trading.common.api; + +/** + * 常用API返回对象状态码 + */ +public enum ResultCode implements IErrorCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(404, "参数检验失败"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + FORBIDDEN(403, "没有相关权限"); + private long code; + private String message; + + private ResultCode(long code, String message) { + this.code = code; + this.message = message; + } + + public long getCode() { + return code; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/entity/BaseEntity.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/entity/BaseEntity.java new file mode 100644 index 0000000..5af0add --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/entity/BaseEntity.java @@ -0,0 +1,29 @@ +package com.stock.trading.common.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础实体类,包含创建时间和更新时间 + */ +@Data +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/ApiException.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/ApiException.java new file mode 100644 index 0000000..ec35020 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/ApiException.java @@ -0,0 +1,31 @@ +package com.stock.trading.common.exception; + +import com.stock.trading.common.api.IErrorCode; + +/** + * 自定义API异常 + */ +public class ApiException extends RuntimeException { + private IErrorCode errorCode; + + public ApiException(IErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ApiException(String message) { + super(message); + } + + public ApiException(Throwable cause) { + super(cause); + } + + public ApiException(String message, Throwable cause) { + super(message, cause); + } + + public IErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/GlobalExceptionHandler.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..45c52e3 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package com.stock.trading.common.exception; + +import com.stock.trading.common.api.CommonResult; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * 全局异常处理 + */ +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理自定义异常 + */ + @ResponseBody + @ExceptionHandler(value = ApiException.class) + public CommonResult handle(ApiException e) { + if (e.getErrorCode() != null) { + return CommonResult.failed(e.getErrorCode()); + } + return CommonResult.failed(e.getMessage()); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/DateUtil.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/DateUtil.java new file mode 100644 index 0000000..2179124 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/DateUtil.java @@ -0,0 +1,34 @@ +package com.stock.trading.common.util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 日期工具类 + */ +public class DateUtil { + + /** + * 格式化日期时间 + */ + public static String formatDateTime(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.format(date); + } + + /** + * 格式化日期 + */ + public static String formatDate(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(date); + } + + /** + * 格式化时间 + */ + public static String formatTime(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); + return sdf.format(date); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/JwtTokenUtil.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/JwtTokenUtil.java new file mode 100644 index 0000000..e56556e --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/JwtTokenUtil.java @@ -0,0 +1,143 @@ +package com.stock.trading.common.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JwtToken生成工具类 + * JWT token的格式:header.payload.signature + * header的格式(算法、token的类型): + * { + * "alg": "HS512", + * "typ": "JWT" + * } + * payload的格式(用户名、创建时间、过期时间): + * { + * "sub": "wang", + * "created": 1489079981398, + * "exp": 1489689981 + * } + * signature的生成算法: + * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) + */ +public class JwtTokenUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); + private static final String CLAIM_KEY_USERNAME = "sub"; + private static final String CLAIM_KEY_CREATED = "created"; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.expiration}") + private Long expiration; + @Value("${jwt.tokenHead}") + private String tokenHead; + + /** + * 根据负责生成JWT的token + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * 从token中获取JWT中的负载 + */ + private Claims getClaimsFromToken(String token) { + Claims claims = null; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + LOGGER.info("JWT格式验证失败: {}", token); + } + return claims; + } + + /** + * 生成token的过期时间 + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + /** + * 从token中获取登录用户名 + */ + public String getUserNameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 验证token是否还有效 + * + * @param token 客户端传入的token + * @param userDetails 从数据库中查询出来的用户信息 + */ + public boolean validateToken(String token, UserDetails userDetails) { + String username = getUserNameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + /** + * 判断token是否已经失效 + */ + private boolean isTokenExpired(String token) { + Date expiredDate = getExpiredDateFromToken(token); + return expiredDate.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + private Date getExpiredDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 根据用户信息生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + /** + * 判断token是否可以被刷新 + */ + public boolean canRefresh(String token) { + return !isTokenExpired(token); + } + + /** + * 刷新token + */ + public String refreshToken(String token) { + Claims claims = getClaimsFromToken(token); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonPage.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonPage.java new file mode 100644 index 0000000..61f0362 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonPage.java @@ -0,0 +1,49 @@ +package com.trade.common.api; + +import com.github.pagehelper.PageInfo; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 分页数据封装类 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class CommonPage { + private Integer pageNum; + private Integer pageSize; + private Integer totalPage; + private Long total; + private List list; + + /** + * 将PageHelper分页后的list转为分页信息 + */ + public static CommonPage restPage(List list) { + CommonPage result = new CommonPage<>(); + com.baomidou.mybatisplus.extension.plugins.pagination.Page pageInfo = page; + result.setTotalPage((int)pageInfo.getPages()); + result.setPageNum((int)pageInfo.getCurrent()); + result.setPageSize((int)pageInfo.getSize()); + result.setTotal(pageInfo.getTotal()); + result.setList(pageInfo.getRecords()); + return result; + } + + /** + * 将MyBatis Plus分页后的Page转为分页信息 + */ + public static CommonPage restPage(com.baomidou.mybatisplus.extension.plugins.pagination.Page page) { + CommonPage result = new CommonPage<>(); + com.baomidou.mybatisplus.extension.plugins.pagination.Page pageInfo = page; + result.setTotalPage((int)pageInfo.getPages()); + result.setPageNum((int)pageInfo.getCurrent()); + result.setPageSize((int)pageInfo.getSize()); + result.setTotal(pageInfo.getTotal()); + result.setList(pageInfo.getRecords()); + return result; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonResult.java new file mode 100644 index 0000000..f1d7844 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonResult.java @@ -0,0 +1,105 @@ +package com.trade.common.api; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 通用返回对象 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class CommonResult { + private long code; + private String message; + private T data; + + protected CommonResult() { + } + + protected CommonResult(long code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(T data) { + return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + * @param message 提示信息 + */ + public static CommonResult success(T data, String message) { + return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + */ + public static CommonResult failed(IErrorCode errorCode) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + * @param message 错误信息 + */ + public static CommonResult failed(IErrorCode errorCode, String message) { + return new CommonResult(errorCode.getCode(), message, null); + } + + /** + * 失败返回结果 + * @param message 提示信息 + */ + public static CommonResult failed(String message) { + return new CommonResult(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败返回结果 + */ + public static CommonResult failed() { + return failed(ResultCode.FAILED); + } + + /** + * 参数验证失败返回结果 + */ + public static CommonResult validateFailed() { + return failed(ResultCode.VALIDATE_FAILED); + } + + /** + * 参数验证失败返回结果 + * @param message 提示信息 + */ + public static CommonResult validateFailed(String message) { + return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); + } + + /** + * 未登录返回结果 + */ + public static CommonResult unauthorized(T data) { + return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); + } + + /** + * 未授权返回结果 + */ + public static CommonResult forbidden(T data) { + return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/IErrorCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/IErrorCode.java new file mode 100644 index 0000000..fbc366d --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/IErrorCode.java @@ -0,0 +1,11 @@ +package com.trade.common.api; + +/** + * 封装API的错误码 + * @author Trade Team + */ +public interface IErrorCode { + long getCode(); + + String getMessage(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/ResultCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/ResultCode.java new file mode 100644 index 0000000..a0ce018 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/ResultCode.java @@ -0,0 +1,30 @@ +package com.trade.common.api; + +/** + * 枚举常用API操作码 + * @author Trade Team + */ +public enum ResultCode implements IErrorCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(404, "参数检验失败"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + FORBIDDEN(403, "没有相关权限"); + private long code; + private String message; + + private ResultCode(long code, String message) { + this.code = code; + this.message = message; + } + + @Override + public long getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/BaseSwaggerConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/BaseSwaggerConfig.java new file mode 100644 index 0000000..607c062 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/BaseSwaggerConfig.java @@ -0,0 +1,46 @@ +package com.trade.common.config; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; + +/** + * Swagger基础配置 + * @author Trade Team + */ +public abstract class BaseSwaggerConfig { + + /** + * 自定义Swagger配置 + */ + public abstract SwaggerProperties swaggerProperties(); + + @Bean + public GroupedOpenApi publicApi() { + SwaggerProperties swaggerProperties = swaggerProperties(); + return GroupedOpenApi.builder() + .group(swaggerProperties.getGroup()) + .pathsToMatch(swaggerProperties.getApiBasePackage()) + .build(); + } + + @Bean + public OpenAPI springShopOpenAPI() { + SwaggerProperties swaggerProperties = swaggerProperties(); + return new OpenAPI() + .info(new Info().title(swaggerProperties.getTitle()) + .description(swaggerProperties.getDescription()) + .version(swaggerProperties.getVersion()) + .license(new License().name(swaggerProperties.getLicense()).url(swaggerProperties.getLicenseUrl()))) + .externalDocs(new ExternalDocumentation() + .description(swaggerProperties.getDocDescription()) + .url(swaggerProperties.getDocUrl())) + .components(new io.swagger.v3.oas.models.Components() + .addSecuritySchemes("BearerAuth", + new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"))); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/GlobalExceptionHandler.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..52aff0e --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/GlobalExceptionHandler.java @@ -0,0 +1,29 @@ +package com.trade.common.config; + +import com.trade.common.exception.ApiException; +import com.trade.common.response.CommonResult; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * 全局异常处理 + * @author Trade Team + */ +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理自定义API异常 + * @param e API异常 + * @return 统一结果封装 + */ + @ResponseBody + @ExceptionHandler(value = ApiException.class) + public CommonResult handle(ApiException e) { + if (e.getErrorCode() != null) { + return CommonResult.failed(e.getErrorCode()); + } + return CommonResult.failed(e.getMessage()); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisConfig.java new file mode 100644 index 0000000..6b3e756 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisConfig.java @@ -0,0 +1,13 @@ +package com.trade.common.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; + +/** + * MyBatis配置类 + * @author Trade Team + */ +@Configuration +@MapperScan({"com.trade.common.mapper"}) +public class MyBatisConfig { +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisPlusConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..dc9b32d --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisPlusConfig.java @@ -0,0 +1,58 @@ +package com.trade.common.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * MyBatis Plus 配置类 + * + * @author creator + * @since 2024-01-01 + */ +@Configuration +@EnableTransactionManagement // 开启事务管理 +@MapperScan("com.trade.*.mapper") // 扫描 Mapper 接口,根据实际模块调整或在各模块单独配置 +public class MyBatisPlusConfig { + + /** + * 配置 MyBatis Plus 拦截器 + * + * @return MybatisPlusInterceptor 实例 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + + // 1. 添加分页插件 + // DbType 用于指定数据库类型,MySQL为例 + // optimizeJoin 是否优化left join连接查询,默认为false,建议保持false以保证结果正确性 + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); + // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false + // paginationInnerInterceptor.setOverflow(false); + // 设置最大单页限制数量,默认 500 条,-1 不受限制 + // paginationInnerInterceptor.setMaxLimit(500L); + interceptor.addInnerInterceptor(paginationInnerInterceptor); + + // 2. 添加乐观锁插件 (如果需要) + // 当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新 + // 需要在实体类的字段上加上 @Version 注解 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + + // 3. 防止全表更新与删除插件 (如果需要, 注意在生产环境谨慎使用,因为它会阻止没有 WHERE 条件的 UPDATE 和 DELETE 操作) + // interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + + return interceptor; + } + + // 如果使用了 MyBatis Plus 的逻辑删除功能,可以在这里配置全局的逻辑删除字段等 + // 例如,在 application.yml/properties 中配置: + // mybatis-plus.global-config.db-config.logic-delete-field=deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + // mybatis-plus.global-config.db-config.logic-delete-value=1 # 逻辑已删除值(默认为 1) + // mybatis-plus.global-config.db-config.logic-not-delete-value=0 # 逻辑未删除值(默认为 0) +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/RedisConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/RedisConfig.java new file mode 100644 index 0000000..ae48ed7 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/RedisConfig.java @@ -0,0 +1,46 @@ +package com.trade.common.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + * @author Trade Team + */ +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + serializer.setObjectMapper(om); + + // String序列化配置 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + + // key采用String的序列化方式 + redisTemplate.setKeySerializer(stringRedisSerializer); + // hash的key也采用String的序列化方式 + redisTemplate.setHashKeySerializer(stringRedisSerializer); + // value序列化方式采用jackson + redisTemplate.setValueSerializer(serializer); + // hash的value序列化方式采用jackson + redisTemplate.setHashValueSerializer(serializer); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SecurityConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SecurityConfig.java new file mode 100644 index 0000000..57d3bcb --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SecurityConfig.java @@ -0,0 +1,38 @@ +package com.trade.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * Spring Security配置 + * @author Trade Team + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf() + .disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/login", "/register", "/swagger-ui/**", "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SpringDocConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SpringDocConfig.java new file mode 100644 index 0000000..5495f8d --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SpringDocConfig.java @@ -0,0 +1,101 @@ +package com.trade.common.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * SpringDoc OpenAPI 配置类 + * 用于生成API接口文档 + * + * @author creator + * @since 2024-01-01 + */ +@Configuration +public class SpringDocConfig { + + @Value("${spring.application.name:default-service}") + private String applicationName; + + /** + * 配置 OpenAPI 基本信息 + * + * @return OpenAPI 实例 + */ + @Bean + public OpenAPI customOpenAPI( + @Value("${springdoc.version:1.0.0}") String appVersion, + @Value("${springdoc.title:API Documentation}") String title, + @Value("${springdoc.description:API documentation for the service}") String description, + @Value("${springdoc.contact.name:API Support}") String contactName, + @Value("${springdoc.contact.email:support@example.com}") String contactEmail, + @Value("${springdoc.contact.url:https://example.com}") String contactUrl, + @Value("${springdoc.license.name:Apache 2.0}") String licenseName, + @Value("${springdoc.license.url:https://www.apache.org/licenses/LICENSE-2.0.html}") String licenseUrl) { + + // 优化:将 applicationName 用于 title 和 description (如果它们是默认值) + String effectiveTitle = title.equals("API Documentation") ? applicationName + " API Documentation" : title; + String effectiveDescription = description.equals("API documentation for the service") ? + "API documentation for the " + applicationName + " service." : description; + + return new OpenAPI() + .info(new Info() + .title(effectiveTitle) + .version(appVersion) + .description(effectiveDescription) + .contact(new Contact() + .name(contactName) + .email(contactEmail) + .url(contactUrl)) + .license(new License() + .name(licenseName) + .url(licenseUrl))); + } + + /** + * 创建一个默认的 API 分组 + * + * 可以根据需要创建多个 GroupedOpenApi Bean 来对 API 进行分组展示。 + * 例如,可以按模块、按版本或按访问权限进行分组。 + * + * + * @return GroupedOpenApi 实例 + */ + @Bean + public GroupedOpenApi defaultApiGroup() { + // 优化:使用 applicationName 作为 group 名称,使其更具辨识度 + String groupName = applicationName.replace("-service", "") + "-apis"; + return GroupedOpenApi.builder() + .group(groupName) // API 分组的名称 + .packagesToScan("com.trade."+ applicationName.replace("trade-","") + ".controller") // 指定扫描的包路径,需要根据模块名动态调整 + // .pathsToMatch("/api/**") // 可以通过路径匹配来包含特定的API + // .displayName(applicationName + " APIs") // 分组的显示名称 + .build(); + } + + // 示例:为特定模块创建API分组 (如果需要) + // @Bean + // public GroupedOpenApi userApiGroup() { + // return GroupedOpenApi.builder() + // .group("user-management") + // .packagesToScan("com.trade.user.controller") // 假设用户模块的controller在此包下 + // .pathsToMatch("/user/**") + // .displayName("User Management APIs") + // .build(); + // } + + // @Bean + // public GroupedOpenApi productApiGroup() { + // return GroupedOpenApi.builder() + // .group("product-catalog") + // .packagesToScan("com.trade.product.controller") // 假设产品模块的controller在此包下 + // .pathsToMatch("/product/**") + // .displayName("Product Catalog APIs") + // .build(); + // } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SwaggerProperties.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SwaggerProperties.java new file mode 100644 index 0000000..33e1113 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SwaggerProperties.java @@ -0,0 +1,55 @@ +package com.trade.common.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Swagger属性配置 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +@ConfigurationProperties(prefix = "swagger") +public class SwaggerProperties { + /** + * API文档生成基础路径 + */ + private String apiBasePackage; + /** + * 是否开启Swagger + */ + private boolean enable; + /** + * 页面标题 + */ + private String title; + /** + * 页面描述 + */ + private String description; + /** + * 版本号 + */ + private String version; + /** + * 许可证名称 + */ + private String license; + /** + * 许可证URL + */ + private String licenseUrl; + /** + * 外部文档地址 + */ + private String docUrl; + /** + * 外部文档描述 + */ + private String docDescription; + /** + * 分组名称 + */ + private String group; +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/constant/CommonConstant.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/constant/CommonConstant.java new file mode 100644 index 0000000..e38dc64 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/constant/CommonConstant.java @@ -0,0 +1,119 @@ +package com.trade.common.constant; + +/** + * 通用常量类 + * + * @author creator + * @since 2024-01-01 + */ +public final class CommonConstant { + + private CommonConstant() { + // 私有构造函数,防止实例化 + } + + // --------------------------- 符号常量 --------------------------- + public static final String EMPTY_STRING = ""; + public static final String SPACE = " "; + public static final String COMMA = ","; + public static final String PERIOD = "."; + public static final String COLON = ":"; + public static final String SEMICOLON = ";"; + public static final String HYPHEN = "-"; + public static final String UNDERSCORE = "_"; + public static final String SLASH = "/"; + public static final String BACKSLASH = "\\"; + public static final String PIPE = "|"; + + // --------------------------- HTTP 相关常量 --------------------------- + /** + * HTTP 请求头:认证 (Authorization) + */ + public static final String HTTP_HEADER_AUTHORIZATION = "Authorization"; + /** + * HTTP 请求头:Bearer Token 前缀 + */ + public static final String HTTP_HEADER_BEARER_PREFIX = "Bearer "; + /** + * HTTP 请求头:内容类型 (Content-Type) + */ + public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; + /** + * HTTP 内容类型:JSON + */ + public static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8"; + /** + * HTTP 内容类型:表单 + */ + public static final String CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8"; + /** + * HTTP 内容类型:文件上传 + */ + public static final String CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data"; + + // --------------------------- 编码常量 --------------------------- + public static final String CHARSET_UTF8 = "UTF-8"; + public static final String CHARSET_GBK = "GBK"; + + // --------------------------- 日期时间格式常量 --------------------------- + public static final String DATETIME_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss"; + public static final String DATE_FORMAT_DEFAULT = "yyyy-MM-dd"; + public static final String TIME_FORMAT_DEFAULT = "HH:mm:ss"; + public static final String DATETIME_FORMAT_NO_SEPARATOR = "yyyyMMddHHmmss"; + public static final String DATE_FORMAT_NO_SEPARATOR = "yyyyMMdd"; + + // --------------------------- 缓存相关常量 --------------------------- + /** + * 缓存键分隔符 + */ + public static final String CACHE_KEY_SEPARATOR = "::"; + /** + * 用户信息缓存键前缀 + */ + public static final String CACHE_USER_PREFIX = "user"; + /** + * Token 缓存键前缀 + */ + public static final String CACHE_TOKEN_PREFIX = "token"; + + // --------------------------- 逻辑删除状态 --------------------------- + /** + * 逻辑未删除 + */ + public static final Integer LOGIC_NOT_DELETED = 0; + /** + * 逻辑已删除 + */ + public static final Integer LOGIC_DELETED = 1; + + // --------------------------- 通用状态 (启用/禁用) --------------------------- + /** + * 状态:启用 + */ + public static final Integer STATUS_ENABLED = 1; + /** + * 状态:禁用 + */ + public static final Integer STATUS_DISABLED = 0; + + // --------------------------- 默认值常量 --------------------------- + /** + * 默认分页大小 + */ + public static final int DEFAULT_PAGE_SIZE = 10; + /** + * 默认当前页码 + */ + public static final int DEFAULT_PAGE_NUM = 1; + + // --------------------------- 其他常量 --------------------------- + /** + * 默认的超级管理员用户ID (示例) + */ + public static final Long SUPER_ADMIN_ID = 1L; + /** + * 默认的超级管理员角色代码 (示例) + */ + public static final String ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN"; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/domain/UserDto.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/domain/UserDto.java new file mode 100644 index 0000000..c4c99e8 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/domain/UserDto.java @@ -0,0 +1,71 @@ +package com.trade.common.domain; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 登录用户信息 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class UserDto implements UserDetails { + private Long id; + private String username; + private String password; + private Integer status; + private List permissionList; + + public UserDto(Long id, String username, String password, Integer status, List permissionList) { + this.id = id; + this.username = username; + this.password = password; + this.status = status; + this.permissionList = permissionList; + } + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return permissionList.stream() + .filter(permission -> permission!=null) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return status == 1; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ApiException.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ApiException.java new file mode 100644 index 0000000..d96020a --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ApiException.java @@ -0,0 +1,32 @@ +package com.trade.common.exception; + +/** + * 自定义API异常 + * @author Trade Team + */ +import com.trade.common.api.IErrorCode; + +public class ApiException extends RuntimeException { + private IErrorCode errorCode; + + public ApiException(IErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ApiException(String message) { + super(message); + } + + public ApiException(Throwable cause) { + super(cause); + } + + public ApiException(String message, Throwable cause) { + super(message, cause); + } + + public IErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/Asserts.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/Asserts.java new file mode 100644 index 0000000..abe320c --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/Asserts.java @@ -0,0 +1,17 @@ +package com.trade.common.exception; + +import com.trade.common.api.IErrorCode; + +/** + * 断言处理类,用于抛出各种API异常 + * @author Trade Team + */ +public class Asserts { + public static void fail(String message) { + throw new ApiException(message); + } + + public static void fail(IErrorCode errorCode) { + throw new ApiException(errorCode); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/BusinessException.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/BusinessException.java new file mode 100644 index 0000000..0a8e457 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/BusinessException.java @@ -0,0 +1,97 @@ +package com.trade.common.exception; + +import com.trade.common.vo.ResultCodeEnum; +import lombok.Getter; + +/** + * 自定义业务异常类 + * + * @author creator + * @since 2024-01-01 + */ +@Getter +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private final int code; + + /** + * 构造器,使用自定义消息 + * + * @param message 异常消息 + */ + public BusinessException(String message) { + super(message); + this.code = ResultCodeEnum.OPERATION_FAILED.getCode(); // 默认为操作失败 + } + + /** + * 构造器,使用自定义错误码和消息 + * + * @param code 错误码 + * @param message 异常消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 构造器,使用 ResultCodeEnum 定义的错误 + * + * @param resultCodeEnum 结果代码枚举 + */ + public BusinessException(ResultCodeEnum resultCodeEnum) { + super(resultCodeEnum.getMessage()); + this.code = resultCodeEnum.getCode(); + } + + /** + * 构造器,使用 ResultCodeEnum 定义的错误,并附加额外信息 + * + * @param resultCodeEnum 结果代码枚举 + * @param detailMessage 详细错误信息,将追加到枚举定义的消息后 + */ + public BusinessException(ResultCodeEnum resultCodeEnum, String detailMessage) { + super(resultCodeEnum.getMessage() + ": " + detailMessage); + this.code = resultCodeEnum.getCode(); + } + + /** + * 构造器,包装另一个异常 + * + * @param message 异常消息 + * @param cause 原始异常 + */ + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(); // 默认为内部服务器错误 + } + + /** + * 构造器,使用自定义错误码、消息并包装另一个异常 + * + * @param code 错误码 + * @param message 异常消息 + * @param cause 原始异常 + */ + public BusinessException(int code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } + + /** + * 构造器,使用 ResultCodeEnum 并包装另一个异常 + * + * @param resultCodeEnum 结果代码枚举 + * @param cause 原始异常 + */ + public BusinessException(ResultCodeEnum resultCodeEnum, Throwable cause) { + super(resultCodeEnum.getMessage(), cause); + this.code = resultCodeEnum.getCode(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ErrorCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ErrorCode.java new file mode 100644 index 0000000..5114546 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ErrorCode.java @@ -0,0 +1,10 @@ +package com.trade.common.exception; + +/** + * 封装API的错误码 + * @author Trade Team + */ +public interface ErrorCode { + long getCode(); + String getMessage(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/GlobalExceptionHandler.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..7d91863 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,205 @@ +package com.trade.common.exception; + +import com.trade.common.vo.ResultCodeEnum; +import com.trade.common.vo.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 全局异常处理器 + * 捕获controller层抛出的异常,并返回统一的JSON格式响应 + * + * @author creator + * @since 2024-01-01 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理自定义业务异常 BusinessException + * + * @param e BusinessException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.OK) // 通常业务异常返回200,通过code和message区分 + public Result> handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage(), e); + return Result.error(e.getCode(), e.getMessage()); + } + + /** + * 处理参数校验异常 (Query Param / Path Variable) + * + * @param e ConstraintViolationException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result> handleConstraintViolationException(ConstraintViolationException e) { + Set> violations = e.getConstraintViolations(); + String message = violations.stream() + .map(violation -> String.format("%s: %s", getFieldName(violation), violation.getMessage())) + .collect(Collectors.joining("; ")); + log.warn("参数校验失败 (ConstraintViolationException): {}", message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + private String getFieldName(ConstraintViolation> violation) { + String propertyPath = violation.getPropertyPath().toString(); + // propertyPath 通常是 "methodName.argName.fieldName" 或 "fieldName" + // 我们尝试获取最后一个点之后的部分作为字段名 + int lastDotIndex = propertyPath.lastIndexOf('.'); + return (lastDotIndex == -1) ? propertyPath : propertyPath.substring(lastDotIndex + 1); + } + + /** + * 处理参数校验异常 (RequestBody @Valid) + * + * @param e MethodArgumentNotValidException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + List fieldErrors = e.getBindingResult().getFieldErrors(); + String message = fieldErrors.stream() + .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage())) + .collect(Collectors.joining("; ")); + log.warn("参数校验失败 (MethodArgumentNotValidException): {}", message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理参数绑定异常 (form-data/x-www-form-urlencoded @Valid) + * + * @param e BindException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result> handleBindException(BindException e) { + List fieldErrors = e.getBindingResult().getFieldErrors(); + String message = fieldErrors.stream() + .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage())) + .collect(Collectors.joining("; ")); + log.warn("参数绑定失败 (BindException): {}", message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理缺少请求参数异常 + * + * @param e MissingServletRequestParameterException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { + String message = String.format("缺少必要的请求参数: %s (类型: %s)", e.getParameterName(), e.getParameterType()); + log.warn(message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理参数类型不匹配异常 + * + * @param e MethodArgumentTypeMismatchException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + String message = String.format("参数类型不匹配: 参数 '%s' 需要类型 '%s', 但提供了值 '%s'", + e.getName(), e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "N/A", e.getValue()); + log.warn(message, e); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理HTTP请求方法不支持异常 + * + * @param e HttpRequestMethodNotSupportedException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public Result> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + String message = String.format("不支持的请求方法: %s. 支持的方法有: %s", e.getMethod(), e.getSupportedHttpMethods()); + log.warn(message); + return Result.error(ResultCodeEnum.FORBIDDEN, message); + } + + /** + * 处理404 NoHandlerFoundException + * 需要在 application.properties 中配置 spring.mvc.throw-exception-if-no-handler-found=true + * 和 spring.web.resources.add-mappings=false (如果使用了静态资源映射) + * + * @param e NoHandlerFoundException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result> handleNoHandlerFoundException(NoHandlerFoundException e) { + String message = String.format("接口 [%s %s] 不存在", e.getHttpMethod(), e.getRequestURL()); + log.warn(message, e); + return Result.error(ResultCodeEnum.NOT_FOUND, message); + } + + /** + * 处理请求体不可读异常 + * + * @param e HttpMessageNotReadableException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.warn("请求体不可读或JSON格式错误: {}", e.getMessage()); + return Result.error(ResultCodeEnum.BAD_REQUEST, "请求体不可读或JSON格式错误"); + } + + /** + * 处理其他所有未捕获的运行时异常 + * + * @param e RuntimeException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result> handleRuntimeException(RuntimeException e) { + log.error("服务器发生运行时异常: {}", e.getMessage(), e); + return Result.error(ResultCodeEnum.INTERNAL_SERVER_ERROR, "服务器发生运行时异常,请联系管理员"); + } + + /** + * 处理其他所有未捕获的顶级异常 Exception + * + * @param e Exception 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result> handleException(Exception e) { + log.error("服务器发生未知异常: {}", e.getMessage(), e); + return Result.error(ResultCodeEnum.INTERNAL_SERVER_ERROR, "服务器发生未知异常,请联系管理员"); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/response/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/CommonResult.java new file mode 100644 index 0000000..d0da436 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/CommonResult.java @@ -0,0 +1,126 @@ +package com.trade.common.response; + +import com.trade.common.api.IErrorCode; + +/** + * 通用返回对象 + * @author Trade Team + */ +public class CommonResult { + private long code; + private String message; + private T data; + + protected CommonResult() { + } + + protected CommonResult(long code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(T data) { + return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + * @param message 提示信息 + */ + public static CommonResult success(T data, String message) { + return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + */ + public static CommonResult failed(IErrorCode errorCode) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + * @param message 错误信息 + */ + public static CommonResult failed(IErrorCode errorCode, String message) { + return new CommonResult(errorCode.getCode(), message, null); + } + + /** + * 失败返回结果 + * @param message 提示信息 + */ + public static CommonResult failed(String message) { + return new CommonResult(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败返回结果 + */ + public static CommonResult failed() { + return failed(ResultCode.FAILED); + } + + /** + * 参数验证失败返回结果 + */ + public static CommonResult validateFailed() { + return failed(ResultCode.VALIDATE_FAILED); + } + + /** + * 参数验证失败返回结果 + * @param message 提示信息 + */ + public static CommonResult validateFailed(String message) { + return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); + } + + /** + * 未登录返回结果 + */ + public static CommonResult unauthorized(T data) { + return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); + } + + /** + * 未授权返回结果 + */ + public static CommonResult forbidden(T data) { + return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); + } + + public long getCode() { + return code; + } + + public void setCode(long code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/response/ResultCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/ResultCode.java new file mode 100644 index 0000000..024e4b4 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/ResultCode.java @@ -0,0 +1,34 @@ +package com.trade.common.response; + +import com.trade.common.api.IErrorCode; + +import com.trade.common.exception.ErrorCode; + +/** + * 枚举常用API操作码 + * @author Trade Team + */ +public enum ResultCode implements IErrorCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(404, "参数检验失败"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + FORBIDDEN(403, "没有相关权限"); + private long code; + private String message; + + private ResultCode(long code, String message) { + this.code = code; + this.message = message; + } + + @Override + public long getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/service/RedisService.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/RedisService.java new file mode 100644 index 0000000..748836f --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/RedisService.java @@ -0,0 +1,132 @@ +package com.trade.common.service; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * redis操作Service, + * 对象和数组都以json形式进行存储 + * @author Trade Team + */ +public interface RedisService { + /** + * 存储数据 + */ + void set(String key, Object value); + + /** + * 存储数据并设置过期时间 + */ + void set(String key, Object value, long expire); + + /** + * 获取数据 + */ + Object get(String key); + + /** + * 设置过期时间 + */ + boolean expire(String key, long expire); + + /** + * 删除数据 + */ + void del(String key); + + /** + * 批量删除数据 + */ + void del(List keys); + + /** + * 批量删除key + */ + Long delByPrefix(String prefix); + + /** + * 对某个key的value进行递增操作 + */ + Long increment(String key, Long delta); + + /** + * 对某个key的value进行递减操作 + */ + Long decrement(String key, Long delta); + + /** + * 获取所有key + */ + Set keys(String pattern); + + /** + * 判断key是否存在 + */ + Boolean hasKey(String key); + + /** + * 将数据放入redis的list中 + */ + Long lPush(String key, Object value); + + /** + * 将数据放入redis的list中并设置过期时间 + */ + Long lPush(String key, Object value, long expire); + + /** + * 从redis的list中获取数据 + */ + Object lPop(String key); + + /** + * 将数据放入redis的set中 + */ + Long sAdd(String key, Object... values); + + /** + * 将数据放入redis的set中并设置过期时间 + */ + Long sAdd(String key, long expire, Object... values); + + /** + * 从redis的set中获取数据 + */ + Set sMembers(String key); + + /** + * 将数据放入redis的hash中 + */ + void hPut(String key, String hashKey, Object value); + + /** + * 将数据放入redis的hash中并设置过期时间 + */ + void hPut(String key, String hashKey, Object value, long expire); + + /** + * 从redis的hash中获取数据 + */ + Object hGet(String key, String hashKey); + + /** + * 将map放入redis的hash中 + */ + void hPutAll(String key, Map map); + + /** + * 将map放入redis的hash中并设置过期时间 + */ + void hPutAll(String key, Map map, long expire); + + /** + * 从redis的hash中获取所有数据 + */ + Map hGetAll(String key); + + /** + * 删除redis的hash中的数据 + */ + void hDel(String key, Object... hashKeys); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/service/impl/RedisServiceImpl.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/impl/RedisServiceImpl.java new file mode 100644 index 0000000..34b8911 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/impl/RedisServiceImpl.java @@ -0,0 +1,151 @@ +package com.trade.common.service.impl; + +import com.trade.common.service.RedisService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * redis操作Service的实现类 + * @author Trade Team + */ +@Service +public class RedisServiceImpl implements RedisService { + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void set(String key, Object value) { + redisTemplate.opsForValue().set(key, value); + } + + @Override + public void set(String key, Object value, long expire) { + redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS); + } + + @Override + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + @Override + public boolean expire(String key, long expire) { + return redisTemplate.expire(key, expire, TimeUnit.SECONDS); + } + + @Override + public void del(String key) { + redisTemplate.delete(key); + } + + @Override + public void del(List keys) { + redisTemplate.delete(keys); + } + + @Override + public Long delByPrefix(String prefix) { + Set keys = redisTemplate.keys(prefix + "*"); + if (keys != null && !keys.isEmpty()) { + return redisTemplate.delete(keys); + } + return 0L; + } + + @Override + public Long increment(String key, Long delta) { + return redisTemplate.opsForValue().increment(key, delta); + } + + @Override + public Long decrement(String key, Long delta) { + return redisTemplate.opsForValue().decrement(key, delta); + } + + @Override + public Set keys(String pattern) { + return redisTemplate.keys(pattern); + } + + @Override + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + @Override + public Long lPush(String key, Object value) { + return redisTemplate.opsForList().leftPush(key, value); + } + + @Override + public Long lPush(String key, Object value, long expire) { + Long result = redisTemplate.opsForList().leftPush(key, value); + expire(key, expire); + return result; + } + + @Override + public Object lPop(String key) { + return redisTemplate.opsForList().leftPop(key); + } + + @Override + public Long sAdd(String key, Object... values) { + return redisTemplate.opsForSet().add(key, values); + } + + @Override + public Long sAdd(String key, long expire, Object... values) { + Long result = redisTemplate.opsForSet().add(key, values); + expire(key, expire); + return result; + } + + @Override + public Set sMembers(String key) { + return redisTemplate.opsForSet().members(key); + } + + @Override + public void hPut(String key, String hashKey, Object value) { + redisTemplate.opsForHash().put(key, hashKey, value); + } + + @Override + public void hPut(String key, String hashKey, Object value, long expire) { + redisTemplate.opsForHash().put(key, hashKey, value); + expire(key, expire); + } + + @Override + public Object hGet(String key, String hashKey) { + return redisTemplate.opsForHash().get(key, hashKey); + } + + @Override + public void hPutAll(String key, Map map) { + redisTemplate.opsForHash().putAll(key, map); + } + + @Override + public void hPutAll(String key, Map map, long expire) { + redisTemplate.opsForHash().putAll(key, map); + expire(key, expire); + } + + @Override + public Map hGetAll(String key) { + return (Map) (Map) redisTemplate.opsForHash().entries(key); + } + + @Override + public void hDel(String key, Object... hashKeys) { + redisTemplate.opsForHash().delete(key, hashKeys); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/DateUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/DateUtil.java new file mode 100644 index 0000000..01d349b --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/DateUtil.java @@ -0,0 +1,48 @@ +package com.trade.common.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 日期工具类 + * @author Trade Team + */ +public class DateUtil { + + /** + * 将LocalDateTime格式化为指定字符串 + * @param dateTime LocalDateTime对象 + * @param pattern 格式模式,如 "yyyy-MM-dd HH:mm:ss" + * @return 格式化后的日期字符串 + */ + public static String formatLocalDateTime(LocalDateTime dateTime, String pattern) { + if (dateTime == null || pattern == null || pattern.isEmpty()) { + return null; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return dateTime.format(formatter); + } + + /** + * 将当前LocalDateTime格式化为默认字符串 "yyyy-MM-dd HH:mm:ss" + * @param dateTime LocalDateTime对象 + * @return 格式化后的日期字符串 + */ + public static String formatLocalDateTime(LocalDateTime dateTime) { + return formatLocalDateTime(dateTime, "yyyy-MM-dd HH:mm:ss"); + } + + /** + * 将字符串解析为LocalDateTime对象 + * @param dateString 日期字符串 + * @param pattern 格式模式,如 "yyyy-MM-dd HH:mm:ss" + * @return 解析后的LocalDateTime对象 + */ + public static LocalDateTime parseLocalDateTime(String dateString, String pattern) { + if (dateString == null || dateString.isEmpty() || pattern == null || pattern.isEmpty()) { + return null; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return LocalDateTime.parse(dateString, formatter); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtTokenUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtTokenUtil.java new file mode 100644 index 0000000..da4c2d8 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtTokenUtil.java @@ -0,0 +1,143 @@ +package com.trade.common.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JwtToken生成工具类 + * JWT token的格式:header.payload.signature + * header的格式(算法、token的类型): + * { + * "alg": "HS512", + * "typ": "JWT" + * } + * payload的格式(用户名、创建时间、生成时间): + * { + * "sub":"wang", + * "created":1489079981398 + * } + * signature的生成算法: + * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) + * @author Trade Team + */ +public class JwtTokenUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); + private static final String CLAIM_KEY_USERNAME = "sub"; + private static final String CLAIM_KEY_CREATED = "created"; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.expiration}") + private Long expiration; + @Value("${jwt.tokenHead}") + private String tokenHead; + + /** + * 根据负责生成JWT的token + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * 从token中获取JWT中的负载 + */ + private Claims getClaimsFromToken(String token) { + Claims claims = null; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + LOGGER.info("JWT格式验证失败: {}", token); + } + return claims; + } + + /** + * 生成token的过期时间 + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + /** + * 从token中获取登录用户名 + */ + public String getUserNameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 验证token是否还有效 + * + * @param token 客户端传入的token + * @param userDetails 从数据库中查询出来的用户信息 + */ + public boolean validateToken(String token, UserDetails userDetails) { + String username = getUserNameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + /** + * 判断token是否已经失效 + */ + private boolean isTokenExpired(String token) { + Date expiredDate = getExpiredDateFromToken(token); + return expiredDate.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + private Date getExpiredDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 根据用户信息生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + /** + * 判断token是否可以被刷新 + */ + public boolean canRefresh(String token) { + return !isTokenExpired(token); + } + + /** + * 刷新token + */ + public String refreshToken(String token) { + Claims claims = getClaimsFromToken(token); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtUtils.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtUtils.java new file mode 100644 index 0000000..403df13 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtUtils.java @@ -0,0 +1,231 @@ +package com.trade.common.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +/** + * JWT (JSON Web Token) 工具类 + * 用于生成、解析和验证JWT + * + * @author creator + * @since 2024-01-01 + */ +@Slf4j +@Component // 使其可以被Spring管理,方便注入配置 +public class JwtUtils { + + /** + * JWT 密钥,从配置文件读取,必须足够复杂以保证安全 + * 建议长度至少为256位 (32个ASCII字符) + */ + @Value("${jwt.secret:defaultSecretKey_must_be_at_least_32_characters_long_for_HS256}") + private String secret; + + /** + * JWT 过期时间(毫秒),从配置文件读取,默认为1小时 + */ + @Value("${jwt.expiration:3600000}") + private long expiration; + + private SecretKey secretKey; + + /** + * 初始化密钥 + */ + private SecretKey getSecretKey() { + if (secretKey == null) { + // 优化:确保密钥长度符合HS256要求,如果不足则进行提示或使用默认安全密钥 + if (!StringUtils.hasText(secret) || secret.length() < 32) { + log.warn("JWT secret key is not configured or too short (must be at least 32 chars for HS256). Using a default secure key. THIS IS NOT RECOMMENDED FOR PRODUCTION!"); + // Keys.secretKeyFor(SignatureAlgorithm.HS256) 会生成一个安全的随机密钥 + this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); + } else { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); + } + } + return secretKey; + } + + /** + * 生成JWT Token + * + * @param subject 主题,通常是用户ID或其他唯一标识 + * @param claims 自定义声明 (payload) + * @return 生成的JWT字符串 + */ + public String generateToken(String subject, Map claims) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + return Jwts.builder() + .setClaims(claims) // 设置自定义声明 + .setSubject(subject) // 设置主题 + .setId(UUID.randomUUID().toString()) // JWT的唯一身份标识 + .setIssuedAt(now) // 设置签发时间 + .setExpiration(expiryDate) // 设置过期时间 + .signWith(getSecretKey(), SignatureAlgorithm.HS256) // 设置签名算法和密钥 + .compact(); + } + + /** + * 生成JWT Token,仅包含 subject + * + * @param subject 主题,通常是用户ID或其他唯一标识 + * @return 生成的JWT字符串 + */ + public String generateToken(String subject) { + return generateToken(subject, null); + } + + /** + * 从JWT Token中获取 Claims (Payload) + * + * @param token JWT字符串 + * @return Claims 对象 + * @throws ExpiredJwtException 如果JWT已过期 + * @throws UnsupportedJwtException 如果JWT格式不受支持 + * @throws MalformedJwtException 如果JWT格式错误 + * @throws SignatureException 如果签名验证失败 + * @throws IllegalArgumentException 如果token为空或无效 + */ + private Claims getClaimsFromToken(String token) { + if (!StringUtils.hasText(token)) { + throw new IllegalArgumentException("JWT token cannot be null or empty."); + } + return Jwts.parserBuilder() + .setSigningKey(getSecretKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从JWT Token中获取主题 (Subject) + * + * @param token JWT字符串 + * @return 主题 + */ + public String getSubjectFromToken(String token) { + try { + return getClaimsFromToken(token).getSubject(); + } catch (Exception e) { + log.debug("Failed to get subject from token: {}", e.getMessage()); + return null; + } + } + + /** + * 从JWT Token中获取指定的 Claim 值 + * + * @param token JWT字符串 + * @param claimKey Claim的键 + * @param type Claim值的类型 + * @param Claim值的泛型 + * @return Claim值,如果不存在或类型不匹配则返回null + */ + public T getClaimFromToken(String token, String claimKey, Class type) { + try { + Claims claims = getClaimsFromToken(token); + return claims.get(claimKey, type); + } catch (Exception e) { + log.debug("Failed to get claim '{}' from token: {}", claimKey, e.getMessage()); + return null; + } + } + + /** + * 验证JWT Token是否有效 + * + * @param token JWT字符串 + * @return 如果token有效返回true,否则返回false + */ + public boolean validateToken(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + try { + Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token); + return true; + } catch (SignatureException ex) { + log.error("Invalid JWT signature: {}", ex.getMessage()); + } catch (MalformedJwtException ex) { + log.error("Invalid JWT token: {}", ex.getMessage()); + } catch (ExpiredJwtException ex) { + log.warn("Expired JWT token: {}", ex.getMessage()); + } catch (UnsupportedJwtException ex) { + log.error("Unsupported JWT token: {}", ex.getMessage()); + } catch (IllegalArgumentException ex) { + log.error("JWT claims string is empty: {}", ex.getMessage()); + } + return false; + } + + /** + * 判断JWT Token是否已过期 + * + * @param token JWT字符串 + * @return 如果已过期返回true,否则返回false。如果token无效也返回true。 + */ + public boolean isTokenExpired(String token) { + try { + Date expirationDate = getClaimsFromToken(token).getExpiration(); + return expirationDate.before(new Date()); + } catch (ExpiredJwtException e) { + return true; // 明确已过期 + } catch (Exception e) { + return true; // 其他解析错误,视为无效或已过期 + } + } + + /** + * 刷新JWT Token的过期时间 + * + * 注意:这实际上是重新签发了一个新的Token,包含了原有Token的claims和subject。 + * + * + * @param token 旧的JWT字符串 + * @return 新的JWT字符串,如果旧token无效则返回null + */ + public String refreshToken(String token) { + if (!StringUtils.hasText(token)) { + return null; + } + try { + Claims claims = getClaimsFromToken(token); + // 检查是否允许刷新 (例如,可以在claims中设置一个特定的刷新标记或时间窗口) + // 此处简单实现为只要未过期即可刷新 + if (claims.getExpiration().after(new Date())) { + return generateToken(claims.getSubject(), claims); + } + } catch (ExpiredJwtException ex) { + log.warn("Cannot refresh an already expired JWT token: {}", ex.getMessage()); + } catch (Exception e) { + log.error("Error refreshing JWT token: {}", e.getMessage()); + } + return null; + } + + /** + * 获取JWT的过期时间 + * + * @return 过期时间(毫秒) + */ + public long getExpiration() { + return expiration; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/RequestUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/RequestUtil.java new file mode 100644 index 0000000..0658051 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/RequestUtil.java @@ -0,0 +1,77 @@ +package com.trade.common.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * 请求工具类 + * @author Trade Team + */ +public class RequestUtil { + + /** + * 获取当前请求的HttpServletRequest对象 + * @return HttpServletRequest对象,如果不在请求上下文中则返回null + */ + public static HttpServletRequest getCurrentRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return null; + } + return attributes.getRequest(); + } + + /** + * 获取请求的IP地址 + * @return IP地址字符串 + */ + public static String getRequestIp() { + HttpServletRequest request = getCurrentRequest(); + if (request == null) { + return ""; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + /** + * 获取请求的User-Agent + * @return User-Agent字符串 + */ + public static String getUserAgent() { + HttpServletRequest request = getCurrentRequest(); + if (request == null) { + return ""; + } + return request.getHeader("User-Agent"); + } + + /** + * 获取请求的Referer + * @return Referer字符串 + */ + public static String getReferer() { + HttpServletRequest request = getCurrentRequest(); + if (request == null) { + return ""; + } + return request.getHeader("Referer"); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/SpringUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/SpringUtil.java new file mode 100644 index 0000000..328e811 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/SpringUtil.java @@ -0,0 +1,61 @@ +package com.trade.common.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Spring工具类,用于获取Spring上下文中的Bean + * @author Trade Team + */ +@Component +public class SpringUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringUtil.applicationContext == null) { + SpringUtil.applicationContext = applicationContext; + } + } + + /** + * 获取applicationContext + * @return ApplicationContext + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 通过name获取 Bean. + * @param name Bean的名称 + * @return Object + */ + public static Object getBean(String name) { + return getApplicationContext().getBean(name); + } + + /** + * 通过class获取Bean. + * @param clazz Bean的类型 + * @param 泛型 + * @return T + */ + public static T getBean(Class clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * 通过name,以及Clazz返回指定的Bean + * @param name Bean的名称 + * @param clazz Bean的类型 + * @param 泛型 + * @return T + */ + public static T getBean(String name, Class clazz) { + return getApplicationContext().getBean(name, clazz); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/ValidationUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/ValidationUtil.java new file mode 100644 index 0000000..c66adb4 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/ValidationUtil.java @@ -0,0 +1,50 @@ +package com.trade.common.util; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; + +/** + * 参数校验工具类 + * @author Trade Team + */ +public class ValidationUtil { + + private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + + /** + * 校验对象 + * @param obj 待校验对象 + * @param 对象类型 + * @throws IllegalArgumentException 如果校验失败,则抛出此异常 + */ + public static void validate(T obj) { + Set> violations = VALIDATOR.validate(obj); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (ConstraintViolation violation : violations) { + sb.append(violation.getMessage()).append(";"); + } + throw new IllegalArgumentException(sb.toString()); + } + } + + /** + * 校验对象的指定属性 + * @param obj 待校验对象 + * @param propertyName 待校验属性名 + * @param 对象类型 + * @throws IllegalArgumentException 如果校验失败,则抛出此异常 + */ + public static void validateProperty(T obj, String propertyName) { + Set> violations = VALIDATOR.validateProperty(obj, propertyName); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (ConstraintViolation violation : violations) { + sb.append(violation.getMessage()).append(";"); + } + throw new IllegalArgumentException(sb.toString()); + } + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/Result.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/Result.java new file mode 100644 index 0000000..2ba8824 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/Result.java @@ -0,0 +1,128 @@ +package com.trade.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用API接口返回结果封装类 + * + * @author creator + * @since 2024-01-01 + */ +@Data +@Schema(description = "通用API接口返回结果") +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 成功标志 + */ + @Schema(description = "成功标志", example = "true") + private boolean success = true; + + /** + * 返回处理消息 + */ + @Schema(description = "返回处理消息", example = "操作成功!") + private String message = "操作成功!"; + + /** + * 返回代码 + */ + @Schema(description = "返回代码", example = "200") + private Integer code = 200; + + /** + * 返回数据对象 data + */ + @Schema(description = "返回数据对象") + private T result; + + /** + * 时间戳 + */ + @Schema(description = "时间戳", example = "1609459200000") + private long timestamp = System.currentTimeMillis(); + + public Result() { + } + + public Result(boolean success, String message, Integer code) { + this.success = success; + this.message = message; + this.code = code; + this.timestamp = System.currentTimeMillis(); + } + + public static Result ok() { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); + return r; + } + + public static Result ok(String msg) { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(msg); + return r; + } + + public static Result ok(T data) { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); + r.setResult(data); + return r; + } + + public static Result ok(String msg, T data) { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(msg); + r.setResult(data); + return r; + } + + public static Result error(String msg) { + return error(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), msg); + } + + public static Result error(Integer code, String msg) { + Result r = new Result<>(); + r.setSuccess(false); + r.setCode(code); + r.setMessage(msg); + return r; + } + + public static Result error(ResultCodeEnum resultCodeEnum) { + Result r = new Result<>(); + r.setSuccess(false); + r.setCode(resultCodeEnum.getCode()); + r.setMessage(resultCodeEnum.getMessage()); + return r; + } + + public static Result error(ResultCodeEnum resultCodeEnum, String msg) { + Result r = new Result<>(); + r.setSuccess(false); + r.setCode(resultCodeEnum.getCode()); + r.setMessage(msg); // Use custom message + return r; + } + + /** + * 无权限访问返回结果 + */ + public static Result noauth(String msg) { + return error(ResultCodeEnum.UNAUTHORIZED.getCode(), msg); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/ResultCodeEnum.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/ResultCodeEnum.java new file mode 100644 index 0000000..0845efd --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/ResultCodeEnum.java @@ -0,0 +1,30 @@ +package com.trade.common.vo; + +import lombok.Getter; + +/** + * 结果代码枚举 + * + * @author creator + * @since 2024-01-01 + */ +@Getter +public enum ResultCodeEnum { + + SUCCESS(200, "操作成功"), + OPERATION_FAILED(5001, "操作失败"), + BAD_REQUEST(400, "请求参数错误"), + UNAUTHORIZED(401, "未经授权"), + FORBIDDEN(403, "访问被拒绝"), + NOT_FOUND(404, "资源不存在"), + INTERNAL_SERVER_ERROR(500, "服务器内部错误"), + SERVICE_UNAVAILABLE(503, "服务不可用"); + + private final Integer code; + private final String message; + + ResultCodeEnum(Integer code, String message) { + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/pom.xml b/samples/existing-system/trade-gateway/pom.xml new file mode 100644 index 0000000..06c7be4 --- /dev/null +++ b/samples/existing-system/trade-gateway/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + com.stock + trade-gateway + jar + + trade-gateway + API Gateway for the trading system + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.projectlombok + lombok + provided + + + + + com.stock + trade-common + ${project.parent.version} + + + + + + + + + + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + org.springdoc + springdoc-openapi-starter-webflux-ui + ${springdoc-openapi.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/GatewayApplication.java b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/GatewayApplication.java new file mode 100644 index 0000000..9e83ebc --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/GatewayApplication.java @@ -0,0 +1,29 @@ +package com.trade.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 网关应用启动类 + * + * @author Trade Team + */ +@SpringBootApplication +@EnableDiscoveryClient // 开启服务注册与发现功能 +public class GatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ Trade Gateway 启动成功 ლ(´ڡ`ლ)゙ \n" + + " .-------. ____ __ \n" + + " | _ _ | \\ \\ / / \n" + + " | ( ' ) | \\ _. / ' \n" + + " |(_ o _) / _( )_ .' \n" + + " | (_,_).' __ ___(_ o _)' \n" + + " | |\ \ | || |(_,_)' \n" + + " | | \ `' /| `-' / \n" + + " | | \ / \ / \n" + + " ''-' `'-' `-..-' "); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/TradeGatewayApplication.java b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/TradeGatewayApplication.java new file mode 100644 index 0000000..9e1fb32 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/TradeGatewayApplication.java @@ -0,0 +1,17 @@ +package com.trade.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 网关服务启动类 + * Created by macro on 2020/6/17. + */ +@EnableDiscoveryClient +@SpringBootApplication +public class TradeGatewayApplication { + public static void main(String[] args) { + SpringApplication.run(TradeGatewayApplication.class, args); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/config/GatewayConfig.java b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/config/GatewayConfig.java new file mode 100644 index 0000000..da89b96 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/config/GatewayConfig.java @@ -0,0 +1,37 @@ +package com.trade.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * 网关配置类 + * + * @author Trade Team + */ +@Configuration +public class GatewayConfig { + + /** + * 配置CORS跨域支持 + * + * @return CorsWebFilter + */ + @Bean + public CorsWebFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedMethod("*"); // 允许所有请求方法 (GET, POST, PUT, DELETE, OPTIONS等) + config.addAllowedOriginPattern("*"); // 允许所有来源,生产环境建议配置具体域名 + config.addAllowedHeader("*"); // 允许所有请求头 + config.setAllowCredentials(true); // 允许发送Cookie + config.setMaxAge(3600L); // 预检请求的有效期,单位秒 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); + source.registerCorsConfiguration("/**", config); // 对所有路径生效 + + return new CorsWebFilter(source); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application-dev.yml b/samples/existing-system/trade-gateway/src/main/resources/application-dev.yml new file mode 100644 index 0000000..0f8823f --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application-dev.yml @@ -0,0 +1,63 @@ +spring: + redis: + database: 0 + host: localhost + port: 6379 + password: # + cloud: + gateway: + routes: + - id: trade-user + uri: lb://trade-user + predicates: + - Path=/user/** + filters: + - StripPrefix=1 + - id: trade-order + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + - id: trade-market-data + uri: lb://trade-market-data + predicates: + - Path=/market-data/** + filters: + - StripPrefix=1 + - id: trade-notification + uri: lb://trade-notification + predicates: + - Path=/notification/** + filters: + - StripPrefix=1 + - id: trade-strategy + uri: lb://trade-strategy + predicates: + - Path=/strategy/** + filters: + - StripPrefix=1 + - id: trade-risk + uri: lb://trade-risk + predicates: + - Path=/risk/** + filters: + - StripPrefix=1 + - id: trade-backtest + uri: lb://trade-backtest + predicates: + - Path=/backtest/** + filters: + - StripPrefix=1 + - id: trade-indicator + uri: lb://trade-indicator + predicates: + - Path=/indicator/** + filters: + - StripPrefix=1 + - id: trade-application + uri: lb://trade-application + predicates: + - Path=/application/** + filters: + - StripPrefix=1 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application-prod.yml b/samples/existing-system/trade-gateway/src/main/resources/application-prod.yml new file mode 100644 index 0000000..0f8823f --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application-prod.yml @@ -0,0 +1,63 @@ +spring: + redis: + database: 0 + host: localhost + port: 6379 + password: # + cloud: + gateway: + routes: + - id: trade-user + uri: lb://trade-user + predicates: + - Path=/user/** + filters: + - StripPrefix=1 + - id: trade-order + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + - id: trade-market-data + uri: lb://trade-market-data + predicates: + - Path=/market-data/** + filters: + - StripPrefix=1 + - id: trade-notification + uri: lb://trade-notification + predicates: + - Path=/notification/** + filters: + - StripPrefix=1 + - id: trade-strategy + uri: lb://trade-strategy + predicates: + - Path=/strategy/** + filters: + - StripPrefix=1 + - id: trade-risk + uri: lb://trade-risk + predicates: + - Path=/risk/** + filters: + - StripPrefix=1 + - id: trade-backtest + uri: lb://trade-backtest + predicates: + - Path=/backtest/** + filters: + - StripPrefix=1 + - id: trade-indicator + uri: lb://trade-indicator + predicates: + - Path=/indicator/** + filters: + - StripPrefix=1 + - id: trade-application + uri: lb://trade-application + predicates: + - Path=/application/** + filters: + - StripPrefix=1 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application-test.yml b/samples/existing-system/trade-gateway/src/main/resources/application-test.yml new file mode 100644 index 0000000..0f8823f --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application-test.yml @@ -0,0 +1,63 @@ +spring: + redis: + database: 0 + host: localhost + port: 6379 + password: # + cloud: + gateway: + routes: + - id: trade-user + uri: lb://trade-user + predicates: + - Path=/user/** + filters: + - StripPrefix=1 + - id: trade-order + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + - id: trade-market-data + uri: lb://trade-market-data + predicates: + - Path=/market-data/** + filters: + - StripPrefix=1 + - id: trade-notification + uri: lb://trade-notification + predicates: + - Path=/notification/** + filters: + - StripPrefix=1 + - id: trade-strategy + uri: lb://trade-strategy + predicates: + - Path=/strategy/** + filters: + - StripPrefix=1 + - id: trade-risk + uri: lb://trade-risk + predicates: + - Path=/risk/** + filters: + - StripPrefix=1 + - id: trade-backtest + uri: lb://trade-backtest + predicates: + - Path=/backtest/** + filters: + - StripPrefix=1 + - id: trade-indicator + uri: lb://trade-indicator + predicates: + - Path=/indicator/** + filters: + - StripPrefix=1 + - id: trade-application + uri: lb://trade-application + predicates: + - Path=/application/** + filters: + - StripPrefix=1 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application.yml b/samples/existing-system/trade-gateway/src/main/resources/application.yml new file mode 100644 index 0000000..5b73330 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application.yml @@ -0,0 +1,125 @@ +server: + port: 8080 # 网关服务端口 + +spring: + application: + name: trade-gateway # 应用名称 + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 # Nacos Server地址 + namespace: # Nacos命名空间ID,根据实际情况配置 + group: DEFAULT_GROUP # Nacos分组,根据实际情况配置 + config: + server-addr: ${spring.cloud.nacos.discovery.server-addr} # Nacos配置中心地址,同服务发现 + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + file-extension: yml # 配置文件格式 + shared-configs: # 共享配置 + - data-id: application-common.yml # 通用配置 + group: ${spring.cloud.nacos.discovery.group} + refresh: true # 是否动态刷新 + # ext-config: # 扩展配置,可以加载多个配置文件 + # - data-id: trade-gateway-ext.yml + # group: ${spring.cloud.nacos.discovery.group} + # refresh: true + + gateway: + discovery: + locator: + enabled: true # 开启从注册中心动态创建路由的功能 + lower-case-service-id: true # 将服务名转为小写进行路由 + routes: + # 示例:路由到用户服务 (trade-user) + - id: trade-user-route + uri: lb://trade-user # lb代表从Nacos负载均衡,trade-user是服务名 + predicates: + - Path=/user/** # 所有/user/**的请求都路由到trade-user服务 + filters: + - StripPrefix=1 # 转发前去掉第一层路径,例如 /user/login -> /login + # - AddRequestHeader=X-Request-Source, gateway # 添加请求头 + + # 示例:路由到行情服务 (trade-market) + - id: trade-market-route + uri: lb://trade-market + predicates: + - Path=/market/** + filters: + - StripPrefix=1 + + # 示例:路由到订单服务 (trade-order) + - id: trade-order-route + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + + # 示例:路由到认证服务 (trade-auth) - 如果有单独的认证服务 + # 如果认证逻辑在网关或者trade-security中,则不需要此路由 + # - id: trade-auth-route + # uri: lb://trade-auth + # predicates: + # - Path=/auth/** + # filters: + # - StripPrefix=1 + + # 静态资源路由 (如果前端项目部署在网关之后) + # - id: static-content-route + # uri: http://localhost:8081 # 前端静态资源服务器地址 + # predicates: + # - Path=/static/**, / + # filters: + # - RewritePath=/static/(?.*), /\${segment} + + # 日志配置 (可选, Spring Boot默认有日志输出) +logging: + level: + org.springframework.cloud.gateway: DEBUG # 网关日志级别 + reactor.netty.http.client: DEBUG # Netty客户端日志级别 + com.trade.gateway: INFO # 自定义包日志级别 + +# Actuator端点配置 (可选, 用于监控和管理) +management: + endpoints: + web: + exposure: + include: '*' # 暴露所有端点,生产环境请按需配置 + endpoint: + health: + show-details: always # 显示健康检查详情 + +# Sentinel配置 (可选, 用于流量控制和熔断降级) +# spring: +# cloud: +# sentinel: +# transport: +# dashboard: localhost:8080 # Sentinel控制台地址 +# port: 8719 # Sentinel API端口 +# datasource: +# ds1: +# nacos: +# server-addr: ${spring.cloud.nacos.discovery.server-addr} +# namespace: ${spring.cloud.nacos.discovery.namespace} +# group-id: ${spring.cloud.nacos.discovery.group} +# data-id: sentinel-rules-gateway.json +# rule-type: flow # 流控规则 + +# Seata 分布式事务配置 (如果需要) +# seata: +# tx-service-group: trade_tx_group # 事务组 +# service: +# vgroup-mapping: +# trade_tx_group: default +# registry: +# type: nacos +# nacos: +# server-addr: ${spring.cloud.nacos.discovery.server-addr} +# namespace: ${spring.cloud.nacos.discovery.namespace} +# group: SEATA_GROUP +# config: +# type: nacos +# nacos: +# server-addr: ${spring.cloud.nacos.discovery.server-addr} +# namespace: ${spring.cloud.nacos.discovery.namespace} +# group: SEATA_GROUP \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/bootstrap.yml b/samples/existing-system/trade-gateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..bcf166d --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/bootstrap.yml @@ -0,0 +1,45 @@ +spring: + application: + name: trade-gateway # 应用名称,必须与Nacos中配置的Data ID相关联或作为前缀 + cloud: + nacos: + config: + server-addr: 127.0.0.1:8848 # Nacos配置中心地址 + namespace: # Nacos命名空间ID,根据实际情况配置 + group: DEFAULT_GROUP # Nacos分组,根据实际情况配置 + file-extension: yml # 拉取配置文件的格式 + # 如果配置了shared-configs或者ext-config,这里可以不指定data-id,会默认加载 {spring.application.name}.{file-extension} + # data-id: trade-gateway.yml # 如果需要显式指定主配置文件Data ID + # refresh-enabled: true # 是否开启动态刷新,默认为true + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} # Nacos服务发现地址,通常与配置中心地址一致 + namespace: ${spring.cloud.nacos.config.namespace} + group: ${spring.cloud.nacos.config.group} + +# 如果使用了Spring Cloud Alibaba Sentinel,可以在这里配置 +# spring: +# cloud: +# sentinel: +# transport: +# dashboard: localhost:8080 # Sentinel控制台地址 +# port: 8719 # Sentinel客户端与控制台通信的端口,默认8719 +# # Nacos数据源配置,用于持久化规则 +# datasource: +# flow: +# nacos: +# server-addr: ${spring.cloud.nacos.config.server-addr} +# namespace: ${spring.cloud.nacos.config.namespace} +# group-id: ${spring.cloud.nacos.config.group} +# data-id: ${spring.application.name}-flow-rules.json +# rule-type: flow +# degrade: +# nacos: +# server-addr: ${spring.cloud.nacos.config.server-addr} +# namespace: ${spring.cloud.nacos.config.namespace} +# group-id: ${spring.cloud.nacos.config.group} +# data-id: ${spring.application.name}-degrade-rules.json +# rule-type: degrade + +# 日志配置,也可以放在application.yml中 +# logging: +# config: classpath:logback-spring.xml # 指定日志配置文件 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/logback-spring.xml b/samples/existing-system/trade-gateway/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..976afc0 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/pom.xml b/samples/existing-system/trade-indicator/pom.xml new file mode 100644 index 0000000..fd19f49 --- /dev/null +++ b/samples/existing-system/trade-indicator/pom.xml @@ -0,0 +1,106 @@ + + + + com.stock + stock-trade-system + 1.0.0 + + 4.0.0 + com.stock + trade-indicator + ${project.artifactId} + 技术指标服务,负责技术指标的定义、计算和存储 + + + + + com.stock + trade-common + ${project.version} + compile + + + com.stock + trade-market-data + ${project.version} + compile + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-processor + + + org.projectlombok + lombok + + + + + org.springdoc + springdoc-openapi-ui + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + com.google.guava + guava + + + cn.hutool + hutool-all + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/IndicatorApplication.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/IndicatorApplication.java new file mode 100644 index 0000000..db9ec41 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/IndicatorApplication.java @@ -0,0 +1,20 @@ +package com.stock.trade.indicator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 技术指标服务启动类 + * + * @author AI Assistant + */ +@SpringBootApplication(scanBasePackages = {"com.stock.trade"}) +@EnableScheduling // 启用定时任务 +public class IndicatorApplication { + + public static void main(String[] args) { + SpringApplication.run(IndicatorApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorDefinitionController.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorDefinitionController.java new file mode 100644 index 0000000..7f6f4b8 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorDefinitionController.java @@ -0,0 +1,183 @@ +package com.stock.trade.indicator.controller; + +import com.stock.trade.framework.common.pojo.CommonResult; +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionCreateReqVO; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionPageReqVO; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionRespVO; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionUpdateReqVO; +import com.stock.trade.indicator.convert.IndicatorDefinitionConvert; +import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO; +import com.stock.trade.indicator.service.IndicatorDefinitionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.stock.trade.framework.common.pojo.CommonResult.success; + +/** + * 技术指标定义 Controller + * + * @author tianxin + */ +@Tag(name = "管理后台 - 技术指标定义") +@RestController +@RequestMapping("/indicator/definition") +@Validated +public class IndicatorDefinitionController { + + @Resource + private IndicatorDefinitionService indicatorDefinitionService; + + /** + * 创建技术指标定义 + * + * @param createReqVO 创建信息 + * @return 指标定义ID + */ + @PostMapping("/create") + @Operation(summary = "创建技术指标定义") + @PreAuthorize("@ss.hasPermission('indicator:definition:create')") + public CommonResult createIndicatorDefinition(@Valid @RequestBody IndicatorDefinitionCreateReqVO createReqVO) { + return success(indicatorDefinitionService.createIndicatorDefinition(createReqVO)); + } + + /** + * 更新技术指标定义 + * + * @param updateReqVO 更新信息 + * @return 是否成功 + */ + @PutMapping("/update") + @Operation(summary = "更新技术指标定义") + @PreAuthorize("@ss.hasPermission('indicator:definition:update')") + public CommonResult updateIndicatorDefinition(@Valid @RequestBody IndicatorDefinitionUpdateReqVO updateReqVO) { + indicatorDefinitionService.updateIndicatorDefinition(updateReqVO); + return success(true); + } + + /** + * 更新技术指标定义状态 + * + * @param id 指标定义ID + * @param enabled 是否启用 + * @return 是否成功 + */ + @PutMapping("/update-status") + @Operation(summary = "更新技术指标定义状态") + @PreAuthorize("@ss.hasPermission('indicator:definition:update')") + public CommonResult updateIndicatorDefinitionStatus(@RequestParam("id") Long id, + @RequestParam("enabled") Boolean enabled) { + indicatorDefinitionService.updateIndicatorDefinitionStatus(id, enabled); + return success(true); + } + + /** + * 删除技术指标定义 + * + * @param id 指标定义ID + * @return 是否成功 + */ + @DeleteMapping("/delete") + @Operation(summary = "删除技术指标定义") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('indicator:definition:delete')") + public CommonResult deleteIndicatorDefinition(@RequestParam("id") Long id) { + indicatorDefinitionService.deleteIndicatorDefinition(id); + return success(true); + } + + /** + * 获取技术指标定义 + * + * @param id 指标定义ID + * @return 指标定义信息 + */ + @GetMapping("/get") + @Operation(summary = "获得技术指标定义") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult getIndicatorDefinition(@RequestParam("id") Long id) { + IndicatorDefinitionDO definition = indicatorDefinitionService.getIndicatorDefinition(id); + return success(IndicatorDefinitionConvert.INSTANCE.convert(definition)); + } + + /** + * 获取技术指标定义列表 + * + * @param ids 指标定义ID列表 + * @return 指标定义列表 + */ + @GetMapping("/list") + @Operation(summary = "获得技术指标定义列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getIndicatorDefinitionList(@RequestParam("ids") List ids) { + List list = indicatorDefinitionService.getIndicatorDefinitionList(ids); + return success(IndicatorDefinitionConvert.INSTANCE.convertList(list)); + } + + /** + * 获取技术指标定义分页 + * + * @param pageVO 分页查询参数 + * @return 指标定义分页结果 + */ + @GetMapping("/page") + @Operation(summary = "获得技术指标定义分页") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getIndicatorDefinitionPage(@Valid IndicatorDefinitionPageReqVO pageVO) { + PageResult pageResult = indicatorDefinitionService.getIndicatorDefinitionPage(pageVO); + return success(IndicatorDefinitionConvert.INSTANCE.convertPage(pageResult)); + } + + /** + * 根据名称获取技术指标定义 + * + * @param name 指标名称 + * @return 指标定义信息 + */ + @GetMapping("/get-by-name") + @Operation(summary = "根据名称获得技术指标定义") + @Parameter(name = "name", description = "指标名称", required = true, example = "SMA") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult getIndicatorDefinitionByName(@RequestParam("name") String name) { + IndicatorDefinitionDO definition = indicatorDefinitionService.getIndicatorDefinitionByName(name); + return success(IndicatorDefinitionConvert.INSTANCE.convert(definition)); + } + + /** + * 根据类型获取技术指标定义列表 + * + * @param type 指标类型 + * @return 指标定义列表 + */ + @GetMapping("/list-by-type") + @Operation(summary = "根据类型获得技术指标定义列表") + @Parameter(name = "type", description = "指标类型", required = true, example = "TREND") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getIndicatorDefinitionListByType(@RequestParam("type") String type) { + List list = indicatorDefinitionService.getIndicatorDefinitionListByType(type); + return success(IndicatorDefinitionConvert.INSTANCE.convertList(list)); + } + + /** + * 获取所有启用的技术指标定义列表 + * + * @return 启用的指标定义列表 + */ + @GetMapping("/list-enabled") + @Operation(summary = "获得所有启用的技术指标定义列表") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getEnabledIndicatorDefinitionList() { + List list = indicatorDefinitionService.getEnabledIndicatorDefinitionList(); + return success(IndicatorDefinitionConvert.INSTANCE.convertList(list)); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorValueController.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorValueController.java new file mode 100644 index 0000000..3ff81da --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorValueController.java @@ -0,0 +1,240 @@ +package com.stock.trade.indicator.controller; + +import com.stock.trade.framework.common.pojo.CommonResult; +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.indicator.value.*; +import com.stock.trade.indicator.convert.IndicatorValueConvert; +import com.stock.trade.indicator.dal.dataobject.IndicatorValueDO; +import com.stock.trade.indicator.service.IndicatorValueService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; + +import static com.stock.trade.framework.common.pojo.CommonResult.success; +import static com.stock.trade.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +/** + * 技术指标计算结果 Controller + * + * @author AI Assistant + */ +@Tag(name = "管理后台 - 技术指标计算结果") +@RestController +@RequestMapping("/indicator/value") +@Validated +public class IndicatorValueController { + + @Resource + private IndicatorValueService indicatorValueService; + + /** + * 创建指标计算结果 + * + * @param createReqVO 创建信息 + * @return 指标计算结果ID + */ + @PostMapping("/create") + @Operation(summary = "创建指标计算结果") + @PreAuthorize("@ss.hasPermission('indicator:value:create')") + public CommonResult createIndicatorValue(@Valid @RequestBody IndicatorValueCreateReqVO createReqVO) { + return success(indicatorValueService.createIndicatorValue(createReqVO)); + } + + /** + * 更新指标计算结果 + * + * @param updateReqVO 更新信息 + * @return 是否成功 + */ + @PutMapping("/update") + @Operation(summary = "更新指标计算结果") + @PreAuthorize("@ss.hasPermission('indicator:value:update')") + public CommonResult updateIndicatorValue(@Valid @RequestBody IndicatorValueUpdateReqVO updateReqVO) { + indicatorValueService.updateIndicatorValue(updateReqVO); + return success(true); + } + + /** + * 删除指标计算结果 + * + * @param id 指标计算结果ID + * @return 是否成功 + */ + @DeleteMapping("/delete") + @Operation(summary = "删除指标计算结果") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('indicator:value:delete')") + public CommonResult deleteIndicatorValue(@RequestParam("id") Long id) { + indicatorValueService.deleteIndicatorValue(id); + return success(true); + } + + /** + * 获取指标计算结果 + * + * @param id 指标计算结果ID + * @return 指标计算结果信息 + */ + @GetMapping("/get") + @Operation(summary = "获得指标计算结果") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult getIndicatorValue(@RequestParam("id") Long id) { + IndicatorValueDO value = indicatorValueService.getIndicatorValue(id); + return success(IndicatorValueConvert.INSTANCE.convert(value)); + } + + /** + * 获取指标计算结果列表 + * + * @param ids 指标计算结果ID列表 + * @return 指标计算结果列表 + */ + @GetMapping("/list") + @Operation(summary = "获得指标计算结果列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValueList(@RequestParam("ids") List ids) { + List list = indicatorValueService.getIndicatorValueList(ids); + return success(IndicatorValueConvert.INSTANCE.convertList(list)); + } + + /** + * 获取指标计算结果分页 + * + * @param pageVO 分页查询参数 + * @return 指标计算结果分页结果 + */ + @GetMapping("/page") + @Operation(summary = "获得指标计算结果分页") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValuePage(@Valid IndicatorValuePageReqVO pageVO) { + PageResult pageResult = indicatorValueService.getIndicatorValuePage(pageVO); + return success(IndicatorValueConvert.INSTANCE.convertPage(pageResult)); + } + + /** + * 导出指标计算结果 Excel + * + * @param response HTTP响应 + * @param exportReqVO 查询条件 + * @throws IOException IO异常 + */ + @GetMapping("/export-excel") + @Operation(summary = "导出指标计算结果 Excel") + @PreAuthorize("@ss.hasPermission('indicator:value:export')") + // @OperateLog(type = EXPORT) // 假设有操作日志注解 + public void exportIndicatorValueExcel(HttpServletResponse response, @Valid IndicatorValueExportReqVO exportReqVO) throws IOException { + List list = indicatorValueService.getIndicatorValueList(exportReqVO); + List excelList = IndicatorValueConvert.INSTANCE.convertList02(list); + // ExcelUtils.write(response, "指标计算结果.xls", "数据", IndicatorValueExcelVO.class, excelList); // 假设有Excel导出工具类 + } + + /** + * 获取最新的指标计算结果 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @param period 时间周期 + * @return 最新的指标计算结果 + */ + @GetMapping("/get-latest") + @Operation(summary = "获取最新的指标计算结果") + @Parameters({ + @Parameter(name = "definitionId", description = "指标定义ID", required = true, example = "1"), + @Parameter(name = "stockCode", description = "股票代码", required = true, example = "600000.SH"), + @Parameter(name = "period", description = "时间周期", required = true, example = "DAY") + }) + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult getLatestIndicatorValue(@RequestParam("definitionId") Long definitionId, + @RequestParam("stockCode") String stockCode, + @RequestParam("period") String period) { + IndicatorValueDO value = indicatorValueService.getLatestIndicatorValue(definitionId, stockCode, period); + return success(IndicatorValueConvert.INSTANCE.convert(value)); + } + + /** + * 获取指定时间范围内的指标计算结果列表 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @param period 时间周期 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 指标计算结果列表 + */ + @GetMapping("/list-by-time-range") + @Operation(summary = "获取指定时间范围内的指标计算结果列表") + @Parameters({ + @Parameter(name = "definitionId", description = "指标定义ID", required = true, example = "1"), + @Parameter(name = "stockCode", description = "股票代码", required = true, example = "600000.SH"), + @Parameter(name = "period", description = "时间周期", required = true, example = "DAY"), + @Parameter(name = "startTime", description = "开始时间", required = true, example = "2023-01-01T00:00:00"), + @Parameter(name = "endTime", description = "结束时间", required = true, example = "2023-12-31T23:59:59") + }) + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValueListByTimeRange( + @RequestParam("definitionId") Long definitionId, + @RequestParam("stockCode") String stockCode, + @RequestParam("period") String period, + @RequestParam("startTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime, + @RequestParam("endTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) { + List list = indicatorValueService.getIndicatorValueListByTimeRange(definitionId, stockCode, period, startTime, endTime); + return success(IndicatorValueConvert.INSTANCE.convertList(list)); + } + + /** + * 获取指定状态的指标计算结果列表 + * + * @param status 计算状态 + * @return 指标计算结果列表 + */ + @GetMapping("/list-by-status") + @Operation(summary = "获取指定状态的指标计算结果列表") + @Parameter(name = "status", description = "计算状态", required = true, example = "SUCCESS") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValueListByStatus(@RequestParam("status") String status) { + List list = indicatorValueService.getIndicatorValueListByStatus(status); + return success(IndicatorValueConvert.INSTANCE.convertList(list)); + } + + /** + * 批量更新指标计算结果状态 + * + * @param updateStatusReqVO 更新状态请求 + * @return 是否成功 + */ + @PutMapping("/update-status-batch") + @Operation(summary = "批量更新指标计算结果状态") + @PreAuthorize("@ss.hasPermission('indicator:value:update')") + public CommonResult updateIndicatorValueStatusBatch(@Valid @RequestBody IndicatorValueUpdateStatusBatchReqVO updateStatusReqVO) { + indicatorValueService.updateIndicatorValueStatusBatch(updateStatusReqVO.getIds(), updateStatusReqVO.getStatus(), updateStatusReqVO.getErrorMessage()); + return success(true); + } + + /** + * 触发指标计算 + * + * @param calculateReqVO 计算请求信息 + * @return 指标计算结果ID列表 + */ + @PostMapping("/calculate") + @Operation(summary = "触发指标计算") + @PreAuthorize("@ss.hasPermission('indicator:value:calculate')") + public CommonResult> calculateIndicatorValue(@Valid @RequestBody IndicatorValueCalculateReqVO calculateReqVO) { + List resultIds = indicatorValueService.calculateIndicatorValue(calculateReqVO); + return success(resultIds); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorDefinitionConvert.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorDefinitionConvert.java new file mode 100644 index 0000000..d54cd2a --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorDefinitionConvert.java @@ -0,0 +1,35 @@ +package com.stock.trade.indicator.convert; + +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.indicator.definition.*; +import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 指标定义 Convert + * + * @author tianxin + */ +@Mapper +public interface IndicatorDefinitionConvert { + + IndicatorDefinitionConvert INSTANCE = Mappers.getMapper(IndicatorDefinitionConvert.class); + + IndicatorDefinitionDO convert(IndicatorDefinitionCreateReqVO bean); + + IndicatorDefinitionDO convert(IndicatorDefinitionUpdateReqVO bean); + + IndicatorDefinitionRespVO convert(IndicatorDefinitionDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + List convertList03(List list); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorValueConvert.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorValueConvert.java new file mode 100644 index 0000000..f2599f7 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorValueConvert.java @@ -0,0 +1,33 @@ +package com.stock.trade.indicator.convert; + +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.indicator.value.*; +import com.stock.trade.indicator.dal.dataobject.IndicatorValueDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 指标计算结果 Convert + * + * @author tianxin + */ +@Mapper +public interface IndicatorValueConvert { + + IndicatorValueConvert INSTANCE = Mappers.getMapper(IndicatorValueConvert.class); + + IndicatorValueDO convert(IndicatorValueCreateReqVO bean); + + IndicatorValueDO convert(IndicatorValueUpdateReqVO bean); + + IndicatorValueRespVO convert(IndicatorValueDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorDefinitionDO.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorDefinitionDO.java new file mode 100644 index 0000000..c501e8a --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorDefinitionDO.java @@ -0,0 +1,68 @@ +package com.stock.trade.indicator.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.stock.trade.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 技术指标定义 DO + * + * @author tianxin + */ +@TableName("indicator_definition") +@Data +@EqualsAndHashCode(callSuper = true) +public class IndicatorDefinitionDO extends BaseDO { + + /** + * 指标ID,主键自增 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 指标名称,例如:Moving Average, MACD, RSI + */ + private String name; + + /** + * 指标简称,例如:MA, MACD, RSI + */ + private String shortName; + + /** + * 指标描述 + */ + private String description; + + /** + * 指标类型,例如:趋势型、震荡型、成交量型 + * 可以使用枚举定义 + */ + private String type; + + /** + * 计算公式描述或类引用 + * 例如:"CLOSE / MA(CLOSE, N)" 或 "com.stock.trade.indicator.calculator.MACDCalculator" + */ + private String formula; + + /** + * 默认参数,JSON格式,例如:{"period": 20} for MA, {"shortPeriod":12, "longPeriod":26, "signalPeriod":9} for MACD + */ + private String defaultParams; + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 备注 + */ + private String remark; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorValueDO.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorValueDO.java new file mode 100644 index 0000000..135b995 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorValueDO.java @@ -0,0 +1,77 @@ +package com.stock.trade.indicator.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.stock.trade.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 技术指标计算结果 DO + * + * @author tianxin + */ +@TableName("indicator_value") +@Data +@EqualsAndHashCode(callSuper = true) +public class IndicatorValueDO extends BaseDO { + + /** + * 指标值ID,主键自增 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 关联的指标定义ID + * {@link IndicatorDefinitionDO#getId()} + */ + private Long definitionId; + + /** + * 股票代码 + */ + private String stockCode; + + /** + * 时间周期,例如:1min, 5min, 1day, 1week + */ + private String timePeriod; + + /** + * 计算时间点 + */ + private LocalDateTime calculationTime; + + /** + * 指标计算参数,JSON格式,覆盖或补充指标定义的默认参数 + * 例如:{"period": 30} + */ + private String params; + + /** + * 指标计算结果,JSON格式 + * 例如:{"ma": 10.5} 或 {"macd": 0.5, "diff": 0.2, "dea": 0.3} + */ + private String results; + + /** + * 计算状态,例如:PENDING, CALCULATING, SUCCESS, FAILED + * 可以使用枚举定义 + */ + private String status; + + /** + * 错误信息,当计算失败时记录 + */ + private String errorMessage; + + /** + * 备注 + */ + private String remark; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorDefinitionMapper.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorDefinitionMapper.java new file mode 100644 index 0000000..921b4f7 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorDefinitionMapper.java @@ -0,0 +1,50 @@ +package com.stock.trade.indicator.dal.mysql; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.stock.trade.framework.mybatis.core.mapper.BaseMapperX; +import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 技术指标定义 Mapper + * + * @author tianxin + */ +@Mapper +public interface IndicatorDefinitionMapper extends BaseMapperX { + + /** + * 根据指标名称查询指标定义 + * + * @param name 指标名称 + * @return 指标定义 + */ + default IndicatorDefinitionDO selectByName(String name) { + return selectOne(new LambdaQueryWrapper() + .eq(IndicatorDefinitionDO::getName, name)); + } + + /** + * 根据指标类型查询指标定义列表 + * + * @param type 指标类型 + * @return 指标定义列表 + */ + default List selectListByType(String type) { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorDefinitionDO::getType, type)); + } + + /** + * 查询所有启用的指标定义 + * + * @return 启用的指标定义列表 + */ + default List selectListByEnabled() { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorDefinitionDO::getEnabled, true)); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorValueMapper.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorValueMapper.java new file mode 100644 index 0000000..00a345a --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorValueMapper.java @@ -0,0 +1,77 @@ +package com.stock.trade.indicator.dal.mysql; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.stock.trade.framework.mybatis.core.mapper.BaseMapperX; +import com.stock.trade.indicator.dal.dataobject.IndicatorValueDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 技术指标计算结果 Mapper + * + * @author tianxin + */ +@Mapper +public interface IndicatorValueMapper extends BaseMapperX { + + /** + * 根据指标定义ID和股票代码查询最新的计算结果 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @return 最新的计算结果 + */ + default IndicatorValueDO selectLatestByDefinitionIdAndStockCode(Long definitionId, String stockCode) { + return selectOne(new LambdaQueryWrapper() + .eq(IndicatorValueDO::getDefinitionId, definitionId) + .eq(IndicatorValueDO::getStockCode, stockCode) + .orderByDesc(IndicatorValueDO::getCalculationTime) + .last("LIMIT 1")); + } + + /** + * 查询指定时间范围内的计算结果 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 计算结果列表 + */ + default List selectListByTimeRange(Long definitionId, String stockCode, + LocalDateTime startTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorValueDO::getDefinitionId, definitionId) + .eq(IndicatorValueDO::getStockCode, stockCode) + .between(IndicatorValueDO::getCalculationTime, startTime, endTime) + .orderByAsc(IndicatorValueDO::getCalculationTime)); + } + + /** + * 查询指定状态的计算结果 + * + * @param status 计算状态 + * @return 计算结果列表 + */ + default List
MyBatis Plus 配置类
SpringDoc OpenAPI 配置类
用于生成API接口文档
+ * 可以根据需要创建多个 GroupedOpenApi Bean 来对 API 进行分组展示。 + * 例如,可以按模块、按版本或按访问权限进行分组。 + *
通用常量类
自定义业务异常类
全局异常处理器
捕获controller层抛出的异常,并返回统一的JSON格式响应
JWT (JSON Web Token) 工具类
用于生成、解析和验证JWT
+ * 注意:这实际上是重新签发了一个新的Token,包含了原有Token的claims和subject。 + *
通用API接口返回结果封装类
结果代码枚举