feat(impact): Impact Agent MVP —— 变更点定位 + 影响调查书(追加改修场景)

- 门控:用户提供 existing_system 路径 → 进入影响调查;未提供 → 原流程不变
- CodeParser 解析 Java(@RestController/@Service/@Entity/@TableName)+ ExistingSystemExplorer 组装
- ImpactAgent 变更点定位(变更区分×既存対応 确定性比对,无 LLM)→ ImpactReport(JSON 可下载)
- 影响调查结果作为 Writer 生成概要设计书的主上下文({{impact}},无专用影响章)
- source_aggregator 解除 existing_system=None 硬编码
- 既有系统样本 sunOnly/stock-trade-system(无 LICENSE,仅测试输入,保留来源标注)
- 新造股票交易域追加改修样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名)
- 全量 351 passed / 99.27% 覆盖;门禁 PASS(16 要素:新规5/変更8/削除3/未受影响50)
This commit is contained in:
lhl
2026-08-23 17:59:08 +08:00
parent da33df92e1
commit 2e11720a2d
383 changed files with 18411 additions and 10 deletions
+3 -1
View File
@@ -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 closedchat 改为每次调用新建 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 RED6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS{{Section:1}}/{{section2}} 注入 + 无锚点章告警) | 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 | 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 RED6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS{{Section:1}}/{{section2}} 注入 + 无锚点章告警) | 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 + 回填 + 默认 metadoc_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 |
@@ -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 │ 新增 CodeParserJava 解析)
▼ ▼
StructuredSource.tables CodeStructure → ExistingSystemInfo
│ │
└──────────────┬───────────────────────────┘
ImpactAgent(变更点定位,确定性比对)
- 取 変更区分 + 既存対応 列
- 与 ExistingSystemInfo 连接比对(存在性校验)
ImpactReportJSON 中间产物,可下载)
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` → ControllerInfobase_path 取 `@RequestMapping`endpoints 取方法级映射)
- `@Service` → ServiceInfomethods
- `@Entity|@Table(name=...)` → EntityInfotable_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 化
+9
View File
@@ -0,0 +1,9 @@
/.idea/
*.class
*.lst
*.log
.idea/*
*/target/*
*/.idea/*
*.iml
*.ipr
+37
View File
@@ -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 配置)。
+3
View File
@@ -0,0 +1,3 @@
project_rules.md
README.md
plan.md
+116
View File
@@ -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 功能优化与代码审查 [未开始]
+154
View File
@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>trade-common</module>
<module>trade-user</module>
<module>trade-security</module>
<module>trade-gateway</module>
<module>trade-mbg</module>
<module>trade-strategy</module>
<module>trade-risk</module>
<module>trade-indicator</module>
<module>trade-market-data</module>
<module>trade-backtest</module>
<module>trade-notifyication</module>
<module>trade-order</module>
<module>trade-application</module>
</modules>
<name>量化交易系统</name>
<description>基于Spring Boot的A股超短线交易系统</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
</parent>
<properties>
<java.version>8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<mysql.version>8.0.33</mysql.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<druid.version>1.2.18</druid.version>
<fastjson.version>2.0.32</fastjson.version>
<hutool.version>5.8.20</hutool.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot Starter AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Druid 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-application</artifactId>
<version>1.0.0</version>
<name>trade-application</name>
<description>trade-application</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Starter AMQP (RabbitMQ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus Spring Boot Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- trade-common -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- trade-user -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-user</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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();
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> 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;
}
}
@@ -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<String> permissionList = new ArrayList<>(); // 假设这里获取用户权限
return new org.springframework.security.core.userdetails.User(admin.getUsername(), admin.getPassword(), new ArrayList<>());
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
}
@@ -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<SecurityScheme> securitySchemes() {
//设置请求头信息
List<SecurityScheme> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");
result.add(apiKey);
return result;
}
}
@@ -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);
}
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="com.trade.application" level="debug"/>
</configuration>
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-backtest</artifactId>
<packaging>jar</packaging>
<name>trade-backtest</name>
<description>Strategy backtesting module for the trading system</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- trade-common -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- trade-strategy (to use defined strategies for backtesting) -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-strategy</artifactId>
<version>${project.version}</version>
</dependency>
<!-- trade-market-data (to get historical market data for backtesting) -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-market-data</artifactId>
<version>${project.version}</version>
</dependency>
<!-- trade-indicator (if backtesting involves indicators not directly part of strategy module) -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-indicator</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Cloud Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringDoc OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="com.trade.backtest" level="debug"/>
</configuration>
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>1.0.0</version>
<name>trade-common</name>
<description>trade-common</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Swagger Annotations -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.20</version>
</dependency>
<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- SpringDoc OpenAPI UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Spring Security Core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.7.11</version>
</dependency>
<!-- Spring Security Config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.7.11</version>
</dependency>
<!-- Spring Security Web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.7.11</version>
</dependency>
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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;
}
@@ -0,0 +1,46 @@
package com.stock.common.pojo;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import java.io.Serializable;
/**
* 通用返回结果
*
* @param <T> 数据泛型
*/
@Data
@Validated
public class CommonResult<T> implements Serializable {
/**
* 错误码
*/
private Integer code;
/**
* 返回数据
*/
private T data;
/**
* 错误提示
*/
private String msg;
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<>();
result.setCode(200); // 默认成功码为200
result.setData(data);
result.setMsg("成功");
return result;
}
public static <T> CommonResult<T> error(Integer code, String message) {
CommonResult<T> result = new CommonResult<>();
result.setCode(code);
result.setMsg(message);
return result;
}
// 可根据需要添加更多静态方法,例如处理特定错误码等
}
@@ -0,0 +1,102 @@
package com.stock.trading.common.api;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 通用API返回对象
* @param <T>
*/
@Data
@NoArgsConstructor
public class CommonResult<T> {
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 <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
}
@@ -0,0 +1,10 @@
package com.stock.trading.common.api;
/**
* 封装API的错误码
*/
public interface IErrorCode {
long getCode();
String getMessage();
}
@@ -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<T> {
private Long pageNum;
private Long pageSize;
private Long totalPage;
private Long total;
private List<T> list;
/**
* 将MyBatisPageHelper分页后的list转为分页信息
*/
public static <T> PageResult<T> restPage(List<T> list) {
PageResult<T> result = new PageResult<T>();
//PageInfo<T> pageInfo = new PageInfo<T>(list);
//result.setTotalPage(pageInfo.getPages());
//result.setPageNum(pageInfo.getPageNum());
//result.setPageSize(pageInfo.getPageSize());
//result.setTotal(pageInfo.getTotal());
//result.setList(pageInfo.getList());
return result;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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<String> handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
@@ -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);
}
}
@@ -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<String, Object> 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<String, Object> 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);
}
}
@@ -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<T> {
private Integer pageNum;
private Integer pageSize;
private Integer totalPage;
private Long total;
private List<T> list;
/**
* 将PageHelper分页后的list转为分页信息
*/
public static <T> CommonPage<T> restPage(List<T> list) {
CommonPage<T> result = new CommonPage<>();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> 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 <T> CommonPage<T> restPage(com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> page) {
CommonPage<T> result = new CommonPage<>();
com.baomidou.mybatisplus.extension.plugins.pagination.Page<T> 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;
}
}
@@ -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<T> {
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 <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
}
@@ -0,0 +1,11 @@
package com.trade.common.api;
/**
* 封装API的错误码
* @author Trade Team
*/
public interface IErrorCode {
long getCode();
String getMessage();
}
@@ -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;
}
}
@@ -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")));
}
}
@@ -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());
}
}
@@ -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 {
}
@@ -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;
/**
* <p>MyBatis Plus 配置类</p>
*
* @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)
}
@@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> 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;
}
}
@@ -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();
}
}
@@ -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;
/**
* <p>SpringDoc OpenAPI 配置类</p>
* <p>用于生成API接口文档</p>
*
* @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:[email protected]}") 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 分组
* <p>
* 可以根据需要创建多个 GroupedOpenApi Bean 来对 API 进行分组展示。
* 例如,可以按模块、按版本或按访问权限进行分组。
* </p>
*
* @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();
// }
}
@@ -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;
}
@@ -0,0 +1,119 @@
package com.trade.common.constant;
/**
* <p>通用常量类</p>
*
* @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";
}
@@ -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<String> permissionList;
public UserDto(Long id, String username, String password, Integer status, List<String> 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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -0,0 +1,97 @@
package com.trade.common.exception;
import com.trade.common.vo.ResultCodeEnum;
import lombok.Getter;
/**
* <p>自定义业务异常类</p>
*
* @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();
}
}
@@ -0,0 +1,10 @@
package com.trade.common.exception;
/**
* 封装API的错误码
* @author Trade Team
*/
public interface ErrorCode {
long getCode();
String getMessage();
}
@@ -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;
/**
* <p>全局异常处理器</p>
* <p>捕获controller层抛出的异常,并返回统一的JSON格式响应</p>
*
* @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<ConstraintViolation<?>> 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<FieldError> 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<FieldError> 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, "服务器发生未知异常,请联系管理员");
}
}
@@ -0,0 +1,126 @@
package com.trade.common.response;
import com.trade.common.api.IErrorCode;
/**
* 通用返回对象
* @author Trade Team
*/
public class CommonResult<T> {
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 <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) {
return new CommonResult<T>(errorCode.getCode(), message, null);
}
/**
* 失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(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;
}
}
@@ -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;
}
}
@@ -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<String> keys);
/**
* 批量删除key
*/
Long delByPrefix(String prefix);
/**
* 对某个key的value进行递增操作
*/
Long increment(String key, Long delta);
/**
* 对某个key的value进行递减操作
*/
Long decrement(String key, Long delta);
/**
* 获取所有key
*/
Set<String> 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<Object> 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<String, Object> map);
/**
* 将map放入redis的hash中并设置过期时间
*/
void hPutAll(String key, Map<String, Object> map, long expire);
/**
* 从redis的hash中获取所有数据
*/
Map<String, Object> hGetAll(String key);
/**
* 删除redis的hash中的数据
*/
void hDel(String key, Object... hashKeys);
}
@@ -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<String, Object> 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<String> keys) {
redisTemplate.delete(keys);
}
@Override
public Long delByPrefix(String prefix) {
Set<String> 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<String> 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<Object> 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<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void hPutAll(String key, Map<String, Object> map, long expire) {
redisTemplate.opsForHash().putAll(key, map);
expire(key, expire);
}
@Override
public Map<String, Object> hGetAll(String key) {
return (Map<String, Object>) (Map) redisTemplate.opsForHash().entries(key);
}
@Override
public void hDel(String key, Object... hashKeys) {
redisTemplate.opsForHash().delete(key, hashKeys);
}
}
@@ -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);
}
}
@@ -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<String, Object> 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<String, Object> 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);
}
}
@@ -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;
/**
* <p>JWT (JSON Web Token) 工具类</p>
* <p>用于生成、解析和验证JWT</p>
*
* @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<String, Object> 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 <T> Claim值的泛型
* @return Claim值,如果不存在或类型不匹配则返回null
*/
public <T> T getClaimFromToken(String token, String claimKey, Class<T> 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的过期时间
* <p>
* 注意:这实际上是重新签发了一个新的Token,包含了原有Token的claims和subject。
* </p>
*
* @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;
}
}
@@ -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");
}
}
@@ -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 <T> 泛型
* @return T
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
* @param name Bean的名称
* @param clazz Bean的类型
* @param <T> 泛型
* @return T
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
@@ -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 <T> 对象类型
* @throws IllegalArgumentException 如果校验失败,则抛出此异常
*/
public static <T> void validate(T obj) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validate(obj);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<T> violation : violations) {
sb.append(violation.getMessage()).append(";");
}
throw new IllegalArgumentException(sb.toString());
}
}
/**
* 校验对象的指定属性
* @param obj 待校验对象
* @param propertyName 待校验属性名
* @param <T> 对象类型
* @throws IllegalArgumentException 如果校验失败,则抛出此异常
*/
public static <T> void validateProperty(T obj, String propertyName) {
Set<ConstraintViolation<T>> violations = VALIDATOR.validateProperty(obj, propertyName);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<T> violation : violations) {
sb.append(violation.getMessage()).append(";");
}
throw new IllegalArgumentException(sb.toString());
}
}
}
@@ -0,0 +1,128 @@
package com.trade.common.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* <p>通用API接口返回结果封装类</p>
*
* @author creator
* @since 2024-01-01
*/
@Data
@Schema(description = "通用API接口返回结果")
public class Result<T> 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 <T> Result<T> ok() {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}
public static <T> Result<T> ok(String msg) {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(msg);
return r;
}
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
r.setResult(data);
return r;
}
public static <T> Result<T> ok(String msg, T data) {
Result<T> r = new Result<>();
r.setSuccess(true);
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(msg);
r.setResult(data);
return r;
}
public static <T> Result<T> error(String msg) {
return error(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), msg);
}
public static <T> Result<T> error(Integer code, String msg) {
Result<T> r = new Result<>();
r.setSuccess(false);
r.setCode(code);
r.setMessage(msg);
return r;
}
public static <T> Result<T> error(ResultCodeEnum resultCodeEnum) {
Result<T> r = new Result<>();
r.setSuccess(false);
r.setCode(resultCodeEnum.getCode());
r.setMessage(resultCodeEnum.getMessage());
return r;
}
public static <T> Result<T> error(ResultCodeEnum resultCodeEnum, String msg) {
Result<T> r = new Result<>();
r.setSuccess(false);
r.setCode(resultCodeEnum.getCode());
r.setMessage(msg); // Use custom message
return r;
}
/**
* 无权限访问返回结果
*/
public static <T> Result<T> noauth(String msg) {
return error(ResultCodeEnum.UNAUTHORIZED.getCode(), msg);
}
}
@@ -0,0 +1,30 @@
package com.trade.common.vo;
import lombok.Getter;
/**
* <p>结果代码枚举</p>
*
* @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;
}
}
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-gateway</artifactId>
<packaging>jar</packaging>
<name>trade-gateway</name>
<description>API Gateway for the trading system</description>
<dependencies>
<!-- Spring Cloud Starter Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Spring Cloud Nacos Discovery (for dynamic routing based on service discovery) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Config (for centralized configuration) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot Starter Actuator (for gateway monitoring) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Trade Common Module -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Spring Boot Starter Security (if gateway handles authentication/authorization) -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency> -->
<!-- Or use Spring Cloud Security if integrating with OAuth2/OIDC -->
<!-- <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency> -->
<!-- Resilience4j for circuit breaking, rate limiting (alternative to Sentinel if preferred) -->
<!-- <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency> -->
<!-- Spring Cloud LoadBalancer (comes with Gateway, but explicit if needed for customization) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- SpringDoc OpenAPI for Gateway (special configuration needed) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId> <!-- Use webflux for Gateway -->
<version>${springdoc-openapi.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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" +
" ''-' `'-' `-..-' ");
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
@@ -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
@@ -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
@@ -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>.*), /\${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
@@ -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 # 指定日志配置文件
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="com.trade.gateway" level="DEBUG"/>
</configuration>
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.stock</groupId>
<artifactId>trade-indicator</artifactId>
<name>${project.artifactId}</name>
<description>技术指标服务,负责技术指标的定义、计算和存储</description>
<dependencies>
<!-- 项目内部依赖 -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-market-data</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<!-- Spring Boot 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 持久层相关依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 工具类相关依赖 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- API 文档相关依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<!-- 参数校验相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 工具库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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<Long> 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<Boolean> 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<Boolean> 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<Boolean> 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<IndicatorDefinitionRespVO> 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<List<IndicatorDefinitionRespVO>> getIndicatorDefinitionList(@RequestParam("ids") List<Long> ids) {
List<IndicatorDefinitionDO> 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<PageResult<IndicatorDefinitionRespVO>> getIndicatorDefinitionPage(@Valid IndicatorDefinitionPageReqVO pageVO) {
PageResult<IndicatorDefinitionDO> 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<IndicatorDefinitionRespVO> 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<List<IndicatorDefinitionRespVO>> getIndicatorDefinitionListByType(@RequestParam("type") String type) {
List<IndicatorDefinitionDO> list = indicatorDefinitionService.getIndicatorDefinitionListByType(type);
return success(IndicatorDefinitionConvert.INSTANCE.convertList(list));
}
/**
* 获取所有启用的技术指标定义列表
*
* @return 启用的指标定义列表
*/
@GetMapping("/list-enabled")
@Operation(summary = "获得所有启用的技术指标定义列表")
@PreAuthorize("@ss.hasPermission('indicator:definition:query')")
public CommonResult<List<IndicatorDefinitionRespVO>> getEnabledIndicatorDefinitionList() {
List<IndicatorDefinitionDO> list = indicatorDefinitionService.getEnabledIndicatorDefinitionList();
return success(IndicatorDefinitionConvert.INSTANCE.convertList(list));
}
}
@@ -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<Long> 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<Boolean> 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<Boolean> 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<IndicatorValueRespVO> 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<List<IndicatorValueRespVO>> getIndicatorValueList(@RequestParam("ids") List<Long> ids) {
List<IndicatorValueDO> 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<PageResult<IndicatorValueRespVO>> getIndicatorValuePage(@Valid IndicatorValuePageReqVO pageVO) {
PageResult<IndicatorValueDO> 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<IndicatorValueDO> list = indicatorValueService.getIndicatorValueList(exportReqVO);
List<IndicatorValueExcelVO> 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<IndicatorValueRespVO> 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<List<IndicatorValueRespVO>> 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<IndicatorValueDO> 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<List<IndicatorValueRespVO>> getIndicatorValueListByStatus(@RequestParam("status") String status) {
List<IndicatorValueDO> 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<Boolean> 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<List<Long>> calculateIndicatorValue(@Valid @RequestBody IndicatorValueCalculateReqVO calculateReqVO) {
List<Long> resultIds = indicatorValueService.calculateIndicatorValue(calculateReqVO);
return success(resultIds);
}
}
@@ -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<IndicatorDefinitionRespVO> convertList(List<IndicatorDefinitionDO> list);
PageResult<IndicatorDefinitionRespVO> convertPage(PageResult<IndicatorDefinitionDO> page);
List<IndicatorDefinitionExcelVO> convertList02(List<IndicatorDefinitionDO> list);
List<IndicatorDefinitionSimpleRespVO> convertList03(List<IndicatorDefinitionDO> list);
}
@@ -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<IndicatorValueRespVO> convertList(List<IndicatorValueDO> list);
PageResult<IndicatorValueRespVO> convertPage(PageResult<IndicatorValueDO> page);
List<IndicatorValueExcelVO> convertList02(List<IndicatorValueDO> list);
}
@@ -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;
}
@@ -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;
}
@@ -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<IndicatorDefinitionDO> {
/**
* 根据指标名称查询指标定义
*
* @param name 指标名称
* @return 指标定义
*/
default IndicatorDefinitionDO selectByName(String name) {
return selectOne(new LambdaQueryWrapper<IndicatorDefinitionDO>()
.eq(IndicatorDefinitionDO::getName, name));
}
/**
* 根据指标类型查询指标定义列表
*
* @param type 指标类型
* @return 指标定义列表
*/
default List<IndicatorDefinitionDO> selectListByType(String type) {
return selectList(new LambdaQueryWrapper<IndicatorDefinitionDO>()
.eq(IndicatorDefinitionDO::getType, type));
}
/**
* 查询所有启用的指标定义
*
* @return 启用的指标定义列表
*/
default List<IndicatorDefinitionDO> selectListByEnabled() {
return selectList(new LambdaQueryWrapper<IndicatorDefinitionDO>()
.eq(IndicatorDefinitionDO::getEnabled, true));
}
}
@@ -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<IndicatorValueDO> {
/**
* 根据指标定义ID和股票代码查询最新的计算结果
*
* @param definitionId 指标定义ID
* @param stockCode 股票代码
* @return 最新的计算结果
*/
default IndicatorValueDO selectLatestByDefinitionIdAndStockCode(Long definitionId, String stockCode) {
return selectOne(new LambdaQueryWrapper<IndicatorValueDO>()
.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<IndicatorValueDO> selectListByTimeRange(Long definitionId, String stockCode,
LocalDateTime startTime, LocalDateTime endTime) {
return selectList(new LambdaQueryWrapper<IndicatorValueDO>()
.eq(IndicatorValueDO::getDefinitionId, definitionId)
.eq(IndicatorValueDO::getStockCode, stockCode)
.between(IndicatorValueDO::getCalculationTime, startTime, endTime)
.orderByAsc(IndicatorValueDO::getCalculationTime));
}
/**
* 查询指定状态的计算结果
*
* @param status 计算状态
* @return 计算结果列表
*/
default List<IndicatorValueDO> selectListByStatus(String status) {
return selectList(new LambdaQueryWrapper<IndicatorValueDO>()
.eq(IndicatorValueDO::getStatus, status));
}
/**
* 批量更新指标计算结果的状态
*
* @param ids 指标值ID列表
* @param status 新状态
* @return 更新的记录数
*/
default int updateStatusBatch(List<Long> ids, String status) {
return update(null, new LambdaUpdateWrapper<IndicatorValueDO>()
.in(IndicatorValueDO::getId, ids)
.set(IndicatorValueDO::getStatus, status));
}
}
@@ -0,0 +1,19 @@
package com.stock.trade.indicator.enums;
import com.stock.trade.framework.common.exception.ErrorCode;
/**
* Indicator 错误码枚举类
*
* indicator 系统,使用 1-004-000-000 段
*/
public interface ErrorCodeConstants {
// ========== 指标定义 1-004-000-000 ========== //
ErrorCode INDICATOR_DEFINITION_NOT_EXISTS = new ErrorCode(1_004_000_000, "指标定义不存在");
ErrorCode INDICATOR_DEFINITION_NAME_EXISTS = new ErrorCode(1_004_000_001, "指标定义名称已存在");
// ========== 指标计算结果 1-004-001-000 ========== //
ErrorCode INDICATOR_VALUE_NOT_EXISTS = new ErrorCode(1_004_001_000, "指标计算结果不存在");
}
@@ -0,0 +1,37 @@
package com.stock.trade.indicator.job;
import com.stock.trade.indicator.service.IndicatorValueService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 技术指标定时计算任务
*
* @author tianxin
*/
@Component
@Slf4j
public class IndicatorJob {
@Resource
private IndicatorValueService indicatorValueService;
/**
* 定时触发所有启用的技术指标计算
* TODO 后续可以优化为分布式任务调度,例如 XXL-Job
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void execute() {
log.info("[execute][开始执行技术指标计算任务]");
try {
indicatorValueService.triggerAllIndicatorCalculation();
log.info("[execute][技术指标计算任务执行成功]");
} catch (Exception e) {
log.error("[execute][技术指标计算任务执行失败]", e);
}
}
}
@@ -0,0 +1,96 @@
package com.stock.trade.indicator.service;
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.IndicatorDefinitionUpdateReqVO;
import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO;
import javax.validation.Valid;
import java.util.List;
/**
* 技术指标定义 Service 接口
*
* @author tianxin
*/
public interface IndicatorDefinitionService {
/**
* 创建技术指标定义
*
* @param createReqVO 创建信息
* @return 指标定义ID
*/
Long createIndicatorDefinition(@Valid IndicatorDefinitionCreateReqVO createReqVO);
/**
* 更新技术指标定义
*
* @param updateReqVO 更新信息
*/
void updateIndicatorDefinition(@Valid IndicatorDefinitionUpdateReqVO updateReqVO);
/**
* 删除技术指标定义
*
* @param id 指标定义ID
*/
void deleteIndicatorDefinition(Long id);
/**
* 获取技术指标定义
*
* @param id 指标定义ID
* @return 指标定义
*/
IndicatorDefinitionDO getIndicatorDefinition(Long id);
/**
* 获取技术指标定义列表
*
* @param ids 指标定义ID列表
* @return 指标定义列表
*/
List<IndicatorDefinitionDO> getIndicatorDefinitionList(List<Long> ids);
/**
* 获取技术指标定义分页
*
* @param pageReqVO 分页查询
* @return 指标定义分页
*/
PageResult<IndicatorDefinitionDO> getIndicatorDefinitionPage(IndicatorDefinitionPageReqVO pageReqVO);
/**
* 根据指标名称获取技术指标定义
*
* @param name 指标名称
* @return 指标定义
*/
IndicatorDefinitionDO getIndicatorDefinitionByName(String name);
/**
* 根据指标类型获取技术指标定义列表
*
* @param type 指标类型
* @return 指标定义列表
*/
List<IndicatorDefinitionDO> getIndicatorDefinitionListByType(String type);
/**
* 获取所有启用的技术指标定义
*
* @return 启用的指标定义列表
*/
List<IndicatorDefinitionDO> getEnabledIndicatorDefinitionList();
/**
* 更新技术指标定义状态
*
* @param id 指标定义ID
* @param enabled 是否启用
*/
void updateIndicatorDefinitionStatus(Long id, Boolean enabled);
}
@@ -0,0 +1,120 @@
package com.stock.trade.indicator.service;
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 javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
/**
* 技术指标计算结果 Service 接口
*
* @author AI Assistant
*/
public interface IndicatorValueService {
/**
* 创建指标计算结果
*
* @param createReqVO 创建信息
* @return 指标计算结果ID
*/
Long createIndicatorValue(@Valid IndicatorValueCreateReqVO createReqVO);
/**
* 更新指标计算结果
*
* @param updateReqVO 更新信息
*/
void updateIndicatorValue(@Valid IndicatorValueUpdateReqVO updateReqVO);
/**
* 删除指标计算结果
*
* @param id 指标计算结果ID
*/
void deleteIndicatorValue(Long id);
/**
* 获取指标计算结果
*
* @param id 指标计算结果ID
* @return 指标计算结果
*/
IndicatorValueDO getIndicatorValue(Long id);
/**
* 获取指标计算结果列表
*
* @param ids 指标计算结果ID列表
* @return 指标计算结果列表
*/
List<IndicatorValueDO> getIndicatorValueList(List<Long> ids);
/**
* 获取指标计算结果分页
*
* @param pageReqVO 分页查询
* @return 指标计算结果分页
*/
PageResult<IndicatorValueDO> getIndicatorValuePage(IndicatorValuePageReqVO pageReqVO);
/**
* 获取指标计算结果列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 指标计算结果列表
*/
List<IndicatorValueDO> getIndicatorValueList(IndicatorValueExportReqVO exportReqVO);
/**
* 获取最新的指标计算结果
*
* @param indicatorDefinitionId 指标定义ID
* @param stockCode 股票代码
* @param period 时间周期
* @return 指标计算结果
*/
IndicatorValueDO getLatestIndicatorValue(Long indicatorDefinitionId, String stockCode, String period);
/**
* 获取指定时间范围内的指标计算结果列表
*
* @param indicatorDefinitionId 指标定义ID
* @param stockCode 股票代码
* @param period 时间周期
* @param startTime 开始时间
* @param endTime 结束时间
* @return 指标计算结果列表
*/
List<IndicatorValueDO> getIndicatorValueListByTimeRange(Long indicatorDefinitionId, String stockCode,
String period, LocalDateTime startTime, LocalDateTime endTime);
/**
* 获取指定状态的指标计算结果列表
*
* @param status 计算状态
* @return 指标计算结果列表
*/
List<IndicatorValueDO> getIndicatorValueListByStatus(String status);
/**
* 批量更新指标计算结果状态
*
* @param ids 指标计算结果ID列表
* @param status 新状态
* @param errorMessage 错误信息(可选)
*/
void updateIndicatorValueStatusBatch(List<Long> ids, String status, String errorMessage);
/**
* 触发指标计算
*
* @param calculateReqVO 计算请求信息
* @return 指标计算结果ID列表
*/
List<Long> calculateIndicatorValue(@Valid IndicatorValueCalculateReqVO calculateReqVO);
}
@@ -0,0 +1,192 @@
package com.stock.trade.indicator.service.impl;
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.IndicatorDefinitionUpdateReqVO;
import com.stock.trade.indicator.convert.IndicatorDefinitionConvert;
import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO;
import com.stock.trade.indicator.dal.mysql.IndicatorDefinitionMapper;
import com.stock.trade.indicator.service.IndicatorDefinitionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.List;
import static com.stock.trade.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.stock.trade.indicator.enums.ErrorCodeConstants.*;
/**
* 技术指标定义 Service 实现类
*
* @author tianxin
*/
@Service
@Validated
@Slf4j
public class IndicatorDefinitionServiceImpl implements IndicatorDefinitionService {
@Resource
private IndicatorDefinitionMapper indicatorDefinitionMapper;
/**
* 创建技术指标定义
*
* @param createReqVO 创建信息
* @return 指标定义ID
*/
@Override
public Long createIndicatorDefinition(IndicatorDefinitionCreateReqVO createReqVO) {
// 校验名称唯一性
validateIndicatorDefinitionNameUnique(null, createReqVO.getName());
// 插入
IndicatorDefinitionDO indicatorDefinition = IndicatorDefinitionConvert.INSTANCE.convert(createReqVO);
indicatorDefinitionMapper.insert(indicatorDefinition);
// 返回
return indicatorDefinition.getId();
}
/**
* 更新技术指标定义
*
* @param updateReqVO 更新信息
*/
@Override
public void updateIndicatorDefinition(IndicatorDefinitionUpdateReqVO updateReqVO) {
// 校验存在
validateIndicatorDefinitionExists(updateReqVO.getId());
// 校验名称唯一性
validateIndicatorDefinitionNameUnique(updateReqVO.getId(), updateReqVO.getName());
// 更新
IndicatorDefinitionDO updateObj = IndicatorDefinitionConvert.INSTANCE.convert(updateReqVO);
indicatorDefinitionMapper.updateById(updateObj);
}
/**
* 删除技术指标定义
*
* @param id 指标定义ID
*/
@Override
public void deleteIndicatorDefinition(Long id) {
// 校验存在
validateIndicatorDefinitionExists(id);
// 删除
indicatorDefinitionMapper.deleteById(id);
}
/**
* 校验技术指标定义是否存在
*
* @param id 指标定义ID
*/
private void validateIndicatorDefinitionExists(Long id) {
if (indicatorDefinitionMapper.selectById(id) == null) {
throw exception(INDICATOR_DEFINITION_NOT_EXISTS);
}
}
/**
* 校验技术指标定义名称的唯一性
*
* @param id 指标定义ID,可为空,用于更新时排除自身
* @param name 指标名称
*/
private void validateIndicatorDefinitionNameUnique(Long id, String name) {
IndicatorDefinitionDO definition = indicatorDefinitionMapper.selectByName(name);
if (definition == null) {
return;
}
// 如果 id 为空,说明不用排除自身
if (id == null) {
throw exception(INDICATOR_DEFINITION_NAME_DUPLICATE);
}
if (!definition.getId().equals(id)) {
throw exception(INDICATOR_DEFINITION_NAME_DUPLICATE);
}
}
/**
* 获取技术指标定义
*
* @param id 指标定义ID
* @return 指标定义
*/
@Override
public IndicatorDefinitionDO getIndicatorDefinition(Long id) {
return indicatorDefinitionMapper.selectById(id);
}
/**
* 获取技术指标定义列表
*
* @param ids 指标定义ID列表
* @return 指标定义列表
*/
@Override
public List<IndicatorDefinitionDO> getIndicatorDefinitionList(List<Long> ids) {
return indicatorDefinitionMapper.selectBatchIds(ids);
}
/**
* 获取技术指标定义分页
*
* @param pageReqVO 分页查询
* @return 指标定义分页
*/
@Override
public PageResult<IndicatorDefinitionDO> getIndicatorDefinitionPage(IndicatorDefinitionPageReqVO pageReqVO) {
return indicatorDefinitionMapper.selectPage(pageReqVO, IndicatorDefinitionConvert.INSTANCE.convert(pageReqVO));
}
/**
* 根据指标名称获取技术指标定义
*
* @param name 指标名称
* @return 指标定义
*/
@Override
public IndicatorDefinitionDO getIndicatorDefinitionByName(String name) {
return indicatorDefinitionMapper.selectByName(name);
}
/**
* 根据指标类型获取技术指标定义列表
*
* @param type 指标类型
* @return 指标定义列表
*/
@Override
public List<IndicatorDefinitionDO> getIndicatorDefinitionListByType(String type) {
return indicatorDefinitionMapper.selectListByType(type);
}
/**
* 获取所有启用的技术指标定义
*
* @return 启用的指标定义列表
*/
@Override
public List<IndicatorDefinitionDO> getEnabledIndicatorDefinitionList() {
return indicatorDefinitionMapper.selectListByEnabled(true);
}
/**
* 更新技术指标定义状态
*
* @param id 指标定义ID
* @param enabled 是否启用
*/
@Override
public void updateIndicatorDefinitionStatus(Long id, Boolean enabled) {
// 校验存在
validateIndicatorDefinitionExists(id);
// 更新状态
IndicatorDefinitionDO updateObj = new IndicatorDefinitionDO();
updateObj.setId(id);
updateObj.setEnabled(enabled);
indicatorDefinitionMapper.updateById(updateObj);
}
}
@@ -0,0 +1,241 @@
package com.stock.trade.indicator.service.impl;
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.dal.mysql.IndicatorValueMapper;
import com.stock.trade.indicator.service.IndicatorDefinitionService;
import com.stock.trade.indicator.service.IndicatorValueService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import static com.stock.trade.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.stock.trade.indicator.enums.ErrorCodeConstants.*;
/**
* 技术指标计算结果 Service 实现类
*
* @author AI Assistant
*/
@Service
@Validated
@Slf4j
public class IndicatorValueServiceImpl implements IndicatorValueService {
@Resource
private IndicatorValueMapper indicatorValueMapper;
@Resource
private IndicatorDefinitionService indicatorDefinitionService; // 用于校验指标定义是否存在等
/**
* 创建指标计算结果
*
* @param createReqVO 创建信息
* @return 指标计算结果ID
*/
@Override
public Long createIndicatorValue(IndicatorValueCreateReqVO createReqVO) {
// 校验指标定义是否存在
validateIndicatorDefinitionExists(createReqVO.getDefinitionId());
IndicatorValueDO indicatorValue = IndicatorValueConvert.INSTANCE.convert(createReqVO);
indicatorValueMapper.insert(indicatorValue);
return indicatorValue.getId();
}
/**
* 更新指标计算结果
*
* @param updateReqVO 更新信息
*/
@Override
public void updateIndicatorValue(IndicatorValueUpdateReqVO updateReqVO) {
// 校验存在
validateIndicatorValueExists(updateReqVO.getId());
// 校验指标定义是否存在
validateIndicatorDefinitionExists(updateReqVO.getDefinitionId());
IndicatorValueDO updateObj = IndicatorValueConvert.INSTANCE.convert(updateReqVO);
indicatorValueMapper.updateById(updateObj);
}
/**
* 删除指标计算结果
*
* @param id 指标计算结果ID
*/
@Override
public void deleteIndicatorValue(Long id) {
// 校验存在
validateIndicatorValueExists(id);
indicatorValueMapper.deleteById(id);
}
/**
* 获取指标计算结果
*
* @param id 指标计算结果ID
* @return 指标计算结果
*/
@Override
public IndicatorValueDO getIndicatorValue(Long id) {
return indicatorValueMapper.selectById(id);
}
/**
* 获取指标计算结果列表
*
* @param ids 指标计算结果ID列表
* @return 指标计算结果列表
*/
@Override
public List<IndicatorValueDO> getIndicatorValueList(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
return indicatorValueMapper.selectBatchIds(ids);
}
/**
* 获取指标计算结果分页
*
* @param pageReqVO 分页查询
* @return 指标计算结果分页
*/
@Override
public PageResult<IndicatorValueDO> getIndicatorValuePage(IndicatorValuePageReqVO pageReqVO) {
return indicatorValueMapper.selectPage(pageReqVO, IndicatorValueConvert.INSTANCE.convert(pageReqVO));
}
/**
* 获取指标计算结果列表, 用于 Excel 导出
*
* @param exportReqVO 查询条件
* @return 指标计算结果列表
*/
@Override
public List<IndicatorValueDO> getIndicatorValueList(IndicatorValueExportReqVO exportReqVO) {
return indicatorValueMapper.selectList(IndicatorValueConvert.INSTANCE.convert(exportReqVO));
}
/**
* 获取最新的指标计算结果
*
* @param indicatorDefinitionId 指标定义ID
* @param stockCode 股票代码
* @param period 时间周期
* @return 指标计算结果
*/
@Override
public IndicatorValueDO getLatestIndicatorValue(Long indicatorDefinitionId, String stockCode, String period) {
// 校验指标定义是否存在
validateIndicatorDefinitionExists(indicatorDefinitionId);
return indicatorValueMapper.selectLatest(indicatorDefinitionId, stockCode, period);
}
/**
* 获取指定时间范围内的指标计算结果列表
*
* @param indicatorDefinitionId 指标定义ID
* @param stockCode 股票代码
* @param period 时间周期
* @param startTime 开始时间
* @param endTime 结束时间
* @return 指标计算结果列表
*/
@Override
public List<IndicatorValueDO> getIndicatorValueListByTimeRange(Long indicatorDefinitionId, String stockCode, String period, LocalDateTime startTime, LocalDateTime endTime) {
// 校验指标定义是否存在
validateIndicatorDefinitionExists(indicatorDefinitionId);
return indicatorValueMapper.selectListByTimeRange(indicatorDefinitionId, stockCode, period, startTime, endTime);
}
/**
* 获取指定状态的指标计算结果列表
*
* @param status 计算状态
* @return 指标计算结果列表
*/
@Override
public List<IndicatorValueDO> getIndicatorValueListByStatus(String status) {
return indicatorValueMapper.selectListByStatus(status);
}
/**
* 批量更新指标计算结果状态
*
* @param ids 指标计算结果ID列表
* @param status 新状态
* @param errorMessage 错误信息(可选)
*/
@Override
@Transactional
public void updateIndicatorValueStatusBatch(List<Long> ids, String status, String errorMessage) {
if (ids == null || ids.isEmpty()) {
return;
}
indicatorValueMapper.updateStatusBatch(ids, status, errorMessage);
}
/**
* 触发指标计算
*
* @param calculateReqVO 计算请求信息
* @return 指标计算结果ID列表 (这里仅为示例,实际计算逻辑可能更复杂,并可能异步执行)
*/
@Override
public List<Long> calculateIndicatorValue(IndicatorValueCalculateReqVO calculateReqVO) {
// 校验指标定义是否存在
validateIndicatorDefinitionExists(calculateReqVO.getDefinitionId());
// 实际的指标计算逻辑会在这里实现,可能涉及调用行情数据、计算引擎等
// 此处仅为示例,创建一个虚拟的计算结果并返回ID
log.info("触发指标计算: {}, 股票: {}, 周期: {}, 参数: {}",
calculateReqVO.getDefinitionId(), calculateReqVO.getStockCode(),
calculateReqVO.getPeriod(), calculateReqVO.getParams());
// 模拟创建计算结果
IndicatorValueCreateReqVO createReq = new IndicatorValueCreateReqVO();
createReq.setDefinitionId(calculateReqVO.getDefinitionId());
createReq.setStockCode(calculateReqVO.getStockCode());
createReq.setTimePeriod(calculateReqVO.getPeriod());
createReq.setCalculationTime(LocalDateTime.now());
createReq.setParams("{ \"info\": \"triggered by calculateIndicatorValue\" }"); // 示例参数
createReq.setValues("{ \"MA5\": 10.5, \"MA10\": 10.2 }"); // 示例结果
createReq.setStatus("PENDING"); // 初始状态为待计算或计算中
Long valueId = createIndicatorValue(createReq);
return Collections.singletonList(valueId);
}
/**
* 校验指标计算结果是否存在
*
* @param id 指标计算结果ID
*/
private void validateIndicatorValueExists(Long id) {
if (indicatorValueMapper.selectById(id) == null) {
throw exception(INDICATOR_VALUE_NOT_EXISTS);
}
}
/**
* 校验关联的技术指标定义是否存在
*
* @param definitionId 指标定义ID
*/
private void validateIndicatorDefinitionExists(Long definitionId) {
if (indicatorDefinitionService.getIndicatorDefinition(definitionId) == null) {
throw exception(INDICATOR_DEFINITION_NOT_EXISTS);
}
}
}
@@ -0,0 +1,18 @@
package com.trade.indicator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 交易指标服务启动类
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeIndicatorApplication {
public static void main(String[] args) {
SpringApplication.run(TradeIndicatorApplication.class, args);
}
}
@@ -0,0 +1,24 @@
# 开发环境配置
spring:
# 数据库配置
datasource:
url: jdbc:mysql://127.0.0.1:3306/stock_trade_indicator?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
username: root # 数据库用户名
password: password # 数据库密码
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8.x 驱动
# HikariCP 连接池配置
hikari:
connection-timeout: 30000 # 连接超时时间 (毫秒)
idle-timeout: 600000 # 空闲连接超时时间 (毫秒)
max-lifetime: 1800000 # 连接最大生命周期 (毫秒)
maximum-pool-size: 10 # 最大连接数
minimum-idle: 5 # 最小空闲连接数
pool-name: IndicatorHikariPool # 连接池名称
# 日志文件配置
logging:
file:
name: ./logs/trade-indicator-dev.log # 日志文件路径和名称
pattern:
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n" # 日志文件输出格式
@@ -0,0 +1,11 @@
server:
port: 8009
spring:
profiles:
active: prod
application:
name: trade-indicator
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8009
spring:
profiles:
active: test
application:
name: trade-indicator
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,65 @@
# Spring Boot 应用配置
server:
port: 18083 # 服务端口,避免冲突
spring:
application:
name: trade-indicator # 应用名称
profiles:
active: dev # 默认激活 dev 环境
# MyBatis Plus 配置
mybatis-plus:
# Mapper XML 文件的位置
mapper-locations: classpath*:mapper/**/*.xml
# 实体类所在的包
type-aliases-package: com.stock.trade.indicator.dal.dataobject
global-config:
db-config:
# 主键类型 AUTO 自增
id-type: AUTO
# 逻辑删除配置
logic-delete-field: deleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
banner: false # 关闭 MyBatis Plus 的 Banner
configuration:
# 是否开启驼峰命名自动映射
map-underscore-to-camel-case: true
# SQL 日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# SpringDoc (OpenAPI 3) 配置
springdoc:
api-docs:
# 是否开启 API 文档接口
enabled: true
# API 文档访问路径
path: /v3/api-docs
swagger-ui:
# 是否开启 Swagger UI
enabled: true
# Swagger UI 访问路径
path: /swagger-ui.html
# 默认 API 分组名称
display-request-duration: true
# tagsSorter 和 operationsSorter 这两个参数可以设置为 alpha 或者 method
tags-sorter: alpha
operations-sorter: alpha
# 配置扫描的包路径,默认为全部
packages-to-scan: com.stock.trade.indicator.controller
# 配置 API 的基本信息
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
# API 分组配置
group-configs:
- group: default
paths-to-match: /**
packages-to-scan: com.stock.trade.indicator.controller
# Logging 配置
logging:
level:
com.stock.trade: DEBUG # 项目日志级别
org.springframework: INFO
org.mybatis: INFO
@@ -0,0 +1,15 @@
# Spring Boot 引导配置
spring:
application:
name: trade-indicator # 应用名称
# 如果使用 Nacos 作为配置中心,可以添加以下配置
# spring:
# cloud:
# nacos:
# config:
# server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# file-extension: yaml # 配置文件格式
# # namespace: # Nacos 命名空间ID
# # group: DEFAULT_GROUP # Nacos 分组
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定义日志文件的存储地址 -->
<property name="LOG_HOME" value="./logs" />
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/trade-indicator.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/trade-indicator.%d{yyyy-MM-dd}.log</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志文件最大的大小 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 按照每天生成错误日志文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/trade-indicator-error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/trade-indicator-error.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 开发环境 -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
<logger name="com.stock.trade" level="DEBUG" />
</springProfile>
<!-- 测试环境 -->
<springProfile name="test">
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
<logger name="com.stock.trade" level="DEBUG" />
</springProfile>
<!-- 生产环境 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
<logger name="com.stock.trade" level="INFO" />
</springProfile>
</configuration>
@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-market-data</artifactId>
<packaging>jar</packaging>
<name>trade-market-data</name>
<description>Market data service module for the trading system</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- trade-common -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Starter Data Redis (for caching market data) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Starter WebFlux for WebClient (for Tushare API calls) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring Cloud Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringDoc OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Optional: if market data is consumed from Kafka or other message queues -->
<!-- <dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency> -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,17 @@
package com.trade.marketdata;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 行情服务启动类
* Created by macro on 2020/8/3.
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeMarketDataApplication {
public static void main(String[] args) {
SpringApplication.run(TradeMarketDataApplication.class, args);
}
}
@@ -0,0 +1,27 @@
package com.trade.marketdata.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 配置类
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "tushare")
public class TushareConfig {
/**
* Tushare API 的基础 URL
*/
private String apiUrl;
/**
* Tushare API 的 Token
*/
private String token;
}
@@ -0,0 +1,32 @@
package com.trade.marketdata.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author mac
* @date 2024/7/16
* @description WebClient 配置类
*/
@Configuration
public class WebClientConfig {
private final TushareConfig tushareConfig;
public WebClientConfig(TushareConfig tushareConfig) {
this.tushareConfig = tushareConfig;
}
/**
* 配置 WebClient Bean,用于调用 Tushare API
* @return WebClient 实例
*/
@Bean
public WebClient tushareWebClient() {
return WebClient.builder()
.baseUrl(tushareConfig.getApiUrl())
.defaultHeader("Content-Type", "application/json")
.build();
}
}
@@ -0,0 +1,4 @@
/**
* 配置类
*/
package com.trade.marketdata.config;
@@ -0,0 +1,83 @@
package com.trade.marketdata.controller;
import com.trade.marketdata.entity.DailyMarketData;
import com.trade.marketdata.service.DailyMarketDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
/**
* 股票日线行情数据 Controller
*
* @author Trae
* @since 2024-07-26
*/
@RestController
@RequestMapping("/market-data/daily")
@RequiredArgsConstructor
@Tag(name = "股票日线行情管理", description = "提供股票日线行情数据的同步和查询功能")
public class DailyMarketDataController {
private final DailyMarketDataService dailyMarketDataService;
/**
* 从Tushare同步指定日期的所有股票日线行情数据
*
* @param tradeDate 交易日期,格式 yyyyMMdd
* @return 同步结果
*/
@PostMapping("/sync/{tradeDate}")
@Operation(summary = "同步指定日期的股票日线行情数据", description = "从Tushare同步指定日期的所有股票日线行情数据")
public String syncDailyMarketData(@PathVariable String tradeDate) {
dailyMarketDataService.syncDailyMarketData(tradeDate);
return "Sync daily market data for " + tradeDate + " successfully.";
}
/**
* 从Tushare同步指定日期范围的所有股票日线行情数据
*
* @param startDate 开始日期,格式 yyyyMMdd
* @param endDate 结束日期,格式 yyyyMMdd
* @return 同步结果
*/
@PostMapping("/sync/range")
@Operation(summary = "同步指定日期范围的股票日线行情数据", description = "从Tushare同步指定日期范围内的所有股票日线行情数据")
public String syncDailyMarketDataByDateRange(@RequestParam String startDate, @RequestParam String endDate) {
dailyMarketDataService.syncDailyMarketDataByDateRange(startDate, endDate);
return "Sync daily market data from " + startDate + " to " + endDate + " successfully.";
}
/**
* 查询指定股票在指定日期范围内的日线行情数据
*
* @param tsCode 股票代码
* @param startDate 开始日期
* @param endDate 结束日期
* @return 日线行情数据列表
*/
@GetMapping("/query")
@Operation(summary = "查询日线行情数据", description = "查询指定股票在指定日期范围内的日线行情数据")
public List<DailyMarketData> getDailyMarketData(
@RequestParam String tsCode,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
return dailyMarketDataService.getDailyMarketData(tsCode, startDate, endDate);
}
/**
* 查询指定日期的所有股票日线行情数据
*
* @param tradeDate 交易日期
* @return 日线行情数据列表
*/
@GetMapping("/query/{tradeDate}")
@Operation(summary = "查询指定日期的所有股票日线行情数据", description = "查询指定日期的所有股票日线行情数据")
public List<DailyMarketData> getDailyMarketDataByTradeDate(@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate tradeDate) {
return dailyMarketDataService.getDailyMarketDataByTradeDate(tradeDate);
}
}
@@ -0,0 +1,57 @@
package com.trade.marketdata.controller;
import com.trade.marketdata.entity.RealtimeMarketData;
import com.trade.marketdata.service.RealtimeMarketDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 股票实时行情数据 Controller
*
* @author Trae
* @since 2024-07-26
*/
@RestController
@RequestMapping("/market-data/realtime")
@RequiredArgsConstructor
@Tag(name = "股票实时行情管理", description = "提供股票实时行情数据的查询功能")
public class RealtimeMarketDataController {
private final RealtimeMarketDataService realtimeMarketDataService;
/**
* 获取指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔,例如 "600000.SH,000001.SZ"
* @return 实时行情数据列表
*/
@GetMapping("/query")
@Operation(summary = "查询实时行情数据", description = "获取指定股票代码列表的实时行情数据")
public List<RealtimeMarketData> getRealtimeMarketData(@RequestParam String tsCodes) {
List<RealtimeMarketData> realtimeData = realtimeMarketDataService.getRealtimeMarketData(tsCodes);
// 可以选择是否在这里保存获取到的数据
// realtimeMarketDataService.saveRealtimeMarketData(realtimeData);
return realtimeData;
}
/**
* 获取并保存指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔
* @return 操作结果
*/
@PostMapping("/sync-and-save")
@Operation(summary = "同步并保存实时行情数据", description = "获取并保存指定股票代码列表的实时行情数据")
public String syncAndSaveRealtimeMarketData(@RequestParam String tsCodes) {
List<RealtimeMarketData> realtimeData = realtimeMarketDataService.getRealtimeMarketData(tsCodes);
if (realtimeData != null && !realtimeData.isEmpty()) {
realtimeMarketDataService.saveRealtimeMarketData(realtimeData);
return "Successfully fetched and saved realtime market data for: " + tsCodes;
}
return "No realtime market data found for: " + tsCodes;
}
}
@@ -0,0 +1,52 @@
package com.trade.marketdata.controller;
import com.trade.common.api.CommonResult;
import com.trade.marketdata.entity.StockBasic;
import com.trade.marketdata.service.StockBasicService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息控制器
*/
@RestController
@RequestMapping("/stockBasic")
@Tag(name = "StockBasicController", description = "股票基本信息管理")
public class StockBasicController {
private final StockBasicService stockBasicService;
public StockBasicController(StockBasicService stockBasicService) {
this.stockBasicService = stockBasicService;
}
/**
* 同步股票基本信息
* @return 同步结果
*/
@Operation(summary = "同步股票基本信息")
@PostMapping("/sync")
public CommonResult<Integer> syncStockBasic() {
int count = stockBasicService.syncStockBasicFromTushare();
return CommonResult.success(count, "成功同步 " + count + " 条股票基本信息");
}
/**
* 获取所有股票基本信息
* @return 股票基本信息列表
*/
@Operation(summary = "获取所有股票基本信息")
@GetMapping("/listAll")
public CommonResult<List<StockBasic>> listAllStockBasic() {
List<StockBasic> stockBasics = stockBasicService.listAllStockBasic();
return CommonResult.success(stockBasics);
}
}
@@ -0,0 +1,98 @@
package com.trade.marketdata.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("daily_market_data")
public class DailyMarketData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 股票代码
*/
private String tsCode;
/**
* 交易日期
*/
private LocalDate tradeDate;
/**
* 开盘价
*/
private BigDecimal open;
/**
* 最高价
*/
private BigDecimal high;
/**
* 最低价
*/
private BigDecimal low;
/**
* 收盘价
*/
private BigDecimal close;
/**
* 昨收价
*/
private BigDecimal preClose;
/**
* 涨跌额
*/
private BigDecimal change;
/**
* 涨跌幅
*/
private BigDecimal pctChg;
/**
* 成交量 (手)
*/
private BigDecimal vol;
/**
* 成交额 (千元)
*/
private BigDecimal amount;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,86 @@
package com.trade.marketdata.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 股票实时行情数据实体类
*
* @author Trae
* @since 2024-07-26
*/
@Data
@TableName("realtime_market_data")
public class RealtimeMarketData {
/**
* 自增主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 股票代码
*/
private String tsCode;
/**
* 股票名称
*/
private String name;
/**
* 当前价格
*/
private BigDecimal price;
/**
* 涨跌额
*/
private BigDecimal change;
/**
* 涨跌幅
*/
private BigDecimal pctChange;
/**
* 成交量(手)
*/
private Long volume;
/**
* 成交额(万元)
*/
private BigDecimal amount;
/**
* 开盘价
*/
private BigDecimal open;
/**
* 昨日收盘价
*/
private BigDecimal preClose;
/**
* 最高价
*/
private BigDecimal high;
/**
* 最低价
*/
private BigDecimal low;
/**
* 时间戳
*/
private LocalDateTime timestamp;
}
@@ -0,0 +1,118 @@
package com.trade.marketdata.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息实体类
*/
@Data
@TableName("stock_basic")
public class StockBasic implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* TS股票代码
*/
private String tsCode;
/**
* 股票代码
*/
private String symbol;
/**
* 股票名称
*/
private String name;
/**
* 股票简称
*/
private String area;
/**
* 所属省份
*/
private String province;
/**
* 所属城市
*/
private String city;
/**
* 所属行业
*/
private String industry;
/**
* 股票全称
*/
private String fullname;
/**
* 英文全称
*/
private String enname;
/**
* 市场类型 (主板/创业板/科创板等)
*/
private String market;
/**
* 交易所代码
*/
private String exchange;
/**
* 交易货币
*/
private String currType;
/**
* 上市状态 L上市 D退市 P暂停上市
*/
private String listStatus;
/**
* 上市日期
*/
private LocalDate listDate;
/**
* 退市日期
*/
private LocalDate delistDate;
/**
* 是否沪深港通标的,N否 H沪股通 S深股通
*/
private String isHs;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,32 @@
package com.trade.marketdata.entity;
import lombok.Data;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 请求实体类
*/
@Data
public class TushareRequest {
/**
* API 接口名称
*/
private String apiName;
/**
* Tushare Token
*/
private String token;
/**
* 请求参数
*/
private Object params;
/**
* 返回字段
*/
private String fields;
}
@@ -0,0 +1,45 @@
package com.trade.marketdata.entity;
import lombok.Data;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 响应实体类
*/
@Data
public class TushareResponse {
/**
* 返回码,0 表示成功
*/
private Integer code;
/**
* 错误信息
*/
private String msg;
/**
* 响应数据
*/
private TushareData data;
@Data
public static class TushareData {
/**
* 字段列表
*/
private List<String> fields;
/**
* 数据列表
*/
private List<List<Object>> items;
/**
* 总行数
*/
private Integer has_more;
}
}
@@ -0,0 +1,4 @@
/**
* 实体类层
*/
package com.trade.marketdata.entity;
@@ -0,0 +1,14 @@
package com.trade.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.marketdata.entity.DailyMarketData;
import org.apache.ibatis.annotations.Mapper;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据 Mapper 接口
*/
@Mapper
public interface DailyMarketDataMapper extends BaseMapper<DailyMarketData> {
}
@@ -0,0 +1,15 @@
package com.trade.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.marketdata.entity.RealtimeMarketData;
import org.apache.ibatis.annotations.Mapper;
/**
* 股票实时行情数据 Mapper 接口
*
* @author Trae
* @since 2024-07-26
*/
@Mapper
public interface RealtimeMarketDataMapper extends BaseMapper<RealtimeMarketData> {
}
@@ -0,0 +1,14 @@
package com.trade.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.marketdata.entity.StockBasic;
import org.apache.ibatis.annotations.Mapper;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息 Mapper 接口
*/
@Mapper
public interface StockBasicMapper extends BaseMapper<StockBasic> {
}
@@ -0,0 +1,4 @@
/**
* 数据访问层 (MyBatis Mapper)
*/
package com.trade.marketdata.mapper;
@@ -0,0 +1,4 @@
/**
* 行情服务模块主包。
*/
package com.trade.marketdata;
@@ -0,0 +1,33 @@
package com.trade.marketdata.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.marketdata.entity.DailyMarketData;
import java.time.LocalDate;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据服务接口
*/
public interface DailyMarketDataService extends IService<DailyMarketData> {
/**
* 从 Tushare 同步指定股票的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期 (yyyyMMdd)
* @param endDate 结束日期 (yyyyMMdd)
* @return 同步的日线数据数量
*/
int syncDailyMarketDataFromTushare(String tsCode, String startDate, String endDate);
/**
* 查询指定股票在指定日期范围内的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期
* @param endDate 结束日期
* @return 日线行情数据列表
*/
List<DailyMarketData> listDailyMarketData(String tsCode, LocalDate startDate, LocalDate endDate);
}
@@ -0,0 +1,29 @@
package com.trade.marketdata.service;
import com.trade.marketdata.entity.RealtimeMarketData;
import java.util.List;
/**
* 股票实时行情数据 Service 接口
*
* @author Trae
* @since 2024-07-26
*/
public interface RealtimeMarketDataService {
/**
* 获取指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔
* @return 实时行情数据列表
*/
List<RealtimeMarketData> getRealtimeMarketData(String tsCodes);
/**
* 保存实时行情数据列表
*
* @param realtimeMarketDataList 实时行情数据列表
*/
void saveRealtimeMarketData(List<RealtimeMarketData> realtimeMarketDataList);
}
@@ -0,0 +1,26 @@
package com.trade.marketdata.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.marketdata.entity.StockBasic;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息服务接口
*/
public interface StockBasicService extends IService<StockBasic> {
/**
* 从 Tushare 同步股票基本信息
* @return 同步的股票数量
*/
int syncStockBasicFromTushare();
/**
* 查询所有股票基本信息
* @return 股票基本信息列表
*/
List<StockBasic> listAllStockBasic();
}
@@ -0,0 +1,45 @@
package com.trade.marketdata.service;
import com.trade.marketdata.config.TushareConfig;
import com.trade.marketdata.entity.TushareRequest;
import com.trade.marketdata.entity.TushareResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 调用服务
*/
@Service
public class TushareApi {
private final WebClient tushareWebClient;
private final TushareConfig tushareConfig;
public TushareApi(WebClient tushareWebClient, TushareConfig tushareConfig) {
this.tushareWebClient = tushareWebClient;
this.tushareConfig = tushareConfig;
}
/**
* 调用 Tushare API 获取数据
* @param apiName API 名称,例如 "daily"
* @param params 请求参数,JSON 格式
* @param fields 返回字段,逗号分隔
* @return TushareResponse 响应对象
*/
public Mono<TushareResponse> post(String apiName, Object params, String fields) {
TushareRequest request = new TushareRequest();
request.setApiName(apiName);
request.setToken(tushareConfig.getToken());
request.setParams(params);
request.setFields(fields);
return tushareWebClient.post()
.bodyValue(request)
.retrieve()
.bodyToMono(TushareResponse.class);
}
}
@@ -0,0 +1,163 @@
package com.trade.marketdata.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.marketdata.entity.DailyMarketData;
import com.trade.marketdata.entity.TushareResponse;
import com.trade.marketdata.mapper.DailyMarketDataMapper;
import com.trade.marketdata.service.DailyMarketDataService;
import com.trade.marketdata.service.TushareApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据服务实现类
*/
@Service
@Slf4j
public class DailyMarketDataServiceImpl extends ServiceImpl<DailyMarketDataMapper, DailyMarketData> implements DailyMarketDataService {
private final TushareApi tushareApi;
public DailyMarketDataServiceImpl(TushareApi tushareApi) {
this.tushareApi = tushareApi;
}
/**
* 从 Tushare 同步指定股票的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期 (yyyyMMdd)
* @param endDate 结束日期 (yyyyMMdd)
* @return 同步的日线数据数量
*/
@Override
@Transactional
public int syncDailyMarketDataFromTushare(String tsCode, String startDate, String endDate) {
log.info("开始从 Tushare 同步股票 {} 的日线行情数据,日期范围:{} 至 {}...", tsCode, startDate, endDate);
Map<String, String> params = new HashMap<>();
params.put("ts_code", tsCode);
params.put("start_date", startDate);
params.put("end_date", endDate);
String fields = "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount";
TushareResponse response = tushareApi.post("daily", params, fields).block();
if (response == null || response.getCode() != 0 || response.getData() == null) {
log.error("从 Tushare 获取股票 {} 日线行情数据失败: {}", tsCode, response != null ? response.getMsg() : "未知错误");
return 0;
}
List<List<Object>> items = response.getData().getItems();
List<String> fieldsList = response.getData().getFields();
if (items == null || items.isEmpty()) {
log.warn("从 Tushare 获取到股票 {} 的空日线行情数据列表。", tsCode);
return 0;
}
List<DailyMarketData> newDailyDataList = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
for (List<Object> item : items) {
DailyMarketData dailyData = new DailyMarketData();
for (int i = 0; i < fieldsList.size(); i++) {
String fieldName = fieldsList.get(i);
Object value = item.get(i);
if (value == null) continue;
switch (fieldName) {
case "ts_code": dailyData.setTsCode(value.toString()); break;
case "trade_date": dailyData.setTradeDate(LocalDate.parse(value.toString(), formatter)); break;
case "open": dailyData.setOpen(new BigDecimal(value.toString())); break;
case "high": dailyData.setHigh(new BigDecimal(value.toString())); break;
case "low": dailyData.setLow(new BigDecimal(value.toString())); break;
case "close": dailyData.setClose(new BigDecimal(value.toString())); break;
case "pre_close": dailyData.setPreClose(new BigDecimal(value.toString())); break;
case "change": dailyData.setChange(new BigDecimal(value.toString())); break;
case "pct_chg": dailyData.setPctChg(new BigDecimal(value.toString())); break;
case "vol": dailyData.setVol(new BigDecimal(value.toString())); break;
case "amount": dailyData.setAmount(new BigDecimal(value.toString())); break;
}
}
dailyData.setCreateTime(LocalDateTime.now());
dailyData.setUpdateTime(LocalDateTime.now());
newDailyDataList.add(dailyData);
}
// 批量插入或更新
// 获取当前数据库中指定股票在指定日期范围内的所有日线数据的 ts_code 和 trade_date 组合
List<DailyMarketData> existingData = baseMapper.selectList(new QueryWrapper<DailyMarketData>()
.eq("ts_code", tsCode)
.between("trade_date", LocalDate.parse(startDate, formatter), LocalDate.parse(endDate, formatter)));
Map<String, DailyMarketData> existingDataMap = existingData.stream()
.collect(Collectors.toMap(data -> data.getTsCode() + "_" + data.getTradeDate().format(formatter), data -> data));
List<DailyMarketData> toInsert = new ArrayList<>();
List<DailyMarketData> toUpdate = new ArrayList<>();
for (DailyMarketData newData : newDailyDataList) {
String key = newData.getTsCode() + "_" + newData.getTradeDate().format(formatter);
if (existingDataMap.containsKey(key)) {
DailyMarketData existing = existingDataMap.get(key);
newData.setId(existing.getId()); // 设置ID以便更新
toUpdate.add(newData);
} else {
toInsert.add(newData);
}
}
int insertedCount = 0;
if (!toInsert.isEmpty()) {
saveBatch(toInsert);
insertedCount = toInsert.size();
log.info("成功插入 {} 条新的股票 {} 日线行情数据。", insertedCount, tsCode);
}
int updatedCount = 0;
if (!toUpdate.isEmpty()) {
updateBatchById(toUpdate);
updatedCount = toUpdate.size();
log.info("成功更新 {} 条股票 {} 日线行情数据。", updatedCount, tsCode);
}
log.info("股票 {} 日线行情数据同步完成,总计插入 {} 条,更新 {} 条。", tsCode, insertedCount, updatedCount);
return insertedCount + updatedCount;
}
/**
* 查询指定股票在指定日期范围内的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期
* @param endDate 结束日期
* @return 日线行情数据列表
*/
@Override
public List<DailyMarketData> listDailyMarketData(String tsCode, LocalDate startDate, LocalDate endDate) {
QueryWrapper<DailyMarketData> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("ts_code", tsCode);
if (startDate != null) {
queryWrapper.ge("trade_date", startDate);
}
if (endDate != null) {
queryWrapper.le("trade_date", endDate);
}
queryWrapper.orderByAsc("trade_date");
return list(queryWrapper);
}
}
@@ -0,0 +1,140 @@
package com.trade.marketdata.service.impl;
import com.trade.marketdata.entity.RealtimeMarketData;
import com.trade.marketdata.mapper.RealtimeMarketDataMapper;
import com.trade.marketdata.service.RealtimeMarketDataService;
import com.trade.marketdata.util.TushareApi;
import com.trade.marketdata.util.TushareRequest;
import com.trade.marketdata.util.TushareResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 股票实时行情数据 Service 实现类
*
* @author Trae
* @since 2024-07-26
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RealtimeMarketDataServiceImpl implements RealtimeMarketDataService {
private final RealtimeMarketDataMapper realtimeMarketDataMapper;
private final TushareApi tushareApi;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/**
* 获取指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔
* @return 实时行情数据列表
*/
@Override
public List<RealtimeMarketData> getRealtimeMarketData(String tsCodes) {
TushareRequest<Map<String, String>> request = new TushareRequest<>();
request.setApiName("realtime_quotes"); // Tushare 实时行情接口名称,请根据实际情况调整
Map<String, String> params = new HashMap<>();
params.put("ts_code", tsCodes);
request.setParams(params);
TushareResponse<List<List<Object>>> response = tushareApi.call(request, List.class, List.class, Object.class);
List<RealtimeMarketData> resultList = new ArrayList<>();
if (response != null && response.getData() != null && response.getData().getItems() != null) {
List<String> fields = response.getData().getFields();
List<List<Object>> items = response.getData().getItems();
for (List<Object> item : items) {
RealtimeMarketData data = new RealtimeMarketData();
for (int i = 0; i < fields.size(); i++) {
String fieldName = fields.get(i);
Object value = item.get(i);
if (value == null) continue;
switch (fieldName) {
case "ts_code":
data.setTsCode(String.valueOf(value));
break;
case "name":
data.setName(String.valueOf(value));
break;
case "price":
data.setPrice(new BigDecimal(String.valueOf(value)));
break;
case "change":
data.setChange(new BigDecimal(String.valueOf(value)));
break;
case "pct_chg": // Tushare返回的字段名可能为 pct_chg
case "pct_change":
data.setPctChange(new BigDecimal(String.valueOf(value)));
break;
case "volume":
data.setVolume(Long.parseLong(String.valueOf(value)));
break;
case "amount":
data.setAmount(new BigDecimal(String.valueOf(value)));
break;
case "open":
data.setOpen(new BigDecimal(String.valueOf(value)));
break;
case "pre_close":
data.setPreClose(new BigDecimal(String.valueOf(value)));
break;
case "high":
data.setHigh(new BigDecimal(String.valueOf(value)));
break;
case "low":
data.setLow(new BigDecimal(String.valueOf(value)));
break;
case "time": // Tushare返回的时间字段名可能为 time
// 假设Tushare返回的时间格式是 yyyyMMddHHmmss
// 如果是其他格式,需要调整 DateTimeFormatter
// 如果Tushare直接返回的是 HH:mm:ss 格式,需要结合当前日期进行转换
// 这里假设返回的是包含日期的完整时间字符串
try {
data.setTimestamp(LocalDateTime.parse(String.valueOf(value), FORMATTER));
} catch (Exception e) {
log.warn("Failed to parse timestamp: {} for ts_code: {}. Error: {}", value, data.getTsCode(), e.getMessage());
// 可以设置一个默认值或者根据业务需求处理
data.setTimestamp(LocalDateTime.now());
}
break;
default:
break;
}
}
resultList.add(data);
}
}
return resultList;
}
/**
* 保存实时行情数据列表
*
* @param realtimeMarketDataList 实时行情数据列表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveRealtimeMarketData(List<RealtimeMarketData> realtimeMarketDataList) {
if (realtimeMarketDataList == null || realtimeMarketDataList.isEmpty()) {
return;
}
// 实际应用中,可能需要根据 ts_code 和 timestamp 判断数据是否已存在,进行更新或插入操作
// 这里简化为直接批量插入
realtimeMarketDataList.forEach(realtimeMarketDataMapper::insert);
log.info("Successfully saved {} realtime market data records.", realtimeMarketDataList.size());
}
}
@@ -0,0 +1,150 @@
package com.trade.marketdata.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.marketdata.entity.StockBasic;
import com.trade.marketdata.entity.TushareResponse;
import com.trade.marketdata.mapper.StockBasicMapper;
import com.trade.marketdata.service.StockBasicService;
import com.trade.marketdata.service.TushareApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息服务实现类
*/
@Service
@Slf4j
public class StockBasicServiceImpl extends ServiceImpl<StockBasicMapper, StockBasic> implements StockBasicService {
private final TushareApi tushareApi;
public StockBasicServiceImpl(TushareApi tushareApi) {
this.tushareApi = tushareApi;
}
/**
* 从 Tushare 同步股票基本信息
* @return 同步的股票数量
*/
@Override
@Transactional
public int syncStockBasicFromTushare() {
log.info("开始从 Tushare 同步股票基本信息...");
Map<String, String> params = new HashMap<>();
params.put("exchange", ""); // 交易所代码,可选,空表示获取所有
params.put("list_status", "L"); // 上市状态 L上市 D退市 P暂停上市
String fields = "ts_code,symbol,name,area,province,city,industry,fullname,enname,market,exchange,curr_type,list_status,list_date,delist_date,is_hs";
TushareResponse response = tushareApi.post("stock_basic", params, fields).block();
if (response == null || response.getCode() != 0 || response.getData() == null) {
log.error("从 Tushare 获取股票基本信息失败: {}", response != null ? response.getMsg() : "未知错误");
return 0;
}
List<List<Object>> items = response.getData().getItems();
List<String> fieldsList = response.getData().getFields();
if (items == null || items.isEmpty()) {
log.warn("从 Tushare 获取到空股票基本信息列表。");
return 0;
}
List<StockBasic> newStockBasics = new ArrayList<>();
for (List<Object> item : items) {
StockBasic stockBasic = new StockBasic();
for (int i = 0; i < fieldsList.size(); i++) {
String fieldName = fieldsList.get(i);
Object value = item.get(i);
if (value == null) continue;
switch (fieldName) {
case "ts_code": stockBasic.setTsCode(value.toString()); break;
case "symbol": stockBasic.setSymbol(value.toString()); break;
case "name": stockBasic.setName(value.toString()); break;
case "area": stockBasic.setArea(value.toString()); break;
case "province": stockBasic.setProvince(value.toString()); break;
case "city": stockBasic.setCity(value.toString()); break;
case "industry": stockBasic.setIndustry(value.toString()); break;
case "fullname": stockBasic.setFullname(value.toString()); break;
case "enname": stockBasic.setEnname(value.toString()); break;
case "market": stockBasic.setMarket(value.toString()); break;
case "exchange": stockBasic.setExchange(value.toString()); break;
case "curr_type": stockBasic.setCurrType(value.toString()); break;
case "list_status": stockBasic.setListStatus(value.toString()); break;
case "list_date": stockBasic.setListDate(LocalDate.parse(value.toString())); break;
case "delist_date": stockBasic.setDelistDate(value.toString().isEmpty() ? null : LocalDate.parse(value.toString())); break;
case "is_hs": stockBasic.setIsHs(value.toString()); break;
}
}
stockBasic.setCreateTime(LocalDateTime.now());
stockBasic.setUpdateTime(LocalDateTime.now());
newStockBasics.add(stockBasic);
}
// 批量插入或更新
// 考虑到数据量可能较大,且需要判断是否已存在,这里可以先查询现有数据,然后进行区分插入和更新
// 简化处理:先删除所有现有数据,再批量插入新数据 (适用于数据量不大,且更新频率不高的场景)
// 更优方案:根据 ts_code 判断是否存在,存在则更新,不存在则插入
// 获取当前数据库中所有股票的 ts_code 集合
List<String> existingTsCodes = baseMapper.selectList(null).stream()
.map(StockBasic::getTsCode)
.collect(Collectors.toList());
List<StockBasic> toInsert = new ArrayList<>();
List<StockBasic> toUpdate = new ArrayList<>();
for (StockBasic stock : newStockBasics) {
if (existingTsCodes.contains(stock.getTsCode())) {
// 查找现有记录的ID,用于更新
StockBasic existingStock = baseMapper.selectOne(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.<StockBasic>lambdaQuery().eq(StockBasic::getTsCode, stock.getTsCode()));
if (existingStock != null) {
stock.setId(existingStock.getId());
toUpdate.add(stock);
}
} else {
toInsert.add(stock);
}
}
int insertedCount = 0;
if (!toInsert.isEmpty()) {
saveBatch(toInsert);
insertedCount = toInsert.size();
log.info("成功插入 {} 条新的股票基本信息。".formatted(insertedCount));
}
int updatedCount = 0;
if (!toUpdate.isEmpty()) {
updateBatchById(toUpdate);
updatedCount = toUpdate.size();
log.info("成功更新 {} 条股票基本信息。".formatted(updatedCount));
}
log.info("股票基本信息同步完成,总计插入 {} 条,更新 {} 条。".formatted(insertedCount, updatedCount));
return insertedCount + updatedCount;
}
/**
* 查询所有股票基本信息
* @return 股票基本信息列表
*/
@Override
public List<StockBasic> listAllStockBasic() {
return list();
}
}
@@ -0,0 +1,4 @@
/**
* 服务实现层
*/
package com.trade.marketdata.service.impl;
@@ -0,0 +1,4 @@
/**
* 服务接口层
*/
package com.trade.marketdata.service;
@@ -0,0 +1,75 @@
package com.stock.marketdata.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.stock.common.pojo.CommonResult;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.stock.marketdata.service.MarketDataService;
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.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 行情数据 Controller
*
* @author Pure AI
*/
@Tag(name = "行情数据接口")
@RestController
@RequestMapping("/market-data")
public class MarketDataController {
@Resource
private MarketDataService marketDataService;
@PostMapping
@Operation(summary = "创建行情数据")
public CommonResult<Long> createMarketData(@RequestBody MarketDataDO marketData) {
return CommonResult.success(marketDataService.createMarketData(marketData));
}
@PutMapping
@Operation(summary = "更新行情数据")
public CommonResult<Boolean> updateMarketData(@RequestBody MarketDataDO marketData) {
marketDataService.updateMarketData(marketData);
return CommonResult.success(true);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除行情数据")
@Parameter(name = "id", description = "行情数据编号", required = true, example = "1024")
public CommonResult<Boolean> deleteMarketData(@PathVariable("id") Long id) {
marketDataService.deleteMarketData(id);
return CommonResult.success(true);
}
@GetMapping("/{id}")
@Operation(summary = "获取行情数据")
@Parameter(name = "id", description = "行情数据编号", required = true, example = "1024")
public CommonResult<MarketDataDO> getMarketData(@PathVariable("id") Long id) {
return CommonResult.success(marketDataService.getMarketData(id));
}
@GetMapping("/list")
@Operation(summary = "获取行情数据列表")
@Parameter(name = "stockCode", description = "股票代码", example = "000001")
public CommonResult<List<MarketDataDO>> getMarketDataList(@RequestParam(required = false) String stockCode) {
return CommonResult.success(marketDataService.getMarketDataList(stockCode));
}
@GetMapping("/page")
@Operation(summary = "分页查询行情数据")
public CommonResult<Page<MarketDataDO>> pageMarketData(
@Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页条数", example = "10") @RequestParam(defaultValue = "10") Integer pageSize,
@Parameter(description = "股票代码", example = "000001") @RequestParam(required = false) String stockCode) {
Page<MarketDataDO> page = new Page<>(pageNum, pageSize);
return CommonResult.success(marketDataService.pageMarketData(page, stockCode));
}
// TODO: 添加其他接口
}
@@ -0,0 +1,53 @@
package com.stock.marketdata.dal.dataobject;
import com.stock.common.dal.dataobject.BaseDO; // 引入公共BaseDO
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* 行情数据DO
*
* @author TraeAI
*/
@TableName("market_data") // TODO: 确认表名是否正确
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MarketDataDO extends BaseDO {
/**
* ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 股票代码
*/
private String stockCode;
/**
* 股票名称
*/
private String stockName;
/**
* 最新价格
*/
private Double latestPrice;
/**
* 更新时间
*/
private LocalDateTime updateTime;
// TODO: 根据实际需求添加更多字段
}
@@ -0,0 +1,17 @@
package com.stock.marketdata.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 行情数据 Mapper
*
* @author Pure AI
*/
@Mapper
public interface MarketDataMapper extends BaseMapper<MarketDataDO> {
// TODO: 定义自定义的 SQL 查询方法
}
@@ -0,0 +1,15 @@
package com.stock.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 行情数据 Mapper
*
* @author TraeAI
*/
@Mapper
public interface MarketDataMapper extends BaseMapper<MarketDataDO> {
// TODO: 定义行情数据相关的数据库操作方法
}
@@ -0,0 +1,64 @@
package com.stock.marketdata.service;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
/**
* 行情数据服务接口
*
* @author Pure AI
*/
public interface MarketDataService {
/**
* 创建行情数据
*
* @param marketData 行情数据对象
* @return 创建的行情数据ID
*/
Long createMarketData(MarketDataDO marketData);
/**
* 更新行情数据
*
* @param marketData 行情数据对象
*/
void updateMarketData(MarketDataDO marketData);
/**
* 删除行情数据
*
* @param id 行情数据ID
*/
void deleteMarketData(Long id);
/**
* 获取行情数据
*
* @param id 行情数据ID
* @return 行情数据对象
*/
MarketDataDO getMarketData(Long id);
/**
* 获取行情数据列表
*
* @param stockCode 股票代码
* @return 行情数据列表
*/
List<MarketDataDO> getMarketDataList(String stockCode);
/**
* 分页查询行情数据
*
* @param page 分页参数
* @param stockCode 股票代码
* @return 分页结果
*/
Page<MarketDataDO> pageMarketData(Page<MarketDataDO> page, String stockCode);
// TODO: 添加其他业务方法
}
@@ -0,0 +1,105 @@
package com.stock.marketdata.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.stock.marketdata.dal.mapper.MarketDataMapper;
import com.stock.marketdata.service.MarketDataService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;
/**
* 行情数据服务实现类
*
* @author Pure AI
*/
@Service
public class MarketDataServiceImpl implements MarketDataService {
@Resource
private MarketDataMapper marketDataMapper;
/**
* 创建行情数据
*
* @param marketData 行情数据对象
* @return 创建的行情数据ID
*/
@Override
public Long createMarketData(MarketDataDO marketData) {
marketDataMapper.insert(marketData);
return marketData.getId();
}
/**
* 更新行情数据
*
* @param marketData 行情数据对象
*/
@Override
public void updateMarketData(MarketDataDO marketData) {
marketDataMapper.updateById(marketData);
}
/**
* 删除行情数据
*
* @param id 行情数据ID
*/
@Override
public void deleteMarketData(Long id) {
marketDataMapper.deleteById(id);
}
/**
* 获取行情数据
*
* @param id 行情数据ID
* @return 行情数据对象
*/
@Override
public MarketDataDO getMarketData(Long id) {
return marketDataMapper.selectById(id);
}
/**
* 获取行情数据列表
*
* @param stockCode 股票代码
* @return 行情数据列表
*/
@Override
public List<MarketDataDO> getMarketDataList(String stockCode) {
LambdaQueryWrapper<MarketDataDO> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(stockCode)) {
queryWrapper.eq(MarketDataDO::getStockCode, stockCode);
}
// TODO: 根据业务需求添加其他查询条件,例如时间范围等
queryWrapper.orderByDesc(MarketDataDO::getTradingDay); // 默认按交易日降序
return marketDataMapper.selectList(queryWrapper);
}
/**
* 分页查询行情数据
*
* @param page 分页参数
* @param stockCode 股票代码
* @return 分页结果
*/
@Override
public Page<MarketDataDO> pageMarketData(Page<MarketDataDO> page, String stockCode) {
LambdaQueryWrapper<MarketDataDO> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(stockCode)) {
queryWrapper.eq(MarketDataDO::getStockCode, stockCode);
}
// TODO: 根据业务需求添加其他查询条件,例如时间范围等
queryWrapper.orderByDesc(MarketDataDO::getTradingDay); // 默认按交易日降序
return marketDataMapper.selectPage(page, queryWrapper);
}
// TODO: 实现其他业务方法
}
@@ -0,0 +1,11 @@
server:
port: 8004
spring:
profiles:
active: dev
application:
name: trade-market-data
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8004
spring:
profiles:
active: prod
application:
name: trade-market-data
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8004
spring:
profiles:
active: test
application:
name: trade-market-data
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,72 @@
server:
port: 8081 # 服务端口,可以根据实际情况修改
spring:
application:
name: trade-market-data # 应用名称
# datasource: # 数据库配置,可以从 yudao-cloud-mini 的公共配置中获取或在此处覆盖
# url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
# username: your_username
# password: your_password
# driver-class-name: com.mysql.cj.jdbc.Driver
# redis: # Redis 配置,可以从 yudao-cloud-mini 的公共配置中获取或在此处覆盖
# host: localhost
# port: 6379
# password:
# database: 0
# Tushare API 配置
tushare:
api-url: http://api.tushare.pro
token: "YOUR_TUSHARE_TOKEN" # 请替换为您的 Tushare Token
# Mybatis Plus 配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml # Mapper XML 文件路径
#type-aliases-package: com.trade.marketdata.entity # 实体类别名包路径,如果需要的话
global-config:
db-config:
id-type: auto # ID 生成策略
# table-prefix: t_ # 表前缀,如果需要的话
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志,生产环境建议关闭或使用更完善的日志方案
# SpringDoc OpenAPI 配置 (Swagger)
springdoc:
api-docs:
path: /v3/api-docs # API 文档路径
swagger-ui:
path: /swagger-ui.html # Swagger UI 路径
enabled: true # 开启 Swagger UI
group-configs:
- group: default
paths-to-match: /**
packages-to-scan: com.trade.marketdata.controller # Controller 包路径
# Nacos 配置,用于服务注册与发现
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# namespace: # Nacos 命名空间,如果需要的话
# group: # Nacos 分组,如果需要的话
# config:
# server-addr: ${spring.cloud.nacos.discovery.server-addr}
# file-extension: yaml # 配置文件格式
# namespace: ${spring.cloud.nacos.discovery.namespace}
# group: ${spring.cloud.nacos.discovery.group}
# shared-configs[0]: # 共享配置
# data-id: application-common.yaml
# group: DEFAULT_GROUP
# refresh: true
# 日志配置 (可选,Spring Boot 默认使用 Logback)
logging:
level:
com.trade.marketdata: DEBUG # 设置项目包的日志级别
# org.springframework: INFO
# org.apache.ibatis: DEBUG # 如果需要查看 MyBatis 执行的 SQL
# file:
# name: ./logs/trade-market-data.log # 日志文件路径
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="base-logback.xml"/>
<logger name="com.trade.marketdata" level="debug"/>
</configuration>
@@ -0,0 +1,134 @@
package com.stock.marketdata.controller;
import com.stock.marketdata.service.MarketDataService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.stock.common.pojo.CommonResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(MarketDataController.class)
class MarketDataControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MarketDataService marketDataService;
@Autowired
private ObjectMapper objectMapper;
private MarketDataDO createMarketDataDO() {
MarketDataDO marketData = new MarketDataDO();
marketData.setId(1L);
marketData.setStockCode("000001");
marketData.setStockName("平安银行");
marketData.setOpenPrice(BigDecimal.valueOf(10.00));
marketData.setClosePrice(BigDecimal.valueOf(10.50));
marketData.setHighPrice(BigDecimal.valueOf(10.60));
marketData.setLowPrice(BigDecimal.valueOf(9.90));
marketData.setVolume(10000L);
marketData.setTurnover(BigDecimal.valueOf(105000.00));
marketData.setTradeTime(LocalDateTime.now());
return marketData;
}
@Test
void createMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
when(marketDataService.createMarketData(any(MarketDataDO.class))).thenReturn(1L);
mockMvc.perform(post("/market-data")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(marketData)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(1L));
}
@Test
void updateMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
doNothing().when(marketDataService).updateMarketData(any(MarketDataDO.class));
mockMvc.perform(put("/market-data")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(marketData)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(true));
}
@Test
void deleteMarketData() throws Exception {
doNothing().when(marketDataService).deleteMarketData(anyLong());
mockMvc.perform(delete("/market-data/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(true));
}
@Test
void getMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
when(marketDataService.getMarketData(anyLong())).thenReturn(marketData);
mockMvc.perform(get("/market-data/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.id").value(1L))
.andExpect(jsonPath("$.data.stockCode").value("000001"));
}
@Test
void getMarketDataList() throws Exception {
MarketDataDO marketData = createMarketDataDO();
List<MarketDataDO> list = Collections.singletonList(marketData);
when(marketDataService.getMarketDataList(anyString())).thenReturn(list);
mockMvc.perform(get("/market-data/list").param("stockCode", "000001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data[0].id").value(1L))
.andExpect(jsonPath("$.data[0].stockCode").value("000001"));
}
@Test
void pageMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
Page<MarketDataDO> page = new Page<>(1, 10);
page.setRecords(Collections.singletonList(marketData));
page.setTotal(1L);
when(marketDataService.pageMarketData(any(Page.class), anyString())).thenReturn(page);
mockMvc.perform(get("/market-data/page")
.param("pageNum", "1")
.param("pageSize", "10")
.param("stockCode", "000001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.records[0].id").value(1L))
.andExpect(jsonPath("$.data.total").value(1L));
}
}
+104
View File
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-mbg</artifactId>
<packaging>jar</packaging>
<properties>
<mysql.version>8.0.33</mysql.version>
</properties>
<name>trade-mbg</name>
<description>MyBatis Generator module for Trade Platform</description>
<dependencies>
<!-- MyBatis Generator Core -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version> <!-- 请使用最新稳定版本 -->
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter JDBC (for properties resolution) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Swagger Annotations (for CommentGenerator) -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.20</version> <!-- 请使用与SpringDoc兼容的版本 -->
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version> <!-- 与mybatis-generator-core版本一致 -->
<configuration>
<!-- MyBatis Generator 配置文件的位置 -->
<configurationFile>${project.basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<!-- 是否覆盖已生成的文件 -->
<overwrite>true</overwrite>
<!-- 是否在控制台输出详细信息 -->
<verbose>true</verbose>
<!--
允许移动生成的文件。默认情况下,插件会把生成的文件放到一个特定的目录中,
如果设置为true,那么插件会把这些文件移动到 <configurationFile> 中指定的目录。
这对于将生成的文件直接集成到项目中非常有用。
-->
<includeCompileDependencies>true</includeCompileDependencies>
</configuration>
<dependencies>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- 自定义的 CommentGenerator -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-mbg</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,205 @@
package com.trade.mbg.util;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.CompilationUnit;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.InnerClass;
import org.mybatis.generator.api.dom.java.InnerEnum;
import org.mybatis.generator.api.dom.java.JavaElement;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.Parameter;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.config.MergeConstants;
import org.mybatis.generator.config.PropertyRegistry;
import org.mybatis.generator.internal.DefaultCommentGenerator;
import org.mybatis.generator.internal.util.StringUtility;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.Set;
/**
* Custom Comment Generator for MyBatis Generator.
* It adds @ApiModelProperty annotations for Swagger and uses Lombok annotations.
*/
public class CommentGenerator extends DefaultCommentGenerator {
private boolean suppressDate;
private boolean suppressAllComments;
private boolean addRemarkComments;
private SimpleDateFormat dateFormat;
private boolean useLombok;
private boolean useSwagger;
public CommentGenerator() {
super();
suppressDate = false;
suppressAllComments = false;
addRemarkComments = false;
useLombok = true; // Enable Lombok by default
useSwagger = true; // Enable Swagger by default
}
@Override
public void addJavaFileComment(CompilationUnit compilationUnit) {
// add no file level comments
if (suppressAllComments) {
return;
}
if (useLombok) {
compilationUnit.addImportedType(new FullyQualifiedJavaType("lombok.Data"));
compilationUnit.addImportedType(new FullyQualifiedJavaType("lombok.Builder"));
compilationUnit.addImportedType(new FullyQualifiedJavaType("lombok.NoArgsConstructor"));
compilationUnit.addImportedType(new FullyQualifiedJavaType("lombok.AllArgsConstructor"));
}
if (useSwagger) {
compilationUnit.addImportedType(new FullyQualifiedJavaType("io.swagger.v3.oas.annotations.media.Schema"));
}
}
@Override
public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
if (suppressAllComments || !addRemarkComments) {
return;
}
topLevelClass.addJavaDocLine("/**");
String remarks = introspectedTable.getRemarks();
if (addRemarkComments && StringUtility.stringHasValue(remarks)) {
topLevelClass.addJavaDocLine(" * " + remarks);
topLevelClass.addJavaDocLine(" *");
}
topLevelClass.addJavaDocLine(" * " + introspectedTable.getFullyQualifiedTable());
if (!suppressDate) {
topLevelClass.addJavaDocLine(" * @date " + getDateString());
}
topLevelClass.addJavaDocLine(" */");
if (useLombok) {
topLevelClass.addAnnotation("@Data");
topLevelClass.addAnnotation("@Builder");
topLevelClass.addAnnotation("@NoArgsConstructor");
topLevelClass.addAnnotation("@AllArgsConstructor");
}
if (useSwagger) {
if (StringUtility.stringHasValue(remarks)) {
topLevelClass.addAnnotation("@Schema(description = \"" + remarks + "\")");
} else {
topLevelClass.addAnnotation("@Schema(description = \"" + introspectedTable.getFullyQualifiedTable().getDomainObjectName() + "\")");
}
}
}
@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
if (suppressAllComments) {
return;
}
String remarks = introspectedColumn.getRemarks();
if (addRemarkComments && StringUtility.stringHasValue(remarks)) {
field.addJavaDocLine("/**");
field.addJavaDocLine(" * " + remarks);
field.addJavaDocLine(" */");
if (useSwagger) {
field.addAnnotation("@Schema(description = \"" + remarks + "\")");
}
} else if (useSwagger) {
field.addAnnotation("@Schema(description = \"" + introspectedColumn.getActualColumnName() + "\")");
}
}
@Override
public void addGetterComment(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
// Lombok will generate getters, so no need to add comments here if Lombok is enabled.
if (useLombok || suppressAllComments) {
return;
}
super.addGetterComment(method, introspectedTable, introspectedColumn);
}
@Override
public void addSetterComment(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
// Lombok will generate setters, so no need to add comments here if Lombok is enabled.
if (useLombok || suppressAllComments) {
return;
}
super.addSetterComment(method, introspectedTable, introspectedColumn);
}
@Override
public void addConfigurationProperties(Properties properties) {
super.addConfigurationProperties(properties);
suppressDate = StringUtility.isTrue(properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_DATE));
suppressAllComments = StringUtility.isTrue(properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_ALL_COMMENTS));
addRemarkComments = StringUtility.isTrue(properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_ADD_REMARK_COMMENTS));
useLombok = StringUtility.isTrue(properties.getProperty("useLombok", "true")); // Default to true
useSwagger = StringUtility.isTrue(properties.getProperty("useSwagger", "true")); // Default to true
String dateFormatString = properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_DATE_FORMAT);
if (StringUtility.stringHasValue(dateFormatString)) {
dateFormat = new SimpleDateFormat(dateFormatString);
}
}
@Override
protected String getDateString() {
if (suppressDate) {
return null;
}
if (dateFormat != null) {
return dateFormat.format(new Date());
}
return new Date().toString();
}
@Override
public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable) {
if (suppressAllComments) {
return;
}
super.addClassComment(innerClass, introspectedTable);
}
@Override
public void addEnumComment(InnerEnum innerEnum, IntrospectedTable introspectedTable) {
if (suppressAllComments) {
return;
}
super.addEnumComment(innerEnum, introspectedTable);
}
@Override
public void addFieldComment(Field field, IntrospectedTable introspectedTable) {
if (suppressAllComments) {
return;
}
super.addFieldComment(field, introspectedTable);
}
@Override
public void addGeneralMethodComment(Method method, IntrospectedTable introspectedTable) {
if (suppressAllComments) {
return;
}
super.addGeneralMethodComment(method, introspectedTable);
}
@Override
public void addRootComment(XmlElement rootElement) {
// add no xml comments
}
@Override
public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable, boolean markAsDoNotDelete) {
if (suppressAllComments) {
return;
}
super.addClassComment(innerClass, introspectedTable, markAsDoNotDelete);
}
}
@@ -0,0 +1,5 @@
# DataSource Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/trade_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=wodiXIAO1988
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 引入数据库连接配置 -->
<properties resource="application-dev.properties"/>
<!--
context:生成一组对象的环境
id:必选,上下文id,用于在生成错误时提示
defaultModelType:指定生成对象的类型,可选的值为:
conditional: 这是默认值,这个模型会产出最少字段的POJO,只有当表包含多个BLOB字段时,才会单独生成一个BLOB类。
flat: 生成的POJO包含表中的所有字段,即一个表对应一个POJO类。
hierarchical: 如果表有主键,那么该模型会产生一个单独的主键类, 如果表还有BLOB字段, 则会为表生成一个包含所有BLOB字段的POJO类,然后POJO会继承主键类,
如果表没有BLOB字段,只有主键,那么POJO会继承主键类,如果表既没有主键,也没有BLOB字段,那么和flat一样。
targetRuntime: MyBatis3DynamicSql, MyBatis3, MyBatis3Simple (推荐使用MyBatis3)
introspectedColumnImpl: 指定扩展Column类,可选
-->
<context id="MySQLContext" defaultModelType="flat" targetRuntime="MyBatis3Simple">
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressTypeWarnings" value="true"/>
<!-- optional,旨在创建class时,对注释进行控制 -->
<commentGenerator type="com.trade.mbg.util.CommentGenerator">
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true"/>
<property name="suppressDate" value="true"/>
<property name="addRemarkComments" value="true"/>
</commentGenerator>
<!-- JDBC连接 -->
<jdbcConnection driverClass="${spring.datasource.driver-class-name}"
connectionURL="${spring.datasource.url}"
userId="${spring.datasource.username}"
password="${spring.datasource.password}">
<!-- 解决mysql驱动升级到8.0后不生成指定数据库代码的问题 -->
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>
<!--
默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer
true,把JDBC DECIMAL 和 NUMERIC 类型解析为 java.math.BigDecimal
-->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
<property name="useJSR310Types" value="true"/> <!-- 使用Java 8时间类型 -->
</javaTypeResolver>
<!-- 生成Domain模型的包名和位置 -->
<javaModelGenerator targetPackage="com.trade.mbg.model" targetProject="${user.dir}/trade-mbg/src/main/java">
<!-- 是否让schema作为包的后缀,默认为false -->
<property name="enableSubPackages" value="true"/>
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成Mapper XML文件的包名和位置 -->
<sqlMapGenerator targetPackage="com.trade.mbg.mapper" targetProject="${user.dir}/trade-mbg/src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 生成Mapper接口的包名和位置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.trade.mbg.mapper" targetProject="${user.dir}/trade-mbg/src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--
要生成的表
tableName: 表名
domainObjectName: 生成的Domain对象的名称,不配置时根据表名自动生成驼峰命名
enableCountByExample: 是否生成通过Example统计记录数的SQL,默认为true
enableUpdateByExample: 是否生成通过Example更新记录的SQL(Selective表示仅更新非空字段),默认为true
enableDeleteByExample: 是否生成通过Example删除记录的SQL,默认为true
enableSelectByExample: 是否生成通过Example查询记录的SQL,默认为true
selectByExampleQueryId: 指定查询语句的ID,默认为false
modelType: 指定此表的模型类型,可选conditional, flat, hierarchical。会覆盖context的defaultModelType
-->
<!-- 示例表,请根据实际情况修改或添加 -->
<!-- <table tableName="user" domainObjectName="User"/> -->
<!-- <table tableName="product" domainObjectName="Product"/> -->
<!-- 如果要生成所有表,可以使用 % -->
<!-- <table tableName="%">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table> -->
<!-- 股票信息表 (新增) -->
<table tableName="t_stock_info" domainObjectName="StockInfo">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="listing_date" javaType="java.time.LocalDate"/>
<columnOverride column="delisting_date" javaType="java.time.LocalDate"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
</table>
<!-- =========================================================================================== -->
<!-- ================================= 以下为具体业务表的配置 ================================= -->
<!-- =========================================================================================== -->
<!-- 用户表 (trade_user 模块) -->
<table tableName="ums_admin" domainObjectName="UmsAdmin">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_role" domainObjectName="Role">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<table tableName="t_permission" domainObjectName="Permission">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<table tableName="t_user_role_relation" domainObjectName="UserRoleRelation"/>
<table tableName="t_role_permission_relation" domainObjectName="RolePermissionRelation"/>
<!-- 行情数据表 (trade_market_data 模块) -->
<table tableName="t_market_data_kline" domainObjectName="MarketDataKline">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="timestamp" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_market_data_tick" domainObjectName="MarketDataTick">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="timestamp" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_symbol_info" domainObjectName="SymbolInfo">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<!-- 技术指标表 (trade_indicator 模块) -->
<table tableName="t_technical_indicator" domainObjectName="TechnicalIndicator">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="timestamp" javaType="java.time.LocalDateTime"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
</table>
<!-- 交易策略表 (trade_strategy 模块) -->
<table tableName="t_strategy" domainObjectName="Strategy">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_strategy_param" domainObjectName="StrategyParam">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<!-- 订单表 (trade_order 模块) -->
<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_trade_record" domainObjectName="TradeRecord">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="trade_time" javaType="java.time.LocalDateTime"/>
</table>
<!-- 风险管理表 (trade_risk 模块) -->
<table tableName="t_risk_rule" domainObjectName="RiskRule">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_position_limit" domainObjectName="PositionLimit">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<!-- 回测记录表 (trade_backtest 模块) -->
<table tableName="t_backtest_record" domainObjectName="BacktestRecord">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="start_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="end_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="t_backtest_trade_log" domainObjectName="BacktestTradeLog">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="trade_time" javaType="java.time.LocalDateTime"/>
</table>
<!-- 通知记录表 (trade_notification 模块) -->
<table tableName="t_notification_log" domainObjectName="NotificationLog">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="send_time" javaType="java.time.LocalDateTime"/>
</table>
</context>
</generatorConfiguration>
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-notification</artifactId>
<packaging>jar</packaging>
<name>trade-notification</name>
<description>Notification service module for the trading system</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- trade-common -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Starter Mail (for email notifications) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter AMQP (Optional, if using RabbitMQ for async notifications) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Cloud Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringDoc OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Add other notification channel dependencies as needed (e.g., SMS, WebSocket) -->
<!-- Example for WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,17 @@
package com.trade.notification;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 通知服务启动类
* Created by macro on 2020/8/3.
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeNotificationApplication {
public static void main(String[] args) {
SpringApplication.run(TradeNotificationApplication.class, args);
}
}
@@ -0,0 +1,11 @@
server:
port: 8005
spring:
profiles:
active: dev
application:
name: trade-notification
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8005
spring:
profiles:
active: prod
application:
name: trade-notification
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8005
spring:
profiles:
active: test
application:
name: trade-notification
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="base-logback.xml"/>
<logger name="com.trade.notification" level="debug"/>
</configuration>
+100
View File
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.stock</groupId>
<artifactId>trade-order</artifactId>
<name>${project.artifactId}</name>
<description>订单服务,负责订单的创建、撮合、状态管理等</description>
<dependencies>
<!-- 项目内部依赖 -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 持久层相关依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 工具类相关依赖 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- API 文档相关依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<!-- 参数校验相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 工具库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,18 @@
package com.stock.trade.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 订单服务启动类
*
* @author tianxin
*/
@SpringBootApplication(scanBasePackages = {"com.stock.trade"})
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
@@ -0,0 +1,84 @@
package com.stock.trade.order.controller;
import com.stock.trade.common.core.domain.CommonResult;
import com.stock.trade.common.core.domain.PageResult;
import com.stock.trade.order.controller.request.OrderCreateReqVO;
import com.stock.trade.order.controller.request.OrderPageReqVO;
import com.stock.trade.order.controller.request.OrderUpdateReqVO;
import com.stock.trade.order.controller.response.OrderRespVO;
import com.stock.trade.order.convert.OrderConvert;
import com.stock.trade.order.dal.dataobject.OrderDO;
import com.stock.trade.order.service.OrderService;
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 static com.stock.trade.common.core.domain.CommonResult.success;
@Tag(name = "管理后台 - 订单")
@RestController
@RequestMapping("/trade/order")
@Validated
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/create")
@Operation(summary = "创建订单")
@PreAuthorize("@ss.hasPermission('trade:order:create')")
public CommonResult<Long> createOrder(@Valid @RequestBody OrderCreateReqVO createReqVO) {
return success(orderService.createOrder(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新订单")
@PreAuthorize("@ss.hasPermission('trade:order:update')")
public CommonResult<Boolean> updateOrder(@Valid @RequestBody OrderUpdateReqVO updateReqVO) {
orderService.updateOrder(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除订单")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('trade:order:delete')")
public CommonResult<Boolean> deleteOrder(@RequestParam("id") Long id) {
orderService.deleteOrder(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得订单")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('trade:order:query')")
public CommonResult<OrderRespVO> getOrder(@RequestParam("id") Long id) {
OrderDO order = orderService.getOrder(id);
return success(OrderConvert.INSTANCE.convert(order));
}
@GetMapping("/page")
@Operation(summary = "获得订单分页")
@PreAuthorize("@ss.hasPermission('trade:order:query')")
public CommonResult<PageResult<OrderRespVO>> getOrderPage(@Valid OrderPageReqVO pageVO) {
PageResult<OrderDO> pageResult = orderService.getOrderPage(pageVO);
return success(OrderConvert.INSTANCE.convertPage(pageResult));
}
@PostMapping("/cancel")
@Operation(summary = "撤销订单")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('trade:order:cancel')")
public CommonResult<Boolean> cancelOrder(@RequestParam("id") Long id,
@RequestParam("userId") Long userId) {
orderService.cancelOrder(id, userId);
return success(true);
}
}
@@ -0,0 +1,46 @@
package com.stock.trade.order.controller.request;
import com.stock.trade.order.enums.OrderDirectionEnum;
import com.stock.trade.order.enums.OrderTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 订单创建 Request VO")
@Data
public class OrderCreateReqVO {
@Schema(description = "用户编号", required = true, example = "1024")
@NotNull(message = "用户编号不能为空")
private Long userId;
@Schema(description = "股票代码", required = true, example = "600000")
@NotEmpty(message = "股票代码不能为空")
private String stockCode;
@Schema(description = "股票名称", required = true, example = "浦发银行")
@NotEmpty(message = "股票名称不能为空")
private String stockName;
@Schema(description = "订单类型,参见 OrderTypeEnum 枚举", required = true, example = "0")
@NotNull(message = "订单类型不能为空")
private Integer type;
@Schema(description = "订单方向,参见 OrderDirectionEnum 枚举", required = true, example = "0")
@NotNull(message = "订单方向不能为空")
private Integer direction;
@Schema(description = "订单价格,市价单可为空", example = "10.24")
private BigDecimal price;
@Schema(description = "订单数量", required = true, example = "100")
@NotNull(message = "订单数量不能为空")
private Integer quantity;
@Schema(description = "备注", example = "测试订单")
private String remark;
}
@@ -0,0 +1,30 @@
package com.stock.trade.order.controller.request;
import com.stock.trade.common.core.dal.qo.PageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 订单分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class OrderPageReqVO extends PageQuery {
@Schema(description = "用户编号", example = "1024")
private Long userId;
@Schema(description = "股票代码", example = "600000")
private String stockCode;
@Schema(description = "订单类型,参见 OrderTypeEnum 枚举", example = "0")
private Integer type;
@Schema(description = "订单方向,参见 OrderDirectionEnum 枚举", example = "0")
private Integer direction;
@Schema(description = "订单状态,参见 OrderStatusEnum 枚举", example = "0")
private Integer status;
}
@@ -0,0 +1,26 @@
package com.stock.trade.order.controller.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 订单更新 Request VO")
@Data
public class OrderUpdateReqVO {
@Schema(description = "订单编号", required = true, example = "1024")
@NotNull(message = "订单编号不能为空")
private Long id;
@Schema(description = "订单价格,市价单可为空", example = "10.24")
private BigDecimal price;
@Schema(description = "订单数量", example = "100")
private Integer quantity;
@Schema(description = "备注", example = "修改订单")
private String remark;
}
@@ -0,0 +1,37 @@
package com.stock.trade.order.controller.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 订单项 Response VO")
@Data
public class OrderItemRespVO {
@Schema(description = "订单项编号", required = true, example = "1024")
private Long id;
@Schema(description = "订单编号", required = true, example = "2048")
private Long orderId;
@Schema(description = "成交编号", required = true, example = "T202305200001")
private String tradeNo;
@Schema(description = "成交价格", required = true, example = "10.24")
private BigDecimal price;
@Schema(description = "成交数量", required = true, example = "100")
private Integer quantity;
@Schema(description = "成交时间", required = true)
private LocalDateTime tradeTime;
@Schema(description = "交易费用", required = true, example = "5.12")
private BigDecimal fee;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
}
@@ -0,0 +1,36 @@
package com.stock.trade.order.controller.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 订单日志 Response VO")
@Data
public class OrderLogRespVO {
@Schema(description = "日志编号", required = true, example = "1024")
private Long id;
@Schema(description = "订单编号", required = true, example = "2048")
private Long orderId;
@Schema(description = "操作前订单状态,参见 OrderStatusEnum 枚举", required = true, example = "0")
private Integer beforeStatus;
@Schema(description = "操作后订单状态,参见 OrderStatusEnum 枚举", required = true, example = "1")
private Integer afterStatus;
@Schema(description = "操作内容", required = true, example = "创建订单")
private String content;
@Schema(description = "操作时间", required = true)
private LocalDateTime operateTime;
@Schema(description = "操作人编号", required = true, example = "1001")
private Long operatorId;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
}
@@ -0,0 +1,55 @@
package com.stock.trade.order.controller.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 订单 Response VO")
@Data
public class OrderRespVO {
@Schema(description = "订单编号", required = true, example = "1024")
private Long id;
@Schema(description = "用户编号", required = true, example = "1024")
private Long userId;
@Schema(description = "股票代码", required = true, example = "600000")
private String stockCode;
@Schema(description = "股票名称", required = true, example = "浦发银行")
private String stockName;
@Schema(description = "订单类型,参见 OrderTypeEnum 枚举", required = true, example = "0")
private Integer type;
@Schema(description = "订单方向,参见 OrderDirectionEnum 枚举", required = true, example = "0")
private Integer direction;
@Schema(description = "订单价格", example = "10.24")
private BigDecimal price;
@Schema(description = "订单数量", required = true, example = "100")
private Integer quantity;
@Schema(description = "已成交数量", required = true, example = "50")
private Integer filledQuantity;
@Schema(description = "订单状态,参见 OrderStatusEnum 枚举", required = true, example = "0")
private Integer status;
@Schema(description = "订单时间", required = true)
private LocalDateTime orderTime;
@Schema(description = "成交均价", example = "10.24")
private BigDecimal avgFillPrice;
@Schema(description = "备注", example = "测试订单")
private String remark;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
}
@@ -0,0 +1,21 @@
package com.stock.trade.order.convert;
import com.stock.trade.common.core.domain.PageResult;
import com.stock.trade.order.controller.request.OrderCreateReqVO;
import com.stock.trade.order.controller.response.OrderRespVO;
import com.stock.trade.order.dal.dataobject.OrderDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface OrderConvert {
OrderConvert INSTANCE = Mappers.getMapper(OrderConvert.class);
OrderDO convert(OrderCreateReqVO bean);
OrderRespVO convert(OrderDO bean);
PageResult<OrderRespVO> convertPage(PageResult<OrderDO> page);
}
@@ -0,0 +1,19 @@
package com.stock.trade.order.convert;
import com.stock.trade.order.controller.response.OrderItemRespVO;
import com.stock.trade.order.dal.dataobject.OrderItemDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface OrderItemConvert {
OrderItemConvert INSTANCE = Mappers.getMapper(OrderItemConvert.class);
OrderItemRespVO convert(OrderItemDO bean);
List<OrderItemRespVO> convertList(List<OrderItemDO> list);
}
@@ -0,0 +1,19 @@
package com.stock.trade.order.convert;
import com.stock.trade.order.controller.response.OrderLogRespVO;
import com.stock.trade.order.dal.dataobject.OrderLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface OrderLogConvert {
OrderLogConvert INSTANCE = Mappers.getMapper(OrderLogConvert.class);
OrderLogRespVO convert(OrderLogDO bean);
List<OrderLogRespVO> convertList(List<OrderLogDO> list);
}
@@ -0,0 +1,89 @@
package com.stock.trade.order.dal.dataobject;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.stock.trade.common.core.dataobject.BaseDO;
import com.stock.trade.order.enums.OrderDirectionEnum;
import com.stock.trade.order.enums.OrderStatusEnum;
import com.stock.trade.order.enums.OrderTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单 DO
*
* @author xxx
*/
@TableName("trade_order")
@KeySequence("trade_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class OrderDO extends BaseDO {
/**
* 订单编号,主键自增
*/
@TableId
private Long id;
/**
* 用户编号
*/
private Long userId;
/**
* 股票代码
*/
private String stockCode;
/**
* 股票名称
*/
private String stockName;
/**
* 订单类型
*
* 枚举 {@link OrderTypeEnum}
*/
private Integer type;
/**
* 订单方向
*
* 枚举 {@link OrderDirectionEnum}
*/
private Integer direction;
/**
* 订单价格
*/
private BigDecimal price;
/**
* 订单数量
*/
private Integer quantity;
/**
* 已成交数量
*/
private Integer filledQuantity;
/**
* 订单状态
*
* 枚举 {@link OrderStatusEnum}
*/
private Integer status;
/**
* 订单时间
*/
private LocalDateTime orderTime;
/**
* 成交均价
*/
private BigDecimal avgFillPrice;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,56 @@
package com.stock.trade.order.dal.dataobject;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.stock.trade.common.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单项 DO
*
* @author xxx
*/
@TableName("trade_order_item")
@KeySequence("trade_order_item_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class OrderItemDO extends BaseDO {
/**
* 订单项编号,主键自增
*/
@TableId
private Long id;
/**
* 订单编号,关联 {@link OrderDO#getId()}
*/
private Long orderId;
/**
* 成交编号 (如果部分成交,一个订单可能会有多条成交记录)
*/
private String tradeNo;
/**
* 成交价格
*/
private BigDecimal fillPrice;
/**
* 成交数量
*/
private Integer fillQuantity;
/**
* 成交时间
*/
private LocalDateTime fillTime;
/**
* 交易费用
*/
private BigDecimal commission;
}
@@ -0,0 +1,60 @@
package com.stock.trade.order.dal.dataobject;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.stock.trade.common.core.dataobject.BaseDO;
import com.stock.trade.order.enums.OrderStatusEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* 订单日志 DO
*
* @author xxx
*/
@TableName("trade_order_log")
@KeySequence("trade_order_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class OrderLogDO extends BaseDO {
/**
* 日志编号,主键自增
*/
@TableId
private Long id;
/**
* 订单编号,关联 {@link OrderDO#getId()}
*/
private Long orderId;
/**
* 操作前订单状态
*
* 枚举 {@link OrderStatusEnum}
*/
private Integer beforeStatus;
/**
* 操作后订单状态
*
* 枚举 {@link OrderStatusEnum}
*/
private Integer afterStatus;
/**
* 操作内容
*/
private String content;
/**
* 操作时间
*/
private LocalDateTime operationTime;
/**
* 操作人编号 (系统操作时,可以为空)
*/
private Long operatorUserId;
}
@@ -0,0 +1,25 @@
package com.stock.trade.order.dal.mysql;
import com.stock.trade.common.core.dal.mapper.BaseMapperX;
import com.stock.trade.order.dal.dataobject.OrderItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 订单项 Mapper
*
* @author xxx
*/
@Mapper
public interface OrderItemMapper extends BaseMapperX<OrderItemDO> {
default List<OrderItemDO> selectListByOrderId(Long orderId) {
return selectList(OrderItemDO::getOrderId, orderId);
}
default List<OrderItemDO> selectListByOrderIds(List<Long> orderIds) {
return selectList(new LambdaQueryWrapperX<OrderItemDO>()
.in(OrderItemDO::getOrderId, orderIds));
}
}
@@ -0,0 +1,22 @@
package com.stock.trade.order.dal.mysql;
import com.stock.trade.common.core.dal.mapper.BaseMapperX;
import com.stock.trade.order.dal.dataobject.OrderLogDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 订单日志 Mapper
*
* @author xxx
*/
@Mapper
public interface OrderLogMapper extends BaseMapperX<OrderLogDO> {
default List<OrderLogDO> selectListByOrderId(Long orderId) {
return selectList(new LambdaQueryWrapperX<OrderLogDO>()
.eq(OrderLogDO::getOrderId, orderId)
.orderByDesc(OrderLogDO::getId)); // 按时间倒序,最新的日志在前面
}
}
@@ -0,0 +1,41 @@
package com.stock.trade.order.dal.mysql;
import com.stock.trade.common.core.dal.mapper.BaseMapperX;
import com.stock.trade.common.core.dal.qo.PageQuery;
import com.stock.trade.order.dal.dataobject.OrderDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 订单 Mapper
*
* @author xxx
*/
@Mapper
public interface OrderMapper extends BaseMapperX<OrderDO> {
default List<OrderDO> selectPage(PageQuery pageQuery, Long userId, String stockCode, Integer type, Integer direction, Integer status) {
return selectList(new LambdaQueryWrapperX<OrderDO>()
.eqIfPresent(OrderDO::getUserId, userId)
.likeIfPresent(OrderDO::getStockCode, stockCode)
.eqIfPresent(OrderDO::getType, type)
.eqIfPresent(OrderDO::getDirection, direction)
.eqIfPresent(OrderDO::getStatus, status)
.orderByDesc(OrderDO::getId),
pageQuery.getPageNo(), pageQuery.getPageSize());
}
default Long selectCount(Long userId, String stockCode, Integer type, Integer direction, Integer status) {
return selectCount(new LambdaQueryWrapperX<OrderDO>()
.eqIfPresent(OrderDO::getUserId, userId)
.likeIfPresent(OrderDO::getStockCode, stockCode)
.eqIfPresent(OrderDO::getType, type)
.eqIfPresent(OrderDO::getDirection, direction)
.eqIfPresent(OrderDO::getStatus, status));
}
default List<OrderDO> selectListByUserId(Long userId) {
return selectList(OrderDO::getUserId, userId);
}
}
@@ -0,0 +1,29 @@
package com.stock.trade.order.enums;
import com.stock.trade.common.exception.ErrorCode;
/**
* trade-order 模块错误码枚举
*
* @author xxx
*/
public interface ErrorCodeConstants {
// ========== 订单 1003001000 开头 ============
ErrorCode ORDER_NOT_EXISTS = new ErrorCode(1003001000, "订单不存在");
ErrorCode ORDER_CREATE_FAILED = new ErrorCode(1003001001, "订单创建失败");
ErrorCode ORDER_UPDATE_FAILED = new ErrorCode(1003001002, "订单更新失败");
ErrorCode ORDER_DELETE_FAILED = new ErrorCode(1003001003, "订单删除失败");
ErrorCode ORDER_STATUS_INVALID = new ErrorCode(1003001004, "订单状态不合法");
ErrorCode ORDER_PRICE_INVALID = new ErrorCode(1003001005, "订单价格不合法");
ErrorCode ORDER_QUANTITY_INVALID = new ErrorCode(1003001006, "订单数量不合法");
ErrorCode ORDER_TYPE_INVALID = new ErrorCode(1003001007, "订单类型不合法");
ErrorCode ORDER_DIRECTION_INVALID = new ErrorCode(1003001008, "订单方向不合法");
// ========== 订单项 1003002000 开头 ============
ErrorCode ORDER_ITEM_NOT_EXISTS = new ErrorCode(1003002000, "订单项不存在");
// ========== 订单日志 1003003000 开头 ============
ErrorCode ORDER_LOG_NOT_EXISTS = new ErrorCode(1003003000, "订单日志不存在");
}
@@ -0,0 +1,36 @@
package com.stock.trade.order.enums;
import com.stock.trade.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 订单方向枚举
*
* @author xxx
*/
@Getter
@AllArgsConstructor
public enum OrderDirectionEnum implements IntArrayValuable {
BUY(0, "买入"),
SELL(1, "卖出");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(OrderDirectionEnum::getDirection).toArray();
/**
* 方向编码
*/
private final Integer direction;
/**
* 方向描述
*/
private final String desc;
@Override
public int[] array() {
return ARRAYS;
}
}
@@ -0,0 +1,66 @@
package com.stock.trade.order.enums;
import com.stock.trade.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 订单状态枚举
*
* @author xxx
*/
@Getter
@AllArgsConstructor
public enum OrderStatusEnum implements IntArrayValuable {
PENDING_NEW(0, "待报"), // 订单已创建,但尚未发送到交易所
NEW(1, "已报"), // 订单已发送到交易所,等待撮合
PARTIALLY_FILLED(2, "部分成交"),
FILLED(3, "全部成交"),
CANCELED(4, "已撤销"),
REJECTED(5, "已拒绝"), // 订单被交易所拒绝
EXPIRED(6, "已过期"); // 订单因过期未成交而失效
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(OrderStatusEnum::getStatus).toArray();
/**
* 状态编码
*/
private final Integer status;
/**
* 状态描述
*/
private final String desc;
@Override
public int[] array() {
return ARRAYS;
}
/**
* 判断订单是否为最终状态 (不可再变更)
*
* @param status 订单状态
* @return 是否为最终状态
*/
public static boolean isFinalStatus(Integer status) {
return FILLED.getStatus().equals(status)
|| CANCELED.getStatus().equals(status)
|| REJECTED.getStatus().equals(status)
|| EXPIRED.getStatus().equals(status);
}
/**
* 判断订单是否可以被撤销
*
* @param status 订单状态
* @return 是否可以被撤销
*/
public static boolean canCancel(Integer status) {
return PENDING_NEW.getStatus().equals(status)
|| NEW.getStatus().equals(status)
|| PARTIALLY_FILLED.getStatus().equals(status);
}
}
@@ -0,0 +1,39 @@
package com.stock.trade.order.enums;
import com.stock.trade.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 订单类型枚举
*
* @author xxx
*/
@Getter
@AllArgsConstructor
public enum OrderTypeEnum implements IntArrayValuable {
LIMIT(0, "限价单"),
MARKET(1, "市价单"),
STOP(2, "止损单"),
STOP_LIMIT(3, "止损限价单");
// TODO 后续可以根据实际需求扩展更多订单类型,例如 FOK, FAK 等
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(OrderTypeEnum::getType).toArray();
/**
* 类型编码
*/
private final Integer type;
/**
* 类型描述
*/
private final String desc;
@Override
public int[] array() {
return ARRAYS;
}
}
@@ -0,0 +1,32 @@
package com.stock.trade.order.mq.consumer;
import com.stock.trade.order.service.OrderService;
import com.stock.trade.tradeengine.message.TradeOrderReturnMessage; // 假设成交回报消息定义在 trade-engine 模块
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 订单成交回报消息消费者
*/
@Component
@Slf4j
public class TradeOrderReturnConsumer {
@Resource
private OrderService orderService;
@EventListener
public void onMessage(TradeOrderReturnMessage message) {
log.info("[onMessage][消息内容({})]", message);
try {
orderService.processOrderReturn(message);
} catch (Throwable e) {
log.error("[onMessage][处理订单成交回报({}) 异常]", message, e);
// TODO: 考虑增加重试机制或死信队列
}
}
}
@@ -0,0 +1,37 @@
package com.stock.trade.order.mq.message;
import com.stock.trade.common.mq.message.AbstractStreamMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
@Data
@EqualsAndHashCode(callSuper = true)
public class OrderMessage extends AbstractStreamMessage {
public static final String STREAM_KEY = "stock.trade.order.change";
/**
* 订单编号
*/
@NotNull(message = "订单编号不能为空")
private Long orderId;
/**
* 用户编号
*/
@NotNull(message = "用户编号不能为空")
private Long userId;
/**
* 订单状态
*/
@NotNull(message = "订单状态不能为空")
private Integer status;
@Override
public String getStreamKey() {
return STREAM_KEY;
}
}
@@ -0,0 +1,9 @@
package com.stock.trade.order.mq.producer;
import com.stock.trade.common.mq.producer.AbstractStreamProducer;
import com.stock.trade.order.mq.message.OrderMessage;
import org.springframework.stereotype.Component;
@Component
public class OrderProducer extends AbstractStreamProducer<OrderMessage> {
}
@@ -0,0 +1,38 @@
package com.stock.trade.order.service;
import com.stock.trade.order.dal.dataobject.OrderItemDO;
import java.util.List;
/**
* 订单项 Service 接口
*
* @author xxx
*/
public interface OrderItemService {
/**
* 根据订单编号获取订单项列表
*
* @param orderId 订单编号
* @return 订单项列表
*/
List<OrderItemDO> getOrderItemListByOrderId(Long orderId);
/**
* 根据订单编号列表获取订单项列表
*
* @param orderIds 订单编号列表
* @return 订单项列表
*/
List<OrderItemDO> getOrderItemListByOrderIds(List<Long> orderIds);
/**
* 创建订单项
*
* @param orderItem 订单项信息
* @return 订单项编号
*/
Long createOrderItem(OrderItemDO orderItem);
}
@@ -0,0 +1,29 @@
package com.stock.trade.order.service;
import com.stock.trade.order.dal.dataobject.OrderLogDO;
import java.util.List;
/**
* 订单日志 Service 接口
*
* @author xxx
*/
public interface OrderLogService {
/**
* 创建订单日志
*
* @param orderLog 订单日志信息
*/
void createOrderLog(OrderLogDO orderLog);
/**
* 根据订单编号获取订单日志列表
*
* @param orderId 订单编号
* @return 订单日志列表
*/
List<OrderLogDO> getOrderLogListByOrderId(Long orderId);
}
@@ -0,0 +1,101 @@
package com.stock.trade.order.service;
import com.stock.trade.common.core.domain.PageResult;
import com.stock.trade.order.controller.request.OrderCreateReqVO;
import com.stock.trade.order.controller.request.OrderPageReqVO;
import com.stock.trade.order.controller.request.OrderUpdateReqVO;
import com.stock.trade.order.dal.dataobject.OrderDO;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 订单 Service 接口
*
* @author xxx
*/
public interface OrderService {
/**
* 创建订单
*
* @param createReqVO 创建信息
* @return 订单编号
*/
Long createOrder(@Valid OrderCreateReqVO createReqVO);
/**
* 更新订单
*
* @param updateReqVO 更新信息
*/
void updateOrder(@Valid OrderUpdateReqVO updateReqVO);
/**
* 更新订单状态
*
* @param id 订单编号
* @param status 订单状态
* @param remark 备注 (可选)
*/
void updateOrderStatus(Long id, Integer status, String remark);
/**
* 撤销订单
*
* @param id 订单编号
* @param userId 用户编号 (用于权限校验)
*/
void cancelOrder(Long id, Long userId);
/**
* 删除订单 (逻辑删除)
*
* @param id 订单编号
*/
void deleteOrder(Long id);
/**
* 获取订单信息
*
* @param id 订单编号
* @return 订单信息
*/
OrderDO getOrder(Long id);
/**
* 获取订单列表
*
* @param ids 订单编号集合
* @return 订单列表
*/
List<OrderDO> getOrderList(Collection<Long> ids);
/**
* 获取订单分页
*
* @param pageReqVO 分页查询参数
* @return 订单分页结果
*/
PageResult<OrderDO> getOrderPage(OrderPageReqVO pageReqVO);
/**
* 根据用户编号获取其所有未完成订单列表
*
* @param userId 用户编号
* @return 未完成订单列表
*/
List<OrderDO> getUnfinishedOrdersByUserId(Long userId);
/**
* 处理订单成交回报
*
* @param orderId 订单编号
* @param fillPrice 成交价格
* @param fillQuantity 成交数量
* @param tradeNo 成交编号
*/
void processOrderFill(Long orderId, String stockCode, Integer direction, java.math.BigDecimal fillPrice, Integer fillQuantity, String tradeNo);
}
@@ -0,0 +1,45 @@
package com.stock.trade.order.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.stock.trade.order.dal.dataobject.OrderItemDO;
import com.stock.trade.order.dal.mysql.OrderItemMapper;
import com.stock.trade.order.service.OrderItemService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* 订单项 Service 实现类
*
* @author xxx
*/
@Service
@Slf4j
public class OrderItemServiceImpl implements OrderItemService {
@Resource
private OrderItemMapper orderItemMapper;
@Override
public List<OrderItemDO> getOrderItemListByOrderId(Long orderId) {
return orderItemMapper.selectListByOrderId(orderId);
}
@Override
public List<OrderItemDO> getOrderItemListByOrderIds(List<Long> orderIds) {
if (CollUtil.isEmpty(orderIds)) {
return Collections.emptyList();
}
return orderItemMapper.selectListByOrderIds(orderIds);
}
@Override
public Long createOrderItem(OrderItemDO orderItem) {
orderItemMapper.insert(orderItem);
return orderItem.getId();
}
}
@@ -0,0 +1,34 @@
package com.stock.trade.order.service.impl;
import com.stock.trade.order.dal.dataobject.OrderLogDO;
import com.stock.trade.order.dal.mysql.OrderLogMapper;
import com.stock.trade.order.service.OrderLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 订单日志 Service 实现类
*
* @author xxx
*/
@Service
@Slf4j
public class OrderLogServiceImpl implements OrderLogService {
@Resource
private OrderLogMapper orderLogMapper;
@Override
public void createOrderLog(OrderLogDO orderLog) {
orderLogMapper.insert(orderLog);
}
@Override
public List<OrderLogDO> getOrderLogListByOrderId(Long orderId) {
return orderLogMapper.selectListByOrderId(orderId);
}
}
@@ -0,0 +1,345 @@
package com.stock.trade.order.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.stock.trade.common.core.domain.PageResult;
import com.stock.trade.common.core.exception.ServiceException;
import com.stock.trade.order.controller.request.OrderCreateReqVO;
import com.stock.trade.order.controller.request.OrderPageReqVO;
import com.stock.trade.order.controller.request.OrderUpdateReqVO;
import com.stock.trade.order.convert.OrderConvert;
import com.stock.trade.order.dal.dataobject.OrderDO;
import com.stock.trade.order.dal.dataobject.OrderItemDO;
import com.stock.trade.order.dal.dataobject.OrderLogDO;
import com.stock.trade.order.dal.mysql.OrderMapper;
import com.stock.trade.order.enums.ErrorCodeConstants;
import com.stock.trade.order.enums.OrderStatusEnum;
import com.stock.trade.order.service.OrderItemService;
import com.stock.trade.order.service.OrderLogService;
import com.stock.trade.order.service.OrderService;
import com.stock.trade.order.mq.message.OrderMessage;
import com.stock.trade.order.mq.producer.OrderProducer;
import com.stock.trade.order.mq.message.TradeOrderReturnMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static com.stock.trade.common.core.exception.util.ServiceExceptionUtil.exception;
/**
* 订单 Service 实现类
*
* @author xxx
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemService orderItemService;
@Resource
private OrderLogService orderLogService;
@Resource
private OrderProducer orderProducer;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createOrder(OrderCreateReqVO createReqVO) {
// 1. 校验订单参数
validateCreateOrder(createReqVO);
// 2. 创建订单
OrderDO order = OrderConvert.INSTANCE.convert(createReqVO);
order.setStatus(OrderStatusEnum.PENDING_NEW.getStatus());
order.setOrderTime(LocalDateTime.now());
order.setFilledQuantity(0);
orderMapper.insert(order);
// 3. 创建订单日志
OrderLogDO orderLog = new OrderLogDO()
.setOrderId(order.getId())
.setBeforeStatus(null)
.setAfterStatus(order.getStatus())
.setContent("创建订单")
.setOperationTime(LocalDateTime.now())
.setOperatorUserId(order.getUserId());
orderLogService.createOrderLog(orderLog);
// 发送订单状态变更消息
orderProducer.sendOrderMessage(new OrderMessage().setOrderId(order.getId()).setUserId(order.getUserId()).setStatus(order.getStatus()));
return order.getId();
}
private void validateCreateOrder(OrderCreateReqVO createReqVO) {
// 1. 校验价格
if (createReqVO.getPrice() != null && createReqVO.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
throw exception(ErrorCodeConstants.ORDER_PRICE_INVALID);
}
// 2. 校验数量
if (createReqVO.getQuantity() <= 0) {
throw exception(ErrorCodeConstants.ORDER_QUANTITY_INVALID);
}
// 3. 校验订单类型
if (!OrderTypeEnum.ARRAYS.contains(createReqVO.getType())) {
throw exception(ErrorCodeConstants.ORDER_TYPE_INVALID);
}
// 4. 校验订单方向
if (!OrderDirectionEnum.ARRAYS.contains(createReqVO.getDirection())) {
throw exception(ErrorCodeConstants.ORDER_DIRECTION_INVALID);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateOrder(OrderUpdateReqVO updateReqVO) {
// 1. 校验订单存在
OrderDO order = validateOrderExists(updateReqVO.getId());
// 2. 校验订单状态是否允许修改
if (OrderStatusEnum.isFinalStatus(order.getStatus())) {
throw exception(ErrorCodeConstants.ORDER_STATUS_INVALID);
}
// 3. 更新订单
OrderDO updateObj = OrderConvert.INSTANCE.convert(updateReqVO);
orderMapper.updateById(updateObj);
// 4. 创建订单日志
OrderLogDO orderLog = new OrderLogDO()
.setOrderId(order.getId())
.setBeforeStatus(order.getStatus())
.setAfterStatus(order.getStatus())
.setContent("更新订单信息")
.setOperationTime(LocalDateTime.now())
.setOperatorUserId(order.getUserId());
orderLogService.createOrderLog(orderLog);
// 发送订单状态变更消息
orderProducer.sendOrderMessage(new OrderMessage().setOrderId(order.getId()).setUserId(userId).setStatus(OrderStatusEnum.CANCELED.getStatus()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus(Long id, Integer status, String remark) {
// 1. 校验订单存在
OrderDO order = validateOrderExists(id);
// 2. 更新订单状态
OrderDO updateObj = new OrderDO()
.setId(id)
.setStatus(status)
.setRemark(remark);
orderMapper.updateById(updateObj);
// 3. 创建订单日志
OrderLogDO orderLog = new OrderLogDO()
.setOrderId(order.getId())
.setBeforeStatus(order.getStatus())
.setAfterStatus(status)
.setContent("更新订单状态")
.setOperationTime(LocalDateTime.now())
.setOperatorUserId(order.getUserId());
orderLogService.createOrderLog(orderLog);
// 发送订单状态变更消息
orderProducer.sendOrderMessage(new OrderMessage().setOrderId(order.getId()).setUserId(userId).setStatus(OrderStatusEnum.CANCELED.getStatus()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long id, Long userId) {
// 1. 校验订单存在
OrderDO order = validateOrderExists(id);
// 2. 校验订单是否属于当前用户
if (!order.getUserId().equals(userId)) {
throw exception(ErrorCodeConstants.ORDER_NOT_EXISTS);
}
// 3. 校验订单状态是否允许撤销
if (!OrderStatusEnum.canCancel(order.getStatus())) {
throw exception(ErrorCodeConstants.ORDER_STATUS_INVALID);
}
// 4. 更新订单状态为已撤销
OrderDO updateObj = new OrderDO()
.setId(id)
.setStatus(OrderStatusEnum.CANCELED.getStatus())
.setRemark("用户主动撤销");
orderMapper.updateById(updateObj);
// 5. 创建订单日志
OrderLogDO orderLog = new OrderLogDO()
.setOrderId(order.getId())
.setBeforeStatus(order.getStatus())
.setAfterStatus(OrderStatusEnum.CANCELED.getStatus())
.setContent("撤销订单")
.setOperationTime(LocalDateTime.now())
.setOperatorUserId(userId);
orderLogService.createOrderLog(orderLog);
// 发送订单状态变更消息
orderProducer.sendOrderMessage(new OrderMessage().setOrderId(order.getId()).setUserId(userId).setStatus(OrderStatusEnum.CANCELED.getStatus()));
}
@Override
public void deleteOrder(Long id) {
// 校验订单存在
validateOrderExists(id);
// 删除订单
orderMapper.deleteById(id);
}
private OrderDO validateOrderExists(Long id) {
OrderDO order = orderMapper.selectById(id);
if (order == null) {
throw exception(ErrorCodeConstants.ORDER_NOT_EXISTS);
}
return order;
}
@Override
public OrderDO getOrder(Long id) {
return orderMapper.selectById(id);
}
@Override
public List<OrderDO> getOrderList(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return CollUtil.newArrayList();
}
return orderMapper.selectBatchIds(ids);
}
@Override
public PageResult<OrderDO> getOrderPage(OrderPageReqVO pageReqVO) {
// 查询订单分页
List<OrderDO> list = orderMapper.selectPage(pageReqVO.getPageQuery(),
pageReqVO.getUserId(), pageReqVO.getStockCode(), pageReqVO.getType(),
pageReqVO.getDirection(), pageReqVO.getStatus());
// 查询总数
Long total = orderMapper.selectCount(pageReqVO.getUserId(), pageReqVO.getStockCode(),
pageReqVO.getType(), pageReqVO.getDirection(), pageReqVO.getStatus());
return new PageResult<>(list, total);
}
@Override
public List<OrderDO> getUnfinishedOrdersByUserId(Long userId) {
return orderMapper.selectList(new LambdaQueryWrapperX<OrderDO>()
.eq(OrderDO::getUserId, userId)
.in(OrderDO::getStatus, OrderStatusEnum.PENDING_NEW.getStatus(),
OrderStatusEnum.NEW.getStatus(),
OrderStatusEnum.PARTIALLY_FILLED.getStatus()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processOrderReturn(TradeOrderReturnMessage message) {
log.info("[processOrderReturn][接收到订单成交回报消息:{}]", message);
processOrderFillInternal(message.getOrderId(), message.getStockCode(), message.getDirection(),
message.getFillPrice(), message.getFillQuantity(), message.getTradeNo(), true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processOrderFill(Long orderId, String stockCode, Integer direction,
BigDecimal fillPrice, Integer fillQuantity, String tradeNo) {
processOrderFillInternal(orderId, stockCode, direction, fillPrice, fillQuantity, tradeNo, false);
}
private void processOrderFillInternal(Long orderId, String stockCode, Integer direction,
BigDecimal fillPrice, Integer fillQuantity, String tradeNo, boolean fromMq) {
// 1. 校验订单存在
OrderDO order = validateOrderExists(orderId);
// 2. 校验成交回报参数
if (!order.getStockCode().equals(stockCode) || !order.getDirection().equals(direction)) {
log.error("[processOrderFillInternal][订单({})的股票代码({})或方向({})不匹配, fromMq:{}]", orderId, stockCode, direction, fromMq);
throw exception(ErrorCodeConstants.ORDER_NOT_EXISTS);
}
// 3. 创建订单项
OrderItemDO orderItem = new OrderItemDO()
.setOrderId(orderId)
.setTradeNo(tradeNo)
.setFillPrice(fillPrice)
.setFillQuantity(fillQuantity)
.setFillTime(LocalDateTime.now())
.setCommission(calculateCommission(fillPrice, fillQuantity)); // 计算手续费
orderItemService.createOrderItem(orderItem);
// 4. 更新订单成交信息
Integer newFilledQuantity = order.getFilledQuantity() + fillQuantity;
BigDecimal newAvgFillPrice = calculateAvgFillPrice(order.getAvgFillPrice(),
order.getFilledQuantity(), fillPrice, fillQuantity);
Integer newStatus = newFilledQuantity.equals(order.getQuantity()) ?
OrderStatusEnum.FILLED.getStatus() : OrderStatusEnum.PARTIALLY_FILLED.getStatus();
OrderDO updateObj = new OrderDO()
.setId(orderId)
.setFilledQuantity(newFilledQuantity)
.setAvgFillPrice(newAvgFillPrice)
.setStatus(newStatus);
orderMapper.updateById(updateObj);
// 5. 创建订单日志
OrderLogDO orderLog = new OrderLogDO()
.setOrderId(orderId)
.setBeforeStatus(order.getStatus())
.setAfterStatus(newStatus)
.setContent(String.format("订单成交: 成交数量 %d, 成交价格 %.3f", fillQuantity, fillPrice))
.setOperationTime(LocalDateTime.now())
.setOperatorUserId(order.getUserId()); // 补充操作用户ID
orderLogService.createOrderLog(orderLog);
// 6. 发送订单状态变更消息
orderProducer.sendOrderMessage(new OrderMessage().setOrderId(orderId).setUserId(order.getUserId()).setStatus(newStatus));
log.info("[processOrderFillInternal][订单({})处理完成,来源MQ{}]", orderId, fromMq);
}
/**
* 计算手续费
*
* @param price 成交价格
* @param quantity 成交数量
* @return 手续费
*/
private BigDecimal calculateCommission(BigDecimal price, Integer quantity) {
// TODO 根据实际业务规则计算手续费
BigDecimal amount = price.multiply(new BigDecimal(quantity));
return amount.multiply(new BigDecimal("0.0003")).setScale(2, RoundingMode.UP); // 暂定千分之三
}
/**
* 计算成交均价
*
* @param oldAvgPrice 原成交均价
* @param oldQuantity 原成交数量
* @param newPrice 新成交价格
* @param newQuantity 新成交数量
* @return 新的成交均价
*/
private BigDecimal calculateAvgFillPrice(BigDecimal oldAvgPrice, Integer oldQuantity,
BigDecimal newPrice, Integer newQuantity) {
if (oldAvgPrice == null || oldQuantity == 0) {
return newPrice;
}
BigDecimal totalAmount = oldAvgPrice.multiply(new BigDecimal(oldQuantity))
.add(newPrice.multiply(new BigDecimal(newQuantity)));
return totalAmount.divide(new BigDecimal(oldQuantity + newQuantity), 3, RoundingMode.HALF_UP);
}
}
@@ -0,0 +1,39 @@
package com.trade.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
/**
* 订单服务启动类
* @author Trade Team
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.trade.user.api"}) // 扫描Feign客户端,例如用户服务的API
@MapperScan("com.trade.order.mapper") // 扫描MyBatis Mapper接口
@ComponentScan(basePackages = {"com.trade.order", "com.trade.common.config"}) // 扫描通用配置和当前模块
public class OrderApplication {
/**
* 主函数,启动订单服务应用。
*
* @param args 命令行参数
*/
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ Trade Order Service 启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ | \\ \\ / / \n" +
" | ( ' ) | \\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\ \\ | || |(_,_)' \n" +
" | | \\ `' /| `-' / \n" +
" | | \\ / \\ / \n" +
" ''-' `'-' `-..-' ");
}
}
@@ -0,0 +1,17 @@
package com.trade.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 订单服务启动类
* Created by macro on 2020/8/3.
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeOrderApplication {
public static void main(String[] args) {
SpringApplication.run(TradeOrderApplication.class, args);
}
}
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的配置类。
*/
package com.trade.order.config;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块相关的常量定义。
*/
package com.trade.order.constant;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的Controller层,负责处理HTTP请求和响应。
*/
package com.trade.order.controller;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的数据传输对象 (DTO)。
*/
package com.trade.order.dto;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的数据库实体类。
*/
package com.trade.order.entity;
@@ -0,0 +1,5 @@
/**
* 此包存放订单模块相关的枚举类。
* 例如:订单状态枚举、支付方式枚举等。
*/
package com.trade.order.enums;
@@ -0,0 +1,4 @@
/**
* 此包存放订单服务模块的事件监听器,例如监听支付成功事件、库存扣减事件等。
*/
package com.trade.order.listener;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的Mapper接口,用于数据库操作。
*/
package com.trade.order.mapper;
@@ -0,0 +1,4 @@
/**
* 订单服务模块主包。
*/
package com.trade.order;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块Service接口的实现类。
*/
package com.trade.order.service.impl;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的Service层,负责业务逻辑处理。
*/
package com.trade.order.service;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块相关的工具类。
*/
package com.trade.order.util;
@@ -0,0 +1,4 @@
/**
* 此包存放订单服务模块的工具类。
*/
package com.trade.order.utils;
@@ -0,0 +1,4 @@
/**
* 此包存放订单模块的视图对象 (VO)。
*/
package com.trade.order.vo;
@@ -0,0 +1,11 @@
server:
port: 8003
spring:
profiles:
active: dev
application:
name: trade-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8003
spring:
profiles:
active: prod
application:
name: trade-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8003
spring:
profiles:
active: test
application:
name: trade-order
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,111 @@
server:
port: 8082 # 订单服务端口,避免与网关或其他服务冲突
spring:
application:
name: trade-order # 应用名称
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
- data-id: datasource-mysql-config.yml # MySQL数据源配置
group: ${spring.cloud.nacos.discovery.group}
refresh: true
# ext-config: # 可选的扩展配置
# - data-id: trade-order-ext.yml
# group: ${spring.cloud.nacos.discovery.group}
# refresh: true
# Spring Profiles: 用于区分不同环境的配置 (dev, test, prod)
profiles:
active: dev # 默认激活开发环境配置
# MyBatis Plus 配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml # Mapper XML文件位置
# type-aliases-package: com.trade.order.entity # 实体类别名扫描包 (如果需要)
global-config:
db-config:
id-type: auto # 全局主键策略
# table-prefix: t_ # 全局表前缀
# logic-delete-field: deleted # 全局逻辑删除字段名
# logic-not-delete-value: 0
# logic-delete-value: 1
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # MyBatis日志实现
# 日志配置
logging:
level:
com.trade.order: INFO # 订单模块日志级别
com.trade.order.mapper: DEBUG # Mapper接口日志级别 (开发时可设为DEBUG查看SQL)
org.springframework.web: INFO
org.springframework.security: INFO # 如果集成了Spring Security
# file:
# name: ./logs/trade-order.log # 日志文件路径和名称
# Actuator端点配置
management:
endpoints:
web:
exposure:
include: '*' # 暴露所有端点,生产环境请按需配置
endpoint:
health:
show-details: always # 显示健康检查详情
# Feign 配置 (如果需要调用其他服务)
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时时间 (ms)
readTimeout: 5000 # 读取超时时间 (ms)
# sentinel:
# enabled: true # 开启Sentinel对Feign的支持 (如果集成了Sentinel)
# Seata 分布式事务配置 (如果需要)
# seata:
# tx-service-group: trade_tx_group # 事务分组,需要与Seata Server配置一致
# 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: DEFAULT_GROUP
# application: seata-server
# config:
# type: nacos
# nacos:
# server-addr: ${spring.cloud.nacos.discovery.server-addr}
# namespace: ${spring.cloud.nacos.discovery.namespace}
# group: SEATA_GROUP
# data-id: seataServer.properties
# Swagger/OpenAPI 配置 (如果使用SpringDoc)
# springdoc:
# api-docs:
# path: /v3/api-docs
# swagger-ui:
# path: /swagger-ui.html
# display-request-duration: true
# groups-order: DESC
# group-configs:
# - group: 'Order API'
# paths-to-match: '/order/**'
# packages-to-scan: com.trade.order.controller
@@ -0,0 +1,102 @@
spring:
application:
name: trade-order # 服务名
cloud:
nacos:
# Nacos 作为注册中心的配置项
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# namespace: # Nacos 命名空间
# group: # Nacos 分组
# Nacos 作为配置中心的配置项
config:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
file-extension: yaml # 文件后缀名
# namespace: # Nacos 命名空间
# group: # Nacos 分组
# shared-configs: # 共享配置
# - data-id: common.yaml # 共享配置的 Data ID
# group: DEFAULT_GROUP # 共享配置的 Group
# refresh: true # 是否动态刷新
# extension-configs: # 拓展配置
# - data-id:
# group:
# refresh: true
# HTTP Server 相关配置
server:
port: 8083 # 服务器端口,默认为 8080
servlet:
context-path: /trade-order # 应用的访问路径,默认为 /
undertow:
threads:
# 设置 IO 线程数,它主要执行非阻塞的任务,它们会负责多个连接,默认设置每个 CPU 核心一个线程
# 不要设置过大,如果过大,启动项目会报错:打开文件数过多
io: 16
# 阻塞任务线程池,当执行类似 Servlet 请求阻塞 IO 操作,Undertow 会从这个线程池中取得线程
# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是 IO 线程数 * 8
worker: 256
buffer-size: 1024 # 每块 buffer 的大小,越小空间占用越小,一旦设置过小,对于大的请求则会打开更多 buffer,所以建议不要设置过小
direct-buffers: true # 是否分配的直接内存
# MyBatis Plus 相关配置
mybatis-plus:
# Mapper XML 文件的路径
mapper-locations: classpath*:mapper/*.xml
# 类型别名扫描包,多个package用逗号或者分号分隔
type-aliases-package: com.stock.trade.order.dal.dataobject
# 全局配置
global-config:
db-config:
# 全局默认主键类型
id-type: ASSIGN_ID
# 逻辑删除配置
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: true # 逻辑已删除值(默认为 1)
logic-not-delete-value: false # 逻辑未删除值(默认为 0)
banner: false # 是否关闭 MyBatis Plus 的 Banner
# MyBatis Plus 的具体配置
configuration:
# 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射。
map-underscore-to-camel-case: true
# 对于数据库中值为 null 的字段,默认情况下,MyBatis 在返回的 Map 中并不会包含这个字段的键值对。
# 通过设置 callSettersOnNulls 为 trueMyBatis 会在返回的 Map 中包含值为 null 的字段,并将其值设置为 null。
call-setters-on-nulls: true
# 对于数据库中值为 null 的字段,默认情况下,MyBatis 在返回的实体类对象中会将该字段设置为 null。
# 通过设置 default-enum-type-handler 为 org.apache.ibatis.type.EnumOrdinalTypeHandlerMyBatis 会将枚举类型的字段值转换为其序数(ordinal)进行存储和查询。
default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler
# Spring Doc 相关配置
springdoc:
# Knife4j 配置
knife4j:
enable: true # 是否开启 Knife4j,默认为 false
setting:
language: zh_CN # API 列表的语言,可选 zh_CN、en_US
# API 分组
group-configs:
- group: default
paths-to-match: /**
packages-to-scan: com.stock.trade.order.controller # 分组的 Controller 包路径
# Actuator 相关配置
management:
# /actuator/health 端点配置
health:
# 默认情况下,/actuator/health 只会展示整体的应用健康情况,通过设置为 SHOW_ALWAYS 后,可以展示详细的应用健康情况,例如:磁盘、数据库等。
show-details: ALWAYS
# /actuator/metrics 端点配置
metrics:
# /actuator/metrics 端点,默认情况下不会展示 tag 标签。通过设置为 true 后,可以展示 tag 标签,更加清晰。
tags:
application: ${spring.application.name}
# /actuator 端点配置
endpoints:
# Web 端点的暴露范围,设置为 * 时,表示暴露所有端点。
web:
exposure:
include: '*'
# JMX 端点的暴露范围,设置为 * 时,表示暴露所有端点。
jmx:
exposure:
include: '*'
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 应用名称 -->
<springProperty name="APP_NAME" scope="context" source="spring.application.name" defaultValue="trade-order"/>
<!-- 日志路径 -->
<property name="LOG_PATH" value="logs/${APP_NAME}"/>
<!-- 日志格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出 -->
<appender name="ASYNC_FILE_INFO" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>256</queueSize>
<appender-ref ref="FILE_INFO"/>
</appender>
<appender name="ASYNC_FILE_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>256</queueSize>
<appender-ref ref="FILE_ERROR"/>
</appender>
<!-- Spring Profile 相关配置 -->
<springProfile name="dev,test">
<logger name="com.stock.trade.order" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE_INFO"/>
<appender-ref ref="ASYNC_FILE_ERROR"/>
</root>
</springProfile>
<springProfile name="prod">
<logger name="com.stock.trade.order" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE_INFO"/>
<appender-ref ref="ASYNC_FILE_ERROR"/>
</root>
</springProfile>
</configuration>
+100
View File
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>trade-risk</artifactId>
<packaging>jar</packaging>
<name>trade-risk</name>
<description>Risk management module for the trading system</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- trade-common -->
<dependency>
<groupId>com.trade</groupId>
<artifactId>trade-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- trade-order (to monitor orders for risk) -->
<dependency>
<groupId>com.trade</groupId>
<artifactId>trade-order</artifactId>
<version>${project.version}</version>
</dependency>
<!-- trade-market-data (to get market data for risk calculations) -->
<dependency>
<groupId>com.trade</groupId>
<artifactId>trade-market-data</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Cloud Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringDoc OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,17 @@
package com.trade.risk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 风险服务启动类
* Created by macro on 2020/8/3.
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeRiskApplication {
public static void main(String[] args) {
SpringApplication.run(TradeRiskApplication.class, args);
}
}
@@ -0,0 +1,11 @@
server:
port: 8006
spring:
profiles:
active: dev
application:
name: trade-risk
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8006
spring:
profiles:
active: prod
application:
name: trade-risk
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8006
spring:
profiles:
active: test
application:
name: trade-risk
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="base-logback.xml"/>
<logger name="com.trade.risk" level="debug"/>
</configuration>
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>trade-security</artifactId>
<packaging>jar</packaging>
<name>trade-security</name>
<description>Security module for the trading system (Spring Security, JWT)</description>
<dependencies>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Starter Web (needed for security configurations in a web context) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JJWT for JWT support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hutool (for utility functions, e.g., in security utils) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- Fastjson for JSON processing -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version> <!-- Use a recent, stable version -->
</dependency>
<!-- trade-common (for common DTOs, e.g., UserDetails or custom principal) -->
<dependency>
<groupId>com.trade</groupId>
<artifactId>trade-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Redis (Optional, if using Redis for session management or token storage) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip> <!-- This is a library module, not an executable jar -->
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,18 @@
package com.trade.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 安全模块启动类
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(TradeSecurityApplication.class, args);
}
}
@@ -0,0 +1,173 @@
package com.trade.security.config;
import com.trade.common.constant.CommonConstant;
import com.trade.security.filter.JwtAuthenticationTokenFilter;
import com.trade.security.handler.CustomAccessDeniedHandler;
import com.trade.security.handler.CustomAuthenticationEntryPoint;
import com.trade.security.handler.CustomAuthenticationFailureHandler;
import com.trade.security.handler.CustomAuthenticationSuccessHandler;
import com.trade.security.handler.CustomLogoutSuccessHandler;
import com.trade.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* Spring Security 配置类
*
* @author Trade Team
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 配置密码编码器
*
* @return PasswordEncoder 实例
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置认证管理器
*
* @return AuthenticationManager 实例
* @throws Exception 配置异常
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置认证管理器构建器
*
* @param auth AuthenticationManagerBuilder 实例
* @throws Exception 配置异常
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 配置HTTP安全性
*
* @param http HttpSecurity 实例
* @throws Exception 配置异常
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
.and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login /user/register 注册permitAll
.antMatchers(HttpMethod.POST, CommonConstant.LOGIN_URI, "/user/register").permitAll()
// 静态资源,例如htmlcssjs, swagger
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v3/api-docs/**",
"/webjars/**",
"/druid/**",
"/favicon.ico"
).permitAll()
// 对于options请求全部放行
.antMatchers(HttpMethod.OPTIONS).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加JWT filter
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
http.addFilterBefore(corsFilter(), JwtAuthenticationTokenFilter.class);
http.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class);
// 配置登录
http.formLogin()
.loginProcessingUrl(CommonConstant.LOGIN_URI) // 自定义登录URL
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.permitAll();
// 配置登出
http.logout()
.logoutUrl(CommonConstant.LOGOUT_URI) // 自定义登出URL
.logoutSuccessHandler(customLogoutSuccessHandler)
.permitAll();
}
/**
* 配置CORS过滤器
*
* @return CorsFilter 实例
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置允许的源,*表示允许所有源,生产环境建议指定具体的源
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
@@ -0,0 +1,88 @@
package com.trade.security.filter;
import com.trade.common.constant.CommonConstant;
import com.trade.common.util.JwtUtils;
import com.trade.security.service.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT认证令牌过滤器
* <p>
* 该过滤器在每个请求中检查JWT令牌的有效性,如果令牌有效,则将认证信息设置到Spring Security上下文中。
* </p>
*
* @author Trade Team
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
/**
* 执行过滤器逻辑。
*
* @param request HTTP请求
* @param response HTTP响应
* @param filterChain 过滤器链
* @throws ServletException Servlet异常
* @throws IOException IO异常
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(CommonConstant.TOKEN_HEADER);
if (StringUtils.hasText(authHeader) && authHeader.startsWith(CommonConstant.TOKEN_PREFIX)) {
String authToken = authHeader.substring(CommonConstant.TOKEN_PREFIX.length());
try {
if (jwtUtils.validateToken(authToken)) {
String username = jwtUtils.getUsernameFromToken(authToken);
LOGGER.info("Authenticated user: {}, setting security context", username);
// 当token存在并且有效时,设置Spring Security上下文
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtils.validateToken(authToken, userDetails)) { // 再次校验token是否与userDetails匹配
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
LOGGER.debug("User '{}' set in security context", username);
} else {
LOGGER.warn("Token validation failed for user '{}' against UserDetails.", username);
}
} else if (username == null) {
LOGGER.warn("Username from token is null.");
}
} else {
LOGGER.warn("Invalid JWT token: {}", authToken);
}
} catch (Exception e) {
LOGGER.error("Error processing JWT token: {}", e.getMessage());
// 可以选择清除SecurityContext,以防部分认证信息残留
// SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
}
@@ -0,0 +1,54 @@
package com.trade.security.handler;
import com.alibaba.fastjson.JSON;
import com.trade.common.enums.ResultCodeEnum;
import com.trade.common.vo.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义访问拒绝处理器
* <p>
* 当已认证的用户尝试访问其没有权限的资源时,此处理器被调用。
* 它负责返回一个表示访问被拒绝的响应。
* </p>
*
* @author Trade Team
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
/**
* 处理访问被拒绝的情况。
*
* @param request HTTP请求
* @param response HTTP响应
* @param accessDeniedException 访问被拒绝异常
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOGGER.warn("Access denied for user '{}' to '{}': {}",
request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : "anonymous",
request.getRequestURI(),
accessDeniedException.getMessage());
Result<Void> result = Result.failure(ResultCodeEnum.FORBIDDEN, "您没有权限访问该资源");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(JSON.toJSONString(result));
response.getWriter().flush();
}
}
@@ -0,0 +1,51 @@
package com.trade.security.handler;
import com.alibaba.fastjson.JSON;
import com.trade.common.enums.ResultCodeEnum;
import com.trade.common.vo.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义认证入口点
* <p>
* 当匿名用户尝试访问受保护的资源而未提供有效的认证凭证时,此处理器被调用。
* 它负责返回一个表示需要认证的响应。
* </p>
*
* @author Trade Team
*/
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
/**
* 开始认证过程。
*
* @param request HTTP请求
* @param response HTTP响应
* @param authException 认证异常
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOGGER.warn("Unauthorized access attempt to '{}': {}", request.getRequestURI(), authException.getMessage());
Result<Void> result = Result.failure(ResultCodeEnum.UNAUTHORIZED, "请求未授权,请先登录");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(JSON.toJSONString(result));
response.getWriter().flush();
}
}
@@ -0,0 +1,50 @@
package com.trade.security.handler;
import com.alibaba.fastjson.JSON;
import com.trade.common.enums.ResultCodeEnum;
import com.trade.common.vo.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义认证失败处理器
* <p>
* 当用户登录失败时,此处理器负责返回统一格式的错误响应。
* </p>
*
* @author Trade Team
*/
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
/**
* 处理认证失败。
*
* @param request HTTP请求
* @param response HTTP响应
* @param exception 认证异常
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
LOGGER.warn("Authentication failed: {}", exception.getMessage());
Result<Void> result = Result.failure(ResultCodeEnum.LOGIN_FAILURE, exception.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 可以根据具体异常类型设置不同的状态码
response.getWriter().write(JSON.toJSONString(result));
response.getWriter().flush();
}
}
@@ -0,0 +1,65 @@
package com.trade.security.handler;
import com.alibaba.fastjson.JSON;
import com.trade.common.constant.CommonConstant;
import com.trade.common.enums.ResultCodeEnum;
import com.trade.common.util.JwtUtils;
import com.trade.common.vo.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义认证成功处理器
* <p>
* 当用户成功登录后,此处理器负责生成JWT并将其返回给客户端。
* </p>
*
* @author Trade Team
*/
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);
@Autowired
private JwtUtils jwtUtils;
/**
* 处理认证成功。
*
* @param request HTTP请求
* @param response HTTP响应
* @param authentication 认证信息
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
String token = jwtUtils.generateToken(userDetails);
LOGGER.info("User '{}' authenticated successfully. Generating JWT token.", username);
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put(CommonConstant.TOKEN_HEADER_PREFIX_WITH_SPACE.trim(), token);
Result<Map<String, String>> result = Result.success(ResultCodeEnum.LOGIN_SUCCESS, tokenMap);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(result));
response.getWriter().flush();
}
}
@@ -0,0 +1,56 @@
package com.trade.security.handler;
import com.alibaba.fastjson.JSON;
import com.trade.common.enums.ResultCodeEnum;
import com.trade.common.vo.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义登出成功处理器
* <p>
* 当用户成功登出后,此处理器负责返回统一格式的成功响应。
* </p>
*
* @author Trade Team
*/
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class);
/**
* 处理登出成功。
*
* @param request HTTP请求
* @param response HTTP响应
* @param authentication 认证信息(可能为null,如果会话已失效)
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null && authentication.getDetails() != null) {
try {
request.getSession().invalidate();
LOGGER.info("User '{}' logged out successfully.", authentication.getName());
} catch (Exception e) {
LOGGER.error("Error invalidating session during logout for user '{}': {}", authentication.getName(), e.getMessage());
}
}
Result<Void> result = Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(result));
response.getWriter().flush();
}
}
@@ -0,0 +1,126 @@
package com.trade.security.service;
import com.trade.common.exception.BusinessException;
import com.trade.common.enums.ResultCodeEnum;
// import com.trade.user.feign.UserFeignClient; // 假设存在用户服务的Feign客户端
// import com.trade.user.dto.UserDTO; // 假设用户服务返回的用户DTO
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 用户详细信息服务实现类
* <p>
* 该类负责从用户服务加载用户详细信息,用于Spring Security的认证过程。
* </p>
*
* @author Trade Team
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
// @Autowired
// private UserFeignClient userFeignClient; // 注入用户服务的Feign客户端,用于远程调用获取用户信息
/**
* 根据用户名加载用户详细信息。
*
* @param username 用户名
* @return UserDetails 用户详细信息对象
* @throws UsernameNotFoundException 如果用户未找到
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟通过Feign客户端调用用户服务获取用户信息
// 在实际项目中,这里会调用 userFeignClient.getUserByUsername(username) 等方法
// UserDTO userDTO = userFeignClient.getUserByUsername(username).getData(); // 假设Feign接口返回Result<UserDTO>
// ---- 以下为模拟数据,实际项目中应替换为真实的Feign调用 ----
com.trade.security.model.User mockUser = findMockUserByUsername(username);
if (mockUser == null) {
throw new UsernameNotFoundException(ResultCodeEnum.USER_NOT_EXIST.getMessage());
}
// ---- 模拟数据结束 ----
// if (userDTO == null) {
// throw new UsernameNotFoundException(ResultCodeEnum.USER_NOT_EXIST.getMessage());
// }
// 获取用户权限信息
// Set<String> permissions = userFeignClient.getUserPermissions(userDTO.getId()).getData(); // 假设Feign接口返回Result<Set<String>>
// Set<GrantedAuthority> authorities = permissions.stream()
// .map(SimpleGrantedAuthority::new)
// .collect(Collectors.toSet());
// ---- 以下为模拟权限数据 ----
Set<GrantedAuthority> authorities = new HashSet<>();
if ("admin".equals(mockUser.getUsername())) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
authorities.add(new SimpleGrantedAuthority("user:list"));
authorities.add(new SimpleGrantedAuthority("user:create"));
authorities.add(new SimpleGrantedAuthority("user:update"));
authorities.add(new SimpleGrantedAuthority("user:delete"));
} else if ("user".equals(mockUser.getUsername())){
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
authorities.add(new SimpleGrantedAuthority("order:create"));
authorities.add(new SimpleGrantedAuthority("order:list"));
}
// ---- 模拟权限数据结束 ----
return new User(
mockUser.getUsername(),
mockUser.getPassword(),
mockUser.isEnabled(),
mockUser.isAccountNonExpired(),
mockUser.isCredentialsNonExpired(),
mockUser.isAccountNonLocked(),
authorities
);
}
/**
* 模拟根据用户名查找用户(实际项目中应通过数据库或用户服务获取)
*
* @param username 用户名
* @return 模拟的用户对象,如果未找到则返回null
*/
private com.trade.security.model.User findMockUserByUsername(String username) {
// 实际项目中,这里应该调用
// com.trade.user.mapper.UserMapper.selectOne(new QueryWrapper<com.trade.user.model.User>().eq("username", username));
// 或者通过 Feign Client 调用用户微服务
if ("admin".equals(username)) {
com.trade.security.model.User adminUser = new com.trade.security.model.User();
adminUser.setId(1L);
adminUser.setUsername("admin");
// 密码 "password" 使用 BCryptPasswordEncoder 加密后的结果, 在实际应用中,数据库存储的应该是加密后的密码
// 可以使用 new BCryptPasswordEncoder().encode("password") 生成
adminUser.setPassword("$2a$10$EipD5Q5X.YR4V2/A9A7hUuL9gN.o0g8f21nQn2N1n.B0g8f21nQn"); // 假设这是 "password" 加密后的值
adminUser.setEnabled(true);
adminUser.setAccountNonExpired(true);
adminUser.setCredentialsNonExpired(true);
adminUser.setAccountNonLocked(true);
return adminUser;
} else if ("user".equals(username)) {
com.trade.security.model.User normalUser = new com.trade.security.model.User();
normalUser.setId(2L);
normalUser.setUsername("user");
normalUser.setPassword("$2a$10$EipD5Q5X.YR4V2/A9A7hUuL9gN.o0g8f21nQn2N1n.B0g8f21nQn"); // 假设这是 "password" 加密后的值
normalUser.setEnabled(true);
normalUser.setAccountNonExpired(true);
normalUser.setCredentialsNonExpired(true);
normalUser.setAccountNonLocked(true);
return normalUser;
}
return null;
}
}
@@ -0,0 +1,14 @@
server:
port: 8088
spring:
profiles:
active: dev
application:
name: trade-security
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yml
@@ -0,0 +1,14 @@
server:
port: 8088
spring:
profiles:
active: prod
application:
name: trade-security
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yml
@@ -0,0 +1,14 @@
server:
port: 8088
spring:
profiles:
active: test
application:
name: trade-security
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yml
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="com.trade.security" level="debug"/>
</configuration>
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-strategy</artifactId>
<packaging>jar</packaging>
<name>trade-strategy</name>
<description>Trading strategy definition and execution module</description>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- trade-common -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- trade-indicator (to use indicators in strategies) -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-indicator</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- trade-market-data (to get market data for strategy execution) -->
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-market-data</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Spring Cloud Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringDoc OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Optional: Drools or other rule engines for complex strategy logic -->
<!-- <dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>LATEST_VERSION</version>
</dependency> -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
package com.stock.strategy.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 策略控制器
*
* @author TraeAI
*/
@RestController
@RequestMapping("/strategy")
public class StrategyController {
// TODO: 实现策略相关接口
}
@@ -0,0 +1,51 @@
package com.stock.strategy.dal.dataobject;
import com.stock.common.dal.dataobject.BaseDO; // 引入公共BaseDO
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 策略DO
*
* @author TraeAI
*/
@TableName("trade_strategy") // TODO: 确认表名是否正确
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class StrategyDO extends BaseDO {
/**
* ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 策略名称
*/
private String strategyName;
/**
* 策略类型
*/
private String strategyType;
/**
* 策略描述
*/
private String description;
/**
* 策略内容 (例如: JSON格式的参数,或者脚本语言)
*/
private String content;
// TODO: 根据实际需求添加更多字段
}
@@ -0,0 +1,15 @@
package com.stock.strategy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.strategy.dal.dataobject.StrategyDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 策略 Mapper
*
* @author TraeAI
*/
@Mapper
public interface StrategyMapper extends BaseMapper<StrategyDO> {
// TODO: 定义策略相关的数据库操作方法
}
@@ -0,0 +1,10 @@
package com.stock.strategy.service;
/**
* 策略服务接口
*
* @author TraeAI
*/
public interface StrategyService {
// TODO: 定义策略服务相关方法
}
@@ -0,0 +1,14 @@
package com.stock.strategy.service.impl;
import com.stock.strategy.service.StrategyService;
import org.springframework.stereotype.Service;
/**
* 策略服务实现类
*
* @author TraeAI
*/
@Service
public class StrategyServiceImpl implements StrategyService {
// TODO: 实现策略服务相关方法
}
@@ -0,0 +1,111 @@
package com.stock.trade.strategy.controller;
import com.stock.trade.framework.common.pojo.CommonResult;
import com.stock.trade.framework.common.pojo.PageResult;
import com.stock.trade.strategy.controller.vo.StrategyExecutionCreateReqVO;
import com.stock.trade.strategy.controller.vo.StrategyExecutionPageReqVO;
import com.stock.trade.strategy.controller.vo.StrategyExecutionRespVO;
import com.stock.trade.strategy.controller.vo.StrategyExecutionUpdateReqVO;
import com.stock.trade.strategy.convert.StrategyExecutionConvert;
import com.stock.trade.strategy.dal.dataobject.StrategyExecutionDO;
import com.stock.trade.strategy.service.StrategyExecutionService;
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;
@Tag(name = "管理后台 - 策略执行")
@RestController
@RequestMapping("/trade/strategy-execution")
@Validated
public class StrategyExecutionController {
@Resource
private StrategyExecutionService strategyExecutionService;
@PostMapping("/create")
@Operation(summary = "创建策略执行")
@PreAuthorize("@ss.hasPermission('strategy:execution:create')")
public CommonResult<Long> createStrategyExecution(@Valid @RequestBody StrategyExecutionCreateReqVO createReqVO) {
return success(strategyExecutionService.createStrategyExecution(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新策略执行")
@PreAuthorize("@ss.hasPermission('strategy:execution:update')")
public CommonResult<Boolean> updateStrategyExecution(@Valid @RequestBody StrategyExecutionUpdateReqVO updateReqVO) {
strategyExecutionService.updateStrategyExecution(updateReqVO);
return success(true);
}
@PutMapping("/start/{id}")
@Operation(summary = "启动策略执行")
@Parameter(name = "id", description = "策略执行编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:start')")
public CommonResult<Boolean> startStrategyExecution(@PathVariable("id") Long id) {
strategyExecutionService.startStrategyExecution(id);
return success(true);
}
@PutMapping("/pause/{id}")
@Operation(summary = "暂停策略执行")
@Parameter(name = "id", description = "策略执行编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:pause')")
public CommonResult<Boolean> pauseStrategyExecution(@PathVariable("id") Long id) {
strategyExecutionService.pauseStrategyExecution(id);
return success(true);
}
@PutMapping("/stop/{id}")
@Operation(summary = "停止策略执行")
@Parameter(name = "id", description = "策略执行编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:stop')")
public CommonResult<Boolean> stopStrategyExecution(@PathVariable("id") Long id) {
strategyExecutionService.stopStrategyExecution(id);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:delete')")
public CommonResult<Boolean> deleteStrategyExecution(@RequestParam("id") Long id) {
strategyExecutionService.deleteStrategyExecution(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<StrategyExecutionRespVO> getStrategyExecution(@RequestParam("id") Long id) {
StrategyExecutionDO execution = strategyExecutionService.getStrategyExecution(id);
return success(StrategyExecutionConvert.INSTANCE.convert(execution));
}
@GetMapping("/page")
@Operation(summary = "获得策略执行分页")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<PageResult<StrategyExecutionRespVO>> getStrategyExecutionPage(@Valid StrategyExecutionPageReqVO pageVO) {
PageResult<StrategyExecutionDO> pageResult = strategyExecutionService.getStrategyExecutionPage(pageVO);
return success(StrategyExecutionConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/list-by-definition")
@Operation(summary = "根据策略定义ID获得策略执行列表")
@Parameter(name = "strategyDefinitionId", description = "策略定义编号", required = true, example = "2048")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<List<StrategyExecutionRespVO>> getStrategyExecutionListByDefinitionId(@RequestParam("strategyDefinitionId") Long strategyDefinitionId) {
List<StrategyExecutionDO> list = strategyExecutionService.getStrategyExecutionListByDefinitionId(strategyDefinitionId);
return success(StrategyExecutionConvert.INSTANCE.convertList(list));
}
}
@@ -0,0 +1,33 @@
package com.stock.trade.strategy.enums;
import com.stock.trade.framework.common.exception.ErrorCode;
/**
* Trade Strategy 模块错误码常量
*
* @author AI Assistant
*/
public interface ErrorCodeConstants {
// ========== 策略定义 模块 1-001-000-000 ==========
ErrorCode STRATEGY_DEFINITION_NOT_EXISTS = new ErrorCode(1001000000, "策略定义不存在");
ErrorCode STRATEGY_DEFINITION_NAME_EXISTS = new ErrorCode(1001000001, "策略定义名称已存在");
ErrorCode STRATEGY_DEFINITION_CANNOT_DELETE_WHEN_ACTIVE = new ErrorCode(1001000002, "策略定义处于活动状态,无法删除");
// ========== 策略回测 模块 1-001-001-000 ==========
ErrorCode STRATEGY_BACKTEST_NOT_EXISTS = new ErrorCode(1001001000, "策略回测记录不存在");
ErrorCode STRATEGY_BACKTEST_CANNOT_START = new ErrorCode(1001001001, "当前状态无法启动策略回测");
ErrorCode STRATEGY_BACKTEST_CANNOT_UPDATE_NON_PENDING = new ErrorCode(1001001002, "只有待处理状态的回测才能更新");
ErrorCode STRATEGY_BACKTEST_CANNOT_DELETE_WHEN_RUNNING = new ErrorCode(1001001003, "运行中的策略回测无法删除");
// ========== 策略执行 模块 1-001-002-000 ==========
ErrorCode STRATEGY_EXECUTION_NOT_EXISTS = new ErrorCode(1001002000, "策略执行记录不存在");
ErrorCode STRATEGY_EXECUTION_STATUS_CANNOT_START = new ErrorCode(1001002001, "当前状态无法启动策略执行");
ErrorCode STRATEGY_EXECUTION_STATUS_CANNOT_PAUSE = new ErrorCode(1001002002, "当前状态无法暂停策略执行");
ErrorCode STRATEGY_EXECUTION_STATUS_CANNOT_STOP = new ErrorCode(1001002003, "当前状态无法停止策略执行");
ErrorCode STRATEGY_EXECUTION_STATUS_CANNOT_COMPLETE = new ErrorCode(1001002004, "当前状态无法完成策略执行");
ErrorCode STRATEGY_EXECUTION_CANNOT_DELETE_WHEN_RUNNING = new ErrorCode(1001002005, "运行中的策略执行无法删除");
}
@@ -0,0 +1,205 @@
package com.stock.trade.strategy.service;
import com.stock.trade.framework.common.exception.util.ServiceExceptionUtil;
import com.stock.trade.framework.common.pojo.PageResult;
import com.stock.trade.strategy.controller.vo.StrategyExecutionCreateReqVO;
import com.stock.trade.strategy.controller.vo.StrategyExecutionPageReqVO;
import com.stock.trade.strategy.controller.vo.StrategyExecutionUpdateReqVO;
import com.stock.trade.strategy.convert.StrategyExecutionConvert;
import com.stock.trade.strategy.dal.dataobject.StrategyExecutionDO;
import com.stock.trade.strategy.dal.mysql.StrategyExecutionMapper;
import com.stock.trade.strategy.enums.ErrorCodeConstants;
import com.stock.trade.strategy.enums.StrategyExecutionStatusEnum;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import static com.stock.trade.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 策略执行 Service 实现类
*
* @author AI Assistant
*/
@Service
@Validated
public class StrategyExecutionServiceImpl implements StrategyExecutionService {
@Resource
private StrategyExecutionMapper strategyExecutionMapper;
/**
* 创建策略执行记录
*
* @param createReqVO 创建信息
* @return 编号
*/
@Override
public Long createStrategyExecution(StrategyExecutionCreateReqVO createReqVO) {
// 插入
StrategyExecutionDO strategyExecution = StrategyExecutionConvert.INSTANCE.convert(createReqVO);
strategyExecution.setStatus(StrategyExecutionStatusEnum.PENDING.getStatus());
strategyExecution.setCreateTime(LocalDateTime.now());
strategyExecution.setUpdateTime(LocalDateTime.now());
strategyExecutionMapper.insert(strategyExecution);
// 返回
return strategyExecution.getId();
}
/**
* 更新策略执行记录
*
* @param updateReqVO 更新信息
*/
@Override
public void updateStrategyExecution(StrategyExecutionUpdateReqVO updateReqVO) {
// 校验存在
validateStrategyExecutionExists(updateReqVO.getId());
// 更新
StrategyExecutionDO updateDO = StrategyExecutionConvert.INSTANCE.convert(updateReqVO);
updateDO.setUpdateTime(LocalDateTime.now());
strategyExecutionMapper.updateById(updateDO);
}
/**
* 启动策略执行
*
* @param id 策略执行ID
*/
@Override
public void startStrategyExecution(Long id) {
StrategyExecutionDO execution = validateStrategyExecutionExists(id);
if (!Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.PENDING.getStatus()) &&
!Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.PAUSED.getStatus())) {
throw exception(ErrorCodeConstants.STRATEGY_EXECUTION_STATUS_CANNOT_START);
}
StrategyExecutionDO updateDO = new StrategyExecutionDO();
updateDO.setId(id);
updateDO.setStatus(StrategyExecutionStatusEnum.RUNNING.getStatus());
updateDO.setStartTime(LocalDateTime.now()); // 记录启动时间
updateDO.setUpdateTime(LocalDateTime.now());
strategyExecutionMapper.updateById(updateDO);
// 此处应有实际的策略启动逻辑,例如与交易API交互,发送到执行引擎等
}
/**
* 暂停策略执行
*
* @param id 策略执行ID
*/
@Override
public void pauseStrategyExecution(Long id) {
StrategyExecutionDO execution = validateStrategyExecutionExists(id);
if (!Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.RUNNING.getStatus())) {
throw exception(ErrorCodeConstants.STRATEGY_EXECUTION_STATUS_CANNOT_PAUSE);
}
StrategyExecutionDO updateDO = new StrategyExecutionDO();
updateDO.setId(id);
updateDO.setStatus(StrategyExecutionStatusEnum.PAUSED.getStatus());
updateDO.setUpdateTime(LocalDateTime.now());
strategyExecutionMapper.updateById(updateDO);
// 此处应有实际的策略暂停逻辑
}
/**
* 停止策略执行
*
* @param id 策略执行ID
*/
@Override
public void stopStrategyExecution(Long id) {
StrategyExecutionDO execution = validateStrategyExecutionExists(id);
if (Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.COMPLETED.getStatus()) ||
Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.FAILED.getStatus()) ||
Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.STOPPED.getStatus())) {
throw exception(ErrorCodeConstants.STRATEGY_EXECUTION_STATUS_CANNOT_STOP);
}
StrategyExecutionDO updateDO = new StrategyExecutionDO();
updateDO.setId(id);
updateDO.setStatus(StrategyExecutionStatusEnum.STOPPED.getStatus());
updateDO.setEndTime(LocalDateTime.now()); // 记录停止时间
updateDO.setUpdateTime(LocalDateTime.now());
strategyExecutionMapper.updateById(updateDO);
// 此处应有实际的策略停止逻辑
}
/**
* 删除策略执行记录
*
* @param id 编号
*/
@Override
public void deleteStrategyExecution(Long id) {
// 校验存在
StrategyExecutionDO execution = validateStrategyExecutionExists(id);
// 校验是否运行中
if (Objects.equals(execution.getStatus(), StrategyExecutionStatusEnum.RUNNING.getStatus())) {
throw exception(ErrorCodeConstants.STRATEGY_EXECUTION_CANNOT_DELETE_WHEN_RUNNING);
}
// 删除
strategyExecutionMapper.deleteById(id);
}
/**
* 校验策略执行记录是否存在
*
* @param id 策略执行ID
* @return 策略执行实体
*/
private StrategyExecutionDO validateStrategyExecutionExists(Long id) {
StrategyExecutionDO execution = strategyExecutionMapper.selectById(id);
if (execution == null) {
throw exception(ErrorCodeConstants.STRATEGY_EXECUTION_NOT_EXISTS);
}
return execution;
}
/**
* 获取策略执行记录
*
* @param id 编号
* @return 策略执行记录
*/
@Override
public StrategyExecutionDO getStrategyExecution(Long id) {
return strategyExecutionMapper.selectById(id);
}
/**
* 获取策略执行记录列表
*
* @param ids 编号列表
* @return 策略执行记录列表
*/
@Override
public List<StrategyExecutionDO> getStrategyExecutionList(List<Long> ids) {
return strategyExecutionMapper.selectBatchIds(ids);
}
/**
* 获取策略执行记录分页
*
* @param pageReqVO 分页查询
* @return 策略执行记录分页
*/
@Override
public PageResult<StrategyExecutionDO> getStrategyExecutionPage(StrategyExecutionPageReqVO pageReqVO) {
return strategyExecutionMapper.selectPage(pageReqVO);
}
/**
* 根据策略定义ID获取策略执行记录列表
*
* @param strategyDefinitionId 策略定义ID
* @return 策略执行记录列表
*/
@Override
public List<StrategyExecutionDO> getStrategyExecutionListByDefinitionId(Long strategyDefinitionId) {
return strategyExecutionMapper.selectListByStrategyDefinitionId(strategyDefinitionId);
}
}
@@ -0,0 +1,30 @@
package com.trade.strategy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 策略服务启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 如果需要调用其他服务,则开启 Feign
public class StrategyApplication {
public static void main(String[] args) {
SpringApplication.run(StrategyApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 策略服务启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \\n" +
" | ( ' ) | \ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\ \ | || |(_,_)' \n" +
" | | \ `' /| `-' / \n" +
" | | \ / \ / \n" +
" ''-' `'-' `-..-' ");
}
}
@@ -0,0 +1,18 @@
package com.trade.strategy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 交易策略服务启动类
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeStrategyApplication {
public static void main(String[] args) {
SpringApplication.run(TradeStrategyApplication.class, args);
}
}
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的切面类 (Aspect Class)。
* <p>
* 切面类用于实现横切关注点,例如日志记录、事务管理、权限校验、性能监控等。
* 通常使用 Spring AOP 或 AspectJ 来定义和织入切面。
* </p>
*/
package com.trade.strategy.aspect;
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的配置类。
*/
package com.trade.strategy.config;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的常量类 (Constant Class)。
* <p>
* 常量类用于定义在应用程序中广泛使用的静态常量值,例如配置键名、默认参数、固定字符串等。
* 将常量集中管理有助于提高代码的可维护性和一致性。
* </p>
*/
package com.trade.strategy.constant;
@@ -0,0 +1,149 @@
package com.trade.strategy.controller;
import com.trade.strategy.entity.StrategyBacktest;
import com.trade.strategy.service.StrategyBacktestService;
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.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 策略回测API接口
*
* @author AI Assistant
*/
@RestController
@RequestMapping("/api/v1/strategies/backtests")
@Tag(name = "StrategyBacktestController", description = "策略回测管理API")
public class StrategyBacktestController {
@Autowired
private StrategyBacktestService strategyBacktestService;
/**
* 创建新的策略回测任务
*
* @param strategyBacktest 回测任务基本信息
* @return 创建的回测任务
*/
@PostMapping
@Operation(summary = "创建新的策略回测任务")
public ResponseEntity<StrategyBacktest> createBacktest(@RequestBody StrategyBacktest strategyBacktest) {
try {
StrategyBacktest createdBacktest = strategyBacktestService.createBacktest(strategyBacktest);
return new ResponseEntity<>(createdBacktest, HttpStatus.CREATED);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(null); // Consider returning error message
}
}
/**
* 启动一个指定ID的回测任务
*
* @param backtestId 回测任务ID
* @return 启动后的回测任务状态
*/
@PostMapping("/{backtestId}/start")
@Operation(summary = "启动一个指定ID的回测任务")
public ResponseEntity<StrategyBacktest> startBacktest(@PathVariable Long backtestId) {
try {
StrategyBacktest startedBacktest = strategyBacktestService.startBacktest(backtestId);
return ResponseEntity.ok(startedBacktest);
} catch (IllegalArgumentException | IllegalStateException e) {
return ResponseEntity.badRequest().body(null); // Consider returning error message
}
}
/**
* (模拟) 更新回测任务的状态和结果 - 实际应由回测引擎回调
*
* @param backtestId 回测任务ID
* @param payload 包含 status 和 resultDetails 的请求体
* @return 更新后的回测任务
*/
@PutMapping("/{backtestId}/update-result") // This endpoint is more for internal/engine callback simulation
@Operation(summary = "(模拟)更新回测任务的状态和结果", description = "实际场景中此接口可能由回测引擎内部调用或通过消息队列触发")
public ResponseEntity<StrategyBacktest> updateBacktestResult(
@PathVariable Long backtestId,
@RequestBody Map<String, String> payload) {
try {
String status = payload.get("status");
String resultDetails = payload.get("resultDetails");
if (status == null || resultDetails == null) {
return ResponseEntity.badRequest().body(null); // Missing parameters
}
StrategyBacktest updatedBacktest = strategyBacktestService.updateBacktestStatusAndResult(backtestId, status, resultDetails);
return ResponseEntity.ok(updatedBacktest);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(null);
}
}
/**
* 根据ID查询策略回测记录
*
* @param backtestId 回测任务ID
* @return 回测任务详情
*/
@GetMapping("/{backtestId}")
@Operation(summary = "根据ID查询策略回测记录")
public ResponseEntity<StrategyBacktest> getBacktestById(@PathVariable Long backtestId) {
StrategyBacktest backtest = strategyBacktestService.getBacktestById(backtestId);
if (backtest != null) {
return ResponseEntity.ok(backtest);
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 查询指定策略定义的所有回测记录
*
* @param strategyDefinitionId 策略定义ID
* @return 回测记录列表
*/
@GetMapping("/by-definition/{strategyDefinitionId}")
@Operation(summary = "查询指定策略定义的所有回测记录")
public ResponseEntity<List<StrategyBacktest>> getBacktestsByStrategyDefinitionId(@PathVariable Long strategyDefinitionId) {
List<StrategyBacktest> backtests = strategyBacktestService.getBacktestsByStrategyDefinitionId(strategyDefinitionId);
return ResponseEntity.ok(backtests);
}
/**
* 查询所有策略回测记录
*
* @return 所有回测记录列表
*/
@GetMapping
@Operation(summary = "查询所有策略回测记录")
public ResponseEntity<List<StrategyBacktest>> getAllBacktests() {
List<StrategyBacktest> backtests = strategyBacktestService.getAllBacktests();
return ResponseEntity.ok(backtests);
}
/**
* 根据ID删除策略回测记录
*
* @param backtestId 回测任务ID
* @return 操作结果
*/
@DeleteMapping("/{backtestId}")
@Operation(summary = "根据ID删除策略回测记录")
public ResponseEntity<Void> deleteBacktest(@PathVariable Long backtestId) {
try {
strategyBacktestService.deleteBacktest(backtestId);
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
} catch (IllegalStateException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); // Or another appropriate status for non-deletable state
}
}
}
@@ -0,0 +1,151 @@
package com.trade.strategy.controller;
import com.trade.strategy.entity.StrategyDefinition;
import com.trade.strategy.service.StrategyDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 交易策略定义API接口
*
* @author AI Assistant
*/
@RestController
@RequestMapping("/api/v1/strategies/definitions")
@Tag(name = "StrategyDefinitionController", description = "交易策略定义管理API")
public class StrategyDefinitionController {
@Autowired
private StrategyDefinitionService strategyDefinitionService;
/**
* 创建新的交易策略定义
*
* @param strategyDefinition 策略定义对象
* @return 创建的策略定义
*/
@PostMapping
@Operation(summary = "创建新的交易策略定义")
public ResponseEntity<StrategyDefinition> createStrategy(@RequestBody StrategyDefinition strategyDefinition) {
try {
StrategyDefinition createdStrategy = strategyDefinitionService.createStrategy(strategyDefinition);
return new ResponseEntity<>(createdStrategy, HttpStatus.CREATED);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(null); // 或者返回更详细的错误信息
}
}
/**
* 更新已有的交易策略定义
*
* @param id 策略ID
* @param strategyDefinition 策略定义对象
* @return 更新后的策略定义
*/
@PutMapping("/{id}")
@Operation(summary = "更新已有的交易策略定义")
public ResponseEntity<StrategyDefinition> updateStrategy(@PathVariable Long id, @RequestBody StrategyDefinition strategyDefinition) {
try {
strategyDefinition.setId(id); // 确保ID一致
StrategyDefinition updatedStrategy = strategyDefinitionService.updateStrategy(strategyDefinition);
return ResponseEntity.ok(updatedStrategy);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(null); // 或者返回更详细的错误信息
}
}
/**
* 根据ID删除交易策略定义
*
* @param id 策略ID
* @return 操作结果
*/
@DeleteMapping("/{id}")
@Operation(summary = "根据ID删除交易策略定义")
public ResponseEntity<Void> deleteStrategy(@PathVariable Long id) {
try {
strategyDefinitionService.deleteStrategy(id);
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
/**
* 根据ID查询交易策略定义
*
* @param id 策略ID
* @return 策略定义
*/
@GetMapping("/{id}")
@Operation(summary = "根据ID查询交易策略定义")
public ResponseEntity<StrategyDefinition> getStrategyById(@PathVariable Long id) {
StrategyDefinition strategy = strategyDefinitionService.getStrategyById(id);
if (strategy != null) {
return ResponseEntity.ok(strategy);
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 查询所有交易策略定义
*
* @return 策略定义列表
*/
@GetMapping
@Operation(summary = "查询所有交易策略定义")
public ResponseEntity<List<StrategyDefinition>> getAllStrategies() {
List<StrategyDefinition> strategies = strategyDefinitionService.getAllStrategies();
return ResponseEntity.ok(strategies);
}
/**
* 根据策略名称查询交易策略定义
*
* @param name 策略名称
* @return 策略定义
*/
@GetMapping("/by-name")
@Operation(summary = "根据策略名称查询交易策略定义")
public ResponseEntity<StrategyDefinition> getStrategyByName(@RequestParam String name) {
StrategyDefinition strategy = strategyDefinitionService.getStrategyByName(name);
if (strategy != null) {
return ResponseEntity.ok(strategy);
} else {
return ResponseEntity.notFound().build();
}
}
/**
* 根据策略类型查询交易策略定义
*
* @param type 策略类型
* @return 策略定义列表
*/
@GetMapping("/by-type")
@Operation(summary = "根据策略类型查询交易策略定义")
public ResponseEntity<List<StrategyDefinition>> getStrategiesByType(@RequestParam String type) {
List<StrategyDefinition> strategies = strategyDefinitionService.getStrategiesByType(type);
return ResponseEntity.ok(strategies);
}
/**
* 根据策略状态查询交易策略定义
*
* @param status 策略状态
* @return 策略定义列表
*/
@GetMapping("/by-status")
@Operation(summary = "根据策略状态查询交易策略定义")
public ResponseEntity<List<StrategyDefinition>> getStrategiesByStatus(@RequestParam String status) {
List<StrategyDefinition> strategies = strategyDefinitionService.getStrategiesByStatus(status);
return ResponseEntity.ok(strategies);
}
}
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的Controller层,负责处理HTTP请求和响应。
*/
package com.trade.strategy.controller;
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的数据传输对象 (DTO)。
*/
package com.trade.strategy.dto;
@@ -0,0 +1,4 @@
/**
* 此包存放策略执行引擎相关的类,包括策略的加载、执行、事件处理等。
*/
package com.trade.strategy.engine;
@@ -0,0 +1,84 @@
package com.trade.strategy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 策略回测记录实体类
*
* @author AI Assistant
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ts_strategy_backtest")
public class StrategyBacktest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 回测ID (主键, 自增)
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 关联的策略定义ID
*/
private Long strategyDefinitionId;
/**
* 回测名称 (用户自定义)
*/
private String backtestName;
/**
* 回测开始日期
*/
private LocalDate startDate;
/**
* 回测结束日期
*/
private LocalDate endDate;
/**
* 初始资金
*/
private BigDecimal initialCapital;
/**
* 手续费率 (例如: 0.0003 表示万分之三)
*/
private BigDecimal commissionRate;
/**
* 回测状态 (例如: "PENDING", "RUNNING", "COMPLETED", "FAILED")
* 可以考虑使用枚举类定义
*/
private String status;
/**
* 回测结果 (JSON格式,存储回测的各项指标)
* 例如: {"total_return": 0.15, "annualized_return": 0.30, "sharpe_ratio": 1.5, "max_drawdown": 0.10}
*/
private String resultDetails; // 数据库中建议使用TEXT/JSON类型
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间 (例如回测完成或失败时更新)
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,84 @@
package com.trade.strategy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 交易策略定义实体类
*
* @author AI Assistant
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ts_strategy_definition")
public class StrategyDefinition implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 策略ID (主键, 自增)
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 策略名称 (必填, 唯一)
*/
private String name;
/**
* 策略描述
*/
private String description;
/**
* 策略作者
*/
private String author;
/**
* 策略版本
*/
private String version;
/**
* 策略类型 (例如: "TREND_FOLLOWING", "MEAN_REVERSION", "EVENT_DRIVEN")
* 可以考虑使用枚举类定义
*/
private String type;
/**
* 策略脚本内容或配置 (存储策略的具体逻辑或配置)
* 对于复杂策略,可能存储脚本语言代码 (如Python, Groovy) 或JSON/XML配置
*/
private String scriptContent; // 数据库中建议使用TEXT类型
/**
* 策略参数 (JSON格式,存储策略的可配置参数及其默认值)
* 例如: {"MA_short": 5, "MA_long": 20, "stop_loss_pct": 0.05}
*/
private String parameters; // 数据库中建议使用TEXT/JSON类型
/**
* 策略状态 (例如: "DRAFT", "ACTIVE", "INACTIVE", "ARCHIVED")
* 可以考虑使用枚举类定义
*/
private String status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的数据库实体类。
*/
package com.trade.strategy.entity;
@@ -0,0 +1,5 @@
/**
* 此包存放策略服务模块相关的枚举类。
* 例如:策略状态枚举、信号类型枚举等。
*/
package com.trade.strategy.enums;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的过滤器类 (Filter Class)。
* <p>
* 过滤器类用于在 Servlet 容器处理请求之前或之后执行某些操作,例如请求参数校验、字符编码转换、安全过滤等。
* 它们是 Servlet规范的一部分。
* </p>
*/
package com.trade.strategy.filter;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的处理器类 (Handler Class)。
* <p>
* 处理器类通常用于处理特定类型的事件、消息或请求,例如消息队列的消费者、WebSocket 处理器、定时任务处理器等。
* 它们封装了对特定输入的响应逻辑。
* </p>
*/
package com.trade.strategy.handler;
@@ -0,0 +1,5 @@
/**
* 此包存放策略中可能用到的技术指标计算相关的类。
* 考虑是否将通用指标计算放到独立的 trade-indicator 模块。
*/
package com.trade.strategy.indicator;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的拦截器类 (Interceptor Class)。
* <p>
* 拦截器类用于在 Spring MVC 处理请求的过程中拦截和处理请求,例如权限验证、日志记录、性能监控等。
* 它们提供了比过滤器更细粒度的控制,并且可以访问 Spring MVC 的上下文信息。
* </p>
*/
package com.trade.strategy.interceptor;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的定时任务类 (Job Class)。
* <p>
* 定时任务类用于执行周期性或预定时间的后台任务,例如数据同步、报表生成、缓存刷新等。
* 通常使用 Spring Task 或 Quartz 等框架来实现。
* </p>
*/
package com.trade.strategy.job;
@@ -0,0 +1,4 @@
/**
* 此包存放事件监听器,例如监听行情事件、订单回报事件等,并根据事件驱动策略执行。
*/
package com.trade.strategy.listener;
@@ -0,0 +1,15 @@
package com.trade.strategy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.strategy.entity.StrategyBacktest;
import org.apache.ibatis.annotations.Mapper;
/**
* 策略回测记录 Mapper 接口
*
* @author AI Assistant
*/
@Mapper
public interface StrategyBacktestMapper extends BaseMapper<StrategyBacktest> {
}
@@ -0,0 +1,15 @@
package com.trade.strategy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.strategy.entity.StrategyDefinition;
import org.apache.ibatis.annotations.Mapper;
/**
* 交易策略定义 Mapper 接口
*
* @author AI Assistant
*/
@Mapper
public interface StrategyDefinitionMapper extends BaseMapper<StrategyDefinition> {
}
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的Mapper接口,用于数据库操作(例如存储策略配置、回测结果等)。
*/
package com.trade.strategy.mapper;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的领域模型 (Domain Model) 或实体类 (Entity)。
* <p>
* 这些类通常映射到数据库表结构,代表了系统中的核心业务对象。
* 它们包含了对象的属性以及可能的操作方法。
* </p>
*/
package com.trade.strategy.model;
@@ -0,0 +1,4 @@
/**
* 策略服务模块主包。
*/
package com.trade.strategy;
@@ -0,0 +1,4 @@
/**
* 此包存放风险管理相关的类,例如仓位管理、止盈止损逻辑等。
*/
package com.trade.strategy.risk;
@@ -0,0 +1,4 @@
/**
* 此包存放定时任务相关的类,例如定时执行策略、数据同步等。
*/
package com.trade.strategy.scheduler;
@@ -0,0 +1,74 @@
package com.trade.strategy.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.strategy.entity.StrategyBacktest;
import java.util.List;
/**
* 策略回测服务接口
*
* @author AI Assistant
*/
public interface StrategyBacktestService extends IService<StrategyBacktest> {
/**
* 创建并初始化一个新的策略回测任务
*
* @param strategyBacktest 回测任务基本信息 (strategyDefinitionId, backtestName, startDate, endDate, initialCapital, commissionRate 必须)
* @return 创建成功后的回测任务对象,包含生成的ID和初始状态
*/
StrategyBacktest createBacktest(StrategyBacktest strategyBacktest);
/**
* 启动一个指定ID的回测任务
* (实际回测逻辑可能异步执行)
*
* @param backtestId 回测任务ID
* @return 更新状态后的回测任务对象 (例如: 状态变为RUNNING)
*/
StrategyBacktest startBacktest(Long backtestId);
/**
* 更新回测任务的状态和结果
* (通常在异步回测任务完成后调用)
*
* @param backtestId 回测任务ID
* @param status 新的状态 (例如: "COMPLETED", "FAILED")
* @param resultDetails 回测结果详情 (JSON格式)
* @return 更新后的回测任务对象
*/
StrategyBacktest updateBacktestStatusAndResult(Long backtestId, String status, String resultDetails);
/**
* 根据ID查询策略回测记录
*
* @param backtestId 回测任务ID
* @return 回测任务对象,如果不存在则返回null
*/
StrategyBacktest getBacktestById(Long backtestId);
/**
* 查询指定策略定义的所有回测记录
*
* @param strategyDefinitionId 策略定义ID
* @return 该策略的回测记录列表
*/
List<StrategyBacktest> getBacktestsByStrategyDefinitionId(Long strategyDefinitionId);
/**
* 查询所有策略回测记录
*
* @return 所有回测记录列表
*/
List<StrategyBacktest> getAllBacktests();
/**
* 根据ID删除策略回测记录
* (通常只允许删除未开始或已失败的回测)
*
* @param backtestId 回测任务ID
*/
void deleteBacktest(Long backtestId);
}
@@ -0,0 +1,77 @@
package com.trade.strategy.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.strategy.entity.StrategyDefinition;
import java.util.List;
/**
* 交易策略定义服务接口
*
* @author AI Assistant
*/
public interface StrategyDefinitionService extends IService<StrategyDefinition> {
/**
* 创建新的交易策略定义
*
* @param strategyDefinition 策略定义对象
* @return 创建成功后的策略定义对象,包含生成的ID
*/
StrategyDefinition createStrategy(StrategyDefinition strategyDefinition);
/**
* 更新已有的交易策略定义
*
* @param strategyDefinition 策略定义对象
* @return 更新成功后的策略定义对象
*/
StrategyDefinition updateStrategy(StrategyDefinition strategyDefinition);
/**
* 根据ID删除交易策略定义
*
* @param id 策略ID
*/
void deleteStrategy(Long id);
/**
* 根据ID查询交易策略定义
*
* @param id 策略ID
* @return 策略定义对象,如果不存在则返回null
*/
StrategyDefinition getStrategyById(Long id);
/**
* 查询所有交易策略定义
*
* @return 策略定义列表
*/
List<StrategyDefinition> getAllStrategies();
/**
* 根据策略名称查询交易策略定义
*
* @param name 策略名称
* @return 策略定义对象,如果不存在则返回null
*/
StrategyDefinition getStrategyByName(String name);
/**
* 根据策略类型查询交易策略定义
*
* @param type 策略类型
* @return 符合类型的策略定义列表
*/
List<StrategyDefinition> getStrategiesByType(String type);
/**
* 根据策略状态查询交易策略定义
*
* @param status 策略状态
* @return 符合状态的策略定义列表
*/
List<StrategyDefinition> getStrategiesByStatus(String status);
}
@@ -0,0 +1,179 @@
package com.trade.strategy.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.strategy.entity.StrategyBacktest;
import com.trade.strategy.entity.StrategyDefinition;
import com.trade.strategy.mapper.StrategyBacktestMapper;
import com.trade.strategy.service.StrategyBacktestService;
import com.trade.strategy.service.StrategyDefinitionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 策略回测服务实现类
*
* @author AI Assistant
*/
@Service
public class StrategyBacktestServiceImpl extends ServiceImpl<StrategyBacktestMapper, StrategyBacktest> implements StrategyBacktestService {
@Autowired
private StrategyDefinitionService strategyDefinitionService; // 用于校验策略定义是否存在
// 假设有一个异步执行回测的组件,这里仅作演示
// @Autowired
// private BacktestExecutionEngine backtestExecutionEngine;
/**
* 创建并初始化一个新的策略回测任务
*
* @param strategyBacktest 回测任务基本信息
* @return 创建成功后的回测任务对象
*/
@Override
@Transactional
public StrategyBacktest createBacktest(StrategyBacktest strategyBacktest) {
// 参数校验
if (strategyBacktest.getStrategyDefinitionId() == null) {
throw new IllegalArgumentException("策略定义ID不能为空");
}
StrategyDefinition definition = strategyDefinitionService.getStrategyById(strategyBacktest.getStrategyDefinitionId());
if (definition == null) {
throw new IllegalArgumentException("指定的策略定义不存在: ID " + strategyBacktest.getStrategyDefinitionId());
}
if (!StringUtils.hasText(strategyBacktest.getBacktestName())) {
throw new IllegalArgumentException("回测名称不能为空");
}
if (strategyBacktest.getStartDate() == null || strategyBacktest.getEndDate() == null) {
throw new IllegalArgumentException("回测开始或结束日期不能为空");
}
if (strategyBacktest.getStartDate().isAfter(strategyBacktest.getEndDate())) {
throw new IllegalArgumentException("回测开始日期不能晚于结束日期");
}
if (strategyBacktest.getInitialCapital() == null || strategyBacktest.getInitialCapital().doubleValue() <= 0) {
throw new IllegalArgumentException("初始资金必须大于0");
}
// 手续费率可以为0
if (strategyBacktest.getCommissionRate() == null || strategyBacktest.getCommissionRate().doubleValue() < 0) {
strategyBacktest.setCommissionRate(java.math.BigDecimal.ZERO);
}
strategyBacktest.setStatus("PENDING"); // 初始状态为待处理
strategyBacktest.setCreateTime(LocalDateTime.now());
strategyBacktest.setUpdateTime(LocalDateTime.now());
this.save(strategyBacktest);
return strategyBacktest;
}
/**
* 启动一个指定ID的回测任务
*
* @param backtestId 回测任务ID
* @return 更新状态后的回测任务对象
*/
@Override
@Transactional
public StrategyBacktest startBacktest(Long backtestId) {
StrategyBacktest backtest = getBacktestById(backtestId);
if (backtest == null) {
throw new IllegalArgumentException("未找到要启动的回测任务,ID: " + backtestId);
}
if (!"PENDING".equals(backtest.getStatus())) {
throw new IllegalStateException("回测任务当前状态为 " + backtest.getStatus() + ",无法启动。");
}
backtest.setStatus("RUNNING");
backtest.setUpdateTime(LocalDateTime.now());
this.updateById(backtest);
// 实际场景中,这里会触发异步回测引擎执行回测
// backtestExecutionEngine.execute(backtest);
// log.info("策略回测任务 [{}] 已启动", backtestId);
return backtest;
}
/**
* 更新回测任务的状态和结果
*
* @param backtestId 回测任务ID
* @param status 新的状态
* @param resultDetails 回测结果详情
* @return 更新后的回测任务对象
*/
@Override
@Transactional
public StrategyBacktest updateBacktestStatusAndResult(Long backtestId, String status, String resultDetails) {
StrategyBacktest backtest = getBacktestById(backtestId);
if (backtest == null) {
throw new IllegalArgumentException("未找到要更新的回测任务,ID: " + backtestId);
}
backtest.setStatus(status);
backtest.setResultDetails(resultDetails);
backtest.setUpdateTime(LocalDateTime.now());
this.updateById(backtest);
return backtest;
}
/**
* 根据ID查询策略回测记录
*
* @param backtestId 回测任务ID
* @return 回测任务对象
*/
@Override
public StrategyBacktest getBacktestById(Long backtestId) {
return this.getById(backtestId);
}
/**
* 查询指定策略定义的所有回测记录
*
* @param strategyDefinitionId 策略定义ID
* @return 回测记录列表
*/
@Override
public List<StrategyBacktest> getBacktestsByStrategyDefinitionId(Long strategyDefinitionId) {
if (strategyDefinitionId == null) {
return List.of();
}
return this.list(new QueryWrapper<StrategyBacktest>().eq("strategy_definition_id", strategyDefinitionId)
.orderByDesc("create_time")); // 按创建时间降序
}
/**
* 查询所有策略回测记录
*
* @return 所有回测记录列表
*/
@Override
public List<StrategyBacktest> getAllBacktests() {
return this.list(new QueryWrapper<StrategyBacktest>().orderByDesc("create_time"));
}
/**
* 根据ID删除策略回测记录
*
* @param backtestId 回测任务ID
*/
@Override
@Transactional
public void deleteBacktest(Long backtestId) {
StrategyBacktest backtest = getBacktestById(backtestId);
if (backtest == null) {
throw new IllegalArgumentException("未找到要删除的回测任务,ID: " + backtestId);
}
// 通常只允许删除 PENDING 或 FAILED 的回测,或者根据业务需求调整
if ("RUNNING".equals(backtest.getStatus()) || "COMPLETED".equals(backtest.getStatus())) {
// throw new IllegalStateException("无法删除正在运行或已完成的回测任务。");
}
this.removeById(backtestId);
}
}
@@ -0,0 +1,160 @@
package com.trade.strategy.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.strategy.entity.StrategyDefinition;
import com.trade.strategy.mapper.StrategyDefinitionMapper;
import com.trade.strategy.service.StrategyDefinitionService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
/**
* 交易策略定义服务实现类
*
* @author AI Assistant
*/
@Service
public class StrategyDefinitionServiceImpl extends ServiceImpl<StrategyDefinitionMapper, StrategyDefinition> implements StrategyDefinitionService {
/**
* 创建新的交易策略定义
*
* @param strategyDefinition 策略定义对象
* @return 创建成功后的策略定义对象,包含生成的ID
*/
@Override
@Transactional
public StrategyDefinition createStrategy(StrategyDefinition strategyDefinition) {
// 可以在这里添加校验逻辑,例如名称是否唯一等
if (!StringUtils.hasText(strategyDefinition.getName())) {
throw new IllegalArgumentException("策略名称不能为空");
}
StrategyDefinition existing = getStrategyByName(strategyDefinition.getName());
if (existing != null) {
throw new IllegalArgumentException("策略名称已存在: " + strategyDefinition.getName());
}
strategyDefinition.setCreateTime(LocalDateTime.now());
strategyDefinition.setUpdateTime(LocalDateTime.now());
// 默认状态可以设置为 DRAFT
if (!StringUtils.hasText(strategyDefinition.getStatus())){
strategyDefinition.setStatus("DRAFT");
}
this.save(strategyDefinition);
return strategyDefinition;
}
/**
* 更新已有的交易策略定义
*
* @param strategyDefinition 策略定义对象
* @return 更新成功后的策略定义对象
*/
@Override
@Transactional
public StrategyDefinition updateStrategy(StrategyDefinition strategyDefinition) {
if (strategyDefinition.getId() == null) {
throw new IllegalArgumentException("更新策略时,策略ID不能为空");
}
StrategyDefinition existing = getById(strategyDefinition.getId());
if (existing == null) {
throw new IllegalArgumentException("未找到要更新的策略,ID: " + strategyDefinition.getId());
}
// 如果名称被修改,需要检查新名称是否已存在(排除自身)
if (StringUtils.hasText(strategyDefinition.getName()) && !strategyDefinition.getName().equals(existing.getName())){
StrategyDefinition byNewName = getStrategyByName(strategyDefinition.getName());
if(byNewName != null && !byNewName.getId().equals(strategyDefinition.getId())){
throw new IllegalArgumentException("策略名称已存在: " + strategyDefinition.getName());
}
}
strategyDefinition.setUpdateTime(LocalDateTime.now());
this.updateById(strategyDefinition);
return getById(strategyDefinition.getId()); // 返回更新后的完整对象
}
/**
* 根据ID删除交易策略定义
*
* @param id 策略ID
*/
@Override
@Transactional
public void deleteStrategy(Long id) {
if (id == null) {
throw new IllegalArgumentException("删除策略时,策略ID不能为空");
}
if (getById(id) == null) {
throw new IllegalArgumentException("未找到要删除的策略,ID: " + id);
}
this.removeById(id);
}
/**
* 根据ID查询交易策略定义
*
* @param id 策略ID
* @return 策略定义对象,如果不存在则返回null
*/
@Override
public StrategyDefinition getStrategyById(Long id) {
return this.getById(id);
}
/**
* 查询所有交易策略定义
*
* @return 策略定义列表
*/
@Override
public List<StrategyDefinition> getAllStrategies() {
return this.list();
}
/**
* 根据策略名称查询交易策略定义
*
* @param name 策略名称
* @return 策略定义对象,如果不存在则返回null
*/
@Override
public StrategyDefinition getStrategyByName(String name) {
if (!StringUtils.hasText(name)) {
return null;
}
return this.getOne(new QueryWrapper<StrategyDefinition>().eq("name", name));
}
/**
* 根据策略类型查询交易策略定义
*
* @param type 策略类型
* @return 符合类型的策略定义列表
*/
@Override
public List<StrategyDefinition> getStrategiesByType(String type) {
if (!StringUtils.hasText(type)) {
return this.list(); // 如果类型为空,返回所有
}
return this.list(new QueryWrapper<StrategyDefinition>().eq("type", type));
}
/**
* 根据策略状态查询交易策略定义
*
* @param status 策略状态
* @return 符合状态的策略定义列表
*/
@Override
public List<StrategyDefinition> getStrategiesByStatus(String status) {
if (!StringUtils.hasText(status)) {
return this.list(); // 如果状态为空,返回所有
}
return this.list(new QueryWrapper<StrategyDefinition>().eq("status", status));
}
}
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块Service接口的实现类。
*/
package com.trade.strategy.service.impl;
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的Service层,负责业务逻辑处理,例如策略的创建、执行、回测等。
*/
package com.trade.strategy.service;
@@ -0,0 +1,8 @@
/**
* 此包包含策略服务模块的工具类 (Utility Class)。
* <p>
* 工具类提供各种通用的辅助方法,例如日期时间处理、字符串操作、加解密、文件操作等。
* 它们通常包含静态方法,方便在应用程序的各个部分调用。
* </p>
*/
package com.trade.strategy.util;
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的工具类。
*/
package com.trade.strategy.utils;
@@ -0,0 +1,4 @@
/**
* 此包存放策略服务模块的视图对象 (VO)。
*/
package com.trade.strategy.vo;
@@ -0,0 +1,118 @@
package com.yudao.module.strategy.controller.admin.execution;
import com.yudao.framework.common.pojo.CommonResult;
import com.yudao.framework.common.pojo.PageResult;
import com.yudao.module.strategy.controller.admin.execution.vo.*;
import com.yudao.module.strategy.convert.execution.StrategyExecutionConvert;
import com.yudao.module.strategy.dal.dataobject.execution.StrategyExecution;
import com.yudao.module.strategy.service.execution.StrategyExecutionService;
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.Collection;
import java.util.List;
import static com.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 策略执行")
@RestController
@RequestMapping("/strategy/execution")
@Validated
public class StrategyExecutionController {
@Resource
private StrategyExecutionService strategyExecutionService;
@PostMapping("/create")
@Operation(summary = "创建策略执行")
@PreAuthorize("@ss.hasPermission('strategy:execution:create')")
public CommonResult<Long> createStrategyExecution(@Valid @RequestBody StrategyExecutionCreateReqVO createReqVO) {
return success(strategyExecutionService.createStrategyExecution(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新策略执行")
@PreAuthorize("@ss.hasPermission('strategy:execution:update')")
public CommonResult<Boolean> updateStrategyExecution(@Valid @RequestBody StrategyExecutionUpdateReqVO updateReqVO) {
strategyExecutionService.updateStrategyExecution(updateReqVO);
return success(true);
}
@PutMapping("/start")
@Operation(summary = "启动策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:start')")
public CommonResult<Boolean> startStrategyExecution(@RequestParam("id") Long id) {
strategyExecutionService.startStrategyExecution(id);
return success(true);
}
@PutMapping("/pause")
@Operation(summary = "暂停策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:pause')")
public CommonResult<Boolean> pauseStrategyExecution(@RequestParam("id") Long id) {
strategyExecutionService.pauseStrategyExecution(id);
return success(true);
}
@PutMapping("/stop")
@Operation(summary = "停止策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:stop')")
public CommonResult<Boolean> stopStrategyExecution(@RequestParam("id") Long id) {
strategyExecutionService.stopStrategyExecution(id);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:delete')")
public CommonResult<Boolean> deleteStrategyExecution(@RequestParam("id") Long id) {
strategyExecutionService.deleteStrategyExecution(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得策略执行")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<StrategyExecutionRespVO> getStrategyExecution(@RequestParam("id") Long id) {
StrategyExecution execution = strategyExecutionService.getStrategyExecution(id);
return success(StrategyExecutionConvert.INSTANCE.convert(execution));
}
@GetMapping("/list")
@Operation(summary = "获得策略执行列表")
@Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<List<StrategyExecutionRespVO>> getStrategyExecutionList(@RequestParam("ids") Collection<Long> ids) {
List<StrategyExecution> list = strategyExecutionService.getStrategyExecutionList(ids);
return success(StrategyExecutionConvert.INSTANCE.convertList(list));
}
@GetMapping("/page")
@Operation(summary = "获得策略执行分页")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<PageResult<StrategyExecutionRespVO>> getStrategyExecutionPage(@Valid StrategyExecutionPageReqVO pageVO) {
PageResult<StrategyExecution> pageResult = strategyExecutionService.getStrategyExecutionPage(pageVO);
return success(StrategyExecutionConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/list-by-definition")
@Operation(summary = "根据策略定义ID获得策略执行列表")
@Parameter(name = "strategyDefinitionId", description = "策略定义ID", required = true, example = "100")
@PreAuthorize("@ss.hasPermission('strategy:execution:query')")
public CommonResult<List<StrategyExecutionRespVO>> getStrategyExecutionsByDefinitionId(@RequestParam("strategyDefinitionId") Long strategyDefinitionId) {
List<StrategyExecution> list = strategyExecutionService.getStrategyExecutionsByDefinitionId(strategyDefinitionId);
return success(StrategyExecutionConvert.INSTANCE.convertList(list));
}
}
@@ -0,0 +1,40 @@
package com.yudao.module.strategy.controller.admin.execution.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 策略执行创建 Request VO")
@Data
public class StrategyExecutionCreateReqVO {
@Schema(description = "策略定义ID", required = true, example = "1024")
@NotNull(message = "策略定义ID不能为空")
private Long strategyDefinitionId;
@Schema(description = "执行名称", required = true, example = "MA策略实盘执行-20240101")
@NotEmpty(message = "执行名称不能为空")
private String name;
@Schema(description = "执行类型(1:实盘交易,2:模拟交易)", required = true, example = "1")
@NotNull(message = "执行类型不能为空")
private Integer type;
@Schema(description = "初始资金", required = true, example = "100000.00")
@NotNull(message = "初始资金不能为空")
private BigDecimal initialCapital;
@Schema(description = "手续费率", required = true, example = "0.0003")
@NotNull(message = "手续费率不能为空")
private BigDecimal commissionRate;
@Schema(description = "执行参数(JSON格式)", example = "{\"stopLoss\": 0.05}")
private String parameters;
@Schema(description = "备注", example = "测试执行")
private String remark;
}
@@ -0,0 +1,40 @@
package com.yudao.module.strategy.controller.admin.execution.vo;
import com.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 策略执行分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class StrategyExecutionPageReqVO extends PageParam {
@Schema(description = "策略定义ID", example = "1024")
private Long strategyDefinitionId;
@Schema(description = "执行名称", example = "MA策略")
private String name;
@Schema(description = "执行类型(1:实盘交易,2:模拟交易)", example = "1")
private Integer type;
@Schema(description = "执行状态(0:未开始,1:运行中,2:已暂停,3:已停止,4:已完成,5:执行错误)", example = "1")
private Integer status;
@Schema(description = "开始执行时间范围 - 开始")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] startTime;
@Schema(description = "创建时间范围 - 开始")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}
@@ -0,0 +1,64 @@
package com.yudao.module.strategy.controller.admin.execution.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 策略执行 Response VO")
@Data
public class StrategyExecutionRespVO {
@Schema(description = "执行记录ID", required = true, example = "1")
private Long id;
@Schema(description = "策略定义ID", required = true, example = "1024")
private Long strategyDefinitionId;
@Schema(description = "执行名称", required = true, example = "MA策略实盘执行-20240101")
private String name;
@Schema(description = "执行类型(1:实盘交易,2:模拟交易)", required = true, example = "1")
private Integer type;
@Schema(description = "初始资金", required = true, example = "100000.00")
private BigDecimal initialCapital;
@Schema(description = "当前资金", required = true, example = "105000.00")
private BigDecimal currentCapital;
@Schema(description = "总盈亏金额", required = true, example = "5000.00")
private BigDecimal totalProfitLoss;
@Schema(description = "总盈亏比例", required = true, example = "0.05")
private BigDecimal totalProfitLossRatio;
@Schema(description = "手续费率", required = true, example = "0.0003")
private BigDecimal commissionRate;
@Schema(description = "执行状态(0:未开始,1:运行中,2:已暂停,3:已停止,4:已完成,5:执行错误)", required = true, example = "1")
private Integer status;
@Schema(description = "错误信息", example = "策略执行异常:无法获取行情数据")
private String errorMessage;
@Schema(description = "开始执行时间", example = "2024-01-01 09:30:00")
private LocalDateTime startTime;
@Schema(description = "结束执行时间", example = "2024-01-01 15:00:00")
private LocalDateTime endTime;
@Schema(description = "执行参数(JSON格式)", example = "{\"stopLoss\": 0.05}")
private String parameters;
@Schema(description = "备注", example = "测试执行")
private String remark;
@Schema(description = "创建时间", required = true)
private LocalDateTime createTime;
@Schema(description = "更新时间", required = true)
private LocalDateTime updateTime;
}
@@ -0,0 +1,37 @@
package com.yudao.module.strategy.controller.admin.execution.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Schema(description = "管理后台 - 策略执行更新 Request VO")
@Data
public class StrategyExecutionUpdateReqVO {
@Schema(description = "执行记录ID", required = true, example = "1")
@NotNull(message = "执行记录ID不能为空")
private Long id;
@Schema(description = "执行名称", example = "MA策略实盘执行-20240101-V2")
private String name;
@Schema(description = "执行类型(1:实盘交易,2:模拟交易)", example = "1")
private Integer type;
@Schema(description = "初始资金", example = "120000.00")
private BigDecimal initialCapital;
@Schema(description = "手续费率", example = "0.00025")
private BigDecimal commissionRate;
@Schema(description = "执行参数(JSON格式)", example = "{\"stopLoss\": 0.06, \"takeProfit\": 0.1}")
private String parameters;
@Schema(description = "备注", example = "更新执行参数")
private String remark;
// 注意:通常不直接通过API更新执行状态、当前资金、盈亏等,这些应由系统内部逻辑管理
// 如果确实需要手动调整状态(例如管理员干预),可以单独提供接口或在此VO中添加并谨慎处理
}
@@ -0,0 +1,46 @@
package com.yudao.module.strategy.convert.execution;
import com.yudao.framework.common.pojo.PageResult;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionCreateReqVO;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionRespVO;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionUpdateReqVO;
import com.yudao.module.strategy.dal.dataobject.execution.StrategyExecution;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 策略执行 Convert
*/
@Mapper
public interface StrategyExecutionConvert {
StrategyExecutionConvert INSTANCE = Mappers.getMapper(StrategyExecutionConvert.class);
/**
* 创建请求 VO 转换为 DO
*/
StrategyExecution convert(StrategyExecutionCreateReqVO bean);
/**
* 更新请求 VO 转换为 DO
*/
StrategyExecution convert(StrategyExecutionUpdateReqVO bean);
/**
* DO 转换为响应 VO
*/
StrategyExecutionRespVO convert(StrategyExecution bean);
/**
* DO 列表转换为响应 VO 列表
*/
List<StrategyExecutionRespVO> convertList(List<StrategyExecution> list);
/**
* 分页结果转换
*/
PageResult<StrategyExecutionRespVO> convertPage(PageResult<StrategyExecution> page);
}
@@ -0,0 +1,97 @@
package com.yudao.module.strategy.dal.dataobject.execution;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 策略执行记录 DO
*/
@TableName("strategy_execution")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class StrategyExecution extends BaseDO {
/**
* 执行记录ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 策略定义ID
*/
private Long strategyDefinitionId;
/**
* 执行名称
*/
private String name;
/**
* 执行类型(1:实盘交易,2:模拟交易)
*/
private Integer type;
/**
* 初始资金
*/
private BigDecimal initialCapital;
/**
* 当前资金
*/
private BigDecimal currentCapital;
/**
* 总盈亏金额
*/
private BigDecimal totalProfitLoss;
/**
* 总盈亏比例
*/
private BigDecimal totalProfitLossRatio;
/**
* 手续费率
*/
private BigDecimal commissionRate;
/**
* 执行状态(0:未开始,1:运行中,2:已暂停,3:已停止,4:已完成,5:执行错误)
*/
private Integer status;
/**
* 错误信息
*/
private String errorMessage;
/**
* 开始执行时间
*/
private LocalDateTime startTime;
/**
* 结束执行时间
*/
private LocalDateTime endTime;
/**
* 执行参数(JSON格式)
*/
private String parameters;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,15 @@
package com.yudao.module.strategy.dal.mysql.execution;
import com.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.yudao.module.strategy.dal.dataobject.execution.StrategyExecution;
import org.apache.ibatis.annotations.Mapper;
/**
* 策略执行记录 Mapper
*/
@Mapper
public interface StrategyExecutionMapper extends BaseMapperX<StrategyExecution> {
// 可以在此定义特定的查询方法,例如根据策略定义ID查询执行记录等
}
@@ -0,0 +1,93 @@
package com.yudao.module.strategy.service.execution;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionCreateReqVO;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionPageReqVO;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionUpdateReqVO;
import com.yudao.module.strategy.dal.dataobject.execution.StrategyExecution;
import com.yudao.framework.common.pojo.PageResult;
import javax.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
* 策略执行 Service 接口
*/
public interface StrategyExecutionService {
/**
* 创建策略执行记录
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createStrategyExecution(@Valid StrategyExecutionCreateReqVO createReqVO);
/**
* 更新策略执行记录
*
* @param updateReqVO 更新信息
*/
void updateStrategyExecution(@Valid StrategyExecutionUpdateReqVO updateReqVO);
/**
* 启动策略执行
*
* @param id 策略执行编号
*/
void startStrategyExecution(Long id);
/**
* 暂停策略执行
*
* @param id 策略执行编号
*/
void pauseStrategyExecution(Long id);
/**
* 停止策略执行
*
* @param id 策略执行编号
*/
void stopStrategyExecution(Long id);
/**
* 删除策略执行记录
*
* @param id 编号
*/
void deleteStrategyExecution(Long id);
/**
* 获得策略执行记录
*
* @param id 编号
* @return 策略执行记录
*/
StrategyExecution getStrategyExecution(Long id);
/**
* 获得策略执行记录列表
*
* @param ids 编号列表
* @return 策略执行记录列表
*/
List<StrategyExecution> getStrategyExecutionList(Collection<Long> ids);
/**
* 获得策略执行记录分页
*
* @param pageReqVO 分页查询
* @return 策略执行记录分页
*/
PageResult<StrategyExecution> getStrategyExecutionPage(StrategyExecutionPageReqVO pageReqVO);
/**
* 根据策略定义ID查询执行记录列表
*
* @param strategyDefinitionId 策略定义ID
* @return 策略执行记录列表
*/
List<StrategyExecution> getStrategyExecutionsByDefinitionId(Long strategyDefinitionId);
}
@@ -0,0 +1,192 @@
package com.yudao.module.strategy.service.execution;
import com.yudao.framework.common.pojo.PageResult;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionCreateReqVO;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionPageReqVO;
import com.yudao.module.strategy.controller.admin.execution.vo.StrategyExecutionUpdateReqVO;
import com.yudao.module.strategy.convert.execution.StrategyExecutionConvert;
import com.yudao.module.strategy.dal.dataobject.execution.StrategyExecution;
import com.yudao.module.strategy.dal.mysql.execution.StrategyExecutionMapper;
import com.yudao.module.strategy.enums.StrategyExecutionStatusEnum;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import static com.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.yudao.module.strategy.enums.ErrorCodeConstants.*;
/**
* 策略执行 Service 实现类
*/
@Service
@Validated
public class StrategyExecutionServiceImpl implements StrategyExecutionService {
@Resource
private StrategyExecutionMapper strategyExecutionMapper;
/**
* 创建策略执行记录
*
* @param createReqVO 创建信息
* @return 编号
*/
@Override
public Long createStrategyExecution(StrategyExecutionCreateReqVO createReqVO) {
// 校验策略定义是否存在等前置条件 (根据实际业务补充)
// ...
StrategyExecution strategyExecution = StrategyExecutionConvert.INSTANCE.convert(createReqVO);
strategyExecution.setStatus(StrategyExecutionStatusEnum.NOT_STARTED.getStatus()); // 初始状态为未开始
strategyExecutionMapper.insert(strategyExecution);
return strategyExecution.getId();
}
/**
* 更新策略执行记录
*
* @param updateReqVO 更新信息
*/
@Override
public void updateStrategyExecution(StrategyExecutionUpdateReqVO updateReqVO) {
validateStrategyExecutionExists(updateReqVO.getId());
StrategyExecution updateObj = StrategyExecutionConvert.INSTANCE.convert(updateReqVO);
strategyExecutionMapper.updateById(updateObj);
}
/**
* 启动策略执行
*
* @param id 策略执行编号
*/
@Override
public void startStrategyExecution(Long id) {
StrategyExecution execution = validateStrategyExecutionExists(id);
if (!StrategyExecutionStatusEnum.NOT_STARTED.getStatus().equals(execution.getStatus()) &&
!StrategyExecutionStatusEnum.PAUSED.getStatus().equals(execution.getStatus()) &&
!StrategyExecutionStatusEnum.ERROR.getStatus().equals(execution.getStatus())) {
throw exception(STRATEGY_EXECUTION_CANNOT_START, execution.getStatus());
}
// 实际启动逻辑,可能需要与交易引擎交互
// ...
StrategyExecution updateObj = new StrategyExecution();
updateObj.setId(id);
updateObj.setStatus(StrategyExecutionStatusEnum.RUNNING.getStatus());
updateObj.setStartTime(LocalDateTime.now()); // 记录开始时间
strategyExecutionMapper.updateById(updateObj);
}
/**
* 暂停策略执行
*
* @param id 策略执行编号
*/
@Override
public void pauseStrategyExecution(Long id) {
StrategyExecution execution = validateStrategyExecutionExists(id);
if (!StrategyExecutionStatusEnum.RUNNING.getStatus().equals(execution.getStatus())) {
throw exception(STRATEGY_EXECUTION_CANNOT_PAUSE, execution.getStatus());
}
// 实际暂停逻辑
// ...
StrategyExecution updateObj = new StrategyExecution();
updateObj.setId(id);
updateObj.setStatus(StrategyExecutionStatusEnum.PAUSED.getStatus());
strategyExecutionMapper.updateById(updateObj);
}
/**
* 停止策略执行
*
* @param id 策略执行编号
*/
@Override
public void stopStrategyExecution(Long id) {
StrategyExecution execution = validateStrategyExecutionExists(id);
if (StrategyExecutionStatusEnum.COMPLETED.getStatus().equals(execution.getStatus()) ||
StrategyExecutionStatusEnum.STOPPED.getStatus().equals(execution.getStatus())) {
throw exception(STRATEGY_EXECUTION_ALREADY_STOPPED_OR_COMPLETED, execution.getStatus());
}
// 实际停止逻辑,可能需要进行一些清理工作
// ...
StrategyExecution updateObj = new StrategyExecution();
updateObj.setId(id);
updateObj.setStatus(StrategyExecutionStatusEnum.STOPPED.getStatus());
updateObj.setEndTime(LocalDateTime.now()); // 记录结束时间
strategyExecutionMapper.updateById(updateObj);
}
/**
* 删除策略执行记录
*
* @param id 编号
*/
@Override
public void deleteStrategyExecution(Long id) {
validateStrategyExecutionExists(id);
// 需要考虑是否允许删除运行中的策略等
strategyExecutionMapper.deleteById(id);
}
/**
* 校验策略执行记录是否存在
*
* @param id 策略执行ID
* @return 策略执行记录
*/
private StrategyExecution validateStrategyExecutionExists(Long id) {
StrategyExecution execution = strategyExecutionMapper.selectById(id);
if (execution == null) {
throw exception(STRATEGY_EXECUTION_NOT_EXISTS);
}
return execution;
}
/**
* 获得策略执行记录
*
* @param id 编号
* @return 策略执行记录
*/
@Override
public StrategyExecution getStrategyExecution(Long id) {
return strategyExecutionMapper.selectById(id);
}
/**
* 获得策略执行记录列表
*
* @param ids 编号列表
* @return 策略执行记录列表
*/
@Override
public List<StrategyExecution> getStrategyExecutionList(Collection<Long> ids) {
return strategyExecutionMapper.selectBatchIds(ids);
}
/**
* 获得策略执行记录分页
*
* @param pageReqVO 分页查询
* @return 策略执行记录分页
*/
@Override
public PageResult<StrategyExecution> getStrategyExecutionPage(StrategyExecutionPageReqVO pageReqVO) {
return strategyExecutionMapper.selectPage(pageReqVO, StrategyExecutionConvert.INSTANCE.convert(pageReqVO));
}
/**
* 根据策略定义ID查询执行记录列表
*
* @param strategyDefinitionId 策略定义ID
* @return 策略执行记录列表
*/
@Override
public List<StrategyExecution> getStrategyExecutionsByDefinitionId(Long strategyDefinitionId) {
return strategyExecutionMapper.selectList(StrategyExecution::getStrategyDefinitionId, strategyDefinitionId);
}
}
@@ -0,0 +1,11 @@
server:
port: 8007
spring:
profiles:
active: dev
application:
name: trade-strategy
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8007
spring:
profiles:
active: prod
application:
name: trade-strategy
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8007
spring:
profiles:
active: test
application:
name: trade-strategy
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,103 @@
server:
port: 8083 # 策略服务端口
spring:
application:
name: trade-strategy # 应用名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务注册中心地址
namespace: trade_dev # Nacos 命名空间
group: DEFAULT_GROUP # Nacos 分组
config:
server-addr: 127.0.0.1:8848 # Nacos 配置中心地址
namespace: trade_dev # Nacos 命名空间
group: DEFAULT_GROUP # Nacos 分组
file-extension: yml # 配置文件格式
shared-configs: # 共享配置
- data-id: application-common.yml # 通用配置
group: DEFAULT_GROUP
refresh: true
- data-id: trade-redis.yml # Redis 配置
group: DEFAULT_GROUP
refresh: true
- data-id: trade-mybatis-plus.yml # MyBatis Plus 配置
group: DEFAULT_GROUP
refresh: true
# # Sentinel 配置 (按需开启)
# sentinel:
# transport:
# dashboard: localhost:8080 # Sentinel 控制台地址
# port: 8719 # Sentinel 客户端与控制台通信端口
# datasource:
# ds1:
# nacos:
# server-addr: ${spring.cloud.nacos.config.server-addr}
# namespace: ${spring.cloud.nacos.config.namespace}
# dataId: ${spring.application.name}-sentinel-rules
# groupId: SENTINEL_GROUP
# data-type: json
# rule-type: flow
# Seata 配置 (按需开启)
#seata:
# tx-service-group: trade_tx_group # 事务组名称,需要与 Seata Server 配置一致
# service:
# vgroup-mapping:
# trade_tx_group: default # 事务组与 Seata Server 实例的映射关系
# 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.config.server-addr}
# namespace: ${spring.cloud.nacos.config.namespace}
# group: SEATA_GROUP
# 日志配置
logging:
level:
com.trade.strategy: debug
org.springframework: info
org.apache.ibatis: info
# Actuator 配置
management:
endpoints:
web:
exposure:
include: '*' # 开放所有端点,生产环境建议按需开放
endpoint:
health:
show-details: always # 显示健康检查详情
# MyBatis Plus 配置 (如果 trade-mybatis-plus.yml 中未配置或需要覆盖)
#mybatis-plus:
# mapper-locations: classpath*:/mapper/**/*.xml # Mapper XML 文件位置
# global-config:
# db-config:
# id-type: assign_id # ID 生成策略
# 策略引擎相关配置 (示例)
strategy:
engine:
thread-pool-size: 10 # 策略执行线程池大小
data-source:
type: local # 数据源类型,例如: local, remote_api, message_queue
# ... 其他数据源相关配置
# 开放 API 文档配置 (如果需要)
#springdoc:
# api-docs:
# path: /v3/api-docs
# swagger-ui:
# path: /swagger-ui.html
# display-request-duration: true
# groups-order: DESC
# operations-sorter: alpha
# tags-sorter: alpha
@@ -0,0 +1,24 @@
# Spring Cloud Alibaba Nacos Config
spring:
application:
name: trade-strategy # 应用名称,必须与 Nacos 中的 Data ID 对应
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos Server 地址
namespace: trade_dev # Nacos 命名空间,用于隔离不同环境的配置
group: DEFAULT_GROUP # Nacos 分组,默认为 DEFAULT_GROUP
file-extension: yml # Data ID 配置文件格式,默认为 properties
# 如果配置了共享配置,这里可以不指定 Data ID,或者指定应用特有的配置
# data-ids: ${spring.application.name}.${spring.cloud.nacos.config.file-extension}
# auto-refresh: true # 是否自动刷新配置,默认为 true
# bootstrap: true # 是否在 bootstrap 阶段加载配置,默认为 true
discovery:
server-addr: 127.0.0.1:8848 # Nacos Server 地址
namespace: trade_dev # Nacos 命名空间
group: DEFAULT_GROUP # Nacos 分组
# 配置日志文件名称,确保在 Nacos 配置加载前,日志系统就能正确初始化
logging:
file:
name: logs/${spring.application.name}.log
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="com.trade.strategy" level="debug"/>
</configuration>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stock</groupId>
<artifactId>stock-trade-system</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.stock</groupId>
<artifactId>trade-user</artifactId>
<version>1.0.0</version>
<name>trade-user</name>
<description>trade-user</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.stock</groupId>
<artifactId>trade-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.14</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,54 @@
package com.stock.trading.user.controller;
import com.stock.trading.common.api.CommonResult;
import com.stock.trading.user.dto.UmsAdminLoginParam;
import com.stock.trading.user.dto.UmsAdminParam;
import com.stock.trading.user.model.UmsAdmin;
import com.stock.trading.user.service.UmsAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 后台用户管理Controller
*/
@RestController
@Api(tags = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private UmsAdminService adminService;
@ApiOperation(value = "用户注册")
@PostMapping("/register")
public CommonResult<UmsAdmin> register(@Validated @RequestBody UmsAdminParam umsAdminParam) {
UmsAdmin umsAdmin = adminService.register(umsAdminParam);
if (umsAdmin == null) {
return CommonResult.failed();
}
return CommonResult.success(umsAdmin);
}
@ApiOperation(value = "登录以后返回token")
@PostMapping("/login")
public CommonResult<Map<String, String>> login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam) {
String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
if (token == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
}
@@ -0,0 +1,21 @@
package com.stock.trading.user.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* 用户登录参数
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UmsAdminLoginParam {
@ApiModelProperty(value = "用户名", required = true)
@NotEmpty(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true)
@NotEmpty(message = "密码不能为空")
private String password;
}
@@ -0,0 +1,31 @@
package com.stock.trading.user.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
/**
* 用户注册参数
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UmsAdminParam {
@ApiModelProperty(value = "用户名", required = true)
@NotEmpty(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true)
@NotEmpty(message = "密码不能为空")
private String password;
@ApiModelProperty(value = "用户头像")
private String icon;
@ApiModelProperty(value = "邮箱")
@Email(message = "邮箱格式不合法")
private String email;
@ApiModelProperty(value = "用户昵称")
private String nickName;
@ApiModelProperty(value = "备注")
private String note;
}
@@ -0,0 +1,29 @@
package com.stock.trading.user.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.stock.trading.common.entity.BaseEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 后台用户表
* Created by macro on 2020/8/21.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_admin")
public class UmsAdmin extends BaseEntity {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "头像")
private String icon;
@ApiModelProperty(value = "邮箱")
private String email;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "备注信息")
private String note;
}
@@ -0,0 +1,16 @@
package com.stock.trading.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.trading.user.model.UmsAdmin;
/**
* <p>
* 后台用户表 Mapper 接口
* </p>
*
* @author macro
* @since 2020-08-21
*/
public interface UmsAdminMapper extends BaseMapper<UmsAdmin> {
}
@@ -0,0 +1,40 @@
package com.stock.trading.user.model;
import com.baomidou.mybatisplus.annotation.TableName;
import com.stock.trading.common.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 后台用户表
* </p>
*
* @author macro
* @since 2020-08-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_admin")
public class UmsAdmin extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String password;
private String icon;
private String email;
private String nickName;
private String note;
private Integer status;
}
@@ -0,0 +1,28 @@
package com.stock.trading.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.stock.trading.user.dto.UmsAdminParam;
import com.stock.trading.user.model.UmsAdmin;
/**
* 后台用户管理Service
*/
public interface UmsAdminService extends IService<UmsAdmin> {
/**
* 根据用户名获取后台管理员
*/
UmsAdmin getAdminByUsername(String username);
/**
* 注册功能
*/
UmsAdmin register(UmsAdminParam umsAdminParam);
/**
* 登录功能
* @param username 用户名
* @param password 密码
* @return 生成的JWT的token
*/
String login(String username, String password);
}
@@ -0,0 +1,82 @@
package com.stock.trading.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.stock.trading.common.exception.ApiException;
import com.stock.trading.common.util.JwtTokenUtil;
import com.stock.trading.user.dto.UmsAdminParam;
import com.stock.trading.user.mapper.UmsAdminMapper;
import com.stock.trading.user.model.UmsAdmin;
import com.stock.trading.user.service.UmsAdminService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* UmsAdminService实现类
*/
@Service
public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper, UmsAdmin> implements UmsAdminService {
private static final Logger LOGGER = LoggerFactory.getLogger(UmsAdminServiceImpl.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UmsAdminMapper adminMapper;
@Override
public UmsAdmin getAdminByUsername(String username) {
QueryWrapper<UmsAdmin> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
return adminMapper.selectOne(wrapper);
}
@Override
public UmsAdmin register(UmsAdminParam umsAdminParam) {
UmsAdmin umsAdmin = new UmsAdmin();
BeanUtils.copyProperties(umsAdminParam, umsAdmin);
umsAdmin.setCreateTime(new Date());
umsAdmin.setStatus(1);
//查询是否有相同用户名的用户
QueryWrapper<UmsAdmin> wrapper = new QueryWrapper<>();
wrapper.eq("username", umsAdmin.getUsername());
if (adminMapper.selectOne(wrapper) != null) {
throw new ApiException("该用户已经存在");
}
//将密码进行加密操作
String encodePassword = passwordEncoder.encode(umsAdmin.getPassword());
umsAdmin.setPassword(encodePassword);
adminMapper.insert(umsAdmin);
return umsAdmin;
}
@Override
public String login(String username, String password) {
String token = null;
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
} catch (Exception e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
return token;
}
}
@@ -0,0 +1,15 @@
package com.stock.user.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户控制器
*
* @author TraeAI
*/
@RestController
@RequestMapping("/user")
public class UserController {
// TODO: 实现用户相关接口
}
@@ -0,0 +1,56 @@
package com.stock.user.dal.dataobject;
import com.stock.common.dal.dataobject.BaseDO; // 引入公共BaseDO
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 用户DO
*
* @author TraeAI
*/
@TableName("trade_user") // TODO: 确认表名是否正确, 通常用户表名为 sys_user user
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class UserDO extends BaseDO {
/**
* ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password; // TODO: 密码字段需要加密存储
/**
* 手机号
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 用户状态例如启用禁用
*/
private Integer status;
// TODO: 根据实际需求添加更多字段
}
@@ -0,0 +1,15 @@
package com.stock.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.user.dal.dataobject.UserDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户 Mapper
*
* @author TraeAI
*/
@Mapper
public interface UserMapper extends BaseMapper<UserDO> {
// TODO: 定义用户相关的数据库操作方法
}
@@ -0,0 +1,10 @@
package com.stock.user.service;
/**
* 用户服务接口
*
* @author TraeAI
*/
public interface UserService {
// TODO: 定义用户服务相关方法
}
@@ -0,0 +1,14 @@
package com.stock.user.service.impl;
import com.stock.user.service.UserService;
import org.springframework.stereotype.Service;
/**
* 用户服务实现类
*
* @author TraeAI
*/
@Service
public class UserServiceImpl implements UserService {
// TODO: 实现用户服务相关方法
}
@@ -0,0 +1,17 @@
package com.trade.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 用户服务启动类
* @author Trade Team
*/
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = "com.trade")
public class TradeUserApplication {
public static void main(String[] args) {
SpringApplication.run(TradeUserApplication.class, args);
}
}
@@ -0,0 +1,38 @@
package com.trade.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
/**
* 用户服务启动类
*
* @author Trade Team
*/
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.trade.user.mapper") // 扫描Mapper接口
@ComponentScan(basePackages = {"com.trade.user", "com.trade.common.config"}) // 扫描通用配置例如全局异常处理
public class UserApplication {
/**
* 主函数启动用户服务应用
*
* @param args 命令行参数
*/
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ Trade User Service 启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ | \\\\ \\\\ / / \n" +
" | ( ' ) | \\\\ _. / ' \n" +
" |(_ o _) / _( )_ .' \n" +
" | (_,_).' __ ___(_ o _)' \n" +
" | |\\\\\\\\ \\ | || |(_,_)' \n" +
" | | \\\\ `' /| `-' / \n" +
" | | \\\\ / \\\\ / \n" +
" ''-' `'-' `-..-' ");
}
}
@@ -0,0 +1,58 @@
package com.trade.user.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
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 配置类
* <p>
* 用于配置MyBatis Plus的相关特性如分页插件Mapper接口扫描等
* </p>
*
* @author Trade Team
*/
@Configuration
@EnableTransactionManagement // 开启声明式事务管理
@MapperScan("com.trade.user.mapper") // 扫描Mapper接口UserApplication中已有这里可省略或保留以明确
public class MyBatisPlusConfig {
/**
* 配置MyBatis Plus拦截器用于支持分页等功能
* <p>
* 新版MyBatis Plus3.4.0+推荐使用 {@link MybatisPlusInterceptor}
* 旧版的分页插件是 {@code PaginationInterceptor}
* </p>
*
* @return MybatisPlusInterceptor 实例
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件并指定数据库类型为MySQL
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
// 如果需要其他MyBatis Plus的配置例如乐观锁插件SQL性能规范插件等可以在这里添加
// 例如
// /**
// * 乐观锁插件 (如果需要)
// */
// @Bean
// public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
// return new OptimisticLockerInnerInterceptor();
// }
// /**
// * 防止全表更新与删除插件 (如果需要)
// */
// @Bean
// public BlockAttackInnerInterceptor blockAttackInnerInterceptor() {
// return new BlockAttackInnerInterceptor();
// }
}
@@ -0,0 +1,38 @@
package com.trade.user.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();
}
}
@@ -0,0 +1,25 @@
package com.trade.user.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Swagger配置
* @author Trade Team
*/
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("用户服务")
.version("1.0")
.description("用户服务相关接口文档")
.contact(new Contact().name("Trade Team")));
}
}
@@ -0,0 +1,4 @@
/**
* 此包存放用户服务模块的配置类例如 MyBatis Plus 配置Swagger 配置等
*/
package com.trade.user.config;
@@ -0,0 +1,4 @@
/**
* 此包存放用户模块相关的常量定义
*/
package com.trade.user.constant;
@@ -0,0 +1,114 @@
package com.trade.user.controller;
import com.trade.common.api.CommonResult;
import com.trade.user.dto.UmsAdminLoginParam;
import com.trade.user.dto.UmsAdminParam;
import com.trade.user.dto.UmsAdminUpdatePasswordParam;
import com.trade.user.model.UmsAdmin;
import com.trade.user.model.UmsPermission;
import com.trade.user.service.UmsAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 后台用户管理Controller
* Created by macro on 2018/4/26.
*/
@Controller
@Api(tags = "UmsAdminController", description = "后台用户管理")
@RequestMapping("/admin")
public class UmsAdminController {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private UmsAdminService adminService;
@ApiOperation(value = "用户注册")
@RequestMapping(value = "/register", method = RequestMethod.POST)
@ResponseBody
public CommonResult<UmsAdmin> register(@Validated @RequestBody UmsAdminParam umsAdminParam) {
UmsAdmin umsAdmin = adminService.register(umsAdminParam);
if (umsAdmin == null) {
return CommonResult.failed();
}
return CommonResult.success(umsAdmin);
}
@ApiOperation(value = "登录以后返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public CommonResult login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam) {
String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword());
if (token == null) {
return CommonResult.validateFailed("用户名或密码错误");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
@ApiOperation(value = "刷新token")
@RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
@ResponseBody
public CommonResult refreshToken(HttpServletRequest request) {
String token = request.getHeader(tokenHeader);
String refreshToken = adminService.refreshToken(token);
if (refreshToken == null) {
return CommonResult.failed("token已经过期!");
}
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", refreshToken);
tokenMap.put("tokenHead", tokenHead);
return CommonResult.success(tokenMap);
}
@ApiOperation("获取当前登录用户信息")
@RequestMapping(value = "/info", method = RequestMethod.GET)
@ResponseBody
public CommonResult getAdminInfo(Principal principal) {
if(principal==null){
return CommonResult.unauthorized(null);
}
String username = principal.getName();
UmsAdmin umsAdmin = adminService.getAdminByUsername(username);
Map<String, Object> data = new HashMap<>();
data.put("username", umsAdmin.getUsername());
data.put("menus", "");
data.put("icon", umsAdmin.getIcon());
List<UmsPermission> permissionList = adminService.getPermissionList(umsAdmin.getId());
data.put("perms", permissionList);
return CommonResult.success(data);
}
@ApiOperation("修改指定用户密码")
@RequestMapping(value = "/updatePassword", method = RequestMethod.POST)
@ResponseBody
public CommonResult updatePassword(@Validated @RequestBody UmsAdminUpdatePasswordParam param) {
int status = adminService.updatePassword(param);
if (status > 0) {
return CommonResult.success(status);
} else if (status == -1) {
return CommonResult.failed("提交参数不合法");
} else if (status == -2) {
return CommonResult.failed("找不到该用户");
} else if (status == -3) {
return CommonResult.failed("旧密码错误");
} else {
return CommonResult.failed();
}
}
}
@@ -0,0 +1,76 @@
package com.trade.user.controller;
import com.trade.common.api.CommonResult;
import com.trade.user.model.UmsPermission;
import com.trade.user.service.UmsPermissionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
/**
* 后台用户权限管理
* Created by macro on 2018/9/29.
*/
@Controller
@Api(tags = "UmsPermissionController", description = "后台用户权限管理")
@RequestMapping("/permission")
public class UmsPermissionController {
@Autowired
private UmsPermissionService permissionService;
@ApiOperation("添加权限")
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public CommonResult create(@RequestBody UmsPermission permission) {
boolean success = permissionService.save(permission);
if (success) {
return CommonResult.success(permission);
}
return CommonResult.failed();
}
@ApiOperation("修改权限")
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
public CommonResult update(@RequestBody UmsPermission permission) {
boolean success = permissionService.updateById(permission);
if (success) {
return CommonResult.success(permission);
}
return CommonResult.failed();
}
@ApiOperation("根据id删除权限")
@RequestMapping(value = "/delete", method = RequestMethod.POST)
@ResponseBody
public CommonResult delete(@RequestBody List<Long> ids) {
boolean success = permissionService.removeByIds(ids);
if (success) {
return CommonResult.success(null);
}
return CommonResult.failed();
}
@ApiOperation("获取所有权限列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
public CommonResult<List<UmsPermission>> list() {
List<UmsPermission> permissionList = permissionService.list();
return CommonResult.success(permissionList);
}
@ApiOperation("以层级结构返回所有权限")
@RequestMapping(value = "/treeList", method = RequestMethod.GET)
@ResponseBody
public CommonResult<List<UmsPermission>> treeList() {
List<UmsPermission> permissionList = permissionService.treeList();
return CommonResult.success(permissionList);
}
}
@@ -0,0 +1,116 @@
package com.trade.user.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.trade.common.api.CommonPage;
import com.trade.common.api.CommonResult;
import com.trade.user.model.UmsPermission;
import com.trade.user.model.UmsRole;
import com.trade.user.service.UmsAdminService;
import com.trade.user.service.UmsRoleService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 后台用户角色管理
* Created by macro on 2018/9/30.
*/
@Controller
@Api(tags = "UmsRoleController", description = "后台用户角色管理")
@RequestMapping("/role")
public class UmsRoleController {
@Autowired
private UmsRoleService roleService;
@Autowired
private UmsAdminService adminService;
@ApiOperation("添加角色")
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public CommonResult create(@RequestBody UmsRole role) {
boolean success = roleService.save(role);
if (success) {
return CommonResult.success(null);
}
return CommonResult.failed();
}
@ApiOperation("修改角色")
@RequestMapping(value = "/update/{id}", method = RequestMethod.POST)
@ResponseBody
public CommonResult update(@PathVariable Long id, @RequestBody UmsRole role) {
role.setId(id);
boolean success = roleService.updateById(role);
if (success) {
return CommonResult.success(null);
}
return CommonResult.failed();
}
@ApiOperation("删除角色")
@RequestMapping(value = "/delete", method = RequestMethod.POST)
@ResponseBody
public CommonResult delete(@RequestParam("ids") List<Long> ids) {
boolean success = roleService.removeByIds(ids);
if (success) {
return CommonResult.success(null);
}
return CommonResult.failed();
}
@ApiOperation("获取所有角色")
@RequestMapping(value = "/listAll", method = RequestMethod.GET)
@ResponseBody
public CommonResult<List<UmsRole>> listAll() {
List<UmsRole> roleList = roleService.list();
return CommonResult.success(roleList);
}
@ApiOperation("根据角色名称分页获取角色列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
public CommonResult<CommonPage<UmsRole>> list(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "pageSize", defaultValue = "5") Integer pageSize,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum) {
Page<UmsRole> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<UmsRole> lambdaQueryWrapper = new LambdaQueryWrapper<>();
if (keyword!=null && keyword.length()>0){
lambdaQueryWrapper.like(UmsRole::getName, keyword);
}
Page<UmsRole> rolePage = roleService.page(page, lambdaQueryWrapper);
return CommonResult.success(CommonPage.restPage(rolePage));
}
@ApiOperation("获取角色相关权限")
@RequestMapping(value = "/listPermission/{roleId}", method = RequestMethod.GET)
@ResponseBody
public CommonResult<List<UmsPermission>> listPermission(@PathVariable Long roleId) {
List<UmsPermission> permissionList = roleService.getPermissionList(roleId);
return CommonResult.success(permissionList);
}
@ApiOperation("修改角色相关权限")
@RequestMapping(value = "/updatePermission", method = RequestMethod.POST)
@ResponseBody
public CommonResult updatePermission(@RequestParam Long roleId,
@RequestParam List<Long> permissionIds) {
int count = roleService.updatePermission(roleId, permissionIds);
if(count>0){
return CommonResult.success(count);
}
return CommonResult.failed();
}
@ApiOperation("获取角色列表")
@RequestMapping(value = "/listRoleByAdminId/{adminId}", method = RequestMethod.GET)
@ResponseBody
public CommonResult<List<UmsRole>> listRoleByAdminId(@PathVariable Long adminId) {
List<UmsRole> roleList = adminService.getRoleList(adminId);
return CommonResult.success(roleList);
}
}
@@ -0,0 +1,4 @@
/**
* 此包存放用户模块的Controller层代码负责接收前端请求参数校验调用Service层处理业务逻辑并返回响应
*/
package com.trade.user.controller;
@@ -0,0 +1,22 @@
package com.trade.user.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* 用户登录参数
* Created by macro on 2018/4/26.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UmsAdminLoginParam {
@ApiModelProperty(value = "用户名", required = true)
@NotEmpty(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true)
@NotEmpty(message = "密码不能为空")
private String password;
}
@@ -0,0 +1,30 @@
package com.trade.user.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* 用户注册参数
* Created by macro on 2018/4/26.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UmsAdminParam {
@ApiModelProperty(value = "用户名", required = true)
@NotEmpty(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true)
@NotEmpty(message = "密码不能为空")
private String password;
@ApiModelProperty(value = "用户头像")
private String icon;
@ApiModelProperty(value = "邮箱")
private String email;
@ApiModelProperty(value = "用户昵称")
private String nickName;
@ApiModelProperty(value = "备注")
private String note;
}
@@ -0,0 +1,25 @@
package com.trade.user.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* 修改密码参数
* Created by macro on 2019/10/5.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class UmsAdminUpdatePasswordParam {
@ApiModelProperty(value = "用户名", required = true)
@NotEmpty(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "旧密码", required = true)
@NotEmpty(message = "旧密码不能为空")
private String oldPassword;
@ApiModelProperty(value = "新密码", required = true)
@NotEmpty(message = "新密码不能为空")
private String newPassword;
}
@@ -0,0 +1,6 @@
/**
* 此包存放用户模块的DTO (Data Transfer Object) 代码
* DTO用于在不同层之间传递数据例如Controller接收的请求参数Service返回给Controller的数据等
* 它们有助于解耦各层并可以根据需要对数据进行裁剪或聚合
*/
package com.trade.user.dto;
@@ -0,0 +1,5 @@
/**
* 此包存放用户模块的Entity层代码即数据库表对应的Java实体类
* 通常会使用Lombok注解简化代码并可能包含JPA或MyBatis Plus的注解
*/
package com.trade.user.entity;
@@ -0,0 +1,5 @@
/**
* 此包存放用户模块相关的枚举类
* 例如用户状态枚举性别枚举等
*/
package com.trade.user.enums;
@@ -0,0 +1 @@
package com.trade.user.handler;
@@ -0,0 +1,24 @@
package com.trade.user.mapper;
import com.trade.user.model.UmsAdmin;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.user.model.UmsPermission;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 后台用户表 Mapper 接口
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsAdminMapper extends BaseMapper<UmsAdmin> {
/**
* 获取用户所有权限(包括角色权限和+-权限)
*/
List<UmsPermission> getPermissionList(@Param("adminId") Long adminId);
}
@@ -0,0 +1,16 @@
package com.trade.user.mapper;
import com.trade.user.model.UmsAdminPermissionRelation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 后台用户和权限关系表(除角色外的额外权限) Mapper 接口
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsAdminPermissionRelationMapper extends BaseMapper<UmsAdminPermissionRelation> {
}
@@ -0,0 +1,16 @@
package com.trade.user.mapper;
import com.trade.user.model.UmsAdminRoleRelation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 后台用户和角色关系表 Mapper 接口
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsAdminRoleRelationMapper extends BaseMapper<UmsAdminRoleRelation> {
}
@@ -0,0 +1,16 @@
package com.trade.user.mapper;
import com.trade.user.model.UmsPermission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 后台用户权限表 Mapper 接口
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsPermissionMapper extends BaseMapper<UmsPermission> {
}
@@ -0,0 +1,16 @@
package com.trade.user.mapper;
import com.trade.user.model.UmsRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 后台用户角色表 Mapper 接口
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsRoleMapper extends BaseMapper<UmsRole> {
}
@@ -0,0 +1,16 @@
package com.trade.user.mapper;
import com.trade.user.model.UmsRolePermissionRelation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 后台用户角色和权限关系表 Mapper 接口
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsRolePermissionRelationMapper extends BaseMapper<UmsRolePermissionRelation> {
}
@@ -0,0 +1,4 @@
/**
* 此包存放用户模块的Mapper层代码定义与数据库表对应的DAO接口继承自MyBatis Plus的BaseMapper
*/
package com.trade.user.mapper;
@@ -0,0 +1,49 @@
package com.trade.user.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 后台用户表
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_admin")
public class UmsAdmin implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String icon;
private String email;
private String nickName;
private String note;
private Date createTime;
private Date loginTime;
private Integer status;
}
@@ -0,0 +1,34 @@
package com.trade.user.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 后台用户和权限关系表(除角色外的额外权限)
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_admin_permission_relation")
public class UmsAdminPermissionRelation implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long adminId;
private Long permissionId;
}
@@ -0,0 +1,34 @@
package com.trade.user.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 后台用户和角色关系表
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_admin_role_relation")
public class UmsAdminRoleRelation implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long adminId;
private Long roleId;
}
@@ -0,0 +1,49 @@
package com.trade.user.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 后台用户权限表
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_permission")
public class UmsPermission implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long pid;
private String name;
private String value;
private String icon;
private Integer type;
private String uri;
private Integer status;
private Date createTime;
private Integer sort;
}
@@ -0,0 +1,43 @@
package com.trade.user.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 后台用户角色表
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_role")
public class UmsRole implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String name;
private String description;
private Integer adminCount;
private Date createTime;
private Integer status;
private Integer sort;
}
@@ -0,0 +1,34 @@
package com.trade.user.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 后台用户角色和权限关系表
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ums_role_permission_relation")
public class UmsRolePermissionRelation implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Long roleId;
private Long permissionId;
}
@@ -0,0 +1,7 @@
/**
* 此包包含用户服务模块的领域模型 (Domain Model) 或实体类 (Entity)
* <p>
* 这些类通常映射到用户相关的数据库表结构例如用户表角色表权限表等
* </p>
*/
package com.trade.user.model;
@@ -0,0 +1,16 @@
package com.trade.user.service;
import com.trade.user.model.UmsAdminPermissionRelation;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 后台用户和权限关系表(除角色外的额外权限) 服务类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsAdminPermissionRelationService extends IService<UmsAdminPermissionRelation> {
}
@@ -0,0 +1,16 @@
package com.trade.user.service;
import com.trade.user.model.UmsAdminRoleRelation;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 后台用户和角色关系表 服务类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsAdminRoleRelationService extends IService<UmsAdminRoleRelation> {
}
@@ -0,0 +1,60 @@
package com.trade.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.common.domain.UserDto;
import com.trade.user.dto.UmsAdminParam;
import com.trade.user.dto.UmsAdminUpdatePasswordParam;
import com.trade.user.model.UmsAdmin;
import com.trade.user.model.UmsPermission;
import java.util.List;
/**
* 后台管理员Service
* Created by macro on 2018/4/26.
*/
public interface UmsAdminService extends IService<UmsAdmin> {
/**
* 根据用户名获取后台管理员
*/
UmsAdmin getAdminByUsername(String username);
/**
* 注册功能
*/
UmsAdmin register(UmsAdminParam umsAdminParam);
/**
* 登录功能
* @param username 用户名
* @param password 密码
* @return 生成的JWT的token
*/
String login(String username, String password);
/**
* 根据用户id获取用户所有权限角色权限和+-权限
*/
List<UmsPermission> getPermissionList(Long adminId);
/**
* 修改密码
*/
int updatePassword(UmsAdminUpdatePasswordParam umsAdminUpdatePasswordParam);
/**
* 获取用户信息
*/
UserDto loadUserByUsername(String username);
/**
* 获取用户对应角色
*/
List<com.trade.user.model.UmsRole> getRoleList(Long adminId);
/**
* 刷新token的功能
* @param oldToken 旧的token
*/
String refreshToken(String oldToken);
}
@@ -0,0 +1,21 @@
package com.trade.user.service;
import com.trade.user.model.UmsPermission;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 后台用户权限表 服务类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
import java.util.List;
public interface UmsPermissionService extends IService<UmsPermission> {
/**
* 以层级结构返回所有权限
*/
List<UmsPermission> treeList();
}
@@ -0,0 +1,16 @@
package com.trade.user.service;
import com.trade.user.model.UmsRolePermissionRelation;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 后台用户角色和权限关系表 服务类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
public interface UmsRolePermissionRelationService extends IService<UmsRolePermissionRelation> {
}
@@ -0,0 +1,28 @@
package com.trade.user.service;
import com.trade.user.model.UmsRole;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 后台用户角色表 服务类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
import com.trade.user.model.UmsPermission;
import java.util.List;
public interface UmsRoleService extends IService<UmsRole> {
/**
* 获取角色相关权限
*/
List<UmsPermission> getPermissionList(Long roleId);
/**
* 修改角色权限
*/
int updatePermission(Long roleId, List<Long> permissionIds);
}
@@ -0,0 +1,20 @@
package com.trade.user.service.impl;
import com.trade.user.model.UmsAdminPermissionRelation;
import com.trade.user.mapper.UmsAdminPermissionRelationMapper;
import com.trade.user.service.UmsAdminPermissionRelationService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 后台用户和权限关系表(除角色外的额外权限) 服务实现类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Service
public class UmsAdminPermissionRelationServiceImpl extends ServiceImpl<UmsAdminPermissionRelationMapper, UmsAdminPermissionRelation> implements UmsAdminPermissionRelationService {
}
@@ -0,0 +1,20 @@
package com.trade.user.service.impl;
import com.trade.user.model.UmsAdminRoleRelation;
import com.trade.user.mapper.UmsAdminRoleRelationMapper;
import com.trade.user.service.UmsAdminRoleRelationService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 后台用户和角色关系表 服务实现类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Service
public class UmsAdminRoleRelationServiceImpl extends ServiceImpl<UmsAdminRoleRelationMapper, UmsAdminRoleRelation> implements UmsAdminRoleRelationService {
}
@@ -0,0 +1,142 @@
package com.trade.user.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.common.domain.UserDto;
import com.trade.common.util.JwtTokenUtil;
import org.springframework.security.core.userdetails.UserDetails;
import com.trade.user.dto.UmsAdminParam;
import com.trade.user.dto.UmsAdminUpdatePasswordParam;
import com.trade.user.mapper.UmsAdminMapper;
import com.trade.user.model.UmsAdmin;
import com.trade.user.model.UmsPermission;
import com.trade.user.service.UmsAdminService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* UmsAdminService实现类
* Created by macro on 2018/4/26.
*/
@Service
public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper, UmsAdmin> implements UmsAdminService {
private static final Logger LOGGER = LoggerFactory.getLogger(UmsAdminServiceImpl.class);
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UmsAdminMapper adminMapper;
@Override
public UmsAdmin getAdminByUsername(String username) {
LambdaQueryWrapper<UmsAdmin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UmsAdmin::getUsername, username);
UmsAdmin admin = getOne(lambdaQueryWrapper);
return admin;
}
@Override
public UmsAdmin register(UmsAdminParam umsAdminParam) {
UmsAdmin umsAdmin = new UmsAdmin();
BeanUtils.copyProperties(umsAdminParam, umsAdmin);
umsAdmin.setCreateTime(new Date());
umsAdmin.setStatus(1);
//查询是否有相同用户名的用户
LambdaQueryWrapper<UmsAdmin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UmsAdmin::getUsername, umsAdmin.getUsername());
List<UmsAdmin> umsAdminList = list(lambdaQueryWrapper);
if (umsAdminList.size() > 0) {
return null;
}
//将密码进行加密操作
String encodePassword = passwordEncoder.encode(umsAdmin.getPassword());
umsAdmin.setPassword(encodePassword);
baseMapper.insert(umsAdmin);
return umsAdmin;
}
@Override
public String login(String username, String password) {
String token = null;
try {
UserDto userDto = loadUserByUsername(username);
if (!passwordEncoder.matches(password, userDto.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
UserDetails userDetails = userDto;
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
token = jwtTokenUtil.generateToken(userDetails);
} catch (AuthenticationException e) {
LOGGER.warn("登录异常:{}", e.getMessage());
}
return token;
}
@Override
public List<UmsPermission> getPermissionList(Long adminId) {
return adminMapper.getPermissionList(adminId);
}
@Override
public int updatePassword(UmsAdminUpdatePasswordParam param) {
if(StrUtil.isEmpty(param.getUsername())
||StrUtil.isEmpty(param.getOldPassword())
||StrUtil.isEmpty(param.getNewPassword())){
return -1;
}
LambdaQueryWrapper<UmsAdmin> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UmsAdmin::getUsername, param.getUsername());
List<UmsAdmin> adminList = list(lambdaQueryWrapper);
if(CollUtil.isEmpty(adminList)){
return -2;
}
UmsAdmin umsAdmin = adminList.get(0);
if(!passwordEncoder.matches(param.getOldPassword(),umsAdmin.getPassword())){
return -3;
}
umsAdmin.setPassword(passwordEncoder.encode(param.getNewPassword()));
baseMapper.updateById(umsAdmin);
return 1;
}
@Override
public UserDto loadUserByUsername(String username) {
//获取用户信息
UmsAdmin admin = getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = getPermissionList(admin.getId());
List<String> permissionStrList = null;
if (CollUtil.isNotEmpty(permissionList)) {
permissionStrList = permissionList.stream().map(UmsPermission::getValue).collect(java.util.stream.Collectors.toList());
}
return new UserDto(admin.getId(), admin.getUsername(), admin.getPassword(), admin.getStatus(), permissionStrList);
}
return null;
}
@Override
public List<com.trade.user.model.UmsRole> getRoleList(Long adminId) {
return adminMapper.getRoleList(adminId);
}
@Override
public String refreshToken(String oldToken) {
return jwtTokenUtil.refreshHeadToken(oldToken);
}
}
@@ -0,0 +1,46 @@
package com.trade.user.service.impl;
import com.trade.user.model.UmsPermission;
import com.trade.user.mapper.UmsPermissionMapper;
import com.trade.user.service.UmsPermissionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 后台用户权限表 服务实现类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Service
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
public class UmsPermissionServiceImpl extends ServiceImpl<UmsPermissionMapper, UmsPermission> implements UmsPermissionService {
@Override
public List<UmsPermission> treeList() {
List<UmsPermission> permissionList = list();
List<UmsPermission> result = permissionList.stream()
.filter(permission -> permission.getParentId().equals(0L))
.map(permission -> covert(permission, permissionList)).collect(Collectors.toList());
return result;
}
/**
* 将权限转换为带有子级的权限对象
* 当找不到子级权限的时候map操作不会再递归调用covert
*/
private UmsPermission covert(UmsPermission permission, List<UmsPermission> permissionList) {
permission.setChildren(permissionList.stream()
.filter(item -> item.getParentId().equals(permission.getId()))
.map(item -> covert(item, permissionList))
.collect(Collectors.toList()));
return permission;
}
}
@@ -0,0 +1,20 @@
package com.trade.user.service.impl;
import com.trade.user.model.UmsRolePermissionRelation;
import com.trade.user.mapper.UmsRolePermissionRelationMapper;
import com.trade.user.service.UmsRolePermissionRelationService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 后台用户角色和权限关系表 服务实现类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Service
public class UmsRolePermissionRelationServiceImpl extends ServiceImpl<UmsRolePermissionRelationMapper, UmsRolePermissionRelation> implements UmsRolePermissionRelationService {
}
@@ -0,0 +1,59 @@
package com.trade.user.service.impl;
import com.trade.user.model.UmsRole;
import com.trade.user.mapper.UmsRoleMapper;
import com.trade.user.service.UmsRoleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 后台用户角色表 服务实现类
* </p>
*
* @author Trade Team
* @since 2024-03-20
*/
@Service
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.trade.user.mapper.UmsRolePermissionRelationMapper;
import com.trade.user.model.UmsPermission;
import com.trade.user.model.UmsRolePermissionRelation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public class UmsRoleServiceImpl extends ServiceImpl<UmsRoleMapper, UmsRole> implements UmsRoleService {
@Autowired
private UmsRolePermissionRelationMapper rolePermissionRelationMapper;
@Override
public List<UmsPermission> getPermissionList(Long roleId) {
return rolePermissionRelationMapper.getPermissionList(roleId);
}
@Override
public int updatePermission(Long roleId, List<Long> permissionIds) {
//删除之前绑定的关系
LambdaQueryWrapper<UmsRolePermissionRelation> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UmsRolePermissionRelation::getRoleId, roleId);
rolePermissionRelationMapper.delete(wrapper);
//批量插入新的关系
List<UmsRolePermissionRelation> relationList = new ArrayList<>();
for (Long permissionId : permissionIds) {
UmsRolePermissionRelation relation = new UmsRolePermissionRelation();
relation.setRoleId(roleId);
relation.setPermissionId(permissionId);
relationList.add(relation);
}
if (!CollectionUtils.isEmpty(relationList)) {
for (UmsRolePermissionRelation relation : relationList) {
rolePermissionRelationMapper.insert(relation);
}
}
return permissionIds.size();
}
}
@@ -0,0 +1,4 @@
/**
* 此包存放用户模块Service层接口的实现类
*/
package com.trade.user.service.impl;
@@ -0,0 +1,5 @@
/**
* 此包存放用户模块的Service层代码定义业务逻辑接口
* 通常会包含接口定义 (e.g., {@code UserService.java}) 和其实现类 (e.g., {@code UserServiceImpl.java} in a sub-package 'impl').
*/
package com.trade.user.service;
@@ -0,0 +1,4 @@
/**
* 此包存放用户模块相关的工具类
*/
package com.trade.user.util;
@@ -0,0 +1,4 @@
/**
* 此包存放用户服务模块的工具类
*/
package com.trade.user.utils;
@@ -0,0 +1,6 @@
/**
* 此包存放用户模块的VO (View Object) 代码
* VO是专门用于前端展示的对象Controller层会将业务处理结果封装成VO返回给客户端
* VO的设计应贴合前端页面的展示需求
*/
package com.trade.user.vo;
@@ -0,0 +1,55 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/trade-user?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: wodiXIAO1988
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
database: 0
host: localhost
port: 6379
password: #
mail:
host: smtp.qq.com
username: null
password: null
properties:
mail.smtp.ssl.enable: true
default-encoding: UTF-8
thymeleaf:
cache: false
mode: HTML5
encoding: UTF-8
servlet.content-type: text/html
mybatis-plus:
mapper-locations: 'classpath*:mapper/*.xml'
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
table-prefix: ums_
field-strategy: not_empty
logging:
level:
com:
trade:
user: debug
file:
path: D:/log
secure:
ignored:
urls:
- /swagger-resources/**
- /webjars/**
- /v2/**
- /swagger-ui.html
- /doc.html
- /role/**
- /permission/**
- /admin/login
- /admin/register
- /admin/info
- /admin/logout
- /admin/updatePassword
- /admin/refreshToken
@@ -0,0 +1,55 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/trade-user?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
database: 0
host: localhost
port: 6379
password: #
mail:
host: smtp.qq.com
username: null
password: null
properties:
mail.smtp.ssl.enable: true
default-encoding: UTF-8
thymeleaf:
cache: false
mode: HTML5
encoding: UTF-8
servlet.content-type: text/html
mybatis-plus:
mapper-locations: 'classpath*:mapper/*.xml'
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
table-prefix: ums_
field-strategy: not_empty
logging:
level:
com:
trade:
user: info
file:
path: /var/log/trade-user
secure:
ignored:
urls:
- /swagger-resources/**
- /webjars/**
- /v2/**
- /swagger-ui.html
- /doc.html
- /role/**
- /permission/**
- /admin/login
- /admin/register
- /admin/info
- /admin/logout
- /admin/updatePassword
- /admin/refreshToken
@@ -0,0 +1,55 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/trade-user-test?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
database: 0
host: localhost
port: 6379
password: #
mail:
host: smtp.qq.com
username: null
password: null
properties:
mail.smtp.ssl.enable: true
default-encoding: UTF-8
thymeleaf:
cache: false
mode: HTML5
encoding: UTF-8
servlet.content-type: text/html
mybatis-plus:
mapper-locations: 'classpath*:mapper/*.xml'
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
table-prefix: ums_
field-strategy: not_empty
logging:
level:
com:
trade:
user: debug
file:
path: D:/log
secure:
ignored:
urls:
- /swagger-resources/**
- /webjars/**
- /v2/**
- /swagger-ui.html
- /doc.html
- /role/**
- /permission/**
- /admin/login
- /admin/register
- /admin/info
- /admin/logout
- /admin/updatePassword
- /admin/refreshToken
@@ -0,0 +1,93 @@
server:
port: 8081 # 用户服务端口,避免与网关或其他服务冲突
spring:
application:
name: trade-user # 应用名称
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
- data-id: datasource-mysql-config.yml # MySQL数据源配置
group: ${spring.cloud.nacos.discovery.group}
refresh: true
# ext-config: # 可选的扩展配置
# - data-id: trade-user-ext.yml
# group: ${spring.cloud.nacos.discovery.group}
# refresh: true
# Spring Profiles: 用于区分不同环境的配置 (dev, test, prod)
profiles:
active: dev # 默认激活开发环境配置
# MyBatis Plus 配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml # Mapper XML文件位置
# type-aliases-package: com.trade.user.entity # 实体类别名扫描包 (如果需要)
global-config:
db-config:
id-type: auto # 全局主键策略 (推荐使用AUTO或ASSIGN_ID,具体根据业务调整)
# table-prefix: t_ # 全局表前缀 (如果表名有统一前缀)
# logic-delete-field: deleted # 全局逻辑删除字段名 (如果使用逻辑删除)
# logic-not-delete-value: 0 # 逻辑未删除值
# logic-delete-value: 1 # 逻辑已删除值
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # MyBatis日志实现 (开发时可开启)
# 日志配置
logging:
level:
com.trade.user: INFO # 用户模块日志级别
com.trade.user.mapper: DEBUG # Mapper接口日志级别 (开发时可设为DEBUG查看SQL)
org.springframework.web: INFO
org.springframework.security: INFO # 如果集成了Spring Security
# file:
# name: ./logs/trade-user.log # 日志文件路径和名称
# Actuator端点配置
management:
endpoints:
web:
exposure:
include: '*' # 暴露所有端点,生产环境请按需配置
endpoint:
health:
show-details: always # 显示健康检查详情
# 如果需要集成其他组件,如Redis、RabbitMQ等,在此处添加相应配置
# spring:
# data:
# redis:
# host: localhost
# port: 6379
# # password: yourpassword
# rabbitmq:
# host: localhost
# port: 5672
# username: guest
# password: guest
# Swagger/OpenAPI 配置 (如果使用SpringDoc)
# springdoc:
# api-docs:
# path: /v3/api-docs # API文档路径
# swagger-ui:
# path: /swagger-ui.html # Swagger UI路径
# display-request-duration: true
# groups-order: DESC
# group-configs:
# - group: 'User API'
# paths-to-match: '/user/**'
# packages-to-scan: com.trade.user.controller
@@ -0,0 +1,20 @@
spring:
application:
name: trade-user # 应用名称,必须与Nacos中配置的Data ID相关联或作为前缀
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos配置中心地址
namespace: # Nacos命名空间ID,根据实际情况配置
group: DEFAULT_GROUP # Nacos分组,根据实际情况配置
file-extension: yml # 拉取配置文件的格式
# data-id: trade-user.yml # 如果需要显式指定主配置文件Data ID,通常由shared-configs和ext-config管理
# 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}
# 日志配置,也可以放在application.yml中,但bootstrap.yml中配置可以更早生效
# logging:
# config: classpath:logback-spring.xml # 指定日志配置文件
@@ -0,0 +1,15 @@
#数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/trade-user?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#指定生成代码的模块
project.module.name=trade-user
#指定生成代码的表名
#多个表名用逗号隔开
#table.names=ums_admin,ums_role,ums_permission,ums_admin_role_relation,ums_role_permission_relation,ums_admin_permission_relation,ums_resource
table.names=ums_resource
#指定生成代码的作者
author.name=macro
#是否覆盖已有文件
file.override=true
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="com.trade.user" level="DEBUG"/>
</configuration>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsAdminMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsAdmin">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="icon" jdbcType="VARCHAR" property="icon" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="nick_name" jdbcType="VARCHAR" property="nickName" />
<result column="note" jdbcType="VARCHAR" property="note" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="login_time" jdbcType="TIMESTAMP" property="loginTime" />
<result column="status" jdbcType="INTEGER" property="status" />
</resultMap>
<select id="getPermissionList" resultMap="com.trade.user.mapper.UmsPermissionMapper.BaseResultMap">
SELECT
p.*
FROM
ums_admin_role_relation ar
LEFT JOIN ums_role r ON ar.role_id = r.id
LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id
LEFT JOIN ums_permission p ON rp.permission_id = p.id
WHERE
ar.admin_id = #{adminId}
AND p.id IS NOT NULL
AND p.id NOT IN (
SELECT
p.id
FROM
ums_admin_permission_relation apr
LEFT JOIN ums_permission p ON apr.permission_id = p.id
WHERE
apr.admin_id = #{adminId}
AND apr.type = - 1
)
UNION
SELECT
p.*
FROM
ums_admin_permission_relation apr
LEFT JOIN ums_permission p ON apr.permission_id = p.id
WHERE
apr.admin_id = #{adminId}
AND apr.type = 1
</select>
<select id="getRoleList" resultMap="com.trade.user.mapper.UmsRoleMapper.BaseResultMap">
SELECT
r.*
FROM
ums_admin_role_relation ar
LEFT JOIN ums_role r ON ar.role_id = r.id
WHERE
ar.admin_id = #{adminId}
</select>
</mapper>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsAdminPermissionRelationMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsAdminPermissionRelation">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="admin_id" jdbcType="BIGINT" property="adminId" />
<result column="permission_id" jdbcType="BIGINT" property="permissionId" />
<result column="type" jdbcType="INTEGER" property="type" />
</resultMap>
</mapper>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsAdminRoleRelationMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsAdminRoleRelation">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="admin_id" jdbcType="BIGINT" property="adminId" />
<result column="role_id" jdbcType="BIGINT" property="roleId" />
</resultMap>
</mapper>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsPermissionMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsPermission">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="pid" jdbcType="BIGINT" property="pid" />
<result column="name" jdbc="VARCHAR" property="name" />
<result column="value" jdbcType="VARCHAR" property="value" />
<result column="icon" jdbcType="VARCHAR" property="icon" />
<result column="type" jdbcType="INTEGER" property="type" />
<result column="uri" jdbcType="VARCHAR" property="uri" />
<result column="status" jdbcType="INTEGER" property="status" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="sort" jdbcType="INTEGER" property="sort" />
</resultMap>
<resultMap id="treeList" type="com.trade.user.model.UmsPermission" extends="BaseResultMap">
<collection property="children" ofType="com.trade.user.model.UmsPermission" select="com.trade.user.mapper.UmsPermissionMapper.treeList" column="id"></collection>
</resultMap>
<select id="treeList" resultMap="treeList">
SELECT
id,pid,name,value,icon,type,uri,status,create_time,sort
FROM
ums_permission
WHERE
pid = #{id}
ORDER BY sort DESC
</select>
</mapper>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsResourceMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsResource">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="url" jdbcType="VARCHAR" property="url" />
<result column="category_id" jdbcType="BIGINT" property="categoryId" />
</resultMap>
<select id="getResourceList" resultMap="BaseResultMap">
SELECT
ur.id,
ur.create_time,
ur.name,
ur.url,
ur.category_id
FROM
ums_admin_role_relation ar
LEFT JOIN ums_role r ON ar.role_id = r.id
LEFT JOIN ums_role_resource_relation rrr ON r.id = rrr.role_id
LEFT JOIN ums_resource ur ON ur.id = rrr.resource_id
WHERE
ar.admin_id = #{adminId}
AND ur.id IS NOT NULL
GROUP BY
ur.id
</select>
</mapper>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsRoleMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsRole">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="admin_count" jdbcType="INTEGER" property="adminCount" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="status" jdbcType="INTEGER" property="status" />
<result column="sort" jdbcType="INTEGER" property="sort" />
</resultMap>
<select id="getPermissionList" resultMap="com.trade.user.mapper.UmsPermissionMapper.BaseResultMap">
SELECT
p.*
FROM
ums_role_permission_relation rp
LEFT JOIN ums_permission p ON rp.permission_id = p.id
WHERE
rp.role_id = #{roleId}
</select>
</mapper>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.trade.user.mapper.UmsRolePermissionRelationMapper">
<resultMap id="BaseResultMap" type="com.trade.user.model.UmsRolePermissionRelation">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="role_id" jdbcType="BIGINT" property="roleId" />
<result column="permission_id" jdbcType="BIGINT" property="permissionId" />
</resultMap>
</mapper>
Binary file not shown.
+48
View File
@@ -175,6 +175,53 @@ class ExistingSystemInfo:
source_path: str
class ChangeType(Enum):
"""变更点定位的变更区分(对应要件定義 変更区分 列值)"""
NEW = "新規"
MODIFIED = "変更"
DELETED = "削除"
UNCHANGED = "不变"
@dataclass
class ChangeElement:
"""变更点定位结果中的一个要素(Impact Agent MVP"""
element_id: str
element_type: str # 機能/画面/帳票/DB/IF/バッチ(取 ElementType.value 或表名)
name: str
change_type: ChangeType
existing_mapping: list[str] = field(default_factory=list) # 既存対応 声明值(类名清单)
impacted_existing: list[str] = field(default_factory=list) # 确认命中的既有类
evidence: str = "" # 命中的既有类 source_uri / 空
status: str = "ok" # "ok" | "conflict" | "warning"
@dataclass
class ImpactWarning:
"""影响调查告警(不阻断,供用户/QA 关注)"""
element_id: str
issue: str
@dataclass
class ChangeAnalysis:
"""变更点定位结果集合(Impact Agent MVP"""
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:
"""影响调查书(MVP 子集,供 Writer 生成 + 独立下载)"""
metadata: dict
change_analysis: ChangeAnalysis | None = None
summary: dict = field(default_factory=dict)
@dataclass
class UnifiedDocument:
"""FileReader 的统一输出(多格式归一化)"""
@@ -223,6 +270,7 @@ class StructuredSource:
image_analyses: list[ImageAnalysis]
existing_system: ExistingSystemInfo | None
comments: list[CellComment]
impact_report: "ImpactReport | None" = None # 影响调查书(生成后回填,门控未提供时为 None)
@dataclass
+186
View File
@@ -0,0 +1,186 @@
"""CodeParserJava 项目源码解析 → CodeStructureImpact Agent MVP)。
识别 @RestController/@Controller@Service@Entity/@Table 及方法级路由映射
输出供 ExistingSystemExplorer 组装 ExistingSystemInfo 的结构化清单
"""
from __future__ import annotations
import re
from pathlib import Path
from genesis.data_models import (
CodeStructure,
ControllerInfo,
EndpointInfo,
EntityInfo,
ServiceInfo,
)
class CodeParseError(Exception):
"""既有系统解析失败(路径无效或非 Java 源码项目)。"""
_JAVA_EXT = ".java"
_CLASS_RE = re.compile(
r"(?:public\s+|abstract\s+|final\s+)?(?:class|interface|enum|record)\s+(\w+)"
)
_TABLE_RE = re.compile(r"@(?:Table|TableName)\s*\(\s*(?:name\s*=\s*)?[\"']([^\"']+)[\"']")
_CLASS_MAPPING_RE = re.compile(r"@RequestMapping\s*\(\s*[\"']([^\"']+)[\"']")
_METHOD_MAPPING_RE = re.compile(
r"@(Get|Post|Put|Delete|Patch|Request)Mapping\s*(?:\(\s*[\"']([^\"']*)[\"'])?"
)
_METHOD_DECL_RE = re.compile(
r"(?:public|private|protected|)\s+(?:static\s+|final\s+|synchronized\s+)*"
r"[\w<>\[\],.?]+\s+(\w+)\s*\("
)
_FIELD_DECL_RE = re.compile(
r"(?:private|public|protected)\s+[\w<>\[\],]+\s+(\w+)\s*;"
)
_IMPORT_RE = re.compile(r"^import\s+([\w.]+);", re.MULTILINE)
def _relative(path: Path, root: Path) -> str:
return path.relative_to(root).as_posix()
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="ignore")
class CodeParser:
"""解析 Java 项目目录,输出 CodeStructure(控制器/服务/实体/端点/模块)。"""
def parse(self, root_path: str | Path) -> CodeStructure:
root = Path(root_path)
if not root.is_dir():
raise CodeParseError(f"既有系统路径无效或不存在: {root_path}")
java_files = sorted(p for p in root.rglob(f"*{_JAVA_EXT}") if p.is_file())
if not java_files:
raise CodeParseError(f"未找到 Java 源码: {root_path}")
controllers: list[ControllerInfo] = []
services: list[ServiceInfo] = []
entities: list[EntityInfo] = []
endpoints: list[EndpointInfo] = []
classes: list[dict] = []
raw_imports: list[dict] = []
for path in java_files:
text = _read(path)
rel = _relative(path, root)
class_name = self._class_name(text)
imports = _IMPORT_RE.findall(text)
raw_imports.append({"path": rel, "imports": imports})
if not class_name:
# package-info.java 等无类声明文件:仅登记 imports,不参与要素提取
continue
is_controller = "@RestController" in text or "@Controller" in text
is_service = "@Service" in text
# 既有系统实体可能用 JPA @Entity 或 MyBatis-Plus @TableName 标注
is_entity = "@Entity" in text or "@TableName" in text
if is_controller:
controllers.append(self._parse_controller(text, rel, class_name, endpoints))
elif is_service:
services.append(self._parse_service(text, rel, class_name))
elif is_entity:
entities.append(self._parse_entity(text, rel, class_name))
classes.append({"class_name": class_name, "path": rel})
modules = self._modules(root, java_files)
return CodeStructure(
root_path=str(root),
language="java",
modules=modules,
classes=classes,
controllers=controllers,
services=services,
entities=entities,
endpoints=endpoints,
raw_imports=raw_imports,
)
@staticmethod
def _class_name(text: str) -> str | None:
m = _CLASS_RE.search(text)
return m.group(1) if m else None
@staticmethod
def _parse_controller(
text: str, rel: str, class_name: str, endpoints: list[EndpointInfo]
) -> ControllerInfo:
base_path = ""
m = _CLASS_MAPPING_RE.search(text)
if m:
base_path = m.group(1)
ctrl_endpoints: list[str] = []
for m in _METHOD_MAPPING_RE.finditer(text):
verb, sub = m.group(1).upper(), m.group(2) or ""
if verb == "REQUEST":
verb = "ANY"
full = f"{base_path.rstrip('/')}/{sub.lstrip('/')}".rstrip("/") or base_path
ctrl_endpoints.append(full)
endpoints.append(
EndpointInfo(
method=verb,
path=full,
controller=class_name,
description="",
source_uri=rel,
)
)
return ControllerInfo(
name=class_name,
class_name=class_name,
path=rel,
base_path=base_path,
endpoints=ctrl_endpoints,
source_uri=rel,
)
@staticmethod
def _parse_service(text: str, rel: str, class_name: str) -> ServiceInfo:
methods = list(dict.fromkeys(_METHOD_DECL_RE.findall(text)))
return ServiceInfo(
name=class_name,
class_name=class_name,
path=rel,
methods=methods,
source_uri=rel,
)
@staticmethod
def _parse_entity(text: str, rel: str, class_name: str) -> EntityInfo:
table_name = None
m = _TABLE_RE.search(text)
if m:
table_name = m.group(1)
fields = list(dict.fromkeys(_FIELD_DECL_RE.findall(text)))
return EntityInfo(
name=class_name,
class_name=class_name,
path=rel,
table_name=table_name,
fields=fields,
source_uri=rel,
)
@staticmethod
def _modules(root: Path, java_files: list[Path]) -> list[str]:
"""顶层目录中凡包含 Java 源码者视为一个模块(按名排序,保证确定性)。"""
mods = {
p.relative_to(root).parts[0]
for p in java_files
if len(p.relative_to(root).parts) > 1
}
return sorted(mods)
@@ -0,0 +1,20 @@
"""ExistingSystemExplorerCodeStructure → ExistingSystemInfoImpact Agent MVP)。
纯组装 CodeParser 的控制器/服务/实体/端点分层映射到 ExistingSystemInfo
"""
from __future__ import annotations
from genesis.data_models import CodeStructure, ExistingSystemInfo
class ExistingSystemExplorer:
"""将代码结构组装为既有系统信息(供 ImpactAgent 比对)。"""
def explore(self, code: CodeStructure) -> ExistingSystemInfo:
return ExistingSystemInfo(
controller_layer=code.controllers,
service_layer=code.services,
entity_layer=code.entities,
api_endpoints=code.endpoints,
source_path=code.root_path,
)
+271
View File
@@ -0,0 +1,271 @@
"""ImpactAgent:变更点定位(Impact Agent MVP,确定性规则,无 LLM)。
从要件定義各表機能/画面/DB/IF/バッチ 変更区分 + 既存対応
ExistingSystemInfo 连接比对输出 ImpactReport影响调查书
"""
from __future__ import annotations
import logging
from datetime import date
from genesis.data_models import (
ChangeAnalysis,
ChangeElement,
ChangeType,
ExcelTable,
ImpactReport,
ImpactWarning,
SheetType,
StructuredSource,
)
_LOGGER = logging.getLogger(__name__)
_SHEET_ELEMENT_TYPE = {
SheetType.FUNCTION: "機能",
SheetType.SCREEN: "画面",
SheetType.REPORT: "帳票",
SheetType.DATABASE: "DB",
SheetType.INTERFACE: "IF",
SheetType.BATCH: "バッチ",
}
_CHANGE_TYPE_MAP = {
"新規": ChangeType.NEW,
"変更": ChangeType.MODIFIED,
"削除": ChangeType.DELETED,
"不变": ChangeType.UNCHANGED,
}
def _element_to_dict(el: ChangeElement) -> dict:
"""ChangeElement → JSON 可序列化 dictChangeType 枚举转 value)。"""
return {
"element_id": el.element_id,
"element_type": el.element_type,
"name": el.name,
"change_type": el.change_type.value,
"existing_mapping": list(el.existing_mapping),
"impacted_existing": list(el.impacted_existing),
"evidence": el.evidence,
"status": el.status,
}
def impact_report_to_dict(report: ImpactReport) -> dict:
"""影响调查书 → JSON 可序列化 dict(供下载/日志,api-design §2.4 impact-result)。"""
ca = report.change_analysis
return {
"metadata": dict(report.metadata),
"change_analysis": {
"project_type": ca.project_type,
"new_elements": [_element_to_dict(e) for e in ca.new_elements],
"modified_elements": [_element_to_dict(e) for e in ca.modified_elements],
"deleted_elements": [_element_to_dict(e) for e in ca.deleted_elements],
"unchanged_elements": [_element_to_dict(e) for e in ca.unchanged_elements],
"warnings": [{"element_id": w.element_id, "issue": w.issue} for w in ca.warnings],
},
"summary": dict(report.summary),
}
def _header_index(headers: list[str], *keywords: str) -> int | None:
"""按关键词定位列索引(如 変更区分 / 既存対応)。"""
for i, h in enumerate(headers):
hl = str(h).strip()
if any(k in hl for k in keywords):
return i
return None
class ImpactAgent:
"""变更点定位 → 影响调查书(MVP)。"""
def run(
self,
structured_source: StructuredSource,
session_id: str = "impact",
scope: dict | None = None,
) -> ImpactReport:
existing = structured_source.existing_system
if existing is None:
raise ValueError("未提供既有系统(existing_system),无法执行影响调查")
lookup = self._build_lookup(existing)
new_elements: list[ChangeElement] = []
modified_elements: list[ChangeElement] = []
deleted_elements: list[ChangeElement] = []
warnings: list[ImpactWarning] = []
matched: set[str] = set()
for table in structured_source.tables:
self._classify_table(
table, lookup, new_elements, modified_elements, deleted_elements, warnings, matched
)
if scope:
# scope 参数预留:MVP 默认全量调查;模块/深度收窄由调用方确认后传入,当前忽略
_LOGGER.warning("scope 参数预留(MVP 默认全量调查),当前忽略: %s", scope)
unchanged = self._unchanged_count(existing, matched)
change_analysis = ChangeAnalysis(
project_type="enhancement",
new_elements=new_elements,
modified_elements=modified_elements,
deleted_elements=deleted_elements,
unchanged_elements=[],
warnings=warnings,
)
summary = {
"total": len(new_elements) + len(modified_elements) + len(deleted_elements),
"new": len(new_elements),
"modified": len(modified_elements),
"deleted": len(deleted_elements),
"unchanged": unchanged,
"warnings": len(warnings),
}
return ImpactReport(
metadata={
"version": "v1",
"session_id": session_id,
"created_at": date.today().isoformat(),
"llm_model": "none", # MVP 确定性规则,无 LLM 参与
"source": existing.source_path,
},
change_analysis=change_analysis,
summary=summary,
)
# ---------- 内部 ----------
def _classify_table(
self,
table: ExcelTable,
lookup: dict[str, list[dict]],
new_elements: list[ChangeElement],
modified_elements: list[ChangeElement],
deleted_elements: list[ChangeElement],
warnings: list[ImpactWarning],
matched: set[str],
) -> None:
headers = [str(h) for h in table.headers]
change_idx = _header_index(headers, "変更区分", "区分")
if change_idx is None:
return # 无变更区分列的表(如新規開発的帳票一覧)不参与变更点定位
mapping_idx = _header_index(headers, "既存対応")
element_type = _SHEET_ELEMENT_TYPE.get(table.detected_type, table.name)
name_idx = _header_index(headers, "") or 1
for row in table.rows:
change_val = self._cell(row, headers, change_idx)
if not change_val:
continue
change_type = _CHANGE_TYPE_MAP.get(str(change_val).strip())
if change_type is None:
continue
element_id = str(self._cell(row, headers, 0) or "")
name = str(self._cell(row, headers, name_idx) or "")
mapping = (
str(self._cell(row, headers, mapping_idx) or "")
if mapping_idx is not None
else ""
)
tokens = [
t.strip()
for t in mapping.replace("", ",").replace(" ", "").split(",")
if t.strip()
]
hits = self._match_tokens(tokens, lookup)
impacted = [h["class_name"] for h in hits]
evidence = hits[0]["source_uri"] if hits else ""
for h in hits:
matched.add(h["class_name"])
status = "ok"
issue: str | None = None
if change_type is ChangeType.NEW and tokens:
status = "conflict"
issue = f"新規要素却声明了既存対応: {mapping}"
elif change_type in (ChangeType.MODIFIED, ChangeType.DELETED):
if not tokens:
status = "warning"
issue = "缺少既存対応,无法定位修改/删除对象"
elif not hits:
status = "warning"
issue = f"既存対応无法匹配既有类: {mapping}"
element = ChangeElement(
element_id=element_id,
element_type=element_type,
name=name,
change_type=change_type,
existing_mapping=tokens,
impacted_existing=impacted,
evidence=evidence,
status=status,
)
if change_type is ChangeType.NEW:
new_elements.append(element)
elif change_type is ChangeType.MODIFIED:
modified_elements.append(element)
elif change_type is ChangeType.DELETED:
deleted_elements.append(element)
if issue:
warnings.append(ImpactWarning(element_id=element_id, issue=issue))
@staticmethod
def _build_lookup(existing) -> dict[str, list[dict]]:
"""类名/表名(小写)→ 既有要素索引,供 token 匹配。"""
lookup: dict[str, list[dict]] = {}
def add(key: str, item: dict) -> None:
lookup.setdefault(key.lower(), []).append(item)
for c in existing.controller_layer:
add(c.class_name, {"kind": "controller", "class_name": c.class_name, "source_uri": c.source_uri})
for s in existing.service_layer:
add(s.class_name, {"kind": "service", "class_name": s.class_name, "source_uri": s.source_uri})
for e in existing.entity_layer:
add(e.class_name, {"kind": "entity", "class_name": e.class_name, "source_uri": e.source_uri})
if e.table_name:
add(e.table_name, {"kind": "entity", "class_name": e.class_name, "source_uri": e.source_uri})
return lookup
def _match_tokens(self, tokens: list[str], lookup: dict[str, list[dict]]) -> list[dict]:
"""token → 既有类命中列表(去重)。匹配规则:类名/表名完全相等 或 类名前缀匹配。"""
hits: list[dict] = []
seen: set[tuple[str, str]] = set()
for token in tokens:
key = token.lower()
for candidate in lookup.get(key, []):
if (candidate["kind"], candidate["class_name"]) not in seen:
seen.add((candidate["kind"], candidate["class_name"]))
hits.append(candidate)
# 前缀匹配:token 是类名前缀(如 OrderService → OrderServiceImpl
for k, items in lookup.items():
if k.startswith(key) and k != key:
for candidate in items:
if (candidate["kind"], candidate["class_name"]) not in seen:
seen.add((candidate["kind"], candidate["class_name"]))
hits.append(candidate)
return hits
@staticmethod
def _cell(row: dict, headers: list[str], idx: int):
if idx is None or idx >= len(headers):
return ""
cell = row.get(headers[idx])
return cell.value if cell is not None else ""
@staticmethod
def _unchanged_count(existing, matched: set[str]) -> int:
all_classes = (
{c.class_name for c in existing.controller_layer}
| {s.class_name for s in existing.service_layer}
| {e.class_name for e in existing.entity_layer}
)
return len(all_classes - matched)
+14 -2
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from pathlib import Path
from genesis.data_models import StructuredSource
from genesis.impact.code_parser import CodeParser
from genesis.impact.existing_system_explorer import ExistingSystemExplorer
from genesis.parsers.excel_parser import ExcelParser
from genesis.parsers.rule_doc_parser import RuleDocParser
from genesis.parsers.word_template_parser import WordTemplateParser
@@ -42,8 +44,13 @@ class SourceParser:
template_path: str | Path | None = None,
write_instruction_paths: list[str | Path] | None = None,
rule_paths: list[str | Path] | None = None,
existing_system_path: str | Path | None = None,
) -> StructuredSource:
"""扩展名校验先于存在性校验(不存在的文件若扩展名未知将抛出 ValueError 而非 FileNotFoundError)。"""
"""扩展名校验先于存在性校验(不存在的文件若扩展名未知将抛出 ValueError 而非 FileNotFoundError)。
existing_system_path既有系统源码目录追加/改修场景提供时解析为
ExistingSystemInfo门控通过 进入影响调查未提供/解析失败 保持 None
"""
requirement_paths = requirement_paths or []
write_instruction_paths = write_instruction_paths or []
@@ -68,11 +75,16 @@ class SourceParser:
# 做成说明书与记入规则均为 Type A 写入规则 → writeapi-design §2.2
rule_docs.append(RuleDocParser().parse(path, category="write"))
existing_system = None
if existing_system_path is not None:
code = CodeParser().parse(existing_system_path)
existing_system = ExistingSystemExplorer().explore(code)
return StructuredSource(
tables=tables,
template=template,
rule_docs=rule_docs,
image_analyses=[],
existing_system=None,
existing_system=existing_system,
comments=comments,
)
+1
View File
@@ -26,6 +26,7 @@ def build_contexts(structured_source: StructuredSource, samples_dir: str = "samp
design_rules=design_rules,
template_styles=set(used),
prior_state=None,
impact_report=getattr(structured_source, "impact_report", None),
)
)
return out
+32
View File
@@ -76,6 +76,7 @@ class GenerationContext:
design_rules: list[str]
template_styles: set[str]
prior_state: object | None = None # WriterState,避免循环 import 用 object
impact_report: object | None = None # ImpactReport 影响调查书(生成主上下文)
def to_vars(self) -> dict:
"""返回供 prompt 渲染的变量字典。"""
@@ -90,4 +91,35 @@ class GenerationContext:
"template_styles": ", ".join(sorted(self.template_styles)),
"prior_state": str(self.prior_state) if self.prior_state is not None else "",
"source": str(self.structured_source) if self.structured_source is not None else "",
"impact": _format_impact(self.impact_report),
}
def _format_impact(report: object | None) -> str:
"""将影响调查书格式化为 prompt 可读文本(无报告/无分析时为空串)。"""
if report is None:
return ""
ca = getattr(report, "change_analysis", None)
if ca is None:
return ""
summary = getattr(report, "summary", {}) or {}
lines = [f"project_type={getattr(ca, 'project_type', '')}"]
lines.append(
"summary: new={new} modified={modified} deleted={deleted} "
"unchanged={unchanged} warnings={warnings}".format(
new=summary.get("new", 0), modified=summary.get("modified", 0),
deleted=summary.get("deleted", 0), unchanged=summary.get("unchanged", 0),
warnings=summary.get("warnings", 0),
)
)
for el in getattr(ca, "new_elements", []) or []:
lines.append(f"[新規] {el.element_id} {el.element_type} {el.name}")
for el in getattr(ca, "modified_elements", []) or []:
impacted = ", ".join(el.impacted_existing) or "-"
lines.append(f"[変更] {el.element_id} {el.element_type} {el.name} → 受影响: {impacted}")
for el in getattr(ca, "deleted_elements", []) or []:
impacted = ", ".join(el.impacted_existing) or "-"
lines.append(f"[削除] {el.element_id} {el.element_type} {el.name} → 受影响: {impacted}")
for w in getattr(ca, "warnings", []) or []:
lines.append(f"[警告] {w.element_id}: {w.issue}")
return "\n".join(lines)
+24 -2
View File
@@ -1,10 +1,17 @@
"""Writer 编排:上下文装配 → 逐章生成 → 渲染 → docx 注入(Phase 5 垂直切片)。"""
"""Writer 编排:上下文装配 → 逐章生成 → 渲染 → docx 注入(Phase 5 垂直切片)。
Impact Agent MVP2026-08-23门控 = 用户是否提供既有系统existing_system None
门控通过且未显式传入 impact_report 自动运行 ImpactAgent 生成影响调查书
并作为生成主上下文GenerationContext.impact_report prompt {{impact}} 变量
"""
from __future__ import annotations
import logging
from datetime import date
from pathlib import Path
from genesis.data_models import StructuredSource
from genesis.impact.impact_agent import ImpactAgent
from genesis.inference.factory import build_inference_engine
from genesis.inference.prompt_registry import PromptRegistry
from genesis.writer.context_builder import build_contexts
@@ -47,9 +54,18 @@ class WriteOrchestrator:
engine=None,
prompt_registry=None,
template_path: str | None = None,
impact_report=None,
meta: dict | None = None,
) -> list[ChapterContent]:
engine = engine or build_inference_engine()
prompt_registry = prompt_registry or PromptRegistry()
if impact_report is None and getattr(structured_source, "existing_system", None) is not None:
# 门控:用户提供了既有系统(existing_system 非 None)→ 自动执行影响调查
_LOGGER.info("检测到既有系统,自动执行影响调查(追加改修场景)")
impact_report = ImpactAgent().run(structured_source, session_id=session_id)
if impact_report is not None:
# 回填 structured_source,便于 QA/日志/后续下载
structured_source.impact_report = impact_report
ctxs = build_contexts(structured_source, samples_dir)
_warn_unanchored(ctxs)
state = WriterState([c.chapter_id for c in ctxs])
@@ -68,7 +84,13 @@ class WriteOrchestrator:
tpl = template_path or getattr(structured_source.template, "file_name", None)
if not tpl:
raise ValueError("template_path 必须提供(structured_source.template.file_name 为空)")
doc = DocxInjector(tpl).inject(sections, meta={})
if meta is None:
meta = {
"doc_title": Path(tpl).stem,
"version": "v1",
"created_at": date.today().isoformat(),
}
doc = DocxInjector(tpl).inject(sections, meta=meta)
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
doc.save(output_path)
return contents
+1
View File
@@ -21,6 +21,7 @@ WRITER_PROMPT_TEMPLATE = (
"写入规则:\n{{write_rules}}\n"
"设计规则:\n{{design_rules}}\n"
"模板样式:\n{{template_styles}}\n"
"影响调查上下文:\n{{impact}}\n"
"参考资料:\n{{source}}\n"
"请输出符合 schema 的章节内容 JSON。\n"
"【语言约束】章节正文(所有 block 的 text 字段)所使用的自然语言,"
+126
View File
@@ -0,0 +1,126 @@
"""CodeParser 测试(Impact Agent MVP):Java 项目解析 → CodeStructure。"""
from pathlib import Path
import pytest
from genesis.impact.code_parser import CodeParseError, CodeParser
FIXTURE_JAVA = {
"trade-order/src/main/java/com/trade/order/controller/OrderController.java": (
"package com.trade.order.controller;\n"
"import org.springframework.web.bind.annotation.*;\n"
"@RestController\n"
"@RequestMapping(\"/api/order\")\n"
"public class OrderController {\n"
" @GetMapping(\"/{id}\")\n"
" public String getOrder(@PathVariable Long id) { return \"ok\"; }\n"
" @PostMapping(\"/create\")\n"
" public String createOrder() { return \"ok\"; }\n"
"}\n"
),
"trade-order/src/main/java/com/trade/order/service/OrderQueryService.java": (
"package com.trade.order.service;\n"
"import org.springframework.stereotype.Service;\n"
"@Service\n"
"public class OrderQueryService {\n"
" public void query() {}\n"
" public void update() {}\n"
"}\n"
),
"trade-order/src/main/java/com/trade/order/entity/OrderEntity.java": (
"package com.trade.order.entity;\n"
"import javax.persistence.Entity;\n"
"import javax.persistence.Table;\n"
"@Entity\n"
"@Table(name = \"t_order\")\n"
"public class OrderEntity {\n"
" private Long id;\n"
" private String orderNo;\n"
"}\n"
),
}
def _make_project(tmp_path, files: dict[str, str]) -> Path:
for rel, content in files.items():
p = tmp_path / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content, encoding="utf-8")
return tmp_path
def test_parse_controller_detected(tmp_path):
root = _make_project(tmp_path, FIXTURE_JAVA)
result = CodeParser().parse(root)
ctrls = {c.class_name: c for c in result.controllers}
oc = ctrls["OrderController"]
assert oc.base_path == "/api/order"
assert "/api/order/{id}" in oc.endpoints
assert "/api/order/create" in oc.endpoints
assert oc.source_uri.endswith("OrderController.java")
def test_parse_service_detected(tmp_path):
root = _make_project(tmp_path, FIXTURE_JAVA)
result = CodeParser().parse(root)
svcs = {s.class_name: s for s in result.services}
oq = svcs["OrderQueryService"]
assert "query" in oq.methods
assert "update" in oq.methods
def test_parse_entity_detected(tmp_path):
root = _make_project(tmp_path, FIXTURE_JAVA)
result = CodeParser().parse(root)
ents = {e.class_name: e for e in result.entities}
oe = ents["OrderEntity"]
assert oe.table_name == "t_order"
assert "id" in oe.fields
assert "orderNo" in oe.fields
def test_parse_endpoints_detected(tmp_path):
root = _make_project(tmp_path, FIXTURE_JAVA)
result = CodeParser().parse(root)
paths = {(e.method, e.path) for e in result.endpoints}
assert ("GET", "/api/order/{id}") in paths
assert ("POST", "/api/order/create") in paths
assert all(e.controller == "OrderController" for e in result.endpoints)
def test_parse_modules_top_level_dirs(tmp_path):
root = _make_project(tmp_path, FIXTURE_JAVA)
result = CodeParser().parse(root)
assert "trade-order" in result.modules
assert result.language == "java"
def test_parse_no_java_raises(tmp_path):
(tmp_path / "readme.txt").write_text("hello", encoding="utf-8")
with pytest.raises(CodeParseError):
CodeParser().parse(tmp_path)
def test_parse_missing_root_raises(tmp_path):
with pytest.raises(CodeParseError):
CodeParser().parse(tmp_path / "nope")
def test_parse_real_sample_has_order_controller():
root = Path(__file__).resolve().parents[1] / "samples" / "existing-system"
if not root.exists():
pytest.skip("样本缺失")
result = CodeParser().parse(root)
names = {c.class_name for c in result.controllers}
assert "OrderController" in names
assert any(c.base_path for c in result.controllers)
def test_parse_real_sample_has_entities_and_endpoints():
root = Path(__file__).resolve().parents[1] / "samples" / "existing-system"
if not root.exists():
pytest.skip("样本缺失")
result = CodeParser().parse(root)
assert result.entities, "既有系统样本应至少有一个实体类"
assert result.endpoints, "既有系统样本应至少有一个端点"
assert result.services, "既有系统样本应至少有一个 @Service 类"
+56 -2
View File
@@ -1,8 +1,9 @@
from dataclasses import asdict
from genesis.data_models import (
CellComment, CellFormatting, CellValue, Confidence, ElementType,
ExcelTable, ExtractionMethod, ImageAnalysis, ParsedTemplate, Provenance,
CellComment, CellFormatting, CellValue, ChangeAnalysis, ChangeElement,
ChangeType, Confidence, ElementType, ExcelTable, ExtractionMethod,
ImageAnalysis, ImpactReport, ImpactWarning, ParsedTemplate, Provenance,
RelationType, RuleDocument, SheetType, StructuredSource,
)
@@ -75,3 +76,56 @@ def test_asdict_serializable():
prov = Provenance(file_name="f.xlsx", sheet_name="S", row=1, column="A", column_header="h")
d = asdict(CellValue(value=1, provenance=prov))
assert d["provenance"]["row"] == 1
# ---------- Impact Agent MVP 数据模型(2026-08-23 ----------
def test_changetype_members():
assert ChangeType.NEW.value == "新規"
assert ChangeType.MODIFIED.value == "変更"
assert ChangeType.DELETED.value == "削除"
assert ChangeType.UNCHANGED.value == "不变"
def test_change_element_defaults():
el = ChangeElement(element_id="F001", element_type="機能", name="止损风控", change_type=ChangeType.NEW)
assert el.existing_mapping == []
assert el.impacted_existing == []
assert el.evidence == ""
assert el.status == "ok"
def test_change_analysis_holds_lists():
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[ChangeElement("F001", "機能", "止损风控", ChangeType.NEW)],
modified_elements=[],
deleted_elements=[],
unchanged_elements=[],
warnings=[ImpactWarning(element_id="F002", issue="无法定位修改对象")],
)
assert ca.project_type == "enhancement"
assert ca.new_elements[0].element_id == "F001"
assert ca.warnings[0].issue == "无法定位修改对象"
def test_impact_report_defaults():
report = ImpactReport(metadata={"version": "v1"})
assert report.change_analysis is None
assert report.summary == {}
def test_impact_report_holds_analysis():
ca = ChangeAnalysis(project_type="enhancement", new_elements=[], modified_elements=[],
deleted_elements=[], unchanged_elements=[], warnings=[])
report = ImpactReport(metadata={"version": "v1"}, change_analysis=ca, summary={"new": 0})
assert report.change_analysis.project_type == "enhancement"
assert report.summary["new"] == 0
def test_structured_source_impact_report_default_none():
"""StructuredSource 新增 impact_report 字段,缺省 None(门控未提供时保持 None)。"""
source = StructuredSource(
tables=[], template=None, rule_docs=[], image_analyses=[], existing_system=None, comments=[],
)
assert source.impact_report is None
+45
View File
@@ -0,0 +1,45 @@
"""ExistingSystemExplorer 测试:CodeStructure → ExistingSystemInfo 分层组装。"""
import pytest
from genesis.data_models import CodeStructure, ControllerInfo, EntityInfo, ServiceInfo
from genesis.impact.existing_system_explorer import ExistingSystemExplorer
def _code() -> CodeStructure:
return CodeStructure(
root_path="/fake/root",
language="java",
modules=["trade-order"],
classes=[],
controllers=[
ControllerInfo(name="OrderController", class_name="OrderController", path="OrderController.java",
base_path="/api/order", endpoints=["/api/order/{id}"], source_uri="OrderController.java"),
],
services=[ServiceInfo(name="OrderService", class_name="OrderService", path="OrderService.java",
methods=["query"], source_uri="OrderService.java")],
entities=[EntityInfo(name="OrderEntity", class_name="OrderEntity", path="OrderEntity.java",
table_name="t_order", fields=["id"], source_uri="OrderEntity.java")],
endpoints=[],
raw_imports=[],
)
def test_explore_assembles_layers():
info = ExistingSystemExplorer().explore(_code())
assert len(info.controller_layer) == 1
assert info.controller_layer[0].class_name == "OrderController"
assert len(info.service_layer) == 1
assert info.service_layer[0].class_name == "OrderService"
assert len(info.entity_layer) == 1
assert info.entity_layer[0].table_name == "t_order"
assert info.source_path == "/fake/root"
def test_explore_empty_code():
code = CodeStructure(root_path="/x", language="java", modules=[], classes=[], controllers=[],
services=[], entities=[], endpoints=[], raw_imports=[])
info = ExistingSystemExplorer().explore(code)
assert info.controller_layer == []
assert info.service_layer == []
assert info.entity_layer == []
assert info.api_endpoints == []
+203
View File
@@ -0,0 +1,203 @@
"""ImpactAgent 变更点定位测试(MVP 确定性规则)。"""
import pytest
from genesis.data_models import (
CellValue, ChangeType, ControllerInfo, EntityInfo, ExcelTable,
ExistingSystemInfo, Provenance, ServiceInfo, SheetType, StructuredSource,
)
from genesis.impact.impact_agent import ImpactAgent
def _cv(value) -> CellValue:
return CellValue(value=value, provenance=Provenance("f.xlsx", "S", 1, "A", "h"))
def _row(*values: str) -> dict[str, CellValue]:
headers = ["要素ID", "要素名", "変更区分", "既存対応"]
return {h: _cv(v) for h, v in zip(headers, values)}
def _table(name: str, stype: SheetType, rows: list[dict]) -> ExcelTable:
return ExcelTable(name=name, detected_type=stype, extraction_method="openpyxl",
headers=["要素ID", "要素名", "変更区分", "既存対応"], rows=rows)
def _existing() -> ExistingSystemInfo:
return ExistingSystemInfo(
controller_layer=[
ControllerInfo("OrderController", "OrderController", "OrderController.java", "/trade/order",
["/trade/order/{id}"], "trade-order/OrderController.java"),
ControllerInfo("RealtimeMarketDataController", "RealtimeMarketDataController", "R.java", "/market-data/realtime",
[], "trade-market-data/R.java"),
],
service_layer=[
ServiceInfo("OrderServiceImpl", "OrderServiceImpl", "OrderServiceImpl.java", ["createOrder"], "trade-order/OrderServiceImpl.java"),
],
entity_layer=[
EntityInfo("OrderDO", "OrderDO", "OrderDO.java", "trade_order", ["id"], "trade-order/OrderDO.java"),
EntityInfo("StrategyBacktest", "StrategyBacktest", "SB.java", "ts_strategy_backtest", [], "trade-strategy/SB.java"),
],
api_endpoints=[],
source_path="samples/existing-system",
)
def _ss(existing: ExistingSystemInfo, tables: list[ExcelTable]) -> StructuredSource:
return StructuredSource(tables=tables, template=None, rule_docs=[],
image_analyses=[], existing_system=existing, comments=[])
def test_classify_new_modified_deleted():
tables = [
_table("機能一覧", SheetType.FUNCTION, [
_row("F001", "止损风控机能", "新規", ""),
_row("F002", "订单状态查询扩展", "変更", "OrderController,OrderServiceImpl"),
_row("F003", "实时行情显示扩展", "変更", "RealtimeMarketDataController"),
_row("F004", "旧回测机能废止", "削除", "StrategyBacktest"),
]),
]
report = ImpactAgent().run(_ss(_existing(), tables))
ca = report.change_analysis
assert ca.project_type == "enhancement"
assert [e.element_id for e in ca.new_elements] == ["F001"]
assert ca.new_elements[0].change_type is ChangeType.NEW
assert [e.element_id for e in ca.modified_elements] == ["F002", "F003"]
m2 = ca.modified_elements[0]
assert m2.impacted_existing == ["OrderController", "OrderServiceImpl"]
assert m2.evidence # 命中类 source_uri
assert [e.element_id for e in ca.deleted_elements] == ["F004"]
assert ca.deleted_elements[0].impacted_existing == ["StrategyBacktest"]
assert ca.warnings == []
def test_prefix_mapping_matches_impl_and_entity():
tables = [_table("機能一覧", SheetType.FUNCTION, [
_row("F005", "订单查询", "変更", "OrderService"), # 前缀匹配 OrderServiceImpl
_row("F006", "订单表变更", "変更", "OrderDO"),
])]
ca = ImpactAgent().run(_ss(_existing(), tables)).change_analysis
m = {e.element_id: e for e in ca.modified_elements}
assert "OrderServiceImpl" in m["F005"].impacted_existing
assert m["F006"].impacted_existing == ["OrderDO"]
def test_new_with_mapping_is_conflict_warning():
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F007", "新建订单接口", "新規", "OrderController")])]
report = ImpactAgent().run(_ss(_existing(), tables))
el = report.change_analysis.new_elements[0]
assert el.status == "conflict"
assert any(w.element_id == "F007" for w in report.change_analysis.warnings)
def test_modified_without_mapping_warns():
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F008", "订单变更", "変更", "")])]
report = ImpactAgent().run(_ss(_existing(), tables))
assert report.change_analysis.modified_elements[0].status == "warning"
assert any(w.element_id == "F008" for w in report.change_analysis.warnings)
def test_modified_mapping_no_match_warns():
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F009", "账户变更", "変更", "AccountController")])]
report = ImpactAgent().run(_ss(_existing(), tables))
assert report.change_analysis.modified_elements[0].status == "warning"
assert any("AccountController" in w.issue for w in report.change_analysis.warnings)
def test_unchanged_count_in_summary():
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F002", "订单查询", "変更", "OrderController")])]
report = ImpactAgent().run(_ss(_existing(), tables))
assert report.summary["unchanged"] >= 2 # 既有 5 类中未被引用者
def test_table_without_change_column_skipped():
tbl = ExcelTable(name="帳票一覧", detected_type=SheetType.REPORT, extraction_method="openpyxl",
headers=["帳票ID", "帳票名"], rows=[])
report = ImpactAgent().run(_ss(_existing(), [tbl]))
assert report.change_analysis.new_elements == []
assert report.change_analysis.warnings == []
def test_run_without_existing_raises():
ss = StructuredSource(tables=[], template=None, rule_docs=[], image_analyses=[],
existing_system=None, comments=[])
with pytest.raises(ValueError, match="既有系统"):
ImpactAgent().run(ss)
def test_metadata_and_summary_shape():
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F001", "新功能", "新規", "")])]
report = ImpactAgent().run(_ss(_existing(), tables), session_id="sess-1")
assert report.metadata["version"] == "v1"
assert report.metadata["session_id"] == "sess-1"
assert report.metadata["source"] == "samples/existing-system"
assert report.summary["total"] == 1
assert report.summary["new"] == 1
# ---------- 覆盖率补全(分支覆盖) ----------
def test_scope_logs_warning(caplog):
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F001", "新功能", "新規", "")])]
with caplog.at_level("WARNING", logger="genesis.impact.impact_agent"):
ImpactAgent().run(_ss(_existing(), tables), scope={"modules": ["trade-order"]})
assert any("scope" in r.message for r in caplog.records)
def test_empty_and_unknown_change_values_skipped():
def cv(v):
return CellValue(v, Provenance("f.xlsx", "S", 1, "A", "h"))
headers = ["要素ID", "要素名", "変更区分", "既存対応"]
rows = [
{"要素ID": cv("F100"), "要素名": cv("空区分"), "変更区分": cv(""), "既存対応": cv("")},
{"要素ID": cv("F101"), "要素名": cv("未知区分"), "変更区分": cv("未知"), "既存対応": cv("")},
{"要素ID": cv("F102"), "要素名": cv("正常"), "変更区分": cv("新規"), "既存対応": cv("")},
]
tbl = ExcelTable("機能一覧", SheetType.FUNCTION, "openpyxl", headers, rows)
report = ImpactAgent().run(_ss(_existing(), [tbl]))
assert [e.element_id for e in report.change_analysis.new_elements] == ["F102"]
assert report.change_analysis.warnings == []
def test_deleted_without_mapping_warns_and_lists():
tables = [_table("機能一覧", SheetType.FUNCTION, [_row("F030", "旧功能", "削除", "")])]
report = ImpactAgent().run(_ss(_existing(), tables))
assert report.change_analysis.deleted_elements[0].status == "warning"
assert any(w.element_id == "F030" for w in report.change_analysis.warnings)
def test_entity_without_table_name_matches_by_class():
existing = _existing()
existing.entity_layer.append(EntityInfo("NoTable", "NoTable", "NT.java", None, [], "NT.java"))
tables = [_table("DB定義", SheetType.DATABASE, [_row("T100", "无表实体", "変更", "NoTable")])]
report = ImpactAgent().run(_ss(existing, tables))
assert report.change_analysis.modified_elements[0].impacted_existing == ["NoTable"]
def test_matching_dedup_same_class_via_class_and_table():
tables = [_table("DB定義", SheetType.DATABASE, [
_row("T101", "订单表", "変更", "OrderDO,trade_order"),
_row("T102", "订单综合", "変更", "Order,Order"),
])]
ca = ImpactAgent().run(_ss(_existing(), tables)).change_analysis
by_id = {e.element_id: e for e in ca.modified_elements}
assert by_id["T101"].impacted_existing == ["OrderDO"] # 类名+表名 → 同一实体去重
assert by_id["T102"].impacted_existing.count("OrderController") == 1 # 前缀重复去重
def test_cell_out_of_range_returns_empty():
assert ImpactAgent._cell({}, ["A"], 5) == ""
def test_impact_report_to_dict_serializable():
from genesis.impact.impact_agent import impact_report_to_dict
tables = [_table("機能一覧", SheetType.FUNCTION, [
_row("F001", "止损风控", "新規", ""),
_row("F002", "订单查询", "変更", "OrderController"),
])]
report = ImpactAgent().run(_ss(_existing(), tables), session_id="s")
d = impact_report_to_dict(report)
assert d["metadata"]["session_id"] == "s"
assert d["change_analysis"]["new_elements"][0]["change_type"] == "新規"
assert d["change_analysis"]["modified_elements"][0]["impacted_existing"] == ["OrderController"]
assert d["summary"]["modified"] == 1
+17
View File
@@ -28,3 +28,20 @@ def test_build_contexts_maps_chapters():
assert c.design_rules == []
assert c.template_styles == {"Heading1", "Normal"}
assert c.template_marker.section_placeholder == "section:introduction"
def test_build_contexts_without_impact_report_is_none():
ctxs = build_contexts(_ss(), samples_dir="nonexistent_dir_xyz")
assert ctxs[0].impact_report is None
def test_build_contexts_carries_impact_report():
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
ca = ChangeAnalysis(project_type="enhancement", new_elements=[], modified_elements=[],
deleted_elements=[], unchanged_elements=[], warnings=[])
report = ImpactReport(metadata={"version": "v1"}, change_analysis=ca, summary={})
ss = _ss()
ss.impact_report = report
ctxs = build_contexts(ss, samples_dir="nonexistent_dir_xyz")
assert ctxs[0].impact_report is report
assert "project_type" in ctxs[0].to_vars()["impact"] or ctxs[0].to_vars()["impact"] != ""
+57 -2
View File
@@ -14,14 +14,69 @@ def test_chapter_content_holds_blocks():
assert c.version == 1
def test_generation_context_no_impact_field():
def test_generation_context_impact_field_default_none():
ctx = GenerationContext(
chapter_id="db_design", title="DB 设计",
template_marker=ChapterSpec(chapter_id="db_design", title="DB 设计", section_placeholder="{{section:db_design}}"),
structured_source=None, write_rules=["规则1"], design_rules=["规则2"],
template_styles={"Heading 1"},
)
assert not hasattr(ctx, "impact")
assert ctx.impact_report is None
# to_vars 始终提供 impact 变量;无影响调查书时为空串
assert ctx.to_vars()["impact"] == ""
def test_generation_context_impact_var_formats_report():
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[ChangeElement("F001", "機能", "止损风控", ChangeType.NEW)],
modified_elements=[],
deleted_elements=[],
unchanged_elements=[],
warnings=[],
)
report = ImpactReport(metadata={"version": "v1"}, change_analysis=ca, summary={"new": 1})
ctx = GenerationContext(
chapter_id="db_design", title="DB 设计",
template_marker=ChapterSpec(chapter_id="db_design", title="DB 设计", section_placeholder="{{section:db_design}}"),
structured_source=None, write_rules=["规则1"], design_rules=["规则2"],
template_styles={"Heading 1"}, impact_report=report,
)
impact = ctx.to_vars()["impact"]
assert "止损风控" in impact
assert "F001" in impact
def _impact_ctx(report):
return GenerationContext(
chapter_id="db_design", title="DB 设计",
template_marker=ChapterSpec(chapter_id="db_design", title="DB 设计", section_placeholder="{{section:db_design}}"),
structured_source=None, write_rules=[], design_rules=[],
template_styles=set(), impact_report=report,
)
def test_impact_var_empty_when_report_without_analysis():
from genesis.data_models import ImpactReport
report = ImpactReport(metadata={"version": "v1"}, change_analysis=None, summary={})
assert _impact_ctx(report).to_vars()["impact"] == ""
def test_impact_var_formats_deleted_and_warnings():
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType, ImpactReport, ImpactWarning
ca = ChangeAnalysis(
project_type="enhancement",
new_elements=[],
modified_elements=[],
deleted_elements=[ChangeElement("F030", "機能", "旧功能", ChangeType.DELETED)],
unchanged_elements=[],
warnings=[ImpactWarning("F030", "缺少既存対応")],
)
report = ImpactReport(metadata={}, change_analysis=ca, summary={"deleted": 1, "warnings": 1})
impact = _impact_ctx(report).to_vars()["impact"]
assert "[削除]" in impact and "旧功能" in impact
assert "[警告]" in impact and "缺少既存対応" in impact
def test_chapter_spec_placeholder_optional():
+6
View File
@@ -60,6 +60,12 @@ def test_prompt_template_enforces_title_language():
assert "{{title}}" in WRITER_PROMPT_TEMPLATE
def test_prompt_template_has_impact_context_var():
# 影响调查结果作为生成主上下文:模板必须包含 impact 变量(无影响书时渲染为空串)
assert "影响调查上下文" in WRITER_PROMPT_TEMPLATE
assert "{{impact}}" in WRITER_PROMPT_TEMPLATE
def test_generate_chapter_retries():
class Boom(FakeEngine):
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
+95 -1
View File
@@ -2,13 +2,20 @@ import pytest
from types import SimpleNamespace
from docx import Document
from genesis.data_models import ParsedTemplate, ChapterMarker
from genesis.data_models import (
ControllerInfo, EntityInfo, ExistingSystemInfo, ImpactReport, ParsedTemplate,
ChapterMarker, ServiceInfo,
)
from genesis.writer.orchestrator import WriteOrchestrator
from genesis.writer.models import ChapterContent
class FakeEngine:
def __init__(self):
self.last_variables = None
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
self.last_variables = variables
# 返回一个固定章节内容(title + 一个段落块)
return SimpleNamespace(
data={"title": variables["title"], "blocks": [{"type": "paragraph", "text": "自动生成的内容"}]},
@@ -108,3 +115,90 @@ def test_generate_warns_on_unanchored_heading(tmp_path, caplog):
template_path=str(tpl),
)
assert any("附録" in r.message for r in caplog.records)
# ---------- Impact Agent MVP:影响调查结果作为生成主上下文(2026-08-23 ----------
def _ss_with_existing(template_path):
from genesis.data_models import CellValue, ExcelTable, Provenance, SheetType
parsed = ParsedTemplate(
file_name=template_path,
sections=[
ChapterMarker(type="heading", name="はじめに", level=1),
ChapterMarker(type="placeholder", name="section:introduction", level=0),
],
placeholders={},
styles={"defined": ["Heading 1"], "used": ["Heading 1"]},
)
existing = ExistingSystemInfo(
controller_layer=[ControllerInfo("OrderController", "OrderController", "OrderController.java",
"/trade/order", [], "trade-order/OrderController.java")],
service_layer=[],
entity_layer=[EntityInfo("OrderDO", "OrderDO", "OrderDO.java", "trade_order", ["id"], "trade-order/OrderDO.java")],
api_endpoints=[],
source_path="samples/existing-system",
)
headers = ["機能ID", "機能名", "変更区分", "既存対応"]
rows = [{
"機能ID": CellValue("F002", Provenance("f.xlsx", "機能一覧", 3, "A", "機能ID")),
"機能名": CellValue("订单状态查询扩展", Provenance("f.xlsx", "機能一覧", 3, "B", "機能名")),
"変更区分": CellValue("変更", Provenance("f.xlsx", "機能一覧", 3, "G", "変更区分")),
"既存対応": CellValue("OrderController", Provenance("f.xlsx", "機能一覧", 3, "H", "既存対応")),
}]
tables = [ExcelTable(name="機能一覧", detected_type=SheetType.FUNCTION, extraction_method="openpyxl",
headers=headers, rows=rows)]
return SimpleNamespace(template=parsed, existing_system=existing, tables=tables,
impact_report=None)
def test_generate_auto_runs_impact_when_existing_system(tmp_path):
"""门控:existing_system 非 None 且未显式传 impact_report → 自动跑影响调查,注入生成上下文。"""
tpl = tmp_path / "tpl.docx"
out = tmp_path / "out.docx"
_make_template(str(tpl))
engine = FakeEngine()
WriteOrchestrator().generate(
_ss_with_existing(str(tpl)), str(out),
samples_dir="nonexistent_dir_xyz", engine=engine,
)
impact = engine.last_variables["impact"]
assert "project_type=enhancement" in impact
assert "OrderController" in impact
def test_generate_without_existing_system_impact_empty(tmp_path):
tpl = tmp_path / "tpl.docx"
out = tmp_path / "out.docx"
_make_template(str(tpl))
engine = FakeEngine()
WriteOrchestrator().generate(
_ss(str(tpl)), str(out),
samples_dir="nonexistent_dir_xyz", engine=engine,
)
assert engine.last_variables["impact"] == ""
def test_generate_explicit_impact_report_used(tmp_path):
from genesis.data_models import ChangeAnalysis, ChangeElement, ChangeType
tpl = tmp_path / "tpl.docx"
out = tmp_path / "out.docx"
_make_template(str(tpl))
ca = ChangeAnalysis(project_type="enhancement", new_elements=[], modified_elements=[],
deleted_elements=[], unchanged_elements=[], warnings=[])
report = ImpactReport(metadata={"version": "v1"}, change_analysis=ca, summary={"new": 0})
engine = FakeEngine()
ss = _ss_with_existing(str(tpl))
WriteOrchestrator().generate(ss, str(out), samples_dir="nonexistent_dir_xyz",
engine=engine, impact_report=report)
assert engine.last_variables["impact"] != ""
# 显式传入时也回填 structured_source,便于 QA/日志读取
assert ss.impact_report is report
def test_generate_missing_template_path_raises(tmp_path):
parsed = ParsedTemplate(file_name=None, sections=[], placeholders={}, styles={"used": []})
with pytest.raises(ValueError, match="template_path"):
WriteOrchestrator().generate(
SimpleNamespace(template=parsed), str(tmp_path / "out.docx"),
samples_dir="nonexistent_dir_xyz", engine=FakeEngine(),
)
+74
View File
@@ -2,6 +2,7 @@ import pytest
from pathlib import Path
from genesis.data_models import StructuredSource
from genesis.impact.code_parser import CodeParseError
from genesis.parsers.source_aggregator import SourceParser
from tests.docx_helpers import make_rule_doc, new_document, save_document
@@ -125,3 +126,76 @@ def test_validate_path_returns_resolved_path(tmp_path):
result = _validate_path(str(good), (".xlsx",))
assert isinstance(result, Path)
assert result.suffix.lower() == ".xlsx"
# ---------- Impact Agent MVPexisting_system_path2026-08-23 ----------
JAVA_PROJECT = {
"trade-order/OrderController.java": (
"package com.trade.order.controller;\n"
"import org.springframework.web.bind.annotation.*;\n"
"@RestController\n"
"@RequestMapping(\"/api/order\")\n"
"public class OrderController {\n"
" @GetMapping(\"/{id}\")\n"
" public String get(Long id) { return \"ok\"; }\n"
"}\n"
),
"trade-order/OrderServiceImpl.java": (
"package com.trade.order.service;\n"
"import org.springframework.stereotype.Service;\n"
"@Service\n"
"public class OrderServiceImpl {\n"
" public void createOrder() {}\n"
"}\n"
),
"trade-order/OrderDO.java": (
"package com.trade.order.entity;\n"
"import com.baomidou.mybatisplus.annotation.TableName;\n"
"@TableName(\"trade_order\")\n"
"public class OrderDO {\n"
" private Long id;\n"
"}\n"
),
}
def _java_project(tmp_path) -> str:
for rel, content in JAVA_PROJECT.items():
p = tmp_path / "existing" / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content, encoding="utf-8")
return str(tmp_path / "existing")
def test_parse_with_existing_system_path(tmp_path):
xlsx = _xlsx(tmp_path)
existing = _java_project(tmp_path)
result = SourceParser().parse(requirement_paths=[xlsx], existing_system_path=existing)
assert result.existing_system is not None
names = {c.class_name for c in result.existing_system.controller_layer}
assert "OrderController" in names
assert {s.class_name for s in result.existing_system.service_layer} == {"OrderServiceImpl"}
assert {e.class_name for e in result.existing_system.entity_layer} == {"OrderDO"}
def test_parse_without_existing_system_path_keeps_none(tmp_path):
xlsx = _xlsx(tmp_path)
result = SourceParser().parse(requirement_paths=[xlsx])
assert result.existing_system is None
def test_parse_existing_system_path_invalid_dir_raises(tmp_path):
xlsx = _xlsx(tmp_path)
with pytest.raises(CodeParseError):
SourceParser().parse(requirement_paths=[xlsx], existing_system_path=tmp_path / "nope")
def test_parse_existing_system_path_without_java_raises(tmp_path):
xlsx = _xlsx(tmp_path)
empty = tmp_path / "empty"
empty.mkdir()
with pytest.raises(CodeParseError):
SourceParser().parse(requirement_paths=[xlsx], existing_system_path=empty)