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:
+3
-1
@@ -109,4 +109,6 @@
|
||||
| 2026-08-13 23:35 | 测试验证 | P5-T10 门禁诊断:修复 WriterGenerationError 吞掉底层 LLM 错误(如 401 详情),补透传测试 | src/genesis/writer/writer_agent.py; tests/test_phase5_writer_agent.py | hy3-free |
|
||||
| 2026-08-13 23:50 | Agent 实现 | 修复 HttpLLMClient 在同步门禁中多次 asyncio.run 复用已关闭事件循环致 Event loop is closed:chat 改为每次调用新建 client(保留 async with 协议)| src/genesis/inference/client.py | hy3-free |
|
||||
| 2026-08-13 23:58 | Agent 实现 | P5-T10 语言对齐:WRITER_PROMPT_TEMPLATE 增加「正文语言须与章节标题一致」约束(模板日文则输出日文)| src/genesis/writer/writer_agent.py; tests/test_phase5_writer_agent.py | hy3-free |
|
||||
| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash |
|
||||
| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash || 2026-08-23 | 需求理解 | 用户指出写概要设计书需「要件定義 + 既有系统(设计/代码)做影响调查 → 确定修改范围」;确认场景=追加改修、输入=Java 项目源码;选定既有系统样本 sunOnly/stock-trade-system(股票量化交易系统,无 LICENSE,仅作测试输入保留来源标注,引入 samples/existing-system/,13 模块 278 Java 文件);确认领域不一致 → 新造股票交易域追加改修样本 | docs/superpowers/specs/2026-08-23-impact-mvp.md; samples/existing-system/README.md | deepseek-v4-flash |
|
||||
| 2026-08-23 | 架构设计 | Impact Agent MVP 设计(brainstorming 逐问确认):门控=用户是否提供 existing_system 路径(流水线级,非章节级);范围确认=与用户确认调查范围(模块/深度);消费模型=影响调查结果是生成概要设计书的主上下文(整本基于影响结果生成,无专用影响章/无 {{section:impact}} 锚点门控);MVP=变更点定位(确定性规则,无 LLM);spec 落盘 docs/superpowers/specs/2026-08-23-impact-mvp.md | docs/superpowers/specs/2026-08-23-impact-mvp.md | deepseek-v4-flash |
|
||||
| 2026-08-23 | Agent 实现 | Impact Agent MVP 实现(TDD RED→GREEN→全量 351 passed / 99.27%):data_models 新增 ChangeType/ChangeElement/ImpactWarning/ChangeAnalysis/ImpactReport + StructuredSource.impact_report;新增 src/genesis/impact/(code_parser.py Java 解析 @RestController/@Service/@Entity/@TableName、existing_system_explorer.py 组装、impact_agent.py 变更点定位+impact_report_to_dict 序列化);source_aggregator 解除 existing_system=None 硬编码(新增 existing_system_path 门控);writer 集成——GenerationContext.impact_report + to_vars()[impact]、WRITER_PROMPT_TEMPLATE 加「影响调查上下文」、context_builder 透传、WriteOrchestrator.generate 门控自动跑 ImpactAgent + 回填 + 默认 meta(doc_title/version/created_at);新造样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名);门禁(fake 模式)PASS:影响调查 total=16 new=5 modified=8 deleted=3 unchanged=50 warnings=0,概要设计书 13 章产出,影响调查书 JSON 可下载 | src/genesis/data_models.py; src/genesis/impact/code_parser.py; src/genesis/impact/existing_system_explorer.py; src/genesis/impact/impact_agent.py; src/genesis/parsers/source_aggregator.py; src/genesis/writer/models.py; src/genesis/writer/writer_agent.py; src/genesis/writer/context_builder.py; src/genesis/writer/orchestrator.py; tests/test_data_models.py; tests/test_code_parser.py; tests/test_existing_system_explorer.py; tests/test_impact_agent.py; tests/test_source_aggregator.py; tests/test_phase5_models.py; tests/test_phase5_writer_agent.py; tests/test_phase5_context_builder.py; tests/test_phase5_writer_orchestrator.py; samples/要件定義_追加改修_股票.xlsx; samples/existing-system/; docs/superpowers/specs/2026-08-23-impact-mvp.md; _AI_USAGE_LOG.md | deepseek-v4-flash |
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# Impact Agent MVP 设计(2026-08-23)
|
||||
|
||||
## 背景
|
||||
|
||||
用户指出:写概要设计书需要「根据要件定义书 + 既有系统的设计与代码做影响调查 → 确定修改范围」。当前
|
||||
`source_aggregator.py` 硬编码 `existing_system=None`,无 Impact Agent 实现。本次实现 **MVP 变更点定位 + 影响书**。
|
||||
|
||||
关键用户决策(brainstorming 确认):
|
||||
1. 场景 = **追加改修**(需影响调查)
|
||||
2. 既有系统输入形态 = **Java 项目源码**
|
||||
3. 既有系统样本 = `samples/existing-system/`(sunOnly/stock-trade-system,股票量化交易系统,无 LICENSE,
|
||||
仅作测试输入,保留来源标注)
|
||||
4. 里程碑范围 = **MVP**:解析既有系统 → 变更点定位 → 影响书(JSON);不做 LLM 要素抽取/关联推理
|
||||
5. 样本同域化:新造 `samples/要件定義_追加改修_股票.xlsx`(股票交易域,对齐 sunOnly 真实模块)
|
||||
6. **门控**:是否提供 `existing_system` 路径决定是否进入影响调查阶段(非章节级)
|
||||
7. **范围确认**:进入影响调查前与用户确认调查范围(模块/深度)
|
||||
8. **消费模型**:`ImpactReport` 是生成概要设计书的**主上下文**——整本概要设计书基于影响调查结果生成,
|
||||
**无专用影响章、无 `{{section:impact}}` 锚点门控**;`ImpactReport` 同时作为可下载独立产物
|
||||
|
||||
## 架构与数据流
|
||||
|
||||
```
|
||||
要件定義_追加改修_股票.xlsx(新造) samples/existing-system/(sunOnly 源码)
|
||||
│ 现有 ExcelParser │ 新增 CodeParser(Java 解析)
|
||||
▼ ▼
|
||||
StructuredSource.tables CodeStructure → ExistingSystemInfo
|
||||
│ │
|
||||
└──────────────┬───────────────────────────┘
|
||||
▼
|
||||
ImpactAgent(变更点定位,确定性比对)
|
||||
- 取 変更区分 + 既存対応 列
|
||||
- 与 ExistingSystemInfo 连接比对(存在性校验)
|
||||
▼
|
||||
ImpactReport(JSON 中间产物,可下载)
|
||||
▼
|
||||
WriteOrchestrator.generate(impact_report)
|
||||
- ImpactReport 作为主上下文喂给 WriterAgent
|
||||
- 整本概要设计书基于影响结果生成
|
||||
```
|
||||
|
||||
## 门控与范围确认
|
||||
|
||||
- **门控(流水线级)**:`SourceParser.parse(existing_system_path=...)` 提供路径 ⇒ `StructuredSource.existing_system`
|
||||
非 None ⇒ 进入影响调查;`WriteOrchestrator.generate` 检测到 `existing_system` 非 None 且未显式传
|
||||
`impact_report` 时,自动运行 `ImpactAgent` 生成影响书。
|
||||
- **范围确认(human-in-loop)**:MVP 默认范围 = 提供的整个目录树(全模块、Controller/Service/Entity/Endpoint 全层);
|
||||
可按参数收窄(`modules`/`depth`)。完整 UI 确认留后续。
|
||||
- **门控降级**:CodeParser 失败(非 Java/无源码)⇒ 跳过影响调查并告警,生成照常(无 ImpactReport)。
|
||||
|
||||
## 变更点定位规则(确定性,无 LLM)
|
||||
|
||||
| 要件情况 | 判定 |
|
||||
|---|---|
|
||||
| 新規 + 既存対応空 | `new`(确认新增) |
|
||||
| 新規 + 既存対応指定 | `conflict` 告警(自相矛盾) |
|
||||
| 変更 + 既存対応命中且类存在 | `modified`(受影响既有类 = 映射清单) |
|
||||
| 変更 + 既存対応缺失/类不存在 | `warning`(无法定位修改对象) |
|
||||
| 削除 + 既存対応命中 | `deleted`(确认删除) |
|
||||
| 削除 + 既存対応缺失/类不存在 | `warning` |
|
||||
| 既有类未被任何要件引用 | `unchanged`(summary 计数) |
|
||||
|
||||
`既存対応` 列值 = Controller/Service/Entity 类名(逗号分隔);命中判定为类名出现在
|
||||
`ExistingSystemInfo` 三层清单中(不区分大小写,后缀 `Controller`/`Service`/`Entity` 可选)。
|
||||
|
||||
## 数据模型(data_models.py 新增)
|
||||
|
||||
```python
|
||||
class ChangeType(Enum):
|
||||
NEW = "新規"; MODIFIED = "変更"; DELETED = "削除"; UNCHANGED = "不变"
|
||||
|
||||
@dataclass
|
||||
class ChangeElement:
|
||||
element_id: str; element_type: str; name: str
|
||||
change_type: ChangeType
|
||||
existing_mapping: list[str] # 既存対応 声明值
|
||||
impacted_existing: list[str] # 确认命中的既有类
|
||||
evidence: str # 既有类 source_uri / 空
|
||||
status: str # "ok" | "conflict" | "warning"
|
||||
|
||||
@dataclass
|
||||
class ImpactWarning:
|
||||
element_id: str; issue: str
|
||||
|
||||
@dataclass
|
||||
class ChangeAnalysis:
|
||||
project_type: str # "enhancement"
|
||||
new_elements: list[ChangeElement]
|
||||
modified_elements: list[ChangeElement]
|
||||
deleted_elements: list[ChangeElement]
|
||||
unchanged_elements: list[ChangeElement]
|
||||
warnings: list[ImpactWarning]
|
||||
|
||||
@dataclass
|
||||
class ImpactReport:
|
||||
metadata: dict
|
||||
change_analysis: ChangeAnalysis
|
||||
summary: dict
|
||||
```
|
||||
|
||||
## 组件
|
||||
|
||||
### `src/genesis/impact/code_parser.py`(新)
|
||||
- `CodeParser.parse(root_path) -> CodeStructure`:扫描 `**/*.java`,按注解识别:
|
||||
- `@RestController|@Controller` → ControllerInfo(base_path 取 `@RequestMapping`,endpoints 取方法级映射)
|
||||
- `@Service` → ServiceInfo(methods)
|
||||
- `@Entity|@Table(name=...)` → EntityInfo(table_name、fields)
|
||||
- 方法映射 → EndpointInfo
|
||||
- 顶层目录 → modules;失败(无 Java/路径无效)→ 抛 `CodeParseError`
|
||||
|
||||
### `src/genesis/impact/existing_system_explorer.py`(新)
|
||||
- `ExistingSystemExplorer.explore(code: CodeStructure) -> ExistingSystemInfo`
|
||||
|
||||
### `src/genesis/impact/impact_agent.py`(新)
|
||||
- `ImpactAgent.run(structured_source, scope=None) -> ImpactReport`
|
||||
- 从 `tables` 提取带 `変更区分`/`既存対応` 列的表行 → 按上表规则分类 → summary
|
||||
|
||||
### `src/genesis/parsers/source_aggregator.py`(改)
|
||||
- `parse(..., existing_system_path=None)`:提供路径时解析并填充 `existing_system`
|
||||
|
||||
### `src/genesis/writer/`(改)
|
||||
- `GenerationContext` 新增 `impact_report: object | None = None`;`to_vars()` 增加 `"impact"` 变量
|
||||
- `WRITER_PROMPT_TEMPLATE` 增加 `影响调查上下文:\n{{impact}}\n`(在参考资料前)
|
||||
- `context_builder.build_contexts` 从 `structured_source.impact_report` 透传
|
||||
- `WriteOrchestrator.generate(..., impact_report=None)`:门控自动跑 `ImpactAgent`;上下文携带影响书
|
||||
- `StructuredSource` 新增 `impact_report: ImpactReport | None = None`(生成后回填,便于 QA/日志)
|
||||
|
||||
## 错误处理
|
||||
|
||||
- `CodeParseError`(非 Java/无源码/路径无效)→ 门控降级跳过 + 告警
|
||||
- `既存対応` 引用不存在 → `warning`,不阻断
|
||||
- `新規` 却映射既有 → `conflict` warning
|
||||
|
||||
## 测试策略(TDD,全量 ≥99%)
|
||||
|
||||
- `tests/test_data_models.py`:新增 ImpactReport 模型默认值/构造
|
||||
- `tests/test_code_parser.py`:用 `samples/existing-system` 真实文件断言提取(类名/数量/路径)
|
||||
- `tests/test_existing_system_explorer.py`:组装 ExistingSystemInfo 分层
|
||||
- `tests/test_impact_agent.py`:给定 要件定義(新造样本)+ ExistingSystemInfo → 断言分类
|
||||
- `tests/test_source_aggregator.py`:`existing_system_path` 加载;无效路径降级/报错
|
||||
- `tests/test_phase5_models.py`:更新 `test_generation_context_no_impact_field` → 断言 `impact` 变量存在且为空
|
||||
- `tests/test_phase5_writer_agent.py`:断言模板含影响上下文变量
|
||||
- `tests/test_phase5_writer_orchestrator.py`:传 impact_report → 断言 `variables["impact"]` 含影响信息
|
||||
- 门禁(fake 模式):sunOnly + 新造样本端到端产出 ImpactReport + 概要设计书
|
||||
|
||||
## 新造样本规格
|
||||
|
||||
`samples/要件定義_追加改修_股票.xlsx`(股票交易域,对齐 sunOnly 真实模块):
|
||||
- 機能一覧:F001 止损风控机能(新規,无既存対応) / F002 订单状态查询扩展(変更→OrderController,OrderQueryService)
|
||||
/ F003 实时行情显示扩展(変更→RealtimeMarketDataController) / F004 旧回测机能(削除→StrategyBacktestController)
|
||||
- 画面一覧(对应画面 + 変更区分)/DB定義(新規 止损设定表/変更 订单表/削除 旧回测表)/IF定義(新規 风控告警连携IF)
|
||||
- 各表新增 `既存対応` 列(类名或留空)+ `変更区分` 列
|
||||
- `改修ポイント` 自由文本(改修概要)
|
||||
|
||||
## 里程碑外(后续)
|
||||
|
||||
- LLM 要素抽取 / 关联推理 / 影响矩阵
|
||||
- v1→用户逐条修正→v2 生命周期 UI
|
||||
- 范围确认 UI / API 化
|
||||
@@ -0,0 +1,9 @@
|
||||
/.idea/
|
||||
*.class
|
||||
*.lst
|
||||
*.log
|
||||
.idea/*
|
||||
*/target/*
|
||||
*/.idea/*
|
||||
*.iml
|
||||
*.ipr
|
||||
@@ -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 配置)。
|
||||
@@ -0,0 +1,3 @@
|
||||
project_rules.md
|
||||
README.md
|
||||
plan.md
|
||||
@@ -0,0 +1,116 @@
|
||||
# 开发计划
|
||||
|
||||
## `trade-indicator` 模块开发
|
||||
|
||||
- # TASK-001 创建 `IndicatorDefinitionDO.java` 和 `IndicatorValueDO.java` 实体类 [已完成]
|
||||
- # TASK-002 创建 `IndicatorDefinitionMapper.java` 和 `IndicatorValueMapper.java` 数据访问接口 [已完成]
|
||||
- # TASK-003 创建 `IndicatorDefinitionService.java` 和 `IndicatorValueService.java` 业务逻辑接口 [已完成]
|
||||
- # TASK-004 创建 `IndicatorDefinitionServiceImpl.java` 和 `IndicatorValueServiceImpl.java` 业务逻辑实现类 [已完成]
|
||||
- # TASK-005 创建 `IndicatorDefinitionController.java` 和 `IndicatorValueController.java` API 接口 [已完成]
|
||||
- # TASK-006 创建 `IndicatorDefinitionConvert.java` 和 `IndicatorValueConvert.java` 对象转换工具类 [已完成]
|
||||
- # TASK-007 创建 `IndicatorJob.java` 定时任务类并启用调度 [已完成]
|
||||
- # TASK-008 定义 `ErrorCodeConstants.java` 错误码 [已完成]
|
||||
- # TASK-009 配置 `bootstrap.yml` 和 `logback-spring.xml` [已完成]
|
||||
- # TASK-010 完善 `pom.xml` 依赖配置 [已完成]
|
||||
- # TASK-011 编写集成测试 [未开始]
|
||||
- # TASK-012 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-order` 模块开发
|
||||
|
||||
- # TASK-013 创建 `OrderDO.java`, `OrderItemDO.java`, `OrderLogDO.java` 实体类 [已完成]
|
||||
- # TASK-014 创建 `OrderMapper.java`, `OrderItemMapper.java`, `OrderLogMapper.java` 数据访问接口 [已完成]
|
||||
- # TASK-015 创建 `OrderDirectionEnum.java`, `OrderStatusEnum.java`, `OrderTypeEnum.java` 枚举类 [已完成]
|
||||
- # TASK-016 创建 `OrderService.java`, `OrderItemService.java`, `OrderLogService.java` 业务逻辑接口 [已完成]
|
||||
- # TASK-017 创建 `OrderServiceImpl.java`, `OrderItemServiceImpl.java`, `OrderLogServiceImpl.java` 业务逻辑实现类 [已完成]
|
||||
- # TASK-018 创建 `OrderController.java` API 接口 [已完成]
|
||||
- # TASK-019 创建 `OrderCreateReqVO.java`, `OrderUpdateReqVO.java`, `OrderPageReqVO.java` 请求 VO 类 [已完成]
|
||||
- # TASK-020 创建 `OrderRespVO.java`, `OrderItemRespVO.java`, `OrderLogRespVO.java` 响应 VO 类 [已完成]
|
||||
- # TASK-021 创建 `OrderConvert.java`, `OrderItemConvert.java`, `OrderLogConvert.java` 对象转换工具类 [已完成]
|
||||
- # TASK-022 定义 `ErrorCodeConstants.java` 错误码 [已完成]
|
||||
- # TASK-023 配置 `bootstrap.yml` 和 `logback-spring.xml` [已完成]
|
||||
- # TASK-024 完善 `pom.xml` 依赖配置 [已完成]
|
||||
- # TASK-025 创建 `OrderApplication.java` 启动类 [已完成]
|
||||
- # TASK-026 编写集成测试 [未开始]
|
||||
- # TASK-027 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-application` 模块开发
|
||||
|
||||
- # TASK-028 创建基础结构和配置文件 [未开始]
|
||||
- # TASK-029 实现核心应用逻辑 [未开始]
|
||||
- # TASK-030 编写集成测试 [未开始]
|
||||
- # TASK-031 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-backtest` 模块开发
|
||||
|
||||
- # TASK-032 创建回测引擎核心类 [未开始]
|
||||
- # TASK-033 实现回测数据处理逻辑 [未开始]
|
||||
- # TASK-034 实现回测结果分析与展示 [未开始]
|
||||
- # TASK-035 编写集成测试 [未开始]
|
||||
- # TASK-036 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-common` 模块开发
|
||||
|
||||
- # TASK-CMN-001 `trade-common` 模块基础开发(通用工具类, 通用配置)[进行中]
|
||||
- # TASK-037 定义通用工具类和常量 [未开始]
|
||||
- # TASK-038 实现通用配置和基础组件 [未开始]
|
||||
- # TASK-039 编写单元测试 [未开始]
|
||||
|
||||
## `trade-gateway` 模块开发
|
||||
|
||||
- # TASK-040 配置网关路由规则 [未开始]
|
||||
- # TASK-041 实现统一认证和鉴权 [未开始]
|
||||
- # TASK-042 实现API限流和熔断 [未开始]
|
||||
- # TASK-043 编写集成测试 [未开始]
|
||||
- # TASK-044 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-market-data` 模块开发
|
||||
|
||||
- # TASK-MKT-001 `trade-market-data` 模块基础开发(Controller, Service, Mapper, DO)[进行中]
|
||||
- # TASK-045 实现行情数据采集接口 [未开始]
|
||||
- # TASK-046 实现行情数据存储逻辑 [未开始]
|
||||
- # TASK-047 实现行情数据推送机制 [未开始]
|
||||
- # TASK-048 编写集成测试 [未开始]
|
||||
- # TASK-049 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-mbg` 模块开发
|
||||
|
||||
- # TASK-050 配置MyBatis Generator [未开始]
|
||||
- # TASK-051 生成各模块DAO层代码 [未开始]
|
||||
|
||||
## `trade-notification` 模块开发
|
||||
|
||||
- # TASK-052 实现消息通知服务接口 [未开始]
|
||||
- # TASK-053 对接邮件、短信等通知渠道 [未开始]
|
||||
- # TASK-054 编写集成测试 [未开始]
|
||||
- # TASK-055 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-risk` 模块开发
|
||||
|
||||
- # TASK-056 实现风控规则定义模块 [未开始]
|
||||
- # TASK-057 实现风控规则执行引擎 [未开始]
|
||||
- # TASK-058 实现风险预警与处理机制 [未开始]
|
||||
- # TASK-059 编写集成测试 [未开始]
|
||||
- # TASK-060 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-security` 模块开发
|
||||
|
||||
- # TASK-061 实现用户认证与授权逻辑 [未开始]
|
||||
- # TASK-062 实现API接口安全防护 [未开始]
|
||||
- # TASK-063 编写集成测试 [未开始]
|
||||
- # TASK-064 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-strategy` 模块开发
|
||||
|
||||
- # TASK-STR-001 `trade-strategy` 模块基础开发(Controller, Service, Mapper, DO)[进行中]
|
||||
- # TASK-065 实现策略定义与管理模块 [未开始]
|
||||
- # TASK-066 实现策略回测与实盘交易接口 [未开始]
|
||||
- # TASK-067 编写集成测试 [未开始]
|
||||
- # TASK-068 功能优化与代码审查 [未开始]
|
||||
|
||||
## `trade-user` 模块开发
|
||||
|
||||
- # TASK-USR-001 `trade-user` 模块基础开发(Controller, Service, Mapper, DO)[进行中]
|
||||
- # TASK-069 创建用户相关实体类、Mapper、Service、Controller [未开始]
|
||||
- # TASK-070 实现用户注册、登录、信息管理等功能 [未开始]
|
||||
- # TASK-071 编写集成测试 [未开始]
|
||||
- # TASK-072 功能优化与代码审查 [未开始]
|
||||
@@ -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>
|
||||
+55
@@ -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();
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -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;
|
||||
}
|
||||
}
|
||||
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+63
@@ -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("用户名或密码错误");
|
||||
};
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+18
@@ -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>
|
||||
+18
@@ -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>
|
||||
+49
@@ -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;
|
||||
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
|
||||
// 可根据需要添加更多静态方法,例如处理特定错误码等
|
||||
}
|
||||
+102
@@ -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);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.stock.trading.common.api;
|
||||
|
||||
/**
|
||||
* 封装API的错误码
|
||||
*/
|
||||
public interface IErrorCode {
|
||||
long getCode();
|
||||
|
||||
String getMessage();
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
@@ -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());
|
||||
}
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
}
|
||||
+143
@@ -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);
|
||||
}
|
||||
}
|
||||
+49
@@ -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;
|
||||
}
|
||||
}
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.trade.common.api;
|
||||
|
||||
/**
|
||||
* 封装API的错误码
|
||||
* @author Trade Team
|
||||
*/
|
||||
public interface IErrorCode {
|
||||
long getCode();
|
||||
|
||||
String getMessage();
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
}
|
||||
+46
@@ -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")));
|
||||
}
|
||||
}
|
||||
+29
@@ -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());
|
||||
}
|
||||
}
|
||||
+13
@@ -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 {
|
||||
}
|
||||
+58
@@ -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)
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+38
@@ -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();
|
||||
}
|
||||
}
|
||||
+101
@@ -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();
|
||||
// }
|
||||
}
|
||||
+55
@@ -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;
|
||||
}
|
||||
+119
@@ -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";
|
||||
|
||||
}
|
||||
+71
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
+97
@@ -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();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.trade.common.exception;
|
||||
|
||||
/**
|
||||
* 封装API的错误码
|
||||
* @author Trade Team
|
||||
*/
|
||||
public interface ErrorCode {
|
||||
long getCode();
|
||||
String getMessage();
|
||||
}
|
||||
+205
@@ -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, "服务器发生未知异常,请联系管理员");
|
||||
}
|
||||
}
|
||||
+126
@@ -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;
|
||||
}
|
||||
}
|
||||
+34
@@ -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;
|
||||
}
|
||||
}
|
||||
+132
@@ -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);
|
||||
}
|
||||
+151
@@ -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);
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
}
|
||||
+143
@@ -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);
|
||||
}
|
||||
}
|
||||
+231
@@ -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;
|
||||
}
|
||||
}
|
||||
+77
@@ -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");
|
||||
}
|
||||
}
|
||||
+61
@@ -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);
|
||||
}
|
||||
}
|
||||
+50
@@ -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);
|
||||
}
|
||||
}
|
||||
+30
@@ -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>
|
||||
+29
@@ -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" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
+37
@@ -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>
|
||||
+20
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+183
@@ -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));
|
||||
}
|
||||
}
|
||||
+240
@@ -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);
|
||||
}
|
||||
}
|
||||
+35
@@ -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);
|
||||
|
||||
}
|
||||
+33
@@ -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);
|
||||
|
||||
}
|
||||
+68
@@ -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;
|
||||
|
||||
}
|
||||
+77
@@ -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;
|
||||
|
||||
}
|
||||
+50
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -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, "指标计算结果不存在");
|
||||
|
||||
}
|
||||
+37
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+96
@@ -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);
|
||||
|
||||
}
|
||||
+120
@@ -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);
|
||||
|
||||
}
|
||||
+192
@@ -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);
|
||||
}
|
||||
}
|
||||
+241
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -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>
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
|
||||
}
|
||||
+32
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 配置类
|
||||
*/
|
||||
package com.trade.marketdata.config;
|
||||
+83
@@ -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);
|
||||
}
|
||||
}
|
||||
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
}
|
||||
+98
@@ -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;
|
||||
}
|
||||
+86
@@ -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;
|
||||
}
|
||||
+118
@@ -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;
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 实体类层
|
||||
*/
|
||||
package com.trade.marketdata.entity;
|
||||
+14
@@ -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> {
|
||||
}
|
||||
+15
@@ -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> {
|
||||
}
|
||||
+14
@@ -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> {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 数据访问层 (MyBatis Mapper)
|
||||
*/
|
||||
package com.trade.marketdata.mapper;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 行情服务模块主包。
|
||||
*/
|
||||
package com.trade.marketdata;
|
||||
+33
@@ -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);
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
+26
@@ -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();
|
||||
}
|
||||
+45
@@ -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);
|
||||
}
|
||||
}
|
||||
+163
@@ -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);
|
||||
}
|
||||
}
|
||||
+140
@@ -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());
|
||||
}
|
||||
}
|
||||
+150
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 服务实现层
|
||||
*/
|
||||
package com.trade.marketdata.service.impl;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 服务接口层
|
||||
*/
|
||||
package com.trade.marketdata.service;
|
||||
+75
@@ -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: 添加其他接口
|
||||
|
||||
}
|
||||
+53
@@ -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: 根据实际需求添加更多字段
|
||||
|
||||
}
|
||||
+17
@@ -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 查询方法
|
||||
|
||||
}
|
||||
+15
@@ -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: 定义行情数据相关的数据库操作方法
|
||||
}
|
||||
+64
@@ -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: 添加其他业务方法
|
||||
|
||||
}
|
||||
+105
@@ -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>
|
||||
+134
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
+205
@@ -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>
|
||||
+17
@@ -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>
|
||||
@@ -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>
|
||||
+18
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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;
|
||||
|
||||
}
|
||||
+30
@@ -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;
|
||||
|
||||
}
|
||||
+26
@@ -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;
|
||||
|
||||
}
|
||||
+37
@@ -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;
|
||||
|
||||
}
|
||||
+36
@@ -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;
|
||||
|
||||
}
|
||||
+55
@@ -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;
|
||||
|
||||
}
|
||||
+21
@@ -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);
|
||||
|
||||
}
|
||||
+19
@@ -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);
|
||||
|
||||
}
|
||||
+19
@@ -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);
|
||||
|
||||
}
|
||||
+89
@@ -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;
|
||||
|
||||
}
|
||||
+56
@@ -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;
|
||||
|
||||
}
|
||||
+60
@@ -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;
|
||||
|
||||
}
|
||||
+25
@@ -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));
|
||||
}
|
||||
}
|
||||
+22
@@ -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)); // 按时间倒序,最新的日志在前面
|
||||
}
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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, "订单日志不存在");
|
||||
|
||||
}
|
||||
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
+66
@@ -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);
|
||||
}
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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: 考虑增加重试机制或死信队列
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+9
@@ -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> {
|
||||
}
|
||||
+38
@@ -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);
|
||||
|
||||
}
|
||||
+29
@@ -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);
|
||||
|
||||
}
|
||||
+101
@@ -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);
|
||||
|
||||
}
|
||||
+45
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+345
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -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" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块的配置类。
|
||||
*/
|
||||
package com.trade.order.config;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块相关的常量定义。
|
||||
*/
|
||||
package com.trade.order.constant;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块的Controller层,负责处理HTTP请求和响应。
|
||||
*/
|
||||
package com.trade.order.controller;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块的数据传输对象 (DTO)。
|
||||
*/
|
||||
package com.trade.order.dto;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块的数据库实体类。
|
||||
*/
|
||||
package com.trade.order.entity;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 此包存放订单模块相关的枚举类。
|
||||
* 例如:订单状态枚举、支付方式枚举等。
|
||||
*/
|
||||
package com.trade.order.enums;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单服务模块的事件监听器,例如监听支付成功事件、库存扣减事件等。
|
||||
*/
|
||||
package com.trade.order.listener;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块的Mapper接口,用于数据库操作。
|
||||
*/
|
||||
package com.trade.order.mapper;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 订单服务模块主包。
|
||||
*/
|
||||
package com.trade.order;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块Service接口的实现类。
|
||||
*/
|
||||
package com.trade.order.service.impl;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块的Service层,负责业务逻辑处理。
|
||||
*/
|
||||
package com.trade.order.service;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放订单模块相关的工具类。
|
||||
*/
|
||||
package com.trade.order.util;
|
||||
+4
@@ -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 为 true,MyBatis 会在返回的 Map 中包含值为 null 的字段,并将其值设置为 null。
|
||||
call-setters-on-nulls: true
|
||||
# 对于数据库中值为 null 的字段,默认情况下,MyBatis 在返回的实体类对象中会将该字段设置为 null。
|
||||
# 通过设置 default-enum-type-handler 为 org.apache.ibatis.type.EnumOrdinalTypeHandler,MyBatis 会将枚举类型的字段值转换为其序数(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>
|
||||
@@ -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>
|
||||
+17
@@ -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>
|
||||
+18
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+173
@@ -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()
|
||||
// 静态资源,例如html,css,js, 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);
|
||||
}
|
||||
}
|
||||
+88
@@ -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);
|
||||
}
|
||||
}
|
||||
+54
@@ -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();
|
||||
}
|
||||
}
|
||||
+51
@@ -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();
|
||||
}
|
||||
}
|
||||
+50
@@ -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();
|
||||
}
|
||||
}
|
||||
+65
@@ -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();
|
||||
}
|
||||
}
|
||||
+56
@@ -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();
|
||||
}
|
||||
}
|
||||
+126
@@ -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>
|
||||
+15
@@ -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: 实现策略相关接口
|
||||
}
|
||||
+51
@@ -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: 根据实际需求添加更多字段
|
||||
|
||||
}
|
||||
+15
@@ -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: 定义策略相关的数据库操作方法
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.stock.strategy.service;
|
||||
|
||||
/**
|
||||
* 策略服务接口
|
||||
*
|
||||
* @author TraeAI
|
||||
*/
|
||||
public interface StrategyService {
|
||||
// TODO: 定义策略服务相关方法
|
||||
}
|
||||
+14
@@ -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: 实现策略服务相关方法
|
||||
}
|
||||
+111
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+33
@@ -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, "运行中的策略执行无法删除");
|
||||
|
||||
}
|
||||
+205
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -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" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的切面类 (Aspect Class)。
|
||||
* <p>
|
||||
* 切面类用于实现横切关注点,例如日志记录、事务管理、权限校验、性能监控等。
|
||||
* 通常使用 Spring AOP 或 AspectJ 来定义和织入切面。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.aspect;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的配置类。
|
||||
*/
|
||||
package com.trade.strategy.config;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的常量类 (Constant Class)。
|
||||
* <p>
|
||||
* 常量类用于定义在应用程序中广泛使用的静态常量值,例如配置键名、默认参数、固定字符串等。
|
||||
* 将常量集中管理有助于提高代码的可维护性和一致性。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.constant;
|
||||
+149
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的Controller层,负责处理HTTP请求和响应。
|
||||
*/
|
||||
package com.trade.strategy.controller;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的数据传输对象 (DTO)。
|
||||
*/
|
||||
package com.trade.strategy.dto;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略执行引擎相关的类,包括策略的加载、执行、事件处理等。
|
||||
*/
|
||||
package com.trade.strategy.engine;
|
||||
+84
@@ -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;
|
||||
|
||||
}
|
||||
+84
@@ -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;
|
||||
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的数据库实体类。
|
||||
*/
|
||||
package com.trade.strategy.entity;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 此包存放策略服务模块相关的枚举类。
|
||||
* 例如:策略状态枚举、信号类型枚举等。
|
||||
*/
|
||||
package com.trade.strategy.enums;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的过滤器类 (Filter Class)。
|
||||
* <p>
|
||||
* 过滤器类用于在 Servlet 容器处理请求之前或之后执行某些操作,例如请求参数校验、字符编码转换、安全过滤等。
|
||||
* 它们是 Servlet规范的一部分。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.filter;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的处理器类 (Handler Class)。
|
||||
* <p>
|
||||
* 处理器类通常用于处理特定类型的事件、消息或请求,例如消息队列的消费者、WebSocket 处理器、定时任务处理器等。
|
||||
* 它们封装了对特定输入的响应逻辑。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.handler;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 此包存放策略中可能用到的技术指标计算相关的类。
|
||||
* 考虑是否将通用指标计算放到独立的 trade-indicator 模块。
|
||||
*/
|
||||
package com.trade.strategy.indicator;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的拦截器类 (Interceptor Class)。
|
||||
* <p>
|
||||
* 拦截器类用于在 Spring MVC 处理请求的过程中拦截和处理请求,例如权限验证、日志记录、性能监控等。
|
||||
* 它们提供了比过滤器更细粒度的控制,并且可以访问 Spring MVC 的上下文信息。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.interceptor;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的定时任务类 (Job Class)。
|
||||
* <p>
|
||||
* 定时任务类用于执行周期性或预定时间的后台任务,例如数据同步、报表生成、缓存刷新等。
|
||||
* 通常使用 Spring Task 或 Quartz 等框架来实现。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.job;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放事件监听器,例如监听行情事件、订单回报事件等,并根据事件驱动策略执行。
|
||||
*/
|
||||
package com.trade.strategy.listener;
|
||||
+15
@@ -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> {
|
||||
|
||||
}
|
||||
+15
@@ -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> {
|
||||
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的Mapper接口,用于数据库操作(例如存储策略配置、回测结果等)。
|
||||
*/
|
||||
package com.trade.strategy.mapper;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的领域模型 (Domain Model) 或实体类 (Entity)。
|
||||
* <p>
|
||||
* 这些类通常映射到数据库表结构,代表了系统中的核心业务对象。
|
||||
* 它们包含了对象的属性以及可能的操作方法。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.model;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 策略服务模块主包。
|
||||
*/
|
||||
package com.trade.strategy;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放风险管理相关的类,例如仓位管理、止盈止损逻辑等。
|
||||
*/
|
||||
package com.trade.strategy.risk;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放定时任务相关的类,例如定时执行策略、数据同步等。
|
||||
*/
|
||||
package com.trade.strategy.scheduler;
|
||||
+74
@@ -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);
|
||||
|
||||
}
|
||||
+77
@@ -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);
|
||||
|
||||
}
|
||||
+179
@@ -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);
|
||||
}
|
||||
}
|
||||
+160
@@ -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));
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块Service接口的实现类。
|
||||
*/
|
||||
package com.trade.strategy.service.impl;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的Service层,负责业务逻辑处理,例如策略的创建、执行、回测等。
|
||||
*/
|
||||
package com.trade.strategy.service;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 此包包含策略服务模块的工具类 (Utility Class)。
|
||||
* <p>
|
||||
* 工具类提供各种通用的辅助方法,例如日期时间处理、字符串操作、加解密、文件操作等。
|
||||
* 它们通常包含静态方法,方便在应用程序的各个部分调用。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.strategy.util;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的工具类。
|
||||
*/
|
||||
package com.trade.strategy.utils;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放策略服务模块的视图对象 (VO)。
|
||||
*/
|
||||
package com.trade.strategy.vo;
|
||||
+118
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -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;
|
||||
|
||||
}
|
||||
+40
@@ -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;
|
||||
|
||||
}
|
||||
+64
@@ -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;
|
||||
|
||||
}
|
||||
+37
@@ -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中添加并谨慎处理
|
||||
}
|
||||
+46
@@ -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);
|
||||
|
||||
}
|
||||
+97
@@ -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;
|
||||
}
|
||||
+15
@@ -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查询执行记录等
|
||||
|
||||
}
|
||||
+93
@@ -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);
|
||||
|
||||
}
|
||||
+192
@@ -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>
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+40
@@ -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;
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
+82
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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: 实现用户相关接口
|
||||
}
|
||||
+56
@@ -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: 根据实际需求添加更多字段
|
||||
|
||||
}
|
||||
+15
@@ -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: 定义用户相关的数据库操作方法
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.stock.user.service;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
*
|
||||
* @author TraeAI
|
||||
*/
|
||||
public interface UserService {
|
||||
// TODO: 定义用户服务相关方法
|
||||
}
|
||||
+14
@@ -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: 实现用户服务相关方法
|
||||
}
|
||||
+17
@@ -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" +
|
||||
" ''-' `'-' `-..-' ");
|
||||
}
|
||||
}
|
||||
+58
@@ -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 Plus(3.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();
|
||||
// }
|
||||
}
|
||||
+38
@@ -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();
|
||||
}
|
||||
}
|
||||
+25
@@ -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")));
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放用户服务模块的配置类,例如 MyBatis Plus 配置、Swagger 配置等。
|
||||
*/
|
||||
package com.trade.user.config;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放用户模块相关的常量定义。
|
||||
*/
|
||||
package com.trade.user.constant;
|
||||
+114
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -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);
|
||||
}
|
||||
}
|
||||
+116
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放用户模块的Controller层代码,负责接收前端请求、参数校验、调用Service层处理业务逻辑,并返回响应。
|
||||
*/
|
||||
package com.trade.user.controller;
|
||||
+22
@@ -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;
|
||||
}
|
||||
+30
@@ -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;
|
||||
}
|
||||
+25
@@ -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;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 此包存放用户模块的Entity层代码,即数据库表对应的Java实体类。
|
||||
* 通常会使用Lombok注解简化代码,并可能包含JPA或MyBatis Plus的注解。
|
||||
*/
|
||||
package com.trade.user.entity;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 此包存放用户模块相关的枚举类。
|
||||
* 例如:用户状态枚举、性别枚举等。
|
||||
*/
|
||||
package com.trade.user.enums;
|
||||
+1
@@ -0,0 +1 @@
|
||||
package com.trade.user.handler;
|
||||
+24
@@ -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);
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+4
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
+34
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
+34
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
+49
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
+34
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 此包包含用户服务模块的领域模型 (Domain Model) 或实体类 (Entity)。
|
||||
* <p>
|
||||
* 这些类通常映射到用户相关的数据库表结构,例如用户表、角色表、权限表等。
|
||||
* </p>
|
||||
*/
|
||||
package com.trade.user.model;
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
+21
@@ -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();
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+28
@@ -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);
|
||||
}
|
||||
+20
@@ -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 {
|
||||
|
||||
}
|
||||
+20
@@ -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 {
|
||||
|
||||
}
|
||||
+142
@@ -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);
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+20
@@ -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 {
|
||||
|
||||
}
|
||||
+59
@@ -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();
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 此包存放用户模块Service层接口的实现类。
|
||||
*/
|
||||
package com.trade.user.service.impl;
|
||||
+5
@@ -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;
|
||||
+4
@@ -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>
|
||||
+12
@@ -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>
|
||||
+11
@@ -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>
|
||||
+11
@@ -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.
@@ -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
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
"""CodeParser:Java 项目源码解析 → CodeStructure(Impact 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 @@
|
||||
"""ExistingSystemExplorer:CodeStructure → ExistingSystemInfo(Impact 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,
|
||||
)
|
||||
@@ -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 可序列化 dict(ChangeType 枚举转 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)
|
||||
@@ -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 写入规则 → write(api-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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
"""Writer 编排:上下文装配 → 逐章生成 → 渲染 → docx 注入(Phase 5 垂直切片)。"""
|
||||
"""Writer 编排:上下文装配 → 逐章生成 → 渲染 → docx 注入(Phase 5 垂直切片)。
|
||||
|
||||
Impact Agent MVP(2026-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
|
||||
|
||||
@@ -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 字段)所使用的自然语言,"
|
||||
|
||||
@@ -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 类"
|
||||
@@ -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
|
||||
|
||||
@@ -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 == []
|
||||
@@ -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
|
||||
@@ -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"] != ""
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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 MVP:existing_system_path(2026-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)
|
||||
|
||||
Reference in New Issue
Block a user