diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index f541eba..d76eac4 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -109,4 +109,6 @@ | 2026-08-13 23:35 | 测试验证 | P5-T10 门禁诊断:修复 WriterGenerationError 吞掉底层 LLM 错误(如 401 详情),补透传测试 | src/genesis/writer/writer_agent.py; tests/test_phase5_writer_agent.py | hy3-free | | 2026-08-13 23:50 | Agent 实现 | 修复 HttpLLMClient 在同步门禁中多次 asyncio.run 复用已关闭事件循环致 Event loop is closed:chat 改为每次调用新建 client(保留 async with 协议)| src/genesis/inference/client.py | hy3-free | | 2026-08-13 23:58 | Agent 实现 | P5-T10 语言对齐:WRITER_PROMPT_TEMPLATE 增加「正文语言须与章节标题一致」约束(模板日文则输出日文)| src/genesis/writer/writer_agent.py; tests/test_phase5_writer_agent.py | hy3-free | -| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash | \ No newline at end of file +| 2026-08-23 | Agent 实现 | 7 章模板只注入 2/3/5 章缺陷修复(用户圈定两项):①正则宽容化——word_template_parser.PLACEHOLDER_RE 键名大小写不敏感+支持全角冒号(解析归一为小写 section:id);template_mapper._SECTION_RE 与 docx_injector._SECTION_RE 加 IGNORECASE/全角冒号容忍;②防静默丢章——orchestrator 新增 _warn_unanchored(ctxs),对无 {{section:id}} 锚点章打 WARNING 列章名,generate 与 qa_loop._build 调用;TDD RED(6 failed)→ GREEN(子集 25 passed)→ 全量 304 passed / 99.19% 覆盖,fail_under=99 达标;端到端冒烟 PASS({{Section:1}}/{{section:2}} 注入 + 无锚点章告警) | src/genesis/parsers/word_template_parser.py; src/genesis/writer/template_mapper.py; src/genesis/writer/docx_injector.py; src/genesis/writer/orchestrator.py; src/genesis/qa/qa_loop.py; tests/test_word_template_parser.py; tests/test_phase5_template_mapper.py; tests/test_docx_injector.py; tests/test_phase5_writer_orchestrator.py; _AI_USAGE_LOG.md | deepseek-v4-flash || 2026-08-23 | 需求理解 | 用户指出写概要设计书需「要件定義 + 既有系统(设计/代码)做影响调查 → 确定修改范围」;确认场景=追加改修、输入=Java 项目源码;选定既有系统样本 sunOnly/stock-trade-system(股票量化交易系统,无 LICENSE,仅作测试输入保留来源标注,引入 samples/existing-system/,13 模块 278 Java 文件);确认领域不一致 → 新造股票交易域追加改修样本 | docs/superpowers/specs/2026-08-23-impact-mvp.md; samples/existing-system/README.md | deepseek-v4-flash | +| 2026-08-23 | 架构设计 | Impact Agent MVP 设计(brainstorming 逐问确认):门控=用户是否提供 existing_system 路径(流水线级,非章节级);范围确认=与用户确认调查范围(模块/深度);消费模型=影响调查结果是生成概要设计书的主上下文(整本基于影响结果生成,无专用影响章/无 {{section:impact}} 锚点门控);MVP=变更点定位(确定性规则,无 LLM);spec 落盘 docs/superpowers/specs/2026-08-23-impact-mvp.md | docs/superpowers/specs/2026-08-23-impact-mvp.md | deepseek-v4-flash | +| 2026-08-23 | Agent 实现 | Impact Agent MVP 实现(TDD RED→GREEN→全量 351 passed / 99.27%):data_models 新增 ChangeType/ChangeElement/ImpactWarning/ChangeAnalysis/ImpactReport + StructuredSource.impact_report;新增 src/genesis/impact/(code_parser.py Java 解析 @RestController/@Service/@Entity/@TableName、existing_system_explorer.py 组装、impact_agent.py 变更点定位+impact_report_to_dict 序列化);source_aggregator 解除 existing_system=None 硬编码(新增 existing_system_path 门控);writer 集成——GenerationContext.impact_report + to_vars()[impact]、WRITER_PROMPT_TEMPLATE 加「影响调查上下文」、context_builder 透传、WriteOrchestrator.generate 门控自动跑 ImpactAgent + 回填 + 默认 meta(doc_title/version/created_at);新造样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名);门禁(fake 模式)PASS:影响调查 total=16 new=5 modified=8 deleted=3 unchanged=50 warnings=0,概要设计书 13 章产出,影响调查书 JSON 可下载 | src/genesis/data_models.py; src/genesis/impact/code_parser.py; src/genesis/impact/existing_system_explorer.py; src/genesis/impact/impact_agent.py; src/genesis/parsers/source_aggregator.py; src/genesis/writer/models.py; src/genesis/writer/writer_agent.py; src/genesis/writer/context_builder.py; src/genesis/writer/orchestrator.py; tests/test_data_models.py; tests/test_code_parser.py; tests/test_existing_system_explorer.py; tests/test_impact_agent.py; tests/test_source_aggregator.py; tests/test_phase5_models.py; tests/test_phase5_writer_agent.py; tests/test_phase5_context_builder.py; tests/test_phase5_writer_orchestrator.py; samples/要件定義_追加改修_股票.xlsx; samples/existing-system/; docs/superpowers/specs/2026-08-23-impact-mvp.md; _AI_USAGE_LOG.md | deepseek-v4-flash | diff --git a/docs/superpowers/specs/2026-08-23-impact-mvp.md b/docs/superpowers/specs/2026-08-23-impact-mvp.md new file mode 100644 index 0000000..05a03f5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-23-impact-mvp.md @@ -0,0 +1,158 @@ +# Impact Agent MVP 设计(2026-08-23) + +## 背景 + +用户指出:写概要设计书需要「根据要件定义书 + 既有系统的设计与代码做影响调查 → 确定修改范围」。当前 +`source_aggregator.py` 硬编码 `existing_system=None`,无 Impact Agent 实现。本次实现 **MVP 变更点定位 + 影响书**。 + +关键用户决策(brainstorming 确认): +1. 场景 = **追加改修**(需影响调查) +2. 既有系统输入形态 = **Java 项目源码** +3. 既有系统样本 = `samples/existing-system/`(sunOnly/stock-trade-system,股票量化交易系统,无 LICENSE, + 仅作测试输入,保留来源标注) +4. 里程碑范围 = **MVP**:解析既有系统 → 变更点定位 → 影响书(JSON);不做 LLM 要素抽取/关联推理 +5. 样本同域化:新造 `samples/要件定義_追加改修_股票.xlsx`(股票交易域,对齐 sunOnly 真实模块) +6. **门控**:是否提供 `existing_system` 路径决定是否进入影响调查阶段(非章节级) +7. **范围确认**:进入影响调查前与用户确认调查范围(模块/深度) +8. **消费模型**:`ImpactReport` 是生成概要设计书的**主上下文**——整本概要设计书基于影响调查结果生成, + **无专用影响章、无 `{{section:impact}}` 锚点门控**;`ImpactReport` 同时作为可下载独立产物 + +## 架构与数据流 + +``` +要件定義_追加改修_股票.xlsx(新造) samples/existing-system/(sunOnly 源码) + │ 现有 ExcelParser │ 新增 CodeParser(Java 解析) + ▼ ▼ + StructuredSource.tables CodeStructure → ExistingSystemInfo + │ │ + └──────────────┬───────────────────────────┘ + ▼ + ImpactAgent(变更点定位,确定性比对) + - 取 変更区分 + 既存対応 列 + - 与 ExistingSystemInfo 连接比对(存在性校验) + ▼ + ImpactReport(JSON 中间产物,可下载) + ▼ + WriteOrchestrator.generate(impact_report) + - ImpactReport 作为主上下文喂给 WriterAgent + - 整本概要设计书基于影响结果生成 +``` + +## 门控与范围确认 + +- **门控(流水线级)**:`SourceParser.parse(existing_system_path=...)` 提供路径 ⇒ `StructuredSource.existing_system` + 非 None ⇒ 进入影响调查;`WriteOrchestrator.generate` 检测到 `existing_system` 非 None 且未显式传 + `impact_report` 时,自动运行 `ImpactAgent` 生成影响书。 +- **范围确认(human-in-loop)**:MVP 默认范围 = 提供的整个目录树(全模块、Controller/Service/Entity/Endpoint 全层); + 可按参数收窄(`modules`/`depth`)。完整 UI 确认留后续。 +- **门控降级**:CodeParser 失败(非 Java/无源码)⇒ 跳过影响调查并告警,生成照常(无 ImpactReport)。 + +## 变更点定位规则(确定性,无 LLM) + +| 要件情况 | 判定 | +|---|---| +| 新規 + 既存対応空 | `new`(确认新增) | +| 新規 + 既存対応指定 | `conflict` 告警(自相矛盾) | +| 変更 + 既存対応命中且类存在 | `modified`(受影响既有类 = 映射清单) | +| 変更 + 既存対応缺失/类不存在 | `warning`(无法定位修改对象) | +| 削除 + 既存対応命中 | `deleted`(确认删除) | +| 削除 + 既存対応缺失/类不存在 | `warning` | +| 既有类未被任何要件引用 | `unchanged`(summary 计数) | + +`既存対応` 列值 = Controller/Service/Entity 类名(逗号分隔);命中判定为类名出现在 +`ExistingSystemInfo` 三层清单中(不区分大小写,后缀 `Controller`/`Service`/`Entity` 可选)。 + +## 数据模型(data_models.py 新增) + +```python +class ChangeType(Enum): + NEW = "新規"; MODIFIED = "変更"; DELETED = "削除"; UNCHANGED = "不变" + +@dataclass +class ChangeElement: + element_id: str; element_type: str; name: str + change_type: ChangeType + existing_mapping: list[str] # 既存対応 声明值 + impacted_existing: list[str] # 确认命中的既有类 + evidence: str # 既有类 source_uri / 空 + status: str # "ok" | "conflict" | "warning" + +@dataclass +class ImpactWarning: + element_id: str; issue: str + +@dataclass +class ChangeAnalysis: + project_type: str # "enhancement" + new_elements: list[ChangeElement] + modified_elements: list[ChangeElement] + deleted_elements: list[ChangeElement] + unchanged_elements: list[ChangeElement] + warnings: list[ImpactWarning] + +@dataclass +class ImpactReport: + metadata: dict + change_analysis: ChangeAnalysis + summary: dict +``` + +## 组件 + +### `src/genesis/impact/code_parser.py`(新) +- `CodeParser.parse(root_path) -> CodeStructure`:扫描 `**/*.java`,按注解识别: + - `@RestController|@Controller` → ControllerInfo(base_path 取 `@RequestMapping`,endpoints 取方法级映射) + - `@Service` → ServiceInfo(methods) + - `@Entity|@Table(name=...)` → EntityInfo(table_name、fields) + - 方法映射 → EndpointInfo +- 顶层目录 → modules;失败(无 Java/路径无效)→ 抛 `CodeParseError` + +### `src/genesis/impact/existing_system_explorer.py`(新) +- `ExistingSystemExplorer.explore(code: CodeStructure) -> ExistingSystemInfo` + +### `src/genesis/impact/impact_agent.py`(新) +- `ImpactAgent.run(structured_source, scope=None) -> ImpactReport` +- 从 `tables` 提取带 `変更区分`/`既存対応` 列的表行 → 按上表规则分类 → summary + +### `src/genesis/parsers/source_aggregator.py`(改) +- `parse(..., existing_system_path=None)`:提供路径时解析并填充 `existing_system` + +### `src/genesis/writer/`(改) +- `GenerationContext` 新增 `impact_report: object | None = None`;`to_vars()` 增加 `"impact"` 变量 +- `WRITER_PROMPT_TEMPLATE` 增加 `影响调查上下文:\n{{impact}}\n`(在参考资料前) +- `context_builder.build_contexts` 从 `structured_source.impact_report` 透传 +- `WriteOrchestrator.generate(..., impact_report=None)`:门控自动跑 `ImpactAgent`;上下文携带影响书 +- `StructuredSource` 新增 `impact_report: ImpactReport | None = None`(生成后回填,便于 QA/日志) + +## 错误处理 + +- `CodeParseError`(非 Java/无源码/路径无效)→ 门控降级跳过 + 告警 +- `既存対応` 引用不存在 → `warning`,不阻断 +- `新規` 却映射既有 → `conflict` warning + +## 测试策略(TDD,全量 ≥99%) + +- `tests/test_data_models.py`:新增 ImpactReport 模型默认值/构造 +- `tests/test_code_parser.py`:用 `samples/existing-system` 真实文件断言提取(类名/数量/路径) +- `tests/test_existing_system_explorer.py`:组装 ExistingSystemInfo 分层 +- `tests/test_impact_agent.py`:给定 要件定義(新造样本)+ ExistingSystemInfo → 断言分类 +- `tests/test_source_aggregator.py`:`existing_system_path` 加载;无效路径降级/报错 +- `tests/test_phase5_models.py`:更新 `test_generation_context_no_impact_field` → 断言 `impact` 变量存在且为空 +- `tests/test_phase5_writer_agent.py`:断言模板含影响上下文变量 +- `tests/test_phase5_writer_orchestrator.py`:传 impact_report → 断言 `variables["impact"]` 含影响信息 +- 门禁(fake 模式):sunOnly + 新造样本端到端产出 ImpactReport + 概要设计书 + +## 新造样本规格 + +`samples/要件定義_追加改修_股票.xlsx`(股票交易域,对齐 sunOnly 真实模块): +- 機能一覧:F001 止损风控机能(新規,无既存対応) / F002 订单状态查询扩展(変更→OrderController,OrderQueryService) + / F003 实时行情显示扩展(変更→RealtimeMarketDataController) / F004 旧回测机能(削除→StrategyBacktestController) +- 画面一覧(对应画面 + 変更区分)/DB定義(新規 止损设定表/変更 订单表/削除 旧回测表)/IF定義(新規 风控告警连携IF) +- 各表新增 `既存対応` 列(类名或留空)+ `変更区分` 列 +- `改修ポイント` 自由文本(改修概要) + +## 里程碑外(后续) + +- LLM 要素抽取 / 关联推理 / 影响矩阵 +- v1→用户逐条修正→v2 生命周期 UI +- 范围确认 UI / API 化 diff --git a/samples/existing-system/.gitignore b/samples/existing-system/.gitignore new file mode 100644 index 0000000..0c7970f --- /dev/null +++ b/samples/existing-system/.gitignore @@ -0,0 +1,9 @@ +/.idea/ +*.class +*.lst +*.log +.idea/* +*/target/* +*/.idea/* +*.iml +*.ipr \ No newline at end of file diff --git a/samples/existing-system/README.md b/samples/existing-system/README.md new file mode 100644 index 0000000..0d61378 --- /dev/null +++ b/samples/existing-system/README.md @@ -0,0 +1,37 @@ +# 既有系统样本:stock-trade-system(股票量化交易系统) + +## 来源标注 + +- **仓库**: https://github.com/sunOnly/stock-trade-system +- **作者**: sunOnly +- **描述**: 股票量化交易系统(Spring Boot 多模块) +- **许可证**: ⚠️ **仓库未声明 LICENSE**(GitHub 默认保留所有权利) + +## 使用说明(合规声明) + +本目录仅作为本项目(概要设计书自动生成 Agent)的**测试输入样本**使用: + +1. **不进交付物**:本代码不随产品发布、不对外分发、不参与任何形式的再授权。 +2. **仅作影响调查演示**:用于验证「既有系统解析 → 影响调查 → 変更範囲确定 → 概要设计书生成」链路(追加改修场景)。 +3. **来源透明**:保留本 README 与原始 pom.xml/包结构,不篡改作者版权声明。 +4. **许可风险自担**:因上游无 LICENSE,本项目用户需自行确认使用该样本的合规性;正式商用场景建议替换为自有代码或获得作者授权。 + +## 结构(13 个 Spring Boot 模块) + +| 模块 | 职责 | +|------|------| +| trade-application | 应用入口/装配 | +| trade-common | 公共层(dal/entity/util/config/service) | +| trade-gateway | Spring Cloud Gateway 网关 | +| trade-user | 用户/账户域 | +| trade-order | 订单/交易域 | +| trade-risk | 风控域 | +| trade-strategy | 策略域 | +| trade-market-data | 行情数据域 | +| trade-indicator | 指标计算域 | +| trade-notification | 通知域 | +| trade-backtest | 回测域 | +| trade-security | 安全域 | +| trade-mbg | MyBatis 生成器 | + +共 278 个 Java 文件(约 567 KB,已去除 target/ 构建产物与 IDE 配置)。 diff --git a/samples/existing-system/docs/ai.md b/samples/existing-system/docs/ai.md new file mode 100644 index 0000000..e079b50 --- /dev/null +++ b/samples/existing-system/docs/ai.md @@ -0,0 +1,3 @@ +project_rules.md +README.md +plan.md diff --git a/samples/existing-system/docs/plan.md b/samples/existing-system/docs/plan.md new file mode 100644 index 0000000..7a93866 --- /dev/null +++ b/samples/existing-system/docs/plan.md @@ -0,0 +1,116 @@ +# 开发计划 + +## `trade-indicator` 模块开发 + +- # TASK-001 创建 `IndicatorDefinitionDO.java` 和 `IndicatorValueDO.java` 实体类 [已完成] +- # TASK-002 创建 `IndicatorDefinitionMapper.java` 和 `IndicatorValueMapper.java` 数据访问接口 [已完成] +- # TASK-003 创建 `IndicatorDefinitionService.java` 和 `IndicatorValueService.java` 业务逻辑接口 [已完成] +- # TASK-004 创建 `IndicatorDefinitionServiceImpl.java` 和 `IndicatorValueServiceImpl.java` 业务逻辑实现类 [已完成] +- # TASK-005 创建 `IndicatorDefinitionController.java` 和 `IndicatorValueController.java` API 接口 [已完成] +- # TASK-006 创建 `IndicatorDefinitionConvert.java` 和 `IndicatorValueConvert.java` 对象转换工具类 [已完成] +- # TASK-007 创建 `IndicatorJob.java` 定时任务类并启用调度 [已完成] +- # TASK-008 定义 `ErrorCodeConstants.java` 错误码 [已完成] +- # TASK-009 配置 `bootstrap.yml` 和 `logback-spring.xml` [已完成] +- # TASK-010 完善 `pom.xml` 依赖配置 [已完成] +- # TASK-011 编写集成测试 [未开始] +- # TASK-012 功能优化与代码审查 [未开始] + +## `trade-order` 模块开发 + +- # TASK-013 创建 `OrderDO.java`, `OrderItemDO.java`, `OrderLogDO.java` 实体类 [已完成] +- # TASK-014 创建 `OrderMapper.java`, `OrderItemMapper.java`, `OrderLogMapper.java` 数据访问接口 [已完成] +- # TASK-015 创建 `OrderDirectionEnum.java`, `OrderStatusEnum.java`, `OrderTypeEnum.java` 枚举类 [已完成] +- # TASK-016 创建 `OrderService.java`, `OrderItemService.java`, `OrderLogService.java` 业务逻辑接口 [已完成] +- # TASK-017 创建 `OrderServiceImpl.java`, `OrderItemServiceImpl.java`, `OrderLogServiceImpl.java` 业务逻辑实现类 [已完成] +- # TASK-018 创建 `OrderController.java` API 接口 [已完成] +- # TASK-019 创建 `OrderCreateReqVO.java`, `OrderUpdateReqVO.java`, `OrderPageReqVO.java` 请求 VO 类 [已完成] +- # TASK-020 创建 `OrderRespVO.java`, `OrderItemRespVO.java`, `OrderLogRespVO.java` 响应 VO 类 [已完成] +- # TASK-021 创建 `OrderConvert.java`, `OrderItemConvert.java`, `OrderLogConvert.java` 对象转换工具类 [已完成] +- # TASK-022 定义 `ErrorCodeConstants.java` 错误码 [已完成] +- # TASK-023 配置 `bootstrap.yml` 和 `logback-spring.xml` [已完成] +- # TASK-024 完善 `pom.xml` 依赖配置 [已完成] +- # TASK-025 创建 `OrderApplication.java` 启动类 [已完成] +- # TASK-026 编写集成测试 [未开始] +- # TASK-027 功能优化与代码审查 [未开始] + +## `trade-application` 模块开发 + +- # TASK-028 创建基础结构和配置文件 [未开始] +- # TASK-029 实现核心应用逻辑 [未开始] +- # TASK-030 编写集成测试 [未开始] +- # TASK-031 功能优化与代码审查 [未开始] + +## `trade-backtest` 模块开发 + +- # TASK-032 创建回测引擎核心类 [未开始] +- # TASK-033 实现回测数据处理逻辑 [未开始] +- # TASK-034 实现回测结果分析与展示 [未开始] +- # TASK-035 编写集成测试 [未开始] +- # TASK-036 功能优化与代码审查 [未开始] + +## `trade-common` 模块开发 + +- # TASK-CMN-001 `trade-common` 模块基础开发(通用工具类, 通用配置)[进行中] +- # TASK-037 定义通用工具类和常量 [未开始] +- # TASK-038 实现通用配置和基础组件 [未开始] +- # TASK-039 编写单元测试 [未开始] + +## `trade-gateway` 模块开发 + +- # TASK-040 配置网关路由规则 [未开始] +- # TASK-041 实现统一认证和鉴权 [未开始] +- # TASK-042 实现API限流和熔断 [未开始] +- # TASK-043 编写集成测试 [未开始] +- # TASK-044 功能优化与代码审查 [未开始] + +## `trade-market-data` 模块开发 + +- # TASK-MKT-001 `trade-market-data` 模块基础开发(Controller, Service, Mapper, DO)[进行中] +- # TASK-045 实现行情数据采集接口 [未开始] +- # TASK-046 实现行情数据存储逻辑 [未开始] +- # TASK-047 实现行情数据推送机制 [未开始] +- # TASK-048 编写集成测试 [未开始] +- # TASK-049 功能优化与代码审查 [未开始] + +## `trade-mbg` 模块开发 + +- # TASK-050 配置MyBatis Generator [未开始] +- # TASK-051 生成各模块DAO层代码 [未开始] + +## `trade-notification` 模块开发 + +- # TASK-052 实现消息通知服务接口 [未开始] +- # TASK-053 对接邮件、短信等通知渠道 [未开始] +- # TASK-054 编写集成测试 [未开始] +- # TASK-055 功能优化与代码审查 [未开始] + +## `trade-risk` 模块开发 + +- # TASK-056 实现风控规则定义模块 [未开始] +- # TASK-057 实现风控规则执行引擎 [未开始] +- # TASK-058 实现风险预警与处理机制 [未开始] +- # TASK-059 编写集成测试 [未开始] +- # TASK-060 功能优化与代码审查 [未开始] + +## `trade-security` 模块开发 + +- # TASK-061 实现用户认证与授权逻辑 [未开始] +- # TASK-062 实现API接口安全防护 [未开始] +- # TASK-063 编写集成测试 [未开始] +- # TASK-064 功能优化与代码审查 [未开始] + +## `trade-strategy` 模块开发 + +- # TASK-STR-001 `trade-strategy` 模块基础开发(Controller, Service, Mapper, DO)[进行中] +- # TASK-065 实现策略定义与管理模块 [未开始] +- # TASK-066 实现策略回测与实盘交易接口 [未开始] +- # TASK-067 编写集成测试 [未开始] +- # TASK-068 功能优化与代码审查 [未开始] + +## `trade-user` 模块开发 + +- # TASK-USR-001 `trade-user` 模块基础开发(Controller, Service, Mapper, DO)[进行中] +- # TASK-069 创建用户相关实体类、Mapper、Service、Controller [未开始] +- # TASK-070 实现用户注册、登录、信息管理等功能 [未开始] +- # TASK-071 编写集成测试 [未开始] +- # TASK-072 功能优化与代码审查 [未开始] \ No newline at end of file diff --git a/samples/existing-system/pom.xml b/samples/existing-system/pom.xml new file mode 100644 index 0000000..f4e7275 --- /dev/null +++ b/samples/existing-system/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + pom + + + trade-common + trade-user + trade-security + trade-gateway + trade-mbg + trade-strategy + trade-risk + trade-indicator + trade-market-data + trade-backtest + trade-notifyication + trade-order + trade-application + + + 量化交易系统 + 基于Spring Boot的A股超短线交易系统 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.14 + + + + 8 + 8 + 8 + 8.0.33 + UTF-8 + 3.5.3.1 + 1.2.18 + 2.0.32 + 5.8.20 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + mysql + mysql-connector-java + ${mysql.version} + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + + com.alibaba + fastjson + ${fastjson.version} + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-application/pom.xml b/samples/existing-system/trade-application/pom.xml new file mode 100644 index 0000000..bafe4af --- /dev/null +++ b/samples/existing-system/trade-application/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-application + 1.0.0 + trade-application + trade-application + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-amqp + + + + mysql + mysql-connector-java + ${mysql.version} + runtime + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.1 + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + true + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + com.stock + trade-common + 1.0.0 + + + + com.stock + trade-user + 1.0.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/MyBatisGenerator.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/MyBatisGenerator.java new file mode 100644 index 0000000..cafe185 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/MyBatisGenerator.java @@ -0,0 +1,55 @@ +package com.stock.trading; + +import com.baomidou.mybatisplus.generator.AutoGenerator; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.GlobalConfig; +import com.baomidou.mybatisplus.generator.config.PackageConfig; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; + +/** + * MyBatis-Plus代码生成器 + */ +public class MyBatisGenerator { + + public static void main(String[] args) { + // 代码生成器 + AutoGenerator mpg = new AutoGenerator(); + + // 全局配置 + GlobalConfig gc = new GlobalConfig(); + String projectPath = System.getProperty("user.dir"); + gc.setOutputDir(projectPath + "/trade-user/src/main/java"); + gc.setAuthor("macro"); + gc.setOpen(false); + // gc.setSwagger2(true); 实体属性 Swagger2 注解 + mpg.setGlobalConfig(gc); + + // 数据源配置 + DataSourceConfig dsc = new DataSourceConfig(); + dsc.setUrl("jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"); + dsc.setDriverName("com.mysql.cj.jdbc.Driver"); + dsc.setUsername("root"); + dsc.setPassword("password"); + mpg.setDataSource(dsc); + + // 包配置 + PackageConfig pc = new PackageConfig(); + pc.setModuleName("user"); + pc.setParent("com.stock.trading"); + mpg.setPackageInfo(pc); + + // 策略配置 + StrategyConfig strategy = new StrategyConfig(); + strategy.setNaming(NamingStrategy.underline_to_camel); + strategy.setColumnNaming(NamingStrategy.underline_to_camel); + // strategy.setSuperEntityClass("com.stock.trading.common.entity.BaseEntity"); // 设置父类 + strategy.setEntityLombokModel(true); + strategy.setRestControllerStyle(true); + strategy.setInclude("ums_admin"); // 设置需要生成的表名 + strategy.setControllerMappingHyphenStyle(true); + strategy.setTablePrefix(pc.getModuleName() + "_"); + mpg.setStrategy(strategy); + mpg.execute(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/TradeApplication.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/TradeApplication.java new file mode 100644 index 0000000..d73407b --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/TradeApplication.java @@ -0,0 +1,16 @@ +package com.stock.trading; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 交易系统启动类 + */ +@SpringBootApplication +public class TradeApplication { + + public static void main(String[] args) { + SpringApplication.run(TradeApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/MyBatisPlusConfig.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..440df22 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/MyBatisPlusConfig.java @@ -0,0 +1,20 @@ +package com.stock.trading.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MyBatis-Plus配置 + */ +@Configuration +public class MyBatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 + return interceptor; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/RedisConfig.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/RedisConfig.java new file mode 100644 index 0000000..fbed299 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.stock.trading.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置 + */ +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(serializer); + + // HashKey也采用StringRedisSerializer的序列化方式 + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(serializer); + + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/SecurityConfig.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/SecurityConfig.java new file mode 100644 index 0000000..93eced0 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package com.stock.trading.config; + +import com.stock.trading.user.model.UmsAdmin; +import com.stock.trading.user.service.UmsAdminService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Spring Security的配置 + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private UmsAdminService adminService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .authorizeRequests() + .antMatchers("/admin/login", "/admin/register").permitAll() + .anyRequest() + .authenticated(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService()) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public UserDetailsService userDetailsService() { + //获取登录用户信息 + return username -> { + UmsAdmin admin = adminService.getAdminByUsername(username); + if (admin != null) { + List permissionList = new ArrayList<>(); // 假设这里获取用户权限 + return new org.springframework.security.core.userdetails.User(admin.getUsername(), admin.getPassword(), new ArrayList<>()); + } + throw new UsernameNotFoundException("用户名或密码错误"); + }; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/Swagger2Config.java b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/Swagger2Config.java new file mode 100644 index 0000000..f64ae7c --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/stock/trading/config/Swagger2Config.java @@ -0,0 +1,50 @@ +package com.stock.trading.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.SecurityScheme; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.List; + +/** + * Swagger2API文档的配置 + */ +@Configuration +@EnableSwagger2 +public class Swagger2Config { + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.stock.trading.user.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("超短线交易系统") + .description("超短线交易系统") + .contact("macro") + .version("1.0") + .build(); + } + + private List securitySchemes() { + //设置请求头信息 + List result = new ArrayList<>(); + ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header"); + result.add(apiKey); + return result; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/java/com/trade/application/TradeApplication.java b/samples/existing-system/trade-application/src/main/java/com/trade/application/TradeApplication.java new file mode 100644 index 0000000..610defc --- /dev/null +++ b/samples/existing-system/trade-application/src/main/java/com/trade/application/TradeApplication.java @@ -0,0 +1,18 @@ +package com.trade.application; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 交易应用服务启动类 + */ +@EnableDiscoveryClient +@SpringBootApplication +public class TradeApplication { + + public static void main(String[] args) { + SpringApplication.run(TradeApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application-dev.yml b/samples/existing-system/trade-application/src/main/resources/application-dev.yml new file mode 100644 index 0000000..3d8b016 --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application-dev.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: password \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application-prod.yml b/samples/existing-system/trade-application/src/main/resources/application-prod.yml new file mode 100644 index 0000000..268b2bc --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application-prod.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/stock_trade_prod?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: password \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application-test.yml b/samples/existing-system/trade-application/src/main/resources/application-test.yml new file mode 100644 index 0000000..604344c --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application-test.yml @@ -0,0 +1,11 @@ +server: + port: 8010 +spring: + profiles: + active: test + application: + name: trade-application + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/application.yml b/samples/existing-system/trade-application/src/main/resources/application.yml new file mode 100644 index 0000000..d2e698a --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/application.yml @@ -0,0 +1,32 @@ +server: + port: 8080 +spring: + profiles: + active: dev + datasource: + url: jdbc:mysql://localhost:3306/stock_trade?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: wodiXIAO1988 + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + database: 0 + host: localhost + port: 6379 + password: + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest +mybatis-plus: + mapper-locations: classpath*:/mapper/*.xml + global-config: + db-config: + id-type: auto + logic-delete-value: 1 + logic-not-delete-value: 0 + table-underline: true +jwt: + secret: stock-trading-system-secret + expiration: 604800 + tokenHead: Bearer \ No newline at end of file diff --git a/samples/existing-system/trade-application/src/main/resources/logback-spring.xml b/samples/existing-system/trade-application/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..31f0a9b --- /dev/null +++ b/samples/existing-system/trade-application/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/pom.xml b/samples/existing-system/trade-backtest/pom.xml new file mode 100644 index 0000000..c460b77 --- /dev/null +++ b/samples/existing-system/trade-backtest/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-backtest + jar + + trade-backtest + Strategy backtesting module for the trading system + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.stock + trade-common + ${project.version} + + + + + com.stock + trade-strategy + ${project.version} + + + + + com.stock + trade-market-data + ${project.version} + + + + + com.stock + trade-indicator + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/java/com/trade/backtest/TradeBacktestApplication.java b/samples/existing-system/trade-backtest/src/main/java/com/trade/backtest/TradeBacktestApplication.java new file mode 100644 index 0000000..af76e7b --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/java/com/trade/backtest/TradeBacktestApplication.java @@ -0,0 +1,18 @@ +package com.trade.backtest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 交易回测服务启动类 + */ +@EnableDiscoveryClient +@SpringBootApplication +public class TradeBacktestApplication { + + public static void main(String[] args) { + SpringApplication.run(TradeBacktestApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/application-dev.yml b/samples/existing-system/trade-backtest/src/main/resources/application-dev.yml new file mode 100644 index 0000000..7ddc131 --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/application-dev.yml @@ -0,0 +1,11 @@ +server: + port: 8008 +spring: + profiles: + active: dev + application: + name: trade-backtest + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/application-prod.yml b/samples/existing-system/trade-backtest/src/main/resources/application-prod.yml new file mode 100644 index 0000000..5281fbb --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/application-prod.yml @@ -0,0 +1,11 @@ +server: + port: 8008 +spring: + profiles: + active: prod + application: + name: trade-backtest + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/application-test.yml b/samples/existing-system/trade-backtest/src/main/resources/application-test.yml new file mode 100644 index 0000000..6b78129 --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/application-test.yml @@ -0,0 +1,11 @@ +server: + port: 8008 +spring: + profiles: + active: test + application: + name: trade-backtest + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 \ No newline at end of file diff --git a/samples/existing-system/trade-backtest/src/main/resources/logback-spring.xml b/samples/existing-system/trade-backtest/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..720848e --- /dev/null +++ b/samples/existing-system/trade-backtest/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-common/pom.xml b/samples/existing-system/trade-common/pom.xml new file mode 100644 index 0000000..78c41c0 --- /dev/null +++ b/samples/existing-system/trade-common/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-common + 1.0.0 + trade-common + trade-common + + + + org.springframework.boot + spring-boot-starter + + + + org.projectlombok + lombok + true + + + + + io.swagger.core.v3 + swagger-annotations + 2.2.20 + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + + org.springframework.security + spring-security-core + 5.7.11 + + + + + org.springframework.security + spring-security-config + 5.7.11 + + + + + org.springframework.security + spring-security-web + 5.7.11 + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + 1.4.7 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/common/dal/dataobject/BaseDO.java b/samples/existing-system/trade-common/src/main/java/com/stock/common/dal/dataobject/BaseDO.java new file mode 100644 index 0000000..e131b84 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/common/dal/dataobject/BaseDO.java @@ -0,0 +1,49 @@ +package com.stock.common.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础 DO 类,所有模块的 DO 类都应继承此类 + * + * @author TraeAI + */ +@Data +public abstract class BaseDO implements Serializable { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + /** + * 创建者,例如:userId + * TODO: 考虑从上下文中自动获取创建者 + */ + @TableField(fill = FieldFill.INSERT) + private String creator; + + /** + * 更新者,例如:userId + * TODO: 考虑从上下文中自动获取更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updater; + + /** + * 是否删除 + */ + private Boolean deleted; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/common/pojo/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/stock/common/pojo/CommonResult.java new file mode 100644 index 0000000..e261108 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/common/pojo/CommonResult.java @@ -0,0 +1,46 @@ +package com.stock.common.pojo; + +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import java.io.Serializable; + +/** + * 通用返回结果 + * + * @param 数据泛型 + */ +@Data +@Validated +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示 + */ + private String msg; + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.setCode(200); // 默认成功码为200 + result.setData(data); + result.setMsg("成功"); + return result; + } + + public static CommonResult error(Integer code, String message) { + CommonResult result = new CommonResult<>(); + result.setCode(code); + result.setMsg(message); + return result; + } + + // 可根据需要添加更多静态方法,例如处理特定错误码等 +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/CommonResult.java new file mode 100644 index 0000000..4738c35 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/CommonResult.java @@ -0,0 +1,102 @@ +package com.stock.trading.common.api; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 通用API返回对象 + * @param + */ +@Data +@NoArgsConstructor +public class CommonResult { + private long code; + private String message; + private T data; + + protected CommonResult(long code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(T data) { + return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + * @param message 提示信息 + */ + public static CommonResult success(T data, String message) { + return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + */ + public static CommonResult failed(IErrorCode errorCode) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + * @param message 错误信息 + */ + public static CommonResult failed(IErrorCode errorCode, String message) { + return new CommonResult(errorCode.getCode(), message, null); + } + + /** + * 失败返回结果 + * @param message 提示信息 + */ + public static CommonResult failed(String message) { + return new CommonResult(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败返回结果 + */ + public static CommonResult failed() { + return failed(ResultCode.FAILED); + } + + /** + * 参数验证失败返回结果 + */ + public static CommonResult validateFailed() { + return failed(ResultCode.VALIDATE_FAILED); + } + + /** + * 参数验证失败返回结果 + * @param message 提示信息 + */ + public static CommonResult validateFailed(String message) { + return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); + } + + /** + * 未登录返回结果 + */ + public static CommonResult unauthorized(T data) { + return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); + } + + /** + * 未授权返回结果 + */ + public static CommonResult forbidden(T data) { + return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/IErrorCode.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/IErrorCode.java new file mode 100644 index 0000000..e22d4e3 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/IErrorCode.java @@ -0,0 +1,10 @@ +package com.stock.trading.common.api; + +/** + * 封装API的错误码 + */ +public interface IErrorCode { + long getCode(); + + String getMessage(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/PageResult.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/PageResult.java new file mode 100644 index 0000000..1af54b4 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/PageResult.java @@ -0,0 +1,33 @@ +package com.stock.trading.common.api; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 分页数据封装类 + */ +@Data +@NoArgsConstructor +public class PageResult { + private Long pageNum; + private Long pageSize; + private Long totalPage; + private Long total; + private List list; + + /** + * 将MyBatisPageHelper分页后的list转为分页信息 + */ + public static PageResult restPage(List list) { + PageResult result = new PageResult(); + //PageInfo pageInfo = new PageInfo(list); + //result.setTotalPage(pageInfo.getPages()); + //result.setPageNum(pageInfo.getPageNum()); + //result.setPageSize(pageInfo.getPageSize()); + //result.setTotal(pageInfo.getTotal()); + //result.setList(pageInfo.getList()); + return result; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/ResultCode.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/ResultCode.java new file mode 100644 index 0000000..4da4957 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/api/ResultCode.java @@ -0,0 +1,27 @@ +package com.stock.trading.common.api; + +/** + * 常用API返回对象状态码 + */ +public enum ResultCode implements IErrorCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(404, "参数检验失败"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + FORBIDDEN(403, "没有相关权限"); + private long code; + private String message; + + private ResultCode(long code, String message) { + this.code = code; + this.message = message; + } + + public long getCode() { + return code; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/entity/BaseEntity.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/entity/BaseEntity.java new file mode 100644 index 0000000..5af0add --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/entity/BaseEntity.java @@ -0,0 +1,29 @@ +package com.stock.trading.common.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础实体类,包含创建时间和更新时间 + */ +@Data +public abstract class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/ApiException.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/ApiException.java new file mode 100644 index 0000000..ec35020 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/ApiException.java @@ -0,0 +1,31 @@ +package com.stock.trading.common.exception; + +import com.stock.trading.common.api.IErrorCode; + +/** + * 自定义API异常 + */ +public class ApiException extends RuntimeException { + private IErrorCode errorCode; + + public ApiException(IErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ApiException(String message) { + super(message); + } + + public ApiException(Throwable cause) { + super(cause); + } + + public ApiException(String message, Throwable cause) { + super(message, cause); + } + + public IErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/GlobalExceptionHandler.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..45c52e3 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package com.stock.trading.common.exception; + +import com.stock.trading.common.api.CommonResult; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * 全局异常处理 + */ +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理自定义异常 + */ + @ResponseBody + @ExceptionHandler(value = ApiException.class) + public CommonResult handle(ApiException e) { + if (e.getErrorCode() != null) { + return CommonResult.failed(e.getErrorCode()); + } + return CommonResult.failed(e.getMessage()); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/DateUtil.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/DateUtil.java new file mode 100644 index 0000000..2179124 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/DateUtil.java @@ -0,0 +1,34 @@ +package com.stock.trading.common.util; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 日期工具类 + */ +public class DateUtil { + + /** + * 格式化日期时间 + */ + public static String formatDateTime(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.format(date); + } + + /** + * 格式化日期 + */ + public static String formatDate(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(date); + } + + /** + * 格式化时间 + */ + public static String formatTime(Date date) { + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); + return sdf.format(date); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/JwtTokenUtil.java b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/JwtTokenUtil.java new file mode 100644 index 0000000..e56556e --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/stock/trading/common/util/JwtTokenUtil.java @@ -0,0 +1,143 @@ +package com.stock.trading.common.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JwtToken生成工具类 + * JWT token的格式:header.payload.signature + * header的格式(算法、token的类型): + * { + * "alg": "HS512", + * "typ": "JWT" + * } + * payload的格式(用户名、创建时间、过期时间): + * { + * "sub": "wang", + * "created": 1489079981398, + * "exp": 1489689981 + * } + * signature的生成算法: + * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) + */ +public class JwtTokenUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); + private static final String CLAIM_KEY_USERNAME = "sub"; + private static final String CLAIM_KEY_CREATED = "created"; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.expiration}") + private Long expiration; + @Value("${jwt.tokenHead}") + private String tokenHead; + + /** + * 根据负责生成JWT的token + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * 从token中获取JWT中的负载 + */ + private Claims getClaimsFromToken(String token) { + Claims claims = null; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + LOGGER.info("JWT格式验证失败: {}", token); + } + return claims; + } + + /** + * 生成token的过期时间 + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + /** + * 从token中获取登录用户名 + */ + public String getUserNameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 验证token是否还有效 + * + * @param token 客户端传入的token + * @param userDetails 从数据库中查询出来的用户信息 + */ + public boolean validateToken(String token, UserDetails userDetails) { + String username = getUserNameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + /** + * 判断token是否已经失效 + */ + private boolean isTokenExpired(String token) { + Date expiredDate = getExpiredDateFromToken(token); + return expiredDate.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + private Date getExpiredDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 根据用户信息生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + /** + * 判断token是否可以被刷新 + */ + public boolean canRefresh(String token) { + return !isTokenExpired(token); + } + + /** + * 刷新token + */ + public String refreshToken(String token) { + Claims claims = getClaimsFromToken(token); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonPage.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonPage.java new file mode 100644 index 0000000..61f0362 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonPage.java @@ -0,0 +1,49 @@ +package com.trade.common.api; + +import com.github.pagehelper.PageInfo; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 分页数据封装类 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class CommonPage { + private Integer pageNum; + private Integer pageSize; + private Integer totalPage; + private Long total; + private List list; + + /** + * 将PageHelper分页后的list转为分页信息 + */ + public static CommonPage restPage(List list) { + CommonPage result = new CommonPage<>(); + com.baomidou.mybatisplus.extension.plugins.pagination.Page pageInfo = page; + result.setTotalPage((int)pageInfo.getPages()); + result.setPageNum((int)pageInfo.getCurrent()); + result.setPageSize((int)pageInfo.getSize()); + result.setTotal(pageInfo.getTotal()); + result.setList(pageInfo.getRecords()); + return result; + } + + /** + * 将MyBatis Plus分页后的Page转为分页信息 + */ + public static CommonPage restPage(com.baomidou.mybatisplus.extension.plugins.pagination.Page page) { + CommonPage result = new CommonPage<>(); + com.baomidou.mybatisplus.extension.plugins.pagination.Page pageInfo = page; + result.setTotalPage((int)pageInfo.getPages()); + result.setPageNum((int)pageInfo.getCurrent()); + result.setPageSize((int)pageInfo.getSize()); + result.setTotal(pageInfo.getTotal()); + result.setList(pageInfo.getRecords()); + return result; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonResult.java new file mode 100644 index 0000000..f1d7844 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/CommonResult.java @@ -0,0 +1,105 @@ +package com.trade.common.api; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 通用返回对象 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class CommonResult { + private long code; + private String message; + private T data; + + protected CommonResult() { + } + + protected CommonResult(long code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(T data) { + return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + * @param message 提示信息 + */ + public static CommonResult success(T data, String message) { + return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + */ + public static CommonResult failed(IErrorCode errorCode) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + * @param message 错误信息 + */ + public static CommonResult failed(IErrorCode errorCode, String message) { + return new CommonResult(errorCode.getCode(), message, null); + } + + /** + * 失败返回结果 + * @param message 提示信息 + */ + public static CommonResult failed(String message) { + return new CommonResult(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败返回结果 + */ + public static CommonResult failed() { + return failed(ResultCode.FAILED); + } + + /** + * 参数验证失败返回结果 + */ + public static CommonResult validateFailed() { + return failed(ResultCode.VALIDATE_FAILED); + } + + /** + * 参数验证失败返回结果 + * @param message 提示信息 + */ + public static CommonResult validateFailed(String message) { + return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); + } + + /** + * 未登录返回结果 + */ + public static CommonResult unauthorized(T data) { + return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); + } + + /** + * 未授权返回结果 + */ + public static CommonResult forbidden(T data) { + return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/IErrorCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/IErrorCode.java new file mode 100644 index 0000000..fbc366d --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/IErrorCode.java @@ -0,0 +1,11 @@ +package com.trade.common.api; + +/** + * 封装API的错误码 + * @author Trade Team + */ +public interface IErrorCode { + long getCode(); + + String getMessage(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/api/ResultCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/ResultCode.java new file mode 100644 index 0000000..a0ce018 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/api/ResultCode.java @@ -0,0 +1,30 @@ +package com.trade.common.api; + +/** + * 枚举常用API操作码 + * @author Trade Team + */ +public enum ResultCode implements IErrorCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(404, "参数检验失败"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + FORBIDDEN(403, "没有相关权限"); + private long code; + private String message; + + private ResultCode(long code, String message) { + this.code = code; + this.message = message; + } + + @Override + public long getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/BaseSwaggerConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/BaseSwaggerConfig.java new file mode 100644 index 0000000..607c062 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/BaseSwaggerConfig.java @@ -0,0 +1,46 @@ +package com.trade.common.config; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; + +/** + * Swagger基础配置 + * @author Trade Team + */ +public abstract class BaseSwaggerConfig { + + /** + * 自定义Swagger配置 + */ + public abstract SwaggerProperties swaggerProperties(); + + @Bean + public GroupedOpenApi publicApi() { + SwaggerProperties swaggerProperties = swaggerProperties(); + return GroupedOpenApi.builder() + .group(swaggerProperties.getGroup()) + .pathsToMatch(swaggerProperties.getApiBasePackage()) + .build(); + } + + @Bean + public OpenAPI springShopOpenAPI() { + SwaggerProperties swaggerProperties = swaggerProperties(); + return new OpenAPI() + .info(new Info().title(swaggerProperties.getTitle()) + .description(swaggerProperties.getDescription()) + .version(swaggerProperties.getVersion()) + .license(new License().name(swaggerProperties.getLicense()).url(swaggerProperties.getLicenseUrl()))) + .externalDocs(new ExternalDocumentation() + .description(swaggerProperties.getDocDescription()) + .url(swaggerProperties.getDocUrl())) + .components(new io.swagger.v3.oas.models.Components() + .addSecuritySchemes("BearerAuth", + new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"))); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/GlobalExceptionHandler.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..52aff0e --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/GlobalExceptionHandler.java @@ -0,0 +1,29 @@ +package com.trade.common.config; + +import com.trade.common.exception.ApiException; +import com.trade.common.response.CommonResult; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * 全局异常处理 + * @author Trade Team + */ +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理自定义API异常 + * @param e API异常 + * @return 统一结果封装 + */ + @ResponseBody + @ExceptionHandler(value = ApiException.class) + public CommonResult handle(ApiException e) { + if (e.getErrorCode() != null) { + return CommonResult.failed(e.getErrorCode()); + } + return CommonResult.failed(e.getMessage()); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisConfig.java new file mode 100644 index 0000000..6b3e756 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisConfig.java @@ -0,0 +1,13 @@ +package com.trade.common.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; + +/** + * MyBatis配置类 + * @author Trade Team + */ +@Configuration +@MapperScan({"com.trade.common.mapper"}) +public class MyBatisConfig { +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisPlusConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..dc9b32d --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/MyBatisPlusConfig.java @@ -0,0 +1,58 @@ +package com.trade.common.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + *

MyBatis Plus 配置类

+ * + * @author creator + * @since 2024-01-01 + */ +@Configuration +@EnableTransactionManagement // 开启事务管理 +@MapperScan("com.trade.*.mapper") // 扫描 Mapper 接口,根据实际模块调整或在各模块单独配置 +public class MyBatisPlusConfig { + + /** + * 配置 MyBatis Plus 拦截器 + * + * @return MybatisPlusInterceptor 实例 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + + // 1. 添加分页插件 + // DbType 用于指定数据库类型,MySQL为例 + // optimizeJoin 是否优化left join连接查询,默认为false,建议保持false以保证结果正确性 + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); + // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false + // paginationInnerInterceptor.setOverflow(false); + // 设置最大单页限制数量,默认 500 条,-1 不受限制 + // paginationInnerInterceptor.setMaxLimit(500L); + interceptor.addInnerInterceptor(paginationInnerInterceptor); + + // 2. 添加乐观锁插件 (如果需要) + // 当要更新一条记录的时候,希望这条记录没有被别人更新,也就是说实现线程安全的数据更新 + // 需要在实体类的字段上加上 @Version 注解 + interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); + + // 3. 防止全表更新与删除插件 (如果需要, 注意在生产环境谨慎使用,因为它会阻止没有 WHERE 条件的 UPDATE 和 DELETE 操作) + // interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + + return interceptor; + } + + // 如果使用了 MyBatis Plus 的逻辑删除功能,可以在这里配置全局的逻辑删除字段等 + // 例如,在 application.yml/properties 中配置: + // mybatis-plus.global-config.db-config.logic-delete-field=deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) + // mybatis-plus.global-config.db-config.logic-delete-value=1 # 逻辑已删除值(默认为 1) + // mybatis-plus.global-config.db-config.logic-not-delete-value=0 # 逻辑未删除值(默认为 0) +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/RedisConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/RedisConfig.java new file mode 100644 index 0000000..ae48ed7 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/RedisConfig.java @@ -0,0 +1,46 @@ +package com.trade.common.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Redis配置类 + * @author Trade Team + */ +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + ObjectMapper om = new ObjectMapper(); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + serializer.setObjectMapper(om); + + // String序列化配置 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + + // key采用String的序列化方式 + redisTemplate.setKeySerializer(stringRedisSerializer); + // hash的key也采用String的序列化方式 + redisTemplate.setHashKeySerializer(stringRedisSerializer); + // value序列化方式采用jackson + redisTemplate.setValueSerializer(serializer); + // hash的value序列化方式采用jackson + redisTemplate.setHashValueSerializer(serializer); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SecurityConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SecurityConfig.java new file mode 100644 index 0000000..57d3bcb --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SecurityConfig.java @@ -0,0 +1,38 @@ +package com.trade.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * Spring Security配置 + * @author Trade Team + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity.csrf() + .disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/login", "/register", "/swagger-ui/**", "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SpringDocConfig.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SpringDocConfig.java new file mode 100644 index 0000000..5495f8d --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SpringDocConfig.java @@ -0,0 +1,101 @@ +package com.trade.common.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + *

SpringDoc OpenAPI 配置类

+ *

用于生成API接口文档

+ * + * @author creator + * @since 2024-01-01 + */ +@Configuration +public class SpringDocConfig { + + @Value("${spring.application.name:default-service}") + private String applicationName; + + /** + * 配置 OpenAPI 基本信息 + * + * @return OpenAPI 实例 + */ + @Bean + public OpenAPI customOpenAPI( + @Value("${springdoc.version:1.0.0}") String appVersion, + @Value("${springdoc.title:API Documentation}") String title, + @Value("${springdoc.description:API documentation for the service}") String description, + @Value("${springdoc.contact.name:API Support}") String contactName, + @Value("${springdoc.contact.email:support@example.com}") String contactEmail, + @Value("${springdoc.contact.url:https://example.com}") String contactUrl, + @Value("${springdoc.license.name:Apache 2.0}") String licenseName, + @Value("${springdoc.license.url:https://www.apache.org/licenses/LICENSE-2.0.html}") String licenseUrl) { + + // 优化:将 applicationName 用于 title 和 description (如果它们是默认值) + String effectiveTitle = title.equals("API Documentation") ? applicationName + " API Documentation" : title; + String effectiveDescription = description.equals("API documentation for the service") ? + "API documentation for the " + applicationName + " service." : description; + + return new OpenAPI() + .info(new Info() + .title(effectiveTitle) + .version(appVersion) + .description(effectiveDescription) + .contact(new Contact() + .name(contactName) + .email(contactEmail) + .url(contactUrl)) + .license(new License() + .name(licenseName) + .url(licenseUrl))); + } + + /** + * 创建一个默认的 API 分组 + *

+ * 可以根据需要创建多个 GroupedOpenApi Bean 来对 API 进行分组展示。 + * 例如,可以按模块、按版本或按访问权限进行分组。 + *

+ * + * @return GroupedOpenApi 实例 + */ + @Bean + public GroupedOpenApi defaultApiGroup() { + // 优化:使用 applicationName 作为 group 名称,使其更具辨识度 + String groupName = applicationName.replace("-service", "") + "-apis"; + return GroupedOpenApi.builder() + .group(groupName) // API 分组的名称 + .packagesToScan("com.trade."+ applicationName.replace("trade-","") + ".controller") // 指定扫描的包路径,需要根据模块名动态调整 + // .pathsToMatch("/api/**") // 可以通过路径匹配来包含特定的API + // .displayName(applicationName + " APIs") // 分组的显示名称 + .build(); + } + + // 示例:为特定模块创建API分组 (如果需要) + // @Bean + // public GroupedOpenApi userApiGroup() { + // return GroupedOpenApi.builder() + // .group("user-management") + // .packagesToScan("com.trade.user.controller") // 假设用户模块的controller在此包下 + // .pathsToMatch("/user/**") + // .displayName("User Management APIs") + // .build(); + // } + + // @Bean + // public GroupedOpenApi productApiGroup() { + // return GroupedOpenApi.builder() + // .group("product-catalog") + // .packagesToScan("com.trade.product.controller") // 假设产品模块的controller在此包下 + // .pathsToMatch("/product/**") + // .displayName("Product Catalog APIs") + // .build(); + // } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SwaggerProperties.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SwaggerProperties.java new file mode 100644 index 0000000..33e1113 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/config/SwaggerProperties.java @@ -0,0 +1,55 @@ +package com.trade.common.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Swagger属性配置 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +@ConfigurationProperties(prefix = "swagger") +public class SwaggerProperties { + /** + * API文档生成基础路径 + */ + private String apiBasePackage; + /** + * 是否开启Swagger + */ + private boolean enable; + /** + * 页面标题 + */ + private String title; + /** + * 页面描述 + */ + private String description; + /** + * 版本号 + */ + private String version; + /** + * 许可证名称 + */ + private String license; + /** + * 许可证URL + */ + private String licenseUrl; + /** + * 外部文档地址 + */ + private String docUrl; + /** + * 外部文档描述 + */ + private String docDescription; + /** + * 分组名称 + */ + private String group; +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/constant/CommonConstant.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/constant/CommonConstant.java new file mode 100644 index 0000000..e38dc64 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/constant/CommonConstant.java @@ -0,0 +1,119 @@ +package com.trade.common.constant; + +/** + *

通用常量类

+ * + * @author creator + * @since 2024-01-01 + */ +public final class CommonConstant { + + private CommonConstant() { + // 私有构造函数,防止实例化 + } + + // --------------------------- 符号常量 --------------------------- + public static final String EMPTY_STRING = ""; + public static final String SPACE = " "; + public static final String COMMA = ","; + public static final String PERIOD = "."; + public static final String COLON = ":"; + public static final String SEMICOLON = ";"; + public static final String HYPHEN = "-"; + public static final String UNDERSCORE = "_"; + public static final String SLASH = "/"; + public static final String BACKSLASH = "\\"; + public static final String PIPE = "|"; + + // --------------------------- HTTP 相关常量 --------------------------- + /** + * HTTP 请求头:认证 (Authorization) + */ + public static final String HTTP_HEADER_AUTHORIZATION = "Authorization"; + /** + * HTTP 请求头:Bearer Token 前缀 + */ + public static final String HTTP_HEADER_BEARER_PREFIX = "Bearer "; + /** + * HTTP 请求头:内容类型 (Content-Type) + */ + public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; + /** + * HTTP 内容类型:JSON + */ + public static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8"; + /** + * HTTP 内容类型:表单 + */ + public static final String CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8"; + /** + * HTTP 内容类型:文件上传 + */ + public static final String CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data"; + + // --------------------------- 编码常量 --------------------------- + public static final String CHARSET_UTF8 = "UTF-8"; + public static final String CHARSET_GBK = "GBK"; + + // --------------------------- 日期时间格式常量 --------------------------- + public static final String DATETIME_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss"; + public static final String DATE_FORMAT_DEFAULT = "yyyy-MM-dd"; + public static final String TIME_FORMAT_DEFAULT = "HH:mm:ss"; + public static final String DATETIME_FORMAT_NO_SEPARATOR = "yyyyMMddHHmmss"; + public static final String DATE_FORMAT_NO_SEPARATOR = "yyyyMMdd"; + + // --------------------------- 缓存相关常量 --------------------------- + /** + * 缓存键分隔符 + */ + public static final String CACHE_KEY_SEPARATOR = "::"; + /** + * 用户信息缓存键前缀 + */ + public static final String CACHE_USER_PREFIX = "user"; + /** + * Token 缓存键前缀 + */ + public static final String CACHE_TOKEN_PREFIX = "token"; + + // --------------------------- 逻辑删除状态 --------------------------- + /** + * 逻辑未删除 + */ + public static final Integer LOGIC_NOT_DELETED = 0; + /** + * 逻辑已删除 + */ + public static final Integer LOGIC_DELETED = 1; + + // --------------------------- 通用状态 (启用/禁用) --------------------------- + /** + * 状态:启用 + */ + public static final Integer STATUS_ENABLED = 1; + /** + * 状态:禁用 + */ + public static final Integer STATUS_DISABLED = 0; + + // --------------------------- 默认值常量 --------------------------- + /** + * 默认分页大小 + */ + public static final int DEFAULT_PAGE_SIZE = 10; + /** + * 默认当前页码 + */ + public static final int DEFAULT_PAGE_NUM = 1; + + // --------------------------- 其他常量 --------------------------- + /** + * 默认的超级管理员用户ID (示例) + */ + public static final Long SUPER_ADMIN_ID = 1L; + /** + * 默认的超级管理员角色代码 (示例) + */ + public static final String ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN"; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/domain/UserDto.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/domain/UserDto.java new file mode 100644 index 0000000..c4c99e8 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/domain/UserDto.java @@ -0,0 +1,71 @@ +package com.trade.common.domain; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 登录用户信息 + * @author Trade Team + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class UserDto implements UserDetails { + private Long id; + private String username; + private String password; + private Integer status; + private List permissionList; + + public UserDto(Long id, String username, String password, Integer status, List permissionList) { + this.id = id; + this.username = username; + this.password = password; + this.status = status; + this.permissionList = permissionList; + } + + @Override + public Collection getAuthorities() { + return permissionList.stream() + .filter(permission -> permission!=null) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return status == 1; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ApiException.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ApiException.java new file mode 100644 index 0000000..d96020a --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ApiException.java @@ -0,0 +1,32 @@ +package com.trade.common.exception; + +/** + * 自定义API异常 + * @author Trade Team + */ +import com.trade.common.api.IErrorCode; + +public class ApiException extends RuntimeException { + private IErrorCode errorCode; + + public ApiException(IErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ApiException(String message) { + super(message); + } + + public ApiException(Throwable cause) { + super(cause); + } + + public ApiException(String message, Throwable cause) { + super(message, cause); + } + + public IErrorCode getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/Asserts.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/Asserts.java new file mode 100644 index 0000000..abe320c --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/Asserts.java @@ -0,0 +1,17 @@ +package com.trade.common.exception; + +import com.trade.common.api.IErrorCode; + +/** + * 断言处理类,用于抛出各种API异常 + * @author Trade Team + */ +public class Asserts { + public static void fail(String message) { + throw new ApiException(message); + } + + public static void fail(IErrorCode errorCode) { + throw new ApiException(errorCode); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/BusinessException.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/BusinessException.java new file mode 100644 index 0000000..0a8e457 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/BusinessException.java @@ -0,0 +1,97 @@ +package com.trade.common.exception; + +import com.trade.common.vo.ResultCodeEnum; +import lombok.Getter; + +/** + *

自定义业务异常类

+ * + * @author creator + * @since 2024-01-01 + */ +@Getter +public class BusinessException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private final int code; + + /** + * 构造器,使用自定义消息 + * + * @param message 异常消息 + */ + public BusinessException(String message) { + super(message); + this.code = ResultCodeEnum.OPERATION_FAILED.getCode(); // 默认为操作失败 + } + + /** + * 构造器,使用自定义错误码和消息 + * + * @param code 错误码 + * @param message 异常消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 构造器,使用 ResultCodeEnum 定义的错误 + * + * @param resultCodeEnum 结果代码枚举 + */ + public BusinessException(ResultCodeEnum resultCodeEnum) { + super(resultCodeEnum.getMessage()); + this.code = resultCodeEnum.getCode(); + } + + /** + * 构造器,使用 ResultCodeEnum 定义的错误,并附加额外信息 + * + * @param resultCodeEnum 结果代码枚举 + * @param detailMessage 详细错误信息,将追加到枚举定义的消息后 + */ + public BusinessException(ResultCodeEnum resultCodeEnum, String detailMessage) { + super(resultCodeEnum.getMessage() + ": " + detailMessage); + this.code = resultCodeEnum.getCode(); + } + + /** + * 构造器,包装另一个异常 + * + * @param message 异常消息 + * @param cause 原始异常 + */ + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(); // 默认为内部服务器错误 + } + + /** + * 构造器,使用自定义错误码、消息并包装另一个异常 + * + * @param code 错误码 + * @param message 异常消息 + * @param cause 原始异常 + */ + public BusinessException(int code, String message, Throwable cause) { + super(message, cause); + this.code = code; + } + + /** + * 构造器,使用 ResultCodeEnum 并包装另一个异常 + * + * @param resultCodeEnum 结果代码枚举 + * @param cause 原始异常 + */ + public BusinessException(ResultCodeEnum resultCodeEnum, Throwable cause) { + super(resultCodeEnum.getMessage(), cause); + this.code = resultCodeEnum.getCode(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ErrorCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ErrorCode.java new file mode 100644 index 0000000..5114546 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/ErrorCode.java @@ -0,0 +1,10 @@ +package com.trade.common.exception; + +/** + * 封装API的错误码 + * @author Trade Team + */ +public interface ErrorCode { + long getCode(); + String getMessage(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/GlobalExceptionHandler.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..7d91863 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,205 @@ +package com.trade.common.exception; + +import com.trade.common.vo.ResultCodeEnum; +import com.trade.common.vo.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + *

全局异常处理器

+ *

捕获controller层抛出的异常,并返回统一的JSON格式响应

+ * + * @author creator + * @since 2024-01-01 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理自定义业务异常 BusinessException + * + * @param e BusinessException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.OK) // 通常业务异常返回200,通过code和message区分 + public Result handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage(), e); + return Result.error(e.getCode(), e.getMessage()); + } + + /** + * 处理参数校验异常 (Query Param / Path Variable) + * + * @param e ConstraintViolationException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleConstraintViolationException(ConstraintViolationException e) { + Set> violations = e.getConstraintViolations(); + String message = violations.stream() + .map(violation -> String.format("%s: %s", getFieldName(violation), violation.getMessage())) + .collect(Collectors.joining("; ")); + log.warn("参数校验失败 (ConstraintViolationException): {}", message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + private String getFieldName(ConstraintViolation violation) { + String propertyPath = violation.getPropertyPath().toString(); + // propertyPath 通常是 "methodName.argName.fieldName" 或 "fieldName" + // 我们尝试获取最后一个点之后的部分作为字段名 + int lastDotIndex = propertyPath.lastIndexOf('.'); + return (lastDotIndex == -1) ? propertyPath : propertyPath.substring(lastDotIndex + 1); + } + + /** + * 处理参数校验异常 (RequestBody @Valid) + * + * @param e MethodArgumentNotValidException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + List fieldErrors = e.getBindingResult().getFieldErrors(); + String message = fieldErrors.stream() + .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage())) + .collect(Collectors.joining("; ")); + log.warn("参数校验失败 (MethodArgumentNotValidException): {}", message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理参数绑定异常 (form-data/x-www-form-urlencoded @Valid) + * + * @param e BindException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBindException(BindException e) { + List fieldErrors = e.getBindingResult().getFieldErrors(); + String message = fieldErrors.stream() + .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage())) + .collect(Collectors.joining("; ")); + log.warn("参数绑定失败 (BindException): {}", message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理缺少请求参数异常 + * + * @param e MissingServletRequestParameterException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { + String message = String.format("缺少必要的请求参数: %s (类型: %s)", e.getParameterName(), e.getParameterType()); + log.warn(message); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理参数类型不匹配异常 + * + * @param e MethodArgumentTypeMismatchException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + String message = String.format("参数类型不匹配: 参数 '%s' 需要类型 '%s', 但提供了值 '%s'", + e.getName(), e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "N/A", e.getValue()); + log.warn(message, e); + return Result.error(ResultCodeEnum.BAD_REQUEST, message); + } + + /** + * 处理HTTP请求方法不支持异常 + * + * @param e HttpRequestMethodNotSupportedException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + String message = String.format("不支持的请求方法: %s. 支持的方法有: %s", e.getMethod(), e.getSupportedHttpMethods()); + log.warn(message); + return Result.error(ResultCodeEnum.FORBIDDEN, message); + } + + /** + * 处理404 NoHandlerFoundException + * 需要在 application.properties 中配置 spring.mvc.throw-exception-if-no-handler-found=true + * 和 spring.web.resources.add-mappings=false (如果使用了静态资源映射) + * + * @param e NoHandlerFoundException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result handleNoHandlerFoundException(NoHandlerFoundException e) { + String message = String.format("接口 [%s %s] 不存在", e.getHttpMethod(), e.getRequestURL()); + log.warn(message, e); + return Result.error(ResultCodeEnum.NOT_FOUND, message); + } + + /** + * 处理请求体不可读异常 + * + * @param e HttpMessageNotReadableException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.warn("请求体不可读或JSON格式错误: {}", e.getMessage()); + return Result.error(ResultCodeEnum.BAD_REQUEST, "请求体不可读或JSON格式错误"); + } + + /** + * 处理其他所有未捕获的运行时异常 + * + * @param e RuntimeException 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleRuntimeException(RuntimeException e) { + log.error("服务器发生运行时异常: {}", e.getMessage(), e); + return Result.error(ResultCodeEnum.INTERNAL_SERVER_ERROR, "服务器发生运行时异常,请联系管理员"); + } + + /** + * 处理其他所有未捕获的顶级异常 Exception + * + * @param e Exception 实例 + * @return Result 封装的错误响应 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error("服务器发生未知异常: {}", e.getMessage(), e); + return Result.error(ResultCodeEnum.INTERNAL_SERVER_ERROR, "服务器发生未知异常,请联系管理员"); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/response/CommonResult.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/CommonResult.java new file mode 100644 index 0000000..d0da436 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/CommonResult.java @@ -0,0 +1,126 @@ +package com.trade.common.response; + +import com.trade.common.api.IErrorCode; + +/** + * 通用返回对象 + * @author Trade Team + */ +public class CommonResult { + private long code; + private String message; + private T data; + + protected CommonResult() { + } + + protected CommonResult(long code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(T data) { + return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功返回结果 + * + * @param data 获取的数据 + * @param message 提示信息 + */ + public static CommonResult success(T data, String message) { + return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + */ + public static CommonResult failed(IErrorCode errorCode) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); + } + + /** + * 失败返回结果 + * @param errorCode 错误码 + * @param message 错误信息 + */ + public static CommonResult failed(IErrorCode errorCode, String message) { + return new CommonResult(errorCode.getCode(), message, null); + } + + /** + * 失败返回结果 + * @param message 提示信息 + */ + public static CommonResult failed(String message) { + return new CommonResult(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败返回结果 + */ + public static CommonResult failed() { + return failed(ResultCode.FAILED); + } + + /** + * 参数验证失败返回结果 + */ + public static CommonResult validateFailed() { + return failed(ResultCode.VALIDATE_FAILED); + } + + /** + * 参数验证失败返回结果 + * @param message 提示信息 + */ + public static CommonResult validateFailed(String message) { + return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); + } + + /** + * 未登录返回结果 + */ + public static CommonResult unauthorized(T data) { + return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); + } + + /** + * 未授权返回结果 + */ + public static CommonResult forbidden(T data) { + return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); + } + + public long getCode() { + return code; + } + + public void setCode(long code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/response/ResultCode.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/ResultCode.java new file mode 100644 index 0000000..024e4b4 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/response/ResultCode.java @@ -0,0 +1,34 @@ +package com.trade.common.response; + +import com.trade.common.api.IErrorCode; + +import com.trade.common.exception.ErrorCode; + +/** + * 枚举常用API操作码 + * @author Trade Team + */ +public enum ResultCode implements IErrorCode { + SUCCESS(200, "操作成功"), + FAILED(500, "操作失败"), + VALIDATE_FAILED(404, "参数检验失败"), + UNAUTHORIZED(401, "暂未登录或token已经过期"), + FORBIDDEN(403, "没有相关权限"); + private long code; + private String message; + + private ResultCode(long code, String message) { + this.code = code; + this.message = message; + } + + @Override + public long getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/service/RedisService.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/RedisService.java new file mode 100644 index 0000000..748836f --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/RedisService.java @@ -0,0 +1,132 @@ +package com.trade.common.service; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * redis操作Service, + * 对象和数组都以json形式进行存储 + * @author Trade Team + */ +public interface RedisService { + /** + * 存储数据 + */ + void set(String key, Object value); + + /** + * 存储数据并设置过期时间 + */ + void set(String key, Object value, long expire); + + /** + * 获取数据 + */ + Object get(String key); + + /** + * 设置过期时间 + */ + boolean expire(String key, long expire); + + /** + * 删除数据 + */ + void del(String key); + + /** + * 批量删除数据 + */ + void del(List keys); + + /** + * 批量删除key + */ + Long delByPrefix(String prefix); + + /** + * 对某个key的value进行递增操作 + */ + Long increment(String key, Long delta); + + /** + * 对某个key的value进行递减操作 + */ + Long decrement(String key, Long delta); + + /** + * 获取所有key + */ + Set keys(String pattern); + + /** + * 判断key是否存在 + */ + Boolean hasKey(String key); + + /** + * 将数据放入redis的list中 + */ + Long lPush(String key, Object value); + + /** + * 将数据放入redis的list中并设置过期时间 + */ + Long lPush(String key, Object value, long expire); + + /** + * 从redis的list中获取数据 + */ + Object lPop(String key); + + /** + * 将数据放入redis的set中 + */ + Long sAdd(String key, Object... values); + + /** + * 将数据放入redis的set中并设置过期时间 + */ + Long sAdd(String key, long expire, Object... values); + + /** + * 从redis的set中获取数据 + */ + Set sMembers(String key); + + /** + * 将数据放入redis的hash中 + */ + void hPut(String key, String hashKey, Object value); + + /** + * 将数据放入redis的hash中并设置过期时间 + */ + void hPut(String key, String hashKey, Object value, long expire); + + /** + * 从redis的hash中获取数据 + */ + Object hGet(String key, String hashKey); + + /** + * 将map放入redis的hash中 + */ + void hPutAll(String key, Map map); + + /** + * 将map放入redis的hash中并设置过期时间 + */ + void hPutAll(String key, Map map, long expire); + + /** + * 从redis的hash中获取所有数据 + */ + Map hGetAll(String key); + + /** + * 删除redis的hash中的数据 + */ + void hDel(String key, Object... hashKeys); +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/service/impl/RedisServiceImpl.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/impl/RedisServiceImpl.java new file mode 100644 index 0000000..34b8911 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/service/impl/RedisServiceImpl.java @@ -0,0 +1,151 @@ +package com.trade.common.service.impl; + +import com.trade.common.service.RedisService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * redis操作Service的实现类 + * @author Trade Team + */ +@Service +public class RedisServiceImpl implements RedisService { + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void set(String key, Object value) { + redisTemplate.opsForValue().set(key, value); + } + + @Override + public void set(String key, Object value, long expire) { + redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS); + } + + @Override + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + @Override + public boolean expire(String key, long expire) { + return redisTemplate.expire(key, expire, TimeUnit.SECONDS); + } + + @Override + public void del(String key) { + redisTemplate.delete(key); + } + + @Override + public void del(List keys) { + redisTemplate.delete(keys); + } + + @Override + public Long delByPrefix(String prefix) { + Set keys = redisTemplate.keys(prefix + "*"); + if (keys != null && !keys.isEmpty()) { + return redisTemplate.delete(keys); + } + return 0L; + } + + @Override + public Long increment(String key, Long delta) { + return redisTemplate.opsForValue().increment(key, delta); + } + + @Override + public Long decrement(String key, Long delta) { + return redisTemplate.opsForValue().decrement(key, delta); + } + + @Override + public Set keys(String pattern) { + return redisTemplate.keys(pattern); + } + + @Override + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + @Override + public Long lPush(String key, Object value) { + return redisTemplate.opsForList().leftPush(key, value); + } + + @Override + public Long lPush(String key, Object value, long expire) { + Long result = redisTemplate.opsForList().leftPush(key, value); + expire(key, expire); + return result; + } + + @Override + public Object lPop(String key) { + return redisTemplate.opsForList().leftPop(key); + } + + @Override + public Long sAdd(String key, Object... values) { + return redisTemplate.opsForSet().add(key, values); + } + + @Override + public Long sAdd(String key, long expire, Object... values) { + Long result = redisTemplate.opsForSet().add(key, values); + expire(key, expire); + return result; + } + + @Override + public Set sMembers(String key) { + return redisTemplate.opsForSet().members(key); + } + + @Override + public void hPut(String key, String hashKey, Object value) { + redisTemplate.opsForHash().put(key, hashKey, value); + } + + @Override + public void hPut(String key, String hashKey, Object value, long expire) { + redisTemplate.opsForHash().put(key, hashKey, value); + expire(key, expire); + } + + @Override + public Object hGet(String key, String hashKey) { + return redisTemplate.opsForHash().get(key, hashKey); + } + + @Override + public void hPutAll(String key, Map map) { + redisTemplate.opsForHash().putAll(key, map); + } + + @Override + public void hPutAll(String key, Map map, long expire) { + redisTemplate.opsForHash().putAll(key, map); + expire(key, expire); + } + + @Override + public Map hGetAll(String key) { + return (Map) (Map) redisTemplate.opsForHash().entries(key); + } + + @Override + public void hDel(String key, Object... hashKeys) { + redisTemplate.opsForHash().delete(key, hashKeys); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/DateUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/DateUtil.java new file mode 100644 index 0000000..01d349b --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/DateUtil.java @@ -0,0 +1,48 @@ +package com.trade.common.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 日期工具类 + * @author Trade Team + */ +public class DateUtil { + + /** + * 将LocalDateTime格式化为指定字符串 + * @param dateTime LocalDateTime对象 + * @param pattern 格式模式,如 "yyyy-MM-dd HH:mm:ss" + * @return 格式化后的日期字符串 + */ + public static String formatLocalDateTime(LocalDateTime dateTime, String pattern) { + if (dateTime == null || pattern == null || pattern.isEmpty()) { + return null; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return dateTime.format(formatter); + } + + /** + * 将当前LocalDateTime格式化为默认字符串 "yyyy-MM-dd HH:mm:ss" + * @param dateTime LocalDateTime对象 + * @return 格式化后的日期字符串 + */ + public static String formatLocalDateTime(LocalDateTime dateTime) { + return formatLocalDateTime(dateTime, "yyyy-MM-dd HH:mm:ss"); + } + + /** + * 将字符串解析为LocalDateTime对象 + * @param dateString 日期字符串 + * @param pattern 格式模式,如 "yyyy-MM-dd HH:mm:ss" + * @return 解析后的LocalDateTime对象 + */ + public static LocalDateTime parseLocalDateTime(String dateString, String pattern) { + if (dateString == null || dateString.isEmpty() || pattern == null || pattern.isEmpty()) { + return null; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return LocalDateTime.parse(dateString, formatter); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtTokenUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtTokenUtil.java new file mode 100644 index 0000000..da4c2d8 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtTokenUtil.java @@ -0,0 +1,143 @@ +package com.trade.common.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JwtToken生成工具类 + * JWT token的格式:header.payload.signature + * header的格式(算法、token的类型): + * { + * "alg": "HS512", + * "typ": "JWT" + * } + * payload的格式(用户名、创建时间、生成时间): + * { + * "sub":"wang", + * "created":1489079981398 + * } + * signature的生成算法: + * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) + * @author Trade Team + */ +public class JwtTokenUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); + private static final String CLAIM_KEY_USERNAME = "sub"; + private static final String CLAIM_KEY_CREATED = "created"; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.expiration}") + private Long expiration; + @Value("${jwt.tokenHead}") + private String tokenHead; + + /** + * 根据负责生成JWT的token + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * 从token中获取JWT中的负载 + */ + private Claims getClaimsFromToken(String token) { + Claims claims = null; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + LOGGER.info("JWT格式验证失败: {}", token); + } + return claims; + } + + /** + * 生成token的过期时间 + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + /** + * 从token中获取登录用户名 + */ + public String getUserNameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 验证token是否还有效 + * + * @param token 客户端传入的token + * @param userDetails 从数据库中查询出来的用户信息 + */ + public boolean validateToken(String token, UserDetails userDetails) { + String username = getUserNameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + /** + * 判断token是否已经失效 + */ + private boolean isTokenExpired(String token) { + Date expiredDate = getExpiredDateFromToken(token); + return expiredDate.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + private Date getExpiredDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 根据用户信息生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + /** + * 判断token是否可以被刷新 + */ + public boolean canRefresh(String token) { + return !isTokenExpired(token); + } + + /** + * 刷新token + */ + public String refreshToken(String token) { + Claims claims = getClaimsFromToken(token); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtUtils.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtUtils.java new file mode 100644 index 0000000..403df13 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/JwtUtils.java @@ -0,0 +1,231 @@ +package com.trade.common.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +/** + *

JWT (JSON Web Token) 工具类

+ *

用于生成、解析和验证JWT

+ * + * @author creator + * @since 2024-01-01 + */ +@Slf4j +@Component // 使其可以被Spring管理,方便注入配置 +public class JwtUtils { + + /** + * JWT 密钥,从配置文件读取,必须足够复杂以保证安全 + * 建议长度至少为256位 (32个ASCII字符) + */ + @Value("${jwt.secret:defaultSecretKey_must_be_at_least_32_characters_long_for_HS256}") + private String secret; + + /** + * JWT 过期时间(毫秒),从配置文件读取,默认为1小时 + */ + @Value("${jwt.expiration:3600000}") + private long expiration; + + private SecretKey secretKey; + + /** + * 初始化密钥 + */ + private SecretKey getSecretKey() { + if (secretKey == null) { + // 优化:确保密钥长度符合HS256要求,如果不足则进行提示或使用默认安全密钥 + if (!StringUtils.hasText(secret) || secret.length() < 32) { + log.warn("JWT secret key is not configured or too short (must be at least 32 chars for HS256). Using a default secure key. THIS IS NOT RECOMMENDED FOR PRODUCTION!"); + // Keys.secretKeyFor(SignatureAlgorithm.HS256) 会生成一个安全的随机密钥 + this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); + } else { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); + } + } + return secretKey; + } + + /** + * 生成JWT Token + * + * @param subject 主题,通常是用户ID或其他唯一标识 + * @param claims 自定义声明 (payload) + * @return 生成的JWT字符串 + */ + public String generateToken(String subject, Map claims) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + return Jwts.builder() + .setClaims(claims) // 设置自定义声明 + .setSubject(subject) // 设置主题 + .setId(UUID.randomUUID().toString()) // JWT的唯一身份标识 + .setIssuedAt(now) // 设置签发时间 + .setExpiration(expiryDate) // 设置过期时间 + .signWith(getSecretKey(), SignatureAlgorithm.HS256) // 设置签名算法和密钥 + .compact(); + } + + /** + * 生成JWT Token,仅包含 subject + * + * @param subject 主题,通常是用户ID或其他唯一标识 + * @return 生成的JWT字符串 + */ + public String generateToken(String subject) { + return generateToken(subject, null); + } + + /** + * 从JWT Token中获取 Claims (Payload) + * + * @param token JWT字符串 + * @return Claims 对象 + * @throws ExpiredJwtException 如果JWT已过期 + * @throws UnsupportedJwtException 如果JWT格式不受支持 + * @throws MalformedJwtException 如果JWT格式错误 + * @throws SignatureException 如果签名验证失败 + * @throws IllegalArgumentException 如果token为空或无效 + */ + private Claims getClaimsFromToken(String token) { + if (!StringUtils.hasText(token)) { + throw new IllegalArgumentException("JWT token cannot be null or empty."); + } + return Jwts.parserBuilder() + .setSigningKey(getSecretKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从JWT Token中获取主题 (Subject) + * + * @param token JWT字符串 + * @return 主题 + */ + public String getSubjectFromToken(String token) { + try { + return getClaimsFromToken(token).getSubject(); + } catch (Exception e) { + log.debug("Failed to get subject from token: {}", e.getMessage()); + return null; + } + } + + /** + * 从JWT Token中获取指定的 Claim 值 + * + * @param token JWT字符串 + * @param claimKey Claim的键 + * @param type Claim值的类型 + * @param Claim值的泛型 + * @return Claim值,如果不存在或类型不匹配则返回null + */ + public T getClaimFromToken(String token, String claimKey, Class type) { + try { + Claims claims = getClaimsFromToken(token); + return claims.get(claimKey, type); + } catch (Exception e) { + log.debug("Failed to get claim '{}' from token: {}", claimKey, e.getMessage()); + return null; + } + } + + /** + * 验证JWT Token是否有效 + * + * @param token JWT字符串 + * @return 如果token有效返回true,否则返回false + */ + public boolean validateToken(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + try { + Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token); + return true; + } catch (SignatureException ex) { + log.error("Invalid JWT signature: {}", ex.getMessage()); + } catch (MalformedJwtException ex) { + log.error("Invalid JWT token: {}", ex.getMessage()); + } catch (ExpiredJwtException ex) { + log.warn("Expired JWT token: {}", ex.getMessage()); + } catch (UnsupportedJwtException ex) { + log.error("Unsupported JWT token: {}", ex.getMessage()); + } catch (IllegalArgumentException ex) { + log.error("JWT claims string is empty: {}", ex.getMessage()); + } + return false; + } + + /** + * 判断JWT Token是否已过期 + * + * @param token JWT字符串 + * @return 如果已过期返回true,否则返回false。如果token无效也返回true。 + */ + public boolean isTokenExpired(String token) { + try { + Date expirationDate = getClaimsFromToken(token).getExpiration(); + return expirationDate.before(new Date()); + } catch (ExpiredJwtException e) { + return true; // 明确已过期 + } catch (Exception e) { + return true; // 其他解析错误,视为无效或已过期 + } + } + + /** + * 刷新JWT Token的过期时间 + *

+ * 注意:这实际上是重新签发了一个新的Token,包含了原有Token的claims和subject。 + *

+ * + * @param token 旧的JWT字符串 + * @return 新的JWT字符串,如果旧token无效则返回null + */ + public String refreshToken(String token) { + if (!StringUtils.hasText(token)) { + return null; + } + try { + Claims claims = getClaimsFromToken(token); + // 检查是否允许刷新 (例如,可以在claims中设置一个特定的刷新标记或时间窗口) + // 此处简单实现为只要未过期即可刷新 + if (claims.getExpiration().after(new Date())) { + return generateToken(claims.getSubject(), claims); + } + } catch (ExpiredJwtException ex) { + log.warn("Cannot refresh an already expired JWT token: {}", ex.getMessage()); + } catch (Exception e) { + log.error("Error refreshing JWT token: {}", e.getMessage()); + } + return null; + } + + /** + * 获取JWT的过期时间 + * + * @return 过期时间(毫秒) + */ + public long getExpiration() { + return expiration; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/RequestUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/RequestUtil.java new file mode 100644 index 0000000..0658051 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/RequestUtil.java @@ -0,0 +1,77 @@ +package com.trade.common.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * 请求工具类 + * @author Trade Team + */ +public class RequestUtil { + + /** + * 获取当前请求的HttpServletRequest对象 + * @return HttpServletRequest对象,如果不在请求上下文中则返回null + */ + public static HttpServletRequest getCurrentRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return null; + } + return attributes.getRequest(); + } + + /** + * 获取请求的IP地址 + * @return IP地址字符串 + */ + public static String getRequestIp() { + HttpServletRequest request = getCurrentRequest(); + if (request == null) { + return ""; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } + + /** + * 获取请求的User-Agent + * @return User-Agent字符串 + */ + public static String getUserAgent() { + HttpServletRequest request = getCurrentRequest(); + if (request == null) { + return ""; + } + return request.getHeader("User-Agent"); + } + + /** + * 获取请求的Referer + * @return Referer字符串 + */ + public static String getReferer() { + HttpServletRequest request = getCurrentRequest(); + if (request == null) { + return ""; + } + return request.getHeader("Referer"); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/SpringUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/SpringUtil.java new file mode 100644 index 0000000..328e811 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/SpringUtil.java @@ -0,0 +1,61 @@ +package com.trade.common.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Spring工具类,用于获取Spring上下文中的Bean + * @author Trade Team + */ +@Component +public class SpringUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (SpringUtil.applicationContext == null) { + SpringUtil.applicationContext = applicationContext; + } + } + + /** + * 获取applicationContext + * @return ApplicationContext + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 通过name获取 Bean. + * @param name Bean的名称 + * @return Object + */ + public static Object getBean(String name) { + return getApplicationContext().getBean(name); + } + + /** + * 通过class获取Bean. + * @param clazz Bean的类型 + * @param 泛型 + * @return T + */ + public static T getBean(Class clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * 通过name,以及Clazz返回指定的Bean + * @param name Bean的名称 + * @param clazz Bean的类型 + * @param 泛型 + * @return T + */ + public static T getBean(String name, Class clazz) { + return getApplicationContext().getBean(name, clazz); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/util/ValidationUtil.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/ValidationUtil.java new file mode 100644 index 0000000..c66adb4 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/util/ValidationUtil.java @@ -0,0 +1,50 @@ +package com.trade.common.util; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; + +/** + * 参数校验工具类 + * @author Trade Team + */ +public class ValidationUtil { + + private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + + /** + * 校验对象 + * @param obj 待校验对象 + * @param 对象类型 + * @throws IllegalArgumentException 如果校验失败,则抛出此异常 + */ + public static void validate(T obj) { + Set> violations = VALIDATOR.validate(obj); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (ConstraintViolation violation : violations) { + sb.append(violation.getMessage()).append(";"); + } + throw new IllegalArgumentException(sb.toString()); + } + } + + /** + * 校验对象的指定属性 + * @param obj 待校验对象 + * @param propertyName 待校验属性名 + * @param 对象类型 + * @throws IllegalArgumentException 如果校验失败,则抛出此异常 + */ + public static void validateProperty(T obj, String propertyName) { + Set> violations = VALIDATOR.validateProperty(obj, propertyName); + if (!violations.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (ConstraintViolation violation : violations) { + sb.append(violation.getMessage()).append(";"); + } + throw new IllegalArgumentException(sb.toString()); + } + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/Result.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/Result.java new file mode 100644 index 0000000..2ba8824 --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/Result.java @@ -0,0 +1,128 @@ +package com.trade.common.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + *

通用API接口返回结果封装类

+ * + * @author creator + * @since 2024-01-01 + */ +@Data +@Schema(description = "通用API接口返回结果") +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 成功标志 + */ + @Schema(description = "成功标志", example = "true") + private boolean success = true; + + /** + * 返回处理消息 + */ + @Schema(description = "返回处理消息", example = "操作成功!") + private String message = "操作成功!"; + + /** + * 返回代码 + */ + @Schema(description = "返回代码", example = "200") + private Integer code = 200; + + /** + * 返回数据对象 data + */ + @Schema(description = "返回数据对象") + private T result; + + /** + * 时间戳 + */ + @Schema(description = "时间戳", example = "1609459200000") + private long timestamp = System.currentTimeMillis(); + + public Result() { + } + + public Result(boolean success, String message, Integer code) { + this.success = success; + this.message = message; + this.code = code; + this.timestamp = System.currentTimeMillis(); + } + + public static Result ok() { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); + return r; + } + + public static Result ok(String msg) { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(msg); + return r; + } + + public static Result ok(T data) { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); + r.setResult(data); + return r; + } + + public static Result ok(String msg, T data) { + Result r = new Result<>(); + r.setSuccess(true); + r.setCode(ResultCodeEnum.SUCCESS.getCode()); + r.setMessage(msg); + r.setResult(data); + return r; + } + + public static Result error(String msg) { + return error(ResultCodeEnum.INTERNAL_SERVER_ERROR.getCode(), msg); + } + + public static Result error(Integer code, String msg) { + Result r = new Result<>(); + r.setSuccess(false); + r.setCode(code); + r.setMessage(msg); + return r; + } + + public static Result error(ResultCodeEnum resultCodeEnum) { + Result r = new Result<>(); + r.setSuccess(false); + r.setCode(resultCodeEnum.getCode()); + r.setMessage(resultCodeEnum.getMessage()); + return r; + } + + public static Result error(ResultCodeEnum resultCodeEnum, String msg) { + Result r = new Result<>(); + r.setSuccess(false); + r.setCode(resultCodeEnum.getCode()); + r.setMessage(msg); // Use custom message + return r; + } + + /** + * 无权限访问返回结果 + */ + public static Result noauth(String msg) { + return error(ResultCodeEnum.UNAUTHORIZED.getCode(), msg); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/ResultCodeEnum.java b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/ResultCodeEnum.java new file mode 100644 index 0000000..0845efd --- /dev/null +++ b/samples/existing-system/trade-common/src/main/java/com/trade/common/vo/ResultCodeEnum.java @@ -0,0 +1,30 @@ +package com.trade.common.vo; + +import lombok.Getter; + +/** + *

结果代码枚举

+ * + * @author creator + * @since 2024-01-01 + */ +@Getter +public enum ResultCodeEnum { + + SUCCESS(200, "操作成功"), + OPERATION_FAILED(5001, "操作失败"), + BAD_REQUEST(400, "请求参数错误"), + UNAUTHORIZED(401, "未经授权"), + FORBIDDEN(403, "访问被拒绝"), + NOT_FOUND(404, "资源不存在"), + INTERNAL_SERVER_ERROR(500, "服务器内部错误"), + SERVICE_UNAVAILABLE(503, "服务不可用"); + + private final Integer code; + private final String message; + + ResultCodeEnum(Integer code, String message) { + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/pom.xml b/samples/existing-system/trade-gateway/pom.xml new file mode 100644 index 0000000..06c7be4 --- /dev/null +++ b/samples/existing-system/trade-gateway/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + com.stock + trade-gateway + jar + + trade-gateway + API Gateway for the trading system + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.projectlombok + lombok + provided + + + + + com.stock + trade-common + ${project.parent.version} + + + + + + + + + + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + org.springdoc + springdoc-openapi-starter-webflux-ui + ${springdoc-openapi.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/GatewayApplication.java b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/GatewayApplication.java new file mode 100644 index 0000000..9e83ebc --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/GatewayApplication.java @@ -0,0 +1,29 @@ +package com.trade.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 网关应用启动类 + * + * @author Trade Team + */ +@SpringBootApplication +@EnableDiscoveryClient // 开启服务注册与发现功能 +public class GatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + System.out.println("(♥◠‿◠)ノ゙ Trade Gateway 启动成功 ლ(´ڡ`ლ)゙ \n" + + " .-------. ____ __ \n" + + " | _ _ | \\ \\ / / \n" + + " | ( ' ) | \\ _. / ' \n" + + " |(_ o _) / _( )_ .' \n" + + " | (_,_).' __ ___(_ o _)' \n" + + " | |\ \ | || |(_,_)' \n" + + " | | \ `' /| `-' / \n" + + " | | \ / \ / \n" + + " ''-' `'-' `-..-' "); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/TradeGatewayApplication.java b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/TradeGatewayApplication.java new file mode 100644 index 0000000..9e1fb32 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/TradeGatewayApplication.java @@ -0,0 +1,17 @@ +package com.trade.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * 网关服务启动类 + * Created by macro on 2020/6/17. + */ +@EnableDiscoveryClient +@SpringBootApplication +public class TradeGatewayApplication { + public static void main(String[] args) { + SpringApplication.run(TradeGatewayApplication.class, args); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/config/GatewayConfig.java b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/config/GatewayConfig.java new file mode 100644 index 0000000..da89b96 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/java/com/trade/gateway/config/GatewayConfig.java @@ -0,0 +1,37 @@ +package com.trade.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * 网关配置类 + * + * @author Trade Team + */ +@Configuration +public class GatewayConfig { + + /** + * 配置CORS跨域支持 + * + * @return CorsWebFilter + */ + @Bean + public CorsWebFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedMethod("*"); // 允许所有请求方法 (GET, POST, PUT, DELETE, OPTIONS等) + config.addAllowedOriginPattern("*"); // 允许所有来源,生产环境建议配置具体域名 + config.addAllowedHeader("*"); // 允许所有请求头 + config.setAllowCredentials(true); // 允许发送Cookie + config.setMaxAge(3600L); // 预检请求的有效期,单位秒 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); + source.registerCorsConfiguration("/**", config); // 对所有路径生效 + + return new CorsWebFilter(source); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application-dev.yml b/samples/existing-system/trade-gateway/src/main/resources/application-dev.yml new file mode 100644 index 0000000..0f8823f --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application-dev.yml @@ -0,0 +1,63 @@ +spring: + redis: + database: 0 + host: localhost + port: 6379 + password: # + cloud: + gateway: + routes: + - id: trade-user + uri: lb://trade-user + predicates: + - Path=/user/** + filters: + - StripPrefix=1 + - id: trade-order + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + - id: trade-market-data + uri: lb://trade-market-data + predicates: + - Path=/market-data/** + filters: + - StripPrefix=1 + - id: trade-notification + uri: lb://trade-notification + predicates: + - Path=/notification/** + filters: + - StripPrefix=1 + - id: trade-strategy + uri: lb://trade-strategy + predicates: + - Path=/strategy/** + filters: + - StripPrefix=1 + - id: trade-risk + uri: lb://trade-risk + predicates: + - Path=/risk/** + filters: + - StripPrefix=1 + - id: trade-backtest + uri: lb://trade-backtest + predicates: + - Path=/backtest/** + filters: + - StripPrefix=1 + - id: trade-indicator + uri: lb://trade-indicator + predicates: + - Path=/indicator/** + filters: + - StripPrefix=1 + - id: trade-application + uri: lb://trade-application + predicates: + - Path=/application/** + filters: + - StripPrefix=1 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application-prod.yml b/samples/existing-system/trade-gateway/src/main/resources/application-prod.yml new file mode 100644 index 0000000..0f8823f --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application-prod.yml @@ -0,0 +1,63 @@ +spring: + redis: + database: 0 + host: localhost + port: 6379 + password: # + cloud: + gateway: + routes: + - id: trade-user + uri: lb://trade-user + predicates: + - Path=/user/** + filters: + - StripPrefix=1 + - id: trade-order + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + - id: trade-market-data + uri: lb://trade-market-data + predicates: + - Path=/market-data/** + filters: + - StripPrefix=1 + - id: trade-notification + uri: lb://trade-notification + predicates: + - Path=/notification/** + filters: + - StripPrefix=1 + - id: trade-strategy + uri: lb://trade-strategy + predicates: + - Path=/strategy/** + filters: + - StripPrefix=1 + - id: trade-risk + uri: lb://trade-risk + predicates: + - Path=/risk/** + filters: + - StripPrefix=1 + - id: trade-backtest + uri: lb://trade-backtest + predicates: + - Path=/backtest/** + filters: + - StripPrefix=1 + - id: trade-indicator + uri: lb://trade-indicator + predicates: + - Path=/indicator/** + filters: + - StripPrefix=1 + - id: trade-application + uri: lb://trade-application + predicates: + - Path=/application/** + filters: + - StripPrefix=1 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application-test.yml b/samples/existing-system/trade-gateway/src/main/resources/application-test.yml new file mode 100644 index 0000000..0f8823f --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application-test.yml @@ -0,0 +1,63 @@ +spring: + redis: + database: 0 + host: localhost + port: 6379 + password: # + cloud: + gateway: + routes: + - id: trade-user + uri: lb://trade-user + predicates: + - Path=/user/** + filters: + - StripPrefix=1 + - id: trade-order + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + - id: trade-market-data + uri: lb://trade-market-data + predicates: + - Path=/market-data/** + filters: + - StripPrefix=1 + - id: trade-notification + uri: lb://trade-notification + predicates: + - Path=/notification/** + filters: + - StripPrefix=1 + - id: trade-strategy + uri: lb://trade-strategy + predicates: + - Path=/strategy/** + filters: + - StripPrefix=1 + - id: trade-risk + uri: lb://trade-risk + predicates: + - Path=/risk/** + filters: + - StripPrefix=1 + - id: trade-backtest + uri: lb://trade-backtest + predicates: + - Path=/backtest/** + filters: + - StripPrefix=1 + - id: trade-indicator + uri: lb://trade-indicator + predicates: + - Path=/indicator/** + filters: + - StripPrefix=1 + - id: trade-application + uri: lb://trade-application + predicates: + - Path=/application/** + filters: + - StripPrefix=1 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/application.yml b/samples/existing-system/trade-gateway/src/main/resources/application.yml new file mode 100644 index 0000000..5b73330 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/application.yml @@ -0,0 +1,125 @@ +server: + port: 8080 # 网关服务端口 + +spring: + application: + name: trade-gateway # 应用名称 + cloud: + nacos: + discovery: + server-addr: 127.0.0.1:8848 # Nacos Server地址 + namespace: # Nacos命名空间ID,根据实际情况配置 + group: DEFAULT_GROUP # Nacos分组,根据实际情况配置 + config: + server-addr: ${spring.cloud.nacos.discovery.server-addr} # Nacos配置中心地址,同服务发现 + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + file-extension: yml # 配置文件格式 + shared-configs: # 共享配置 + - data-id: application-common.yml # 通用配置 + group: ${spring.cloud.nacos.discovery.group} + refresh: true # 是否动态刷新 + # ext-config: # 扩展配置,可以加载多个配置文件 + # - data-id: trade-gateway-ext.yml + # group: ${spring.cloud.nacos.discovery.group} + # refresh: true + + gateway: + discovery: + locator: + enabled: true # 开启从注册中心动态创建路由的功能 + lower-case-service-id: true # 将服务名转为小写进行路由 + routes: + # 示例:路由到用户服务 (trade-user) + - id: trade-user-route + uri: lb://trade-user # lb代表从Nacos负载均衡,trade-user是服务名 + predicates: + - Path=/user/** # 所有/user/**的请求都路由到trade-user服务 + filters: + - StripPrefix=1 # 转发前去掉第一层路径,例如 /user/login -> /login + # - AddRequestHeader=X-Request-Source, gateway # 添加请求头 + + # 示例:路由到行情服务 (trade-market) + - id: trade-market-route + uri: lb://trade-market + predicates: + - Path=/market/** + filters: + - StripPrefix=1 + + # 示例:路由到订单服务 (trade-order) + - id: trade-order-route + uri: lb://trade-order + predicates: + - Path=/order/** + filters: + - StripPrefix=1 + + # 示例:路由到认证服务 (trade-auth) - 如果有单独的认证服务 + # 如果认证逻辑在网关或者trade-security中,则不需要此路由 + # - id: trade-auth-route + # uri: lb://trade-auth + # predicates: + # - Path=/auth/** + # filters: + # - StripPrefix=1 + + # 静态资源路由 (如果前端项目部署在网关之后) + # - id: static-content-route + # uri: http://localhost:8081 # 前端静态资源服务器地址 + # predicates: + # - Path=/static/**, / + # filters: + # - RewritePath=/static/(?.*), /\${segment} + + # 日志配置 (可选, Spring Boot默认有日志输出) +logging: + level: + org.springframework.cloud.gateway: DEBUG # 网关日志级别 + reactor.netty.http.client: DEBUG # Netty客户端日志级别 + com.trade.gateway: INFO # 自定义包日志级别 + +# Actuator端点配置 (可选, 用于监控和管理) +management: + endpoints: + web: + exposure: + include: '*' # 暴露所有端点,生产环境请按需配置 + endpoint: + health: + show-details: always # 显示健康检查详情 + +# Sentinel配置 (可选, 用于流量控制和熔断降级) +# spring: +# cloud: +# sentinel: +# transport: +# dashboard: localhost:8080 # Sentinel控制台地址 +# port: 8719 # Sentinel API端口 +# datasource: +# ds1: +# nacos: +# server-addr: ${spring.cloud.nacos.discovery.server-addr} +# namespace: ${spring.cloud.nacos.discovery.namespace} +# group-id: ${spring.cloud.nacos.discovery.group} +# data-id: sentinel-rules-gateway.json +# rule-type: flow # 流控规则 + +# Seata 分布式事务配置 (如果需要) +# seata: +# tx-service-group: trade_tx_group # 事务组 +# service: +# vgroup-mapping: +# trade_tx_group: default +# registry: +# type: nacos +# nacos: +# server-addr: ${spring.cloud.nacos.discovery.server-addr} +# namespace: ${spring.cloud.nacos.discovery.namespace} +# group: SEATA_GROUP +# config: +# type: nacos +# nacos: +# server-addr: ${spring.cloud.nacos.discovery.server-addr} +# namespace: ${spring.cloud.nacos.discovery.namespace} +# group: SEATA_GROUP \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/bootstrap.yml b/samples/existing-system/trade-gateway/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..bcf166d --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/bootstrap.yml @@ -0,0 +1,45 @@ +spring: + application: + name: trade-gateway # 应用名称,必须与Nacos中配置的Data ID相关联或作为前缀 + cloud: + nacos: + config: + server-addr: 127.0.0.1:8848 # Nacos配置中心地址 + namespace: # Nacos命名空间ID,根据实际情况配置 + group: DEFAULT_GROUP # Nacos分组,根据实际情况配置 + file-extension: yml # 拉取配置文件的格式 + # 如果配置了shared-configs或者ext-config,这里可以不指定data-id,会默认加载 {spring.application.name}.{file-extension} + # data-id: trade-gateway.yml # 如果需要显式指定主配置文件Data ID + # refresh-enabled: true # 是否开启动态刷新,默认为true + discovery: + server-addr: ${spring.cloud.nacos.config.server-addr} # Nacos服务发现地址,通常与配置中心地址一致 + namespace: ${spring.cloud.nacos.config.namespace} + group: ${spring.cloud.nacos.config.group} + +# 如果使用了Spring Cloud Alibaba Sentinel,可以在这里配置 +# spring: +# cloud: +# sentinel: +# transport: +# dashboard: localhost:8080 # Sentinel控制台地址 +# port: 8719 # Sentinel客户端与控制台通信的端口,默认8719 +# # Nacos数据源配置,用于持久化规则 +# datasource: +# flow: +# nacos: +# server-addr: ${spring.cloud.nacos.config.server-addr} +# namespace: ${spring.cloud.nacos.config.namespace} +# group-id: ${spring.cloud.nacos.config.group} +# data-id: ${spring.application.name}-flow-rules.json +# rule-type: flow +# degrade: +# nacos: +# server-addr: ${spring.cloud.nacos.config.server-addr} +# namespace: ${spring.cloud.nacos.config.namespace} +# group-id: ${spring.cloud.nacos.config.group} +# data-id: ${spring.application.name}-degrade-rules.json +# rule-type: degrade + +# 日志配置,也可以放在application.yml中 +# logging: +# config: classpath:logback-spring.xml # 指定日志配置文件 \ No newline at end of file diff --git a/samples/existing-system/trade-gateway/src/main/resources/logback-spring.xml b/samples/existing-system/trade-gateway/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..976afc0 --- /dev/null +++ b/samples/existing-system/trade-gateway/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/pom.xml b/samples/existing-system/trade-indicator/pom.xml new file mode 100644 index 0000000..fd19f49 --- /dev/null +++ b/samples/existing-system/trade-indicator/pom.xml @@ -0,0 +1,106 @@ + + + + com.stock + stock-trade-system + 1.0.0 + + 4.0.0 + com.stock + trade-indicator + ${project.artifactId} + 技术指标服务,负责技术指标的定义、计算和存储 + + + + + com.stock + trade-common + ${project.version} + compile + + + com.stock + trade-market-data + ${project.version} + compile + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-processor + + + org.projectlombok + lombok + + + + + org.springdoc + springdoc-openapi-ui + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + com.google.guava + guava + + + cn.hutool + hutool-all + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/IndicatorApplication.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/IndicatorApplication.java new file mode 100644 index 0000000..db9ec41 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/IndicatorApplication.java @@ -0,0 +1,20 @@ +package com.stock.trade.indicator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 技术指标服务启动类 + * + * @author AI Assistant + */ +@SpringBootApplication(scanBasePackages = {"com.stock.trade"}) +@EnableScheduling // 启用定时任务 +public class IndicatorApplication { + + public static void main(String[] args) { + SpringApplication.run(IndicatorApplication.class, args); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorDefinitionController.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorDefinitionController.java new file mode 100644 index 0000000..7f6f4b8 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorDefinitionController.java @@ -0,0 +1,183 @@ +package com.stock.trade.indicator.controller; + +import com.stock.trade.framework.common.pojo.CommonResult; +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionCreateReqVO; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionPageReqVO; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionRespVO; +import com.stock.trade.indicator.controller.vo.IndicatorDefinitionUpdateReqVO; +import com.stock.trade.indicator.convert.IndicatorDefinitionConvert; +import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO; +import com.stock.trade.indicator.service.IndicatorDefinitionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.stock.trade.framework.common.pojo.CommonResult.success; + +/** + * 技术指标定义 Controller + * + * @author tianxin + */ +@Tag(name = "管理后台 - 技术指标定义") +@RestController +@RequestMapping("/indicator/definition") +@Validated +public class IndicatorDefinitionController { + + @Resource + private IndicatorDefinitionService indicatorDefinitionService; + + /** + * 创建技术指标定义 + * + * @param createReqVO 创建信息 + * @return 指标定义ID + */ + @PostMapping("/create") + @Operation(summary = "创建技术指标定义") + @PreAuthorize("@ss.hasPermission('indicator:definition:create')") + public CommonResult createIndicatorDefinition(@Valid @RequestBody IndicatorDefinitionCreateReqVO createReqVO) { + return success(indicatorDefinitionService.createIndicatorDefinition(createReqVO)); + } + + /** + * 更新技术指标定义 + * + * @param updateReqVO 更新信息 + * @return 是否成功 + */ + @PutMapping("/update") + @Operation(summary = "更新技术指标定义") + @PreAuthorize("@ss.hasPermission('indicator:definition:update')") + public CommonResult updateIndicatorDefinition(@Valid @RequestBody IndicatorDefinitionUpdateReqVO updateReqVO) { + indicatorDefinitionService.updateIndicatorDefinition(updateReqVO); + return success(true); + } + + /** + * 更新技术指标定义状态 + * + * @param id 指标定义ID + * @param enabled 是否启用 + * @return 是否成功 + */ + @PutMapping("/update-status") + @Operation(summary = "更新技术指标定义状态") + @PreAuthorize("@ss.hasPermission('indicator:definition:update')") + public CommonResult updateIndicatorDefinitionStatus(@RequestParam("id") Long id, + @RequestParam("enabled") Boolean enabled) { + indicatorDefinitionService.updateIndicatorDefinitionStatus(id, enabled); + return success(true); + } + + /** + * 删除技术指标定义 + * + * @param id 指标定义ID + * @return 是否成功 + */ + @DeleteMapping("/delete") + @Operation(summary = "删除技术指标定义") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('indicator:definition:delete')") + public CommonResult deleteIndicatorDefinition(@RequestParam("id") Long id) { + indicatorDefinitionService.deleteIndicatorDefinition(id); + return success(true); + } + + /** + * 获取技术指标定义 + * + * @param id 指标定义ID + * @return 指标定义信息 + */ + @GetMapping("/get") + @Operation(summary = "获得技术指标定义") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult getIndicatorDefinition(@RequestParam("id") Long id) { + IndicatorDefinitionDO definition = indicatorDefinitionService.getIndicatorDefinition(id); + return success(IndicatorDefinitionConvert.INSTANCE.convert(definition)); + } + + /** + * 获取技术指标定义列表 + * + * @param ids 指标定义ID列表 + * @return 指标定义列表 + */ + @GetMapping("/list") + @Operation(summary = "获得技术指标定义列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getIndicatorDefinitionList(@RequestParam("ids") List ids) { + List list = indicatorDefinitionService.getIndicatorDefinitionList(ids); + return success(IndicatorDefinitionConvert.INSTANCE.convertList(list)); + } + + /** + * 获取技术指标定义分页 + * + * @param pageVO 分页查询参数 + * @return 指标定义分页结果 + */ + @GetMapping("/page") + @Operation(summary = "获得技术指标定义分页") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getIndicatorDefinitionPage(@Valid IndicatorDefinitionPageReqVO pageVO) { + PageResult pageResult = indicatorDefinitionService.getIndicatorDefinitionPage(pageVO); + return success(IndicatorDefinitionConvert.INSTANCE.convertPage(pageResult)); + } + + /** + * 根据名称获取技术指标定义 + * + * @param name 指标名称 + * @return 指标定义信息 + */ + @GetMapping("/get-by-name") + @Operation(summary = "根据名称获得技术指标定义") + @Parameter(name = "name", description = "指标名称", required = true, example = "SMA") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult getIndicatorDefinitionByName(@RequestParam("name") String name) { + IndicatorDefinitionDO definition = indicatorDefinitionService.getIndicatorDefinitionByName(name); + return success(IndicatorDefinitionConvert.INSTANCE.convert(definition)); + } + + /** + * 根据类型获取技术指标定义列表 + * + * @param type 指标类型 + * @return 指标定义列表 + */ + @GetMapping("/list-by-type") + @Operation(summary = "根据类型获得技术指标定义列表") + @Parameter(name = "type", description = "指标类型", required = true, example = "TREND") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getIndicatorDefinitionListByType(@RequestParam("type") String type) { + List list = indicatorDefinitionService.getIndicatorDefinitionListByType(type); + return success(IndicatorDefinitionConvert.INSTANCE.convertList(list)); + } + + /** + * 获取所有启用的技术指标定义列表 + * + * @return 启用的指标定义列表 + */ + @GetMapping("/list-enabled") + @Operation(summary = "获得所有启用的技术指标定义列表") + @PreAuthorize("@ss.hasPermission('indicator:definition:query')") + public CommonResult> getEnabledIndicatorDefinitionList() { + List list = indicatorDefinitionService.getEnabledIndicatorDefinitionList(); + return success(IndicatorDefinitionConvert.INSTANCE.convertList(list)); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorValueController.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorValueController.java new file mode 100644 index 0000000..3ff81da --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/controller/IndicatorValueController.java @@ -0,0 +1,240 @@ +package com.stock.trade.indicator.controller; + +import com.stock.trade.framework.common.pojo.CommonResult; +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.indicator.value.*; +import com.stock.trade.indicator.convert.IndicatorValueConvert; +import com.stock.trade.indicator.dal.dataobject.IndicatorValueDO; +import com.stock.trade.indicator.service.IndicatorValueService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; + +import static com.stock.trade.framework.common.pojo.CommonResult.success; +import static com.stock.trade.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +/** + * 技术指标计算结果 Controller + * + * @author AI Assistant + */ +@Tag(name = "管理后台 - 技术指标计算结果") +@RestController +@RequestMapping("/indicator/value") +@Validated +public class IndicatorValueController { + + @Resource + private IndicatorValueService indicatorValueService; + + /** + * 创建指标计算结果 + * + * @param createReqVO 创建信息 + * @return 指标计算结果ID + */ + @PostMapping("/create") + @Operation(summary = "创建指标计算结果") + @PreAuthorize("@ss.hasPermission('indicator:value:create')") + public CommonResult createIndicatorValue(@Valid @RequestBody IndicatorValueCreateReqVO createReqVO) { + return success(indicatorValueService.createIndicatorValue(createReqVO)); + } + + /** + * 更新指标计算结果 + * + * @param updateReqVO 更新信息 + * @return 是否成功 + */ + @PutMapping("/update") + @Operation(summary = "更新指标计算结果") + @PreAuthorize("@ss.hasPermission('indicator:value:update')") + public CommonResult updateIndicatorValue(@Valid @RequestBody IndicatorValueUpdateReqVO updateReqVO) { + indicatorValueService.updateIndicatorValue(updateReqVO); + return success(true); + } + + /** + * 删除指标计算结果 + * + * @param id 指标计算结果ID + * @return 是否成功 + */ + @DeleteMapping("/delete") + @Operation(summary = "删除指标计算结果") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('indicator:value:delete')") + public CommonResult deleteIndicatorValue(@RequestParam("id") Long id) { + indicatorValueService.deleteIndicatorValue(id); + return success(true); + } + + /** + * 获取指标计算结果 + * + * @param id 指标计算结果ID + * @return 指标计算结果信息 + */ + @GetMapping("/get") + @Operation(summary = "获得指标计算结果") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult getIndicatorValue(@RequestParam("id") Long id) { + IndicatorValueDO value = indicatorValueService.getIndicatorValue(id); + return success(IndicatorValueConvert.INSTANCE.convert(value)); + } + + /** + * 获取指标计算结果列表 + * + * @param ids 指标计算结果ID列表 + * @return 指标计算结果列表 + */ + @GetMapping("/list") + @Operation(summary = "获得指标计算结果列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValueList(@RequestParam("ids") List ids) { + List list = indicatorValueService.getIndicatorValueList(ids); + return success(IndicatorValueConvert.INSTANCE.convertList(list)); + } + + /** + * 获取指标计算结果分页 + * + * @param pageVO 分页查询参数 + * @return 指标计算结果分页结果 + */ + @GetMapping("/page") + @Operation(summary = "获得指标计算结果分页") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValuePage(@Valid IndicatorValuePageReqVO pageVO) { + PageResult pageResult = indicatorValueService.getIndicatorValuePage(pageVO); + return success(IndicatorValueConvert.INSTANCE.convertPage(pageResult)); + } + + /** + * 导出指标计算结果 Excel + * + * @param response HTTP响应 + * @param exportReqVO 查询条件 + * @throws IOException IO异常 + */ + @GetMapping("/export-excel") + @Operation(summary = "导出指标计算结果 Excel") + @PreAuthorize("@ss.hasPermission('indicator:value:export')") + // @OperateLog(type = EXPORT) // 假设有操作日志注解 + public void exportIndicatorValueExcel(HttpServletResponse response, @Valid IndicatorValueExportReqVO exportReqVO) throws IOException { + List list = indicatorValueService.getIndicatorValueList(exportReqVO); + List excelList = IndicatorValueConvert.INSTANCE.convertList02(list); + // ExcelUtils.write(response, "指标计算结果.xls", "数据", IndicatorValueExcelVO.class, excelList); // 假设有Excel导出工具类 + } + + /** + * 获取最新的指标计算结果 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @param period 时间周期 + * @return 最新的指标计算结果 + */ + @GetMapping("/get-latest") + @Operation(summary = "获取最新的指标计算结果") + @Parameters({ + @Parameter(name = "definitionId", description = "指标定义ID", required = true, example = "1"), + @Parameter(name = "stockCode", description = "股票代码", required = true, example = "600000.SH"), + @Parameter(name = "period", description = "时间周期", required = true, example = "DAY") + }) + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult getLatestIndicatorValue(@RequestParam("definitionId") Long definitionId, + @RequestParam("stockCode") String stockCode, + @RequestParam("period") String period) { + IndicatorValueDO value = indicatorValueService.getLatestIndicatorValue(definitionId, stockCode, period); + return success(IndicatorValueConvert.INSTANCE.convert(value)); + } + + /** + * 获取指定时间范围内的指标计算结果列表 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @param period 时间周期 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 指标计算结果列表 + */ + @GetMapping("/list-by-time-range") + @Operation(summary = "获取指定时间范围内的指标计算结果列表") + @Parameters({ + @Parameter(name = "definitionId", description = "指标定义ID", required = true, example = "1"), + @Parameter(name = "stockCode", description = "股票代码", required = true, example = "600000.SH"), + @Parameter(name = "period", description = "时间周期", required = true, example = "DAY"), + @Parameter(name = "startTime", description = "开始时间", required = true, example = "2023-01-01T00:00:00"), + @Parameter(name = "endTime", description = "结束时间", required = true, example = "2023-12-31T23:59:59") + }) + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValueListByTimeRange( + @RequestParam("definitionId") Long definitionId, + @RequestParam("stockCode") String stockCode, + @RequestParam("period") String period, + @RequestParam("startTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime, + @RequestParam("endTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) { + List list = indicatorValueService.getIndicatorValueListByTimeRange(definitionId, stockCode, period, startTime, endTime); + return success(IndicatorValueConvert.INSTANCE.convertList(list)); + } + + /** + * 获取指定状态的指标计算结果列表 + * + * @param status 计算状态 + * @return 指标计算结果列表 + */ + @GetMapping("/list-by-status") + @Operation(summary = "获取指定状态的指标计算结果列表") + @Parameter(name = "status", description = "计算状态", required = true, example = "SUCCESS") + @PreAuthorize("@ss.hasPermission('indicator:value:query')") + public CommonResult> getIndicatorValueListByStatus(@RequestParam("status") String status) { + List list = indicatorValueService.getIndicatorValueListByStatus(status); + return success(IndicatorValueConvert.INSTANCE.convertList(list)); + } + + /** + * 批量更新指标计算结果状态 + * + * @param updateStatusReqVO 更新状态请求 + * @return 是否成功 + */ + @PutMapping("/update-status-batch") + @Operation(summary = "批量更新指标计算结果状态") + @PreAuthorize("@ss.hasPermission('indicator:value:update')") + public CommonResult updateIndicatorValueStatusBatch(@Valid @RequestBody IndicatorValueUpdateStatusBatchReqVO updateStatusReqVO) { + indicatorValueService.updateIndicatorValueStatusBatch(updateStatusReqVO.getIds(), updateStatusReqVO.getStatus(), updateStatusReqVO.getErrorMessage()); + return success(true); + } + + /** + * 触发指标计算 + * + * @param calculateReqVO 计算请求信息 + * @return 指标计算结果ID列表 + */ + @PostMapping("/calculate") + @Operation(summary = "触发指标计算") + @PreAuthorize("@ss.hasPermission('indicator:value:calculate')") + public CommonResult> calculateIndicatorValue(@Valid @RequestBody IndicatorValueCalculateReqVO calculateReqVO) { + List resultIds = indicatorValueService.calculateIndicatorValue(calculateReqVO); + return success(resultIds); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorDefinitionConvert.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorDefinitionConvert.java new file mode 100644 index 0000000..d54cd2a --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorDefinitionConvert.java @@ -0,0 +1,35 @@ +package com.stock.trade.indicator.convert; + +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.indicator.definition.*; +import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 指标定义 Convert + * + * @author tianxin + */ +@Mapper +public interface IndicatorDefinitionConvert { + + IndicatorDefinitionConvert INSTANCE = Mappers.getMapper(IndicatorDefinitionConvert.class); + + IndicatorDefinitionDO convert(IndicatorDefinitionCreateReqVO bean); + + IndicatorDefinitionDO convert(IndicatorDefinitionUpdateReqVO bean); + + IndicatorDefinitionRespVO convert(IndicatorDefinitionDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + List convertList03(List list); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorValueConvert.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorValueConvert.java new file mode 100644 index 0000000..f2599f7 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/convert/IndicatorValueConvert.java @@ -0,0 +1,33 @@ +package com.stock.trade.indicator.convert; + +import com.stock.trade.framework.common.pojo.PageResult; +import com.stock.trade.indicator.controller.vo.indicator.value.*; +import com.stock.trade.indicator.dal.dataobject.IndicatorValueDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 指标计算结果 Convert + * + * @author tianxin + */ +@Mapper +public interface IndicatorValueConvert { + + IndicatorValueConvert INSTANCE = Mappers.getMapper(IndicatorValueConvert.class); + + IndicatorValueDO convert(IndicatorValueCreateReqVO bean); + + IndicatorValueDO convert(IndicatorValueUpdateReqVO bean); + + IndicatorValueRespVO convert(IndicatorValueDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorDefinitionDO.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorDefinitionDO.java new file mode 100644 index 0000000..c501e8a --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorDefinitionDO.java @@ -0,0 +1,68 @@ +package com.stock.trade.indicator.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.stock.trade.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 技术指标定义 DO + * + * @author tianxin + */ +@TableName("indicator_definition") +@Data +@EqualsAndHashCode(callSuper = true) +public class IndicatorDefinitionDO extends BaseDO { + + /** + * 指标ID,主键自增 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 指标名称,例如:Moving Average, MACD, RSI + */ + private String name; + + /** + * 指标简称,例如:MA, MACD, RSI + */ + private String shortName; + + /** + * 指标描述 + */ + private String description; + + /** + * 指标类型,例如:趋势型、震荡型、成交量型 + * 可以使用枚举定义 + */ + private String type; + + /** + * 计算公式描述或类引用 + * 例如:"CLOSE / MA(CLOSE, N)" 或 "com.stock.trade.indicator.calculator.MACDCalculator" + */ + private String formula; + + /** + * 默认参数,JSON格式,例如:{"period": 20} for MA, {"shortPeriod":12, "longPeriod":26, "signalPeriod":9} for MACD + */ + private String defaultParams; + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 备注 + */ + private String remark; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorValueDO.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorValueDO.java new file mode 100644 index 0000000..135b995 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/dataobject/IndicatorValueDO.java @@ -0,0 +1,77 @@ +package com.stock.trade.indicator.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.stock.trade.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 技术指标计算结果 DO + * + * @author tianxin + */ +@TableName("indicator_value") +@Data +@EqualsAndHashCode(callSuper = true) +public class IndicatorValueDO extends BaseDO { + + /** + * 指标值ID,主键自增 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 关联的指标定义ID + * {@link IndicatorDefinitionDO#getId()} + */ + private Long definitionId; + + /** + * 股票代码 + */ + private String stockCode; + + /** + * 时间周期,例如:1min, 5min, 1day, 1week + */ + private String timePeriod; + + /** + * 计算时间点 + */ + private LocalDateTime calculationTime; + + /** + * 指标计算参数,JSON格式,覆盖或补充指标定义的默认参数 + * 例如:{"period": 30} + */ + private String params; + + /** + * 指标计算结果,JSON格式 + * 例如:{"ma": 10.5} 或 {"macd": 0.5, "diff": 0.2, "dea": 0.3} + */ + private String results; + + /** + * 计算状态,例如:PENDING, CALCULATING, SUCCESS, FAILED + * 可以使用枚举定义 + */ + private String status; + + /** + * 错误信息,当计算失败时记录 + */ + private String errorMessage; + + /** + * 备注 + */ + private String remark; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorDefinitionMapper.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorDefinitionMapper.java new file mode 100644 index 0000000..921b4f7 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorDefinitionMapper.java @@ -0,0 +1,50 @@ +package com.stock.trade.indicator.dal.mysql; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.stock.trade.framework.mybatis.core.mapper.BaseMapperX; +import com.stock.trade.indicator.dal.dataobject.IndicatorDefinitionDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 技术指标定义 Mapper + * + * @author tianxin + */ +@Mapper +public interface IndicatorDefinitionMapper extends BaseMapperX { + + /** + * 根据指标名称查询指标定义 + * + * @param name 指标名称 + * @return 指标定义 + */ + default IndicatorDefinitionDO selectByName(String name) { + return selectOne(new LambdaQueryWrapper() + .eq(IndicatorDefinitionDO::getName, name)); + } + + /** + * 根据指标类型查询指标定义列表 + * + * @param type 指标类型 + * @return 指标定义列表 + */ + default List selectListByType(String type) { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorDefinitionDO::getType, type)); + } + + /** + * 查询所有启用的指标定义 + * + * @return 启用的指标定义列表 + */ + default List selectListByEnabled() { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorDefinitionDO::getEnabled, true)); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorValueMapper.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorValueMapper.java new file mode 100644 index 0000000..00a345a --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/dal/mysql/IndicatorValueMapper.java @@ -0,0 +1,77 @@ +package com.stock.trade.indicator.dal.mysql; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.stock.trade.framework.mybatis.core.mapper.BaseMapperX; +import com.stock.trade.indicator.dal.dataobject.IndicatorValueDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 技术指标计算结果 Mapper + * + * @author tianxin + */ +@Mapper +public interface IndicatorValueMapper extends BaseMapperX { + + /** + * 根据指标定义ID和股票代码查询最新的计算结果 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @return 最新的计算结果 + */ + default IndicatorValueDO selectLatestByDefinitionIdAndStockCode(Long definitionId, String stockCode) { + return selectOne(new LambdaQueryWrapper() + .eq(IndicatorValueDO::getDefinitionId, definitionId) + .eq(IndicatorValueDO::getStockCode, stockCode) + .orderByDesc(IndicatorValueDO::getCalculationTime) + .last("LIMIT 1")); + } + + /** + * 查询指定时间范围内的计算结果 + * + * @param definitionId 指标定义ID + * @param stockCode 股票代码 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 计算结果列表 + */ + default List selectListByTimeRange(Long definitionId, String stockCode, + LocalDateTime startTime, LocalDateTime endTime) { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorValueDO::getDefinitionId, definitionId) + .eq(IndicatorValueDO::getStockCode, stockCode) + .between(IndicatorValueDO::getCalculationTime, startTime, endTime) + .orderByAsc(IndicatorValueDO::getCalculationTime)); + } + + /** + * 查询指定状态的计算结果 + * + * @param status 计算状态 + * @return 计算结果列表 + */ + default List selectListByStatus(String status) { + return selectList(new LambdaQueryWrapper() + .eq(IndicatorValueDO::getStatus, status)); + } + + /** + * 批量更新指标计算结果的状态 + * + * @param ids 指标值ID列表 + * @param status 新状态 + * @return 更新的记录数 + */ + default int updateStatusBatch(List ids, String status) { + return update(null, new LambdaUpdateWrapper() + .in(IndicatorValueDO::getId, ids) + .set(IndicatorValueDO::getStatus, status)); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/enums/ErrorCodeConstants.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..5879a17 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/enums/ErrorCodeConstants.java @@ -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, "指标计算结果不存在"); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/job/IndicatorJob.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/job/IndicatorJob.java new file mode 100644 index 0000000..f82d7eb --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/job/IndicatorJob.java @@ -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); + } + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/IndicatorDefinitionService.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/IndicatorDefinitionService.java new file mode 100644 index 0000000..0cbb236 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/IndicatorDefinitionService.java @@ -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 getIndicatorDefinitionList(List ids); + + /** + * 获取技术指标定义分页 + * + * @param pageReqVO 分页查询 + * @return 指标定义分页 + */ + PageResult getIndicatorDefinitionPage(IndicatorDefinitionPageReqVO pageReqVO); + + /** + * 根据指标名称获取技术指标定义 + * + * @param name 指标名称 + * @return 指标定义 + */ + IndicatorDefinitionDO getIndicatorDefinitionByName(String name); + + /** + * 根据指标类型获取技术指标定义列表 + * + * @param type 指标类型 + * @return 指标定义列表 + */ + List getIndicatorDefinitionListByType(String type); + + /** + * 获取所有启用的技术指标定义 + * + * @return 启用的指标定义列表 + */ + List getEnabledIndicatorDefinitionList(); + + /** + * 更新技术指标定义状态 + * + * @param id 指标定义ID + * @param enabled 是否启用 + */ + void updateIndicatorDefinitionStatus(Long id, Boolean enabled); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/IndicatorValueService.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/IndicatorValueService.java new file mode 100644 index 0000000..d3ec096 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/IndicatorValueService.java @@ -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 getIndicatorValueList(List ids); + + /** + * 获取指标计算结果分页 + * + * @param pageReqVO 分页查询 + * @return 指标计算结果分页 + */ + PageResult getIndicatorValuePage(IndicatorValuePageReqVO pageReqVO); + + /** + * 获取指标计算结果列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 指标计算结果列表 + */ + List 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 getIndicatorValueListByTimeRange(Long indicatorDefinitionId, String stockCode, + String period, LocalDateTime startTime, LocalDateTime endTime); + + /** + * 获取指定状态的指标计算结果列表 + * + * @param status 计算状态 + * @return 指标计算结果列表 + */ + List getIndicatorValueListByStatus(String status); + + /** + * 批量更新指标计算结果状态 + * + * @param ids 指标计算结果ID列表 + * @param status 新状态 + * @param errorMessage 错误信息(可选) + */ + void updateIndicatorValueStatusBatch(List ids, String status, String errorMessage); + + /** + * 触发指标计算 + * + * @param calculateReqVO 计算请求信息 + * @return 指标计算结果ID列表 + */ + List calculateIndicatorValue(@Valid IndicatorValueCalculateReqVO calculateReqVO); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/impl/IndicatorDefinitionServiceImpl.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/impl/IndicatorDefinitionServiceImpl.java new file mode 100644 index 0000000..ca50d80 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/impl/IndicatorDefinitionServiceImpl.java @@ -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 getIndicatorDefinitionList(List ids) { + return indicatorDefinitionMapper.selectBatchIds(ids); + } + + /** + * 获取技术指标定义分页 + * + * @param pageReqVO 分页查询 + * @return 指标定义分页 + */ + @Override + public PageResult 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 getIndicatorDefinitionListByType(String type) { + return indicatorDefinitionMapper.selectListByType(type); + } + + /** + * 获取所有启用的技术指标定义 + * + * @return 启用的指标定义列表 + */ + @Override + public List 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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/impl/IndicatorValueServiceImpl.java b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/impl/IndicatorValueServiceImpl.java new file mode 100644 index 0000000..8adcda8 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/stock/trade/indicator/service/impl/IndicatorValueServiceImpl.java @@ -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 getIndicatorValueList(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + return indicatorValueMapper.selectBatchIds(ids); + } + + /** + * 获取指标计算结果分页 + * + * @param pageReqVO 分页查询 + * @return 指标计算结果分页 + */ + @Override + public PageResult getIndicatorValuePage(IndicatorValuePageReqVO pageReqVO) { + return indicatorValueMapper.selectPage(pageReqVO, IndicatorValueConvert.INSTANCE.convert(pageReqVO)); + } + + /** + * 获取指标计算结果列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 指标计算结果列表 + */ + @Override + public List 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 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 getIndicatorValueListByStatus(String status) { + return indicatorValueMapper.selectListByStatus(status); + } + + /** + * 批量更新指标计算结果状态 + * + * @param ids 指标计算结果ID列表 + * @param status 新状态 + * @param errorMessage 错误信息(可选) + */ + @Override + @Transactional + public void updateIndicatorValueStatusBatch(List ids, String status, String errorMessage) { + if (ids == null || ids.isEmpty()) { + return; + } + indicatorValueMapper.updateStatusBatch(ids, status, errorMessage); + } + + /** + * 触发指标计算 + * + * @param calculateReqVO 计算请求信息 + * @return 指标计算结果ID列表 (这里仅为示例,实际计算逻辑可能更复杂,并可能异步执行) + */ + @Override + public List 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); + } + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/java/com/trade/indicator/TradeIndicatorApplication.java b/samples/existing-system/trade-indicator/src/main/java/com/trade/indicator/TradeIndicatorApplication.java new file mode 100644 index 0000000..eb3e2e9 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/java/com/trade/indicator/TradeIndicatorApplication.java @@ -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); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/resources/application-dev.yml b/samples/existing-system/trade-indicator/src/main/resources/application-dev.yml new file mode 100644 index 0000000..2ed5cbc --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/resources/application-dev.yml @@ -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" # 日志文件输出格式 \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/resources/application-prod.yml b/samples/existing-system/trade-indicator/src/main/resources/application-prod.yml new file mode 100644 index 0000000..554843d --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/resources/application-test.yml b/samples/existing-system/trade-indicator/src/main/resources/application-test.yml new file mode 100644 index 0000000..3a68521 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/resources/application.yml b/samples/existing-system/trade-indicator/src/main/resources/application.yml new file mode 100644 index 0000000..82b154d --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/resources/bootstrap.yml b/samples/existing-system/trade-indicator/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..fce55ae --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/resources/bootstrap.yml @@ -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 分组 \ No newline at end of file diff --git a/samples/existing-system/trade-indicator/src/main/resources/logback-spring.xml b/samples/existing-system/trade-indicator/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..4e0c990 --- /dev/null +++ b/samples/existing-system/trade-indicator/src/main/resources/logback-spring.xml @@ -0,0 +1,80 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + + ${LOG_HOME}/trade-indicator.log + + + ${LOG_HOME}/trade-indicator.%d{yyyy-MM-dd}.log + + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + 10MB + + + + + + ${LOG_HOME}/trade-indicator-error.log + + ERROR + + + ${LOG_HOME}/trade-indicator-error.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + 10MB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/pom.xml b/samples/existing-system/trade-market-data/pom.xml new file mode 100644 index 0000000..b3e52e2 --- /dev/null +++ b/samples/existing-system/trade-market-data/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + + com.stock + trade-market-data + jar + + trade-market-data + Market data service module for the trading system + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.stock + trade-common + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/TradeMarketDataApplication.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/TradeMarketDataApplication.java new file mode 100644 index 0000000..319af7c --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/TradeMarketDataApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/TushareConfig.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/TushareConfig.java new file mode 100644 index 0000000..0e12b79 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/TushareConfig.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/WebClientConfig.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/WebClientConfig.java new file mode 100644 index 0000000..e768e1c --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/WebClientConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/package-info.java new file mode 100644 index 0000000..e4f8aee --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/config/package-info.java @@ -0,0 +1,4 @@ +/** + * 配置类 + */ +package com.trade.marketdata.config; \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/DailyMarketDataController.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/DailyMarketDataController.java new file mode 100644 index 0000000..6a842a5 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/DailyMarketDataController.java @@ -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 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 getDailyMarketDataByTradeDate(@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate tradeDate) { + return dailyMarketDataService.getDailyMarketDataByTradeDate(tradeDate); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/RealtimeMarketDataController.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/RealtimeMarketDataController.java new file mode 100644 index 0000000..2653a6f --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/RealtimeMarketDataController.java @@ -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 getRealtimeMarketData(@RequestParam String tsCodes) { + List 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 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; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/StockBasicController.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/StockBasicController.java new file mode 100644 index 0000000..f4acb8c --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/StockBasicController.java @@ -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 syncStockBasic() { + int count = stockBasicService.syncStockBasicFromTushare(); + return CommonResult.success(count, "成功同步 " + count + " 条股票基本信息"); + } + + /** + * 获取所有股票基本信息 + * @return 股票基本信息列表 + */ + @Operation(summary = "获取所有股票基本信息") + @GetMapping("/listAll") + public CommonResult> listAllStockBasic() { + List stockBasics = stockBasicService.listAllStockBasic(); + return CommonResult.success(stockBasics); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/controller/package-info.java new file mode 100644 index 0000000..e69de29 diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/DailyMarketData.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/DailyMarketData.java new file mode 100644 index 0000000..ecae703 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/DailyMarketData.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/RealtimeMarketData.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/RealtimeMarketData.java new file mode 100644 index 0000000..369021a --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/RealtimeMarketData.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/StockBasic.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/StockBasic.java new file mode 100644 index 0000000..ee9fc12 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/StockBasic.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/TushareRequest.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/TushareRequest.java new file mode 100644 index 0000000..509ac9c --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/TushareRequest.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/TushareResponse.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/TushareResponse.java new file mode 100644 index 0000000..cec141a --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/TushareResponse.java @@ -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 fields; + /** + * 数据列表 + */ + private List> items; + /** + * 总行数 + */ + private Integer has_more; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/package-info.java new file mode 100644 index 0000000..d91ae3b --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/entity/package-info.java @@ -0,0 +1,4 @@ +/** + * 实体类层 + */ +package com.trade.marketdata.entity; \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/DailyMarketDataMapper.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/DailyMarketDataMapper.java new file mode 100644 index 0000000..35a9365 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/DailyMarketDataMapper.java @@ -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 { +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/RealtimeMarketDataMapper.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/RealtimeMarketDataMapper.java new file mode 100644 index 0000000..e351ef8 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/RealtimeMarketDataMapper.java @@ -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 { +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/StockBasicMapper.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/StockBasicMapper.java new file mode 100644 index 0000000..e17465b --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/StockBasicMapper.java @@ -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 { +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/package-info.java new file mode 100644 index 0000000..696a48b --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * 数据访问层 (MyBatis Mapper) + */ +package com.trade.marketdata.mapper; \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/package-info.java new file mode 100644 index 0000000..3154c1a --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/package-info.java @@ -0,0 +1,4 @@ +/** + * 行情服务模块主包。 + */ +package com.trade.marketdata; \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/DailyMarketDataService.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/DailyMarketDataService.java new file mode 100644 index 0000000..303b7c4 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/DailyMarketDataService.java @@ -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 { + + /** + * 从 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 listDailyMarketData(String tsCode, LocalDate startDate, LocalDate endDate); +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/RealtimeMarketDataService.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/RealtimeMarketDataService.java new file mode 100644 index 0000000..3d4d661 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/RealtimeMarketDataService.java @@ -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 getRealtimeMarketData(String tsCodes); + + /** + * 保存实时行情数据列表 + * + * @param realtimeMarketDataList 实时行情数据列表 + */ + void saveRealtimeMarketData(List realtimeMarketDataList); +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/StockBasicService.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/StockBasicService.java new file mode 100644 index 0000000..49fd375 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/StockBasicService.java @@ -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 { + + /** + * 从 Tushare 同步股票基本信息 + * @return 同步的股票数量 + */ + int syncStockBasicFromTushare(); + + /** + * 查询所有股票基本信息 + * @return 股票基本信息列表 + */ + List listAllStockBasic(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/TushareApi.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/TushareApi.java new file mode 100644 index 0000000..109a9cd --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/TushareApi.java @@ -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 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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/DailyMarketDataServiceImpl.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/DailyMarketDataServiceImpl.java new file mode 100644 index 0000000..4ecb67f --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/DailyMarketDataServiceImpl.java @@ -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 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 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> items = response.getData().getItems(); + List fieldsList = response.getData().getFields(); + + if (items == null || items.isEmpty()) { + log.warn("从 Tushare 获取到股票 {} 的空日线行情数据列表。", tsCode); + return 0; + } + + List newDailyDataList = new ArrayList<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + + for (List 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 existingData = baseMapper.selectList(new QueryWrapper() + .eq("ts_code", tsCode) + .between("trade_date", LocalDate.parse(startDate, formatter), LocalDate.parse(endDate, formatter))); + + Map existingDataMap = existingData.stream() + .collect(Collectors.toMap(data -> data.getTsCode() + "_" + data.getTradeDate().format(formatter), data -> data)); + + List toInsert = new ArrayList<>(); + List 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 listDailyMarketData(String tsCode, LocalDate startDate, LocalDate endDate) { + QueryWrapper 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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/RealtimeMarketDataServiceImpl.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/RealtimeMarketDataServiceImpl.java new file mode 100644 index 0000000..841207e --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/RealtimeMarketDataServiceImpl.java @@ -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 getRealtimeMarketData(String tsCodes) { + TushareRequest> request = new TushareRequest<>(); + request.setApiName("realtime_quotes"); // Tushare 实时行情接口名称,请根据实际情况调整 + Map params = new HashMap<>(); + params.put("ts_code", tsCodes); + request.setParams(params); + + TushareResponse>> response = tushareApi.call(request, List.class, List.class, Object.class); + + List resultList = new ArrayList<>(); + if (response != null && response.getData() != null && response.getData().getItems() != null) { + List fields = response.getData().getFields(); + List> items = response.getData().getItems(); + + for (List 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 realtimeMarketDataList) { + if (realtimeMarketDataList == null || realtimeMarketDataList.isEmpty()) { + return; + } + // 实际应用中,可能需要根据 ts_code 和 timestamp 判断数据是否已存在,进行更新或插入操作 + // 这里简化为直接批量插入 + realtimeMarketDataList.forEach(realtimeMarketDataMapper::insert); + log.info("Successfully saved {} realtime market data records.", realtimeMarketDataList.size()); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/StockBasicServiceImpl.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/StockBasicServiceImpl.java new file mode 100644 index 0000000..e0322de --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/StockBasicServiceImpl.java @@ -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 implements StockBasicService { + + private final TushareApi tushareApi; + + public StockBasicServiceImpl(TushareApi tushareApi) { + this.tushareApi = tushareApi; + } + + /** + * 从 Tushare 同步股票基本信息 + * @return 同步的股票数量 + */ + @Override + @Transactional + public int syncStockBasicFromTushare() { + log.info("开始从 Tushare 同步股票基本信息..."); + Map 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> items = response.getData().getItems(); + List fieldsList = response.getData().getFields(); + + if (items == null || items.isEmpty()) { + log.warn("从 Tushare 获取到空股票基本信息列表。"); + return 0; + } + + List newStockBasics = new ArrayList<>(); + for (List 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 existingTsCodes = baseMapper.selectList(null).stream() + .map(StockBasic::getTsCode) + .collect(Collectors.toList()); + + List toInsert = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + + for (StockBasic stock : newStockBasics) { + if (existingTsCodes.contains(stock.getTsCode())) { + // 查找现有记录的ID,用于更新 + StockBasic existingStock = baseMapper.selectOne(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.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 listAllStockBasic() { + return list(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/package-info.java new file mode 100644 index 0000000..964cf07 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/impl/package-info.java @@ -0,0 +1,4 @@ +/** + * 服务实现层 + */ +package com.trade.marketdata.service.impl; \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/package-info.java b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/package-info.java new file mode 100644 index 0000000..1fb2f8a --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com.stock/trade/marketdata/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 服务接口层 + */ +package com.trade.marketdata.service; \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/controller/MarketDataController.java b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/controller/MarketDataController.java new file mode 100644 index 0000000..4ebd8d5 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/controller/MarketDataController.java @@ -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 createMarketData(@RequestBody MarketDataDO marketData) { + return CommonResult.success(marketDataService.createMarketData(marketData)); + } + + @PutMapping + @Operation(summary = "更新行情数据") + public CommonResult 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 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 getMarketData(@PathVariable("id") Long id) { + return CommonResult.success(marketDataService.getMarketData(id)); + } + + @GetMapping("/list") + @Operation(summary = "获取行情数据列表") + @Parameter(name = "stockCode", description = "股票代码", example = "000001") + public CommonResult> getMarketDataList(@RequestParam(required = false) String stockCode) { + return CommonResult.success(marketDataService.getMarketDataList(stockCode)); + } + + @GetMapping("/page") + @Operation(summary = "分页查询行情数据") + public CommonResult> 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 page = new Page<>(pageNum, pageSize); + return CommonResult.success(marketDataService.pageMarketData(page, stockCode)); + } + + // TODO: 添加其他接口 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/dal/dataobject/MarketDataDO.java b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/dal/dataobject/MarketDataDO.java new file mode 100644 index 0000000..a28f49e --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/dal/dataobject/MarketDataDO.java @@ -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: 根据实际需求添加更多字段 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/dal/mapper/MarketDataMapper.java b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/dal/mapper/MarketDataMapper.java new file mode 100644 index 0000000..2734336 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/dal/mapper/MarketDataMapper.java @@ -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 { + + // TODO: 定义自定义的 SQL 查询方法 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/mapper/MarketDataMapper.java b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/mapper/MarketDataMapper.java new file mode 100644 index 0000000..0514648 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/mapper/MarketDataMapper.java @@ -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 { + // TODO: 定义行情数据相关的数据库操作方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/service/MarketDataService.java b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/service/MarketDataService.java new file mode 100644 index 0000000..96afbee --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/service/MarketDataService.java @@ -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 getMarketDataList(String stockCode); + + /** + * 分页查询行情数据 + * + * @param page 分页参数 + * @param stockCode 股票代码 + * @return 分页结果 + */ + Page pageMarketData(Page page, String stockCode); + + // TODO: 添加其他业务方法 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/service/impl/MarketDataServiceImpl.java b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/service/impl/MarketDataServiceImpl.java new file mode 100644 index 0000000..cd25acf --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/java/com/stock/marketdata/service/impl/MarketDataServiceImpl.java @@ -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 getMarketDataList(String stockCode) { + LambdaQueryWrapper 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 pageMarketData(Page page, String stockCode) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (StringUtils.hasText(stockCode)) { + queryWrapper.eq(MarketDataDO::getStockCode, stockCode); + } + // TODO: 根据业务需求添加其他查询条件,例如时间范围等 + queryWrapper.orderByDesc(MarketDataDO::getTradingDay); // 默认按交易日降序 + return marketDataMapper.selectPage(page, queryWrapper); + } + + // TODO: 实现其他业务方法 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/resources/application-dev.yml b/samples/existing-system/trade-market-data/src/main/resources/application-dev.yml new file mode 100644 index 0000000..fc18b69 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/resources/application-prod.yml b/samples/existing-system/trade-market-data/src/main/resources/application-prod.yml new file mode 100644 index 0000000..9275c63 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/resources/application-test.yml b/samples/existing-system/trade-market-data/src/main/resources/application-test.yml new file mode 100644 index 0000000..c8adf7e --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/resources/application.yml b/samples/existing-system/trade-market-data/src/main/resources/application.yml new file mode 100644 index 0000000..17487fa --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/resources/application.yml @@ -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 # 日志文件路径 \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/main/resources/logback-spring.xml b/samples/existing-system/trade-market-data/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..ef2d867 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-market-data/src/test/java/com/stock/marketdata/controller/MarketDataControllerTest.java b/samples/existing-system/trade-market-data/src/test/java/com/stock/marketdata/controller/MarketDataControllerTest.java new file mode 100644 index 0000000..de9faf1 --- /dev/null +++ b/samples/existing-system/trade-market-data/src/test/java/com/stock/marketdata/controller/MarketDataControllerTest.java @@ -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 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 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)); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-mbg/pom.xml b/samples/existing-system/trade-mbg/pom.xml new file mode 100644 index 0000000..de071a8 --- /dev/null +++ b/samples/existing-system/trade-mbg/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + + com.stock + trade-mbg + jar + + + 8.0.33 + + + trade-mbg + MyBatis Generator module for Trade Platform + + + + + org.mybatis.generator + mybatis-generator-core + 1.4.2 + + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + io.swagger.core.v3 + swagger-annotations + 2.2.20 + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + + org.mybatis.generator + mybatis-generator-maven-plugin + 1.4.2 + + + ${project.basedir}/src/main/resources/generatorConfig.xml + + true + + true + + true + + + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + com.stock + trade-mbg + ${project.version} + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-mbg/src/main/java/com/trade/mbg/util/CommentGenerator.java b/samples/existing-system/trade-mbg/src/main/java/com/trade/mbg/util/CommentGenerator.java new file mode 100644 index 0000000..bc27048 --- /dev/null +++ b/samples/existing-system/trade-mbg/src/main/java/com/trade/mbg/util/CommentGenerator.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-mbg/src/main/resources/application-dev.properties b/samples/existing-system/trade-mbg/src/main/resources/application-dev.properties new file mode 100644 index 0000000..fa6b55c --- /dev/null +++ b/samples/existing-system/trade-mbg/src/main/resources/application-dev.properties @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-mbg/src/main/resources/generatorConfig.xml b/samples/existing-system/trade-mbg/src/main/resources/generatorConfig.xml new file mode 100644 index 0000000..bf3dd9d --- /dev/null +++ b/samples/existing-system/trade-mbg/src/main/resources/generatorConfig.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + +
+ + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + + + + +
+ + + + + + +
+ + +
+ + + + + + +
+ + + +
+ + + + + + +
+ + +
+ + + + + + + +
+ + + +
+ + + + + +
+ +
+
\ No newline at end of file diff --git a/samples/existing-system/trade-notification/pom.xml b/samples/existing-system/trade-notification/pom.xml new file mode 100644 index 0000000..9b29275 --- /dev/null +++ b/samples/existing-system/trade-notification/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + + com.stock + trade-notification + jar + + trade-notification + Notification service module for the trading system + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.stock + trade-common + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-mail + true + + + + + org.springframework.boot + spring-boot-starter-amqp + true + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + + org.springframework.boot + spring-boot-starter-websocket + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-notification/src/main/java/com/trade/notification/TradeNotificationApplication.java b/samples/existing-system/trade-notification/src/main/java/com/trade/notification/TradeNotificationApplication.java new file mode 100644 index 0000000..9603705 --- /dev/null +++ b/samples/existing-system/trade-notification/src/main/java/com/trade/notification/TradeNotificationApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-notification/src/main/resources/application-dev.yml b/samples/existing-system/trade-notification/src/main/resources/application-dev.yml new file mode 100644 index 0000000..5e54374 --- /dev/null +++ b/samples/existing-system/trade-notification/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-notification/src/main/resources/application-prod.yml b/samples/existing-system/trade-notification/src/main/resources/application-prod.yml new file mode 100644 index 0000000..91fe68d --- /dev/null +++ b/samples/existing-system/trade-notification/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-notification/src/main/resources/application-test.yml b/samples/existing-system/trade-notification/src/main/resources/application-test.yml new file mode 100644 index 0000000..2d9c816 --- /dev/null +++ b/samples/existing-system/trade-notification/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-notification/src/main/resources/logback-spring.xml b/samples/existing-system/trade-notification/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..133d7ea --- /dev/null +++ b/samples/existing-system/trade-notification/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-order/pom.xml b/samples/existing-system/trade-order/pom.xml new file mode 100644 index 0000000..34cc392 --- /dev/null +++ b/samples/existing-system/trade-order/pom.xml @@ -0,0 +1,100 @@ + + + + com.stock + stock-trade-system + 1.0.0 + + 4.0.0 + + com.stock + trade-order + ${project.artifactId} + 订单服务,负责订单的创建、撮合、状态管理等 + + + + + com.stock + trade-common + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + + + mysql + mysql-connector-java + + + com.baomidou + mybatis-plus-boot-starter + + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-processor + + + org.projectlombok + lombok + + + + + org.springdoc + springdoc-openapi-ui + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + com.google.guava + guava + + + cn.hutool + hutool-all + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/OrderApplication.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/OrderApplication.java new file mode 100644 index 0000000..df52706 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/OrderApplication.java @@ -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); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/OrderController.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/OrderController.java new file mode 100644 index 0000000..0a9845e --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/OrderController.java @@ -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 createOrder(@Valid @RequestBody OrderCreateReqVO createReqVO) { + return success(orderService.createOrder(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新订单") + @PreAuthorize("@ss.hasPermission('trade:order:update')") + public CommonResult 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 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 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> getOrderPage(@Valid OrderPageReqVO pageVO) { + PageResult 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 cancelOrder(@RequestParam("id") Long id, + @RequestParam("userId") Long userId) { + orderService.cancelOrder(id, userId); + return success(true); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderCreateReqVO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderCreateReqVO.java new file mode 100644 index 0000000..0cfb7d8 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderCreateReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderPageReqVO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderPageReqVO.java new file mode 100644 index 0000000..e38e5c1 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderPageReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderUpdateReqVO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderUpdateReqVO.java new file mode 100644 index 0000000..1499411 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/request/OrderUpdateReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderItemRespVO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderItemRespVO.java new file mode 100644 index 0000000..9e5f76c --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderItemRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderLogRespVO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderLogRespVO.java new file mode 100644 index 0000000..e4f0694 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderLogRespVO.java @@ -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; + +} diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderRespVO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderRespVO.java new file mode 100644 index 0000000..20215d5 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/controller/response/OrderRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderConvert.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderConvert.java new file mode 100644 index 0000000..ff2c24b --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderConvert.java @@ -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 convertPage(PageResult page); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderItemConvert.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderItemConvert.java new file mode 100644 index 0000000..061ef28 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderItemConvert.java @@ -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 convertList(List list); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderLogConvert.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderLogConvert.java new file mode 100644 index 0000000..48e01fc --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/convert/OrderLogConvert.java @@ -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 convertList(List list); + +} diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderDO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderDO.java new file mode 100644 index 0000000..160fd3d --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderDO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderItemDO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderItemDO.java new file mode 100644 index 0000000..b5d4980 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderItemDO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderLogDO.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderLogDO.java new file mode 100644 index 0000000..9a6896a --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/dataobject/OrderLogDO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderItemMapper.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderItemMapper.java new file mode 100644 index 0000000..5d28985 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderItemMapper.java @@ -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 { + + default List selectListByOrderId(Long orderId) { + return selectList(OrderItemDO::getOrderId, orderId); + } + + default List selectListByOrderIds(List orderIds) { + return selectList(new LambdaQueryWrapperX() + .in(OrderItemDO::getOrderId, orderIds)); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderLogMapper.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderLogMapper.java new file mode 100644 index 0000000..ac2cf74 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderLogMapper.java @@ -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 { + + default List selectListByOrderId(Long orderId) { + return selectList(new LambdaQueryWrapperX() + .eq(OrderLogDO::getOrderId, orderId) + .orderByDesc(OrderLogDO::getId)); // 按时间倒序,最新的日志在前面 + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderMapper.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderMapper.java new file mode 100644 index 0000000..a04b354 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/dal/mysql/OrderMapper.java @@ -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 { + + default List selectPage(PageQuery pageQuery, Long userId, String stockCode, Integer type, Integer direction, Integer status) { + return selectList(new LambdaQueryWrapperX() + .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() + .eqIfPresent(OrderDO::getUserId, userId) + .likeIfPresent(OrderDO::getStockCode, stockCode) + .eqIfPresent(OrderDO::getType, type) + .eqIfPresent(OrderDO::getDirection, direction) + .eqIfPresent(OrderDO::getStatus, status)); + } + + default List selectListByUserId(Long userId) { + return selectList(OrderDO::getUserId, userId); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/ErrorCodeConstants.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..ad3b485 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/ErrorCodeConstants.java @@ -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, "订单日志不存在"); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderDirectionEnum.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderDirectionEnum.java new file mode 100644 index 0000000..d038fe0 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderDirectionEnum.java @@ -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; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderStatusEnum.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderStatusEnum.java new file mode 100644 index 0000000..71ab13b --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderStatusEnum.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderTypeEnum.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderTypeEnum.java new file mode 100644 index 0000000..7832c42 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/enums/OrderTypeEnum.java @@ -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; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/consumer/TradeOrderReturnConsumer.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/consumer/TradeOrderReturnConsumer.java new file mode 100644 index 0000000..60c835a --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/consumer/TradeOrderReturnConsumer.java @@ -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: 考虑增加重试机制或死信队列 + } + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/message/OrderMessage.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/message/OrderMessage.java new file mode 100644 index 0000000..aa865cf --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/message/OrderMessage.java @@ -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; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/producer/OrderProducer.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/producer/OrderProducer.java new file mode 100644 index 0000000..5b4915e --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/mq/producer/OrderProducer.java @@ -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 { +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderItemService.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderItemService.java new file mode 100644 index 0000000..75392b7 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderItemService.java @@ -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 getOrderItemListByOrderId(Long orderId); + + /** + * 根据订单编号列表获取订单项列表 + * + * @param orderIds 订单编号列表 + * @return 订单项列表 + */ + List getOrderItemListByOrderIds(List orderIds); + + /** + * 创建订单项 + * + * @param orderItem 订单项信息 + * @return 订单项编号 + */ + Long createOrderItem(OrderItemDO orderItem); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderLogService.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderLogService.java new file mode 100644 index 0000000..774e121 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderLogService.java @@ -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 getOrderLogListByOrderId(Long orderId); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderService.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderService.java new file mode 100644 index 0000000..e8ee65e --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/OrderService.java @@ -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 getOrderList(Collection ids); + + /** + * 获取订单分页 + * + * @param pageReqVO 分页查询参数 + * @return 订单分页结果 + */ + PageResult getOrderPage(OrderPageReqVO pageReqVO); + + /** + * 根据用户编号获取其所有未完成订单列表 + * + * @param userId 用户编号 + * @return 未完成订单列表 + */ + List 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); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderItemServiceImpl.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderItemServiceImpl.java new file mode 100644 index 0000000..24f3261 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderItemServiceImpl.java @@ -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 getOrderItemListByOrderId(Long orderId) { + return orderItemMapper.selectListByOrderId(orderId); + } + + @Override + public List getOrderItemListByOrderIds(List orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return Collections.emptyList(); + } + return orderItemMapper.selectListByOrderIds(orderIds); + } + + @Override + public Long createOrderItem(OrderItemDO orderItem) { + orderItemMapper.insert(orderItem); + return orderItem.getId(); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderLogServiceImpl.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderLogServiceImpl.java new file mode 100644 index 0000000..3d9b88b --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderLogServiceImpl.java @@ -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 getOrderLogListByOrderId(Long orderId) { + return orderLogMapper.selectListByOrderId(orderId); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderServiceImpl.java b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderServiceImpl.java new file mode 100644 index 0000000..2144344 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/stock/trade/order/service/impl/OrderServiceImpl.java @@ -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 getOrderList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return CollUtil.newArrayList(); + } + return orderMapper.selectBatchIds(ids); + } + + @Override + public PageResult getOrderPage(OrderPageReqVO pageReqVO) { + // 查询订单分页 + List 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 getUnfinishedOrdersByUserId(Long userId) { + return orderMapper.selectList(new LambdaQueryWrapperX() + .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); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/OrderApplication.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/OrderApplication.java new file mode 100644 index 0000000..3b556af --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/OrderApplication.java @@ -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" + + " ''-' `'-' `-..-' "); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/TradeOrderApplication.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/TradeOrderApplication.java new file mode 100644 index 0000000..c66d11c --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/TradeOrderApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/config/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/config/package-info.java new file mode 100644 index 0000000..c13ba64 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/config/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的配置类。 + */ +package com.trade.order.config; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/constant/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/constant/package-info.java new file mode 100644 index 0000000..da86067 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/constant/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块相关的常量定义。 + */ +package com.trade.order.constant; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/controller/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/controller/package-info.java new file mode 100644 index 0000000..f964952 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/controller/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的Controller层,负责处理HTTP请求和响应。 + */ +package com.trade.order.controller; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/dto/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/dto/package-info.java new file mode 100644 index 0000000..832c25c --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的数据传输对象 (DTO)。 + */ +package com.trade.order.dto; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/entity/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/entity/package-info.java new file mode 100644 index 0000000..86de092 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/entity/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的数据库实体类。 + */ +package com.trade.order.entity; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/enums/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/enums/package-info.java new file mode 100644 index 0000000..fd2b296 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/enums/package-info.java @@ -0,0 +1,5 @@ +/** + * 此包存放订单模块相关的枚举类。 + * 例如:订单状态枚举、支付方式枚举等。 + */ +package com.trade.order.enums; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/listener/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/listener/package-info.java new file mode 100644 index 0000000..5ccb400 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/listener/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单服务模块的事件监听器,例如监听支付成功事件、库存扣减事件等。 + */ +package com.trade.order.listener; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/mapper/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/mapper/package-info.java new file mode 100644 index 0000000..9323123 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的Mapper接口,用于数据库操作。 + */ +package com.trade.order.mapper; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/package-info.java new file mode 100644 index 0000000..b7b9eda --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/package-info.java @@ -0,0 +1,4 @@ +/** + * 订单服务模块主包。 + */ +package com.trade.order; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/service/impl/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/service/impl/package-info.java new file mode 100644 index 0000000..17109ba --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/service/impl/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块Service接口的实现类。 + */ +package com.trade.order.service.impl; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/service/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/service/package-info.java new file mode 100644 index 0000000..6007cf0 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的Service层,负责业务逻辑处理。 + */ +package com.trade.order.service; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/util/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/util/package-info.java new file mode 100644 index 0000000..223161a --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/util/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块相关的工具类。 + */ +package com.trade.order.util; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/utils/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/utils/package-info.java new file mode 100644 index 0000000..0abf46d --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单服务模块的工具类。 + */ +package com.trade.order.utils; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/java/com/trade/order/vo/package-info.java b/samples/existing-system/trade-order/src/main/java/com/trade/order/vo/package-info.java new file mode 100644 index 0000000..d0153d0 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/java/com/trade/order/vo/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放订单模块的视图对象 (VO)。 + */ +package com.trade.order.vo; \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/resources/application-dev.yml b/samples/existing-system/trade-order/src/main/resources/application-dev.yml new file mode 100644 index 0000000..98438c7 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/resources/application-prod.yml b/samples/existing-system/trade-order/src/main/resources/application-prod.yml new file mode 100644 index 0000000..290d7f9 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/resources/application-test.yml b/samples/existing-system/trade-order/src/main/resources/application-test.yml new file mode 100644 index 0000000..add027c --- /dev/null +++ b/samples/existing-system/trade-order/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/resources/application.yml b/samples/existing-system/trade-order/src/main/resources/application.yml new file mode 100644 index 0000000..6f2b588 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/resources/bootstrap.yml b/samples/existing-system/trade-order/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..134f296 --- /dev/null +++ b/samples/existing-system/trade-order/src/main/resources/bootstrap.yml @@ -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: '*' \ No newline at end of file diff --git a/samples/existing-system/trade-order/src/main/resources/logback-spring.xml b/samples/existing-system/trade-order/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..1a39d3f --- /dev/null +++ b/samples/existing-system/trade-order/src/main/resources/logback-spring.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + ${LOG_PATTERN} + UTF-8 + + + + + + ${LOG_PATH}/info.log + + ${LOG_PATH}/info.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 10GB + + + ${LOG_PATTERN} + UTF-8 + + + INFO + ACCEPT + DENY + + + + + ${LOG_PATH}/error.log + + ${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 10GB + + + ${LOG_PATTERN} + UTF-8 + + + ERROR + ACCEPT + DENY + + + + + + 0 + 256 + + + + + 0 + 256 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-risk/pom.xml b/samples/existing-system/trade-risk/pom.xml new file mode 100644 index 0000000..8588f4d --- /dev/null +++ b/samples/existing-system/trade-risk/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + + trade-risk + jar + + trade-risk + Risk management module for the trading system + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.trade + trade-common + ${project.version} + + + + + com.trade + trade-order + ${project.version} + + + + + com.trade + trade-market-data + ${project.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-risk/src/main/java/com/trade/risk/TradeRiskApplication.java b/samples/existing-system/trade-risk/src/main/java/com/trade/risk/TradeRiskApplication.java new file mode 100644 index 0000000..2b87850 --- /dev/null +++ b/samples/existing-system/trade-risk/src/main/java/com/trade/risk/TradeRiskApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-risk/src/main/resources/application-dev.yml b/samples/existing-system/trade-risk/src/main/resources/application-dev.yml new file mode 100644 index 0000000..66a5c01 --- /dev/null +++ b/samples/existing-system/trade-risk/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-risk/src/main/resources/application-prod.yml b/samples/existing-system/trade-risk/src/main/resources/application-prod.yml new file mode 100644 index 0000000..c670c0b --- /dev/null +++ b/samples/existing-system/trade-risk/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-risk/src/main/resources/application-test.yml b/samples/existing-system/trade-risk/src/main/resources/application-test.yml new file mode 100644 index 0000000..ec57902 --- /dev/null +++ b/samples/existing-system/trade-risk/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-risk/src/main/resources/logback-spring.xml b/samples/existing-system/trade-risk/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..57a84da --- /dev/null +++ b/samples/existing-system/trade-risk/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-security/pom.xml b/samples/existing-system/trade-security/pom.xml new file mode 100644 index 0000000..cdf447e --- /dev/null +++ b/samples/existing-system/trade-security/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + + trade-security + jar + + trade-security + Security module for the trading system (Spring Security, JWT) + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-web + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.alibaba + fastjson + 1.2.83 + + + + + com.trade + trade-common + ${project.parent.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/TradeSecurityApplication.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/TradeSecurityApplication.java new file mode 100644 index 0000000..da20dad --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/TradeSecurityApplication.java @@ -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); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/config/SecurityConfig.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/config/SecurityConfig.java new file mode 100644 index 0000000..9d423ca --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/config/SecurityConfig.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/filter/JwtAuthenticationTokenFilter.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..75fd409 --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/filter/JwtAuthenticationTokenFilter.java @@ -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认证令牌过滤器 + *

+ * 该过滤器在每个请求中检查JWT令牌的有效性,如果令牌有效,则将认证信息设置到Spring Security上下文中。 + *

+ * + * @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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAccessDeniedHandler.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..5484f9d --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAccessDeniedHandler.java @@ -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; + +/** + * 自定义访问拒绝处理器 + *

+ * 当已认证的用户尝试访问其没有权限的资源时,此处理器被调用。 + * 它负责返回一个表示访问被拒绝的响应。 + *

+ * + * @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 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(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationEntryPoint.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..35d20fd --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationEntryPoint.java @@ -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; + +/** + * 自定义认证入口点 + *

+ * 当匿名用户尝试访问受保护的资源而未提供有效的认证凭证时,此处理器被调用。 + * 它负责返回一个表示需要认证的响应。 + *

+ * + * @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 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(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationFailureHandler.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationFailureHandler.java new file mode 100644 index 0000000..d25dd01 --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationFailureHandler.java @@ -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; + +/** + * 自定义认证失败处理器 + *

+ * 当用户登录失败时,此处理器负责返回统一格式的错误响应。 + *

+ * + * @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 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(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationSuccessHandler.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationSuccessHandler.java new file mode 100644 index 0000000..b820140 --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomAuthenticationSuccessHandler.java @@ -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; + +/** + * 自定义认证成功处理器 + *

+ * 当用户成功登录后,此处理器负责生成JWT并将其返回给客户端。 + *

+ * + * @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 tokenMap = new HashMap<>(); + tokenMap.put(CommonConstant.TOKEN_HEADER_PREFIX_WITH_SPACE.trim(), token); + + Result> result = Result.success(ResultCodeEnum.LOGIN_SUCCESS, tokenMap); + + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(JSON.toJSONString(result)); + response.getWriter().flush(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomLogoutSuccessHandler.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomLogoutSuccessHandler.java new file mode 100644 index 0000000..9b48a3f --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/handler/CustomLogoutSuccessHandler.java @@ -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; + +/** + * 自定义登出成功处理器 + *

+ * 当用户成功登出后,此处理器负责返回统一格式的成功响应。 + *

+ * + * @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 result = Result.success(ResultCodeEnum.LOGOUT_SUCCESS); + + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(JSON.toJSONString(result)); + response.getWriter().flush(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/java/com/trade/security/service/UserDetailsServiceImpl.java b/samples/existing-system/trade-security/src/main/java/com/trade/security/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..b8ce0ae --- /dev/null +++ b/samples/existing-system/trade-security/src/main/java/com/trade/security/service/UserDetailsServiceImpl.java @@ -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; + +/** + * 用户详细信息服务实现类 + *

+ * 该类负责从用户服务加载用户详细信息,用于Spring Security的认证过程。 + *

+ * + * @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 + + // ---- 以下为模拟数据,实际项目中应替换为真实的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 permissions = userFeignClient.getUserPermissions(userDTO.getId()).getData(); // 假设Feign接口返回Result> + // Set authorities = permissions.stream() + // .map(SimpleGrantedAuthority::new) + // .collect(Collectors.toSet()); + + // ---- 以下为模拟权限数据 ---- + Set 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().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; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/resources/application-dev.yml b/samples/existing-system/trade-security/src/main/resources/application-dev.yml new file mode 100644 index 0000000..db516a3 --- /dev/null +++ b/samples/existing-system/trade-security/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/resources/application-prod.yml b/samples/existing-system/trade-security/src/main/resources/application-prod.yml new file mode 100644 index 0000000..80eaaba --- /dev/null +++ b/samples/existing-system/trade-security/src/main/resources/application-prod.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 \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/resources/application-test.yml b/samples/existing-system/trade-security/src/main/resources/application-test.yml new file mode 100644 index 0000000..434d569 --- /dev/null +++ b/samples/existing-system/trade-security/src/main/resources/application-test.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 \ No newline at end of file diff --git a/samples/existing-system/trade-security/src/main/resources/logback-spring.xml b/samples/existing-system/trade-security/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..ddc86df --- /dev/null +++ b/samples/existing-system/trade-security/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/pom.xml b/samples/existing-system/trade-strategy/pom.xml new file mode 100644 index 0000000..2aabfd4 --- /dev/null +++ b/samples/existing-system/trade-strategy/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + + + com.stock + trade-strategy + jar + + trade-strategy + Trading strategy definition and execution module + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.mysql + mysql-connector-j + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.hutool + hutool-all + + + + + com.stock + trade-common + ${project.parent.version} + + + + + com.stock + trade-indicator + ${project.parent.version} + + + + + com.stock + trade-market-data + ${project.parent.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + com.alibaba + fastjson + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/controller/StrategyController.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/controller/StrategyController.java new file mode 100644 index 0000000..df2340c --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/controller/StrategyController.java @@ -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: 实现策略相关接口 +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/dal/dataobject/StrategyDO.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/dal/dataobject/StrategyDO.java new file mode 100644 index 0000000..1cdb414 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/dal/dataobject/StrategyDO.java @@ -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: 根据实际需求添加更多字段 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/mapper/StrategyMapper.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/mapper/StrategyMapper.java new file mode 100644 index 0000000..e64d99e --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/mapper/StrategyMapper.java @@ -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 { + // TODO: 定义策略相关的数据库操作方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/service/StrategyService.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/service/StrategyService.java new file mode 100644 index 0000000..390af0c --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/service/StrategyService.java @@ -0,0 +1,10 @@ +package com.stock.strategy.service; + +/** + * 策略服务接口 + * + * @author TraeAI + */ +public interface StrategyService { + // TODO: 定义策略服务相关方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/service/impl/StrategyServiceImpl.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/service/impl/StrategyServiceImpl.java new file mode 100644 index 0000000..f6203a0 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/strategy/service/impl/StrategyServiceImpl.java @@ -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: 实现策略服务相关方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/controller/StrategyExecutionController.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/controller/StrategyExecutionController.java new file mode 100644 index 0000000..696d60a --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/controller/StrategyExecutionController.java @@ -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 createStrategyExecution(@Valid @RequestBody StrategyExecutionCreateReqVO createReqVO) { + return success(strategyExecutionService.createStrategyExecution(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新策略执行") + @PreAuthorize("@ss.hasPermission('strategy:execution:update')") + public CommonResult 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 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 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 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 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 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> getStrategyExecutionPage(@Valid StrategyExecutionPageReqVO pageVO) { + PageResult 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> getStrategyExecutionListByDefinitionId(@RequestParam("strategyDefinitionId") Long strategyDefinitionId) { + List list = strategyExecutionService.getStrategyExecutionListByDefinitionId(strategyDefinitionId); + return success(StrategyExecutionConvert.INSTANCE.convertList(list)); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/enums/ErrorCodeConstants.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..ea51ad5 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/enums/ErrorCodeConstants.java @@ -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, "运行中的策略执行无法删除"); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/service/StrategyExecutionServiceImpl.java b/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/service/StrategyExecutionServiceImpl.java new file mode 100644 index 0000000..8c08596 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/stock/trade/strategy/service/StrategyExecutionServiceImpl.java @@ -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 getStrategyExecutionList(List ids) { + return strategyExecutionMapper.selectBatchIds(ids); + } + + /** + * 获取策略执行记录分页 + * + * @param pageReqVO 分页查询 + * @return 策略执行记录分页 + */ + @Override + public PageResult getStrategyExecutionPage(StrategyExecutionPageReqVO pageReqVO) { + return strategyExecutionMapper.selectPage(pageReqVO); + } + + /** + * 根据策略定义ID获取策略执行记录列表 + * + * @param strategyDefinitionId 策略定义ID + * @return 策略执行记录列表 + */ + @Override + public List getStrategyExecutionListByDefinitionId(Long strategyDefinitionId) { + return strategyExecutionMapper.selectListByStrategyDefinitionId(strategyDefinitionId); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/StrategyApplication.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/StrategyApplication.java new file mode 100644 index 0000000..aea1b4a --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/StrategyApplication.java @@ -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" + + " ''-' `'-' `-..-' "); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/TradeStrategyApplication.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/TradeStrategyApplication.java new file mode 100644 index 0000000..40ebe59 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/TradeStrategyApplication.java @@ -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); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/aspect/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/aspect/package-info.java new file mode 100644 index 0000000..fa12146 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/aspect/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的切面类 (Aspect Class)。 + *

+ * 切面类用于实现横切关注点,例如日志记录、事务管理、权限校验、性能监控等。 + * 通常使用 Spring AOP 或 AspectJ 来定义和织入切面。 + *

+ */ +package com.trade.strategy.aspect; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/config/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/config/package-info.java new file mode 100644 index 0000000..4e7c2e1 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/config/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的配置类。 + */ +package com.trade.strategy.config; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/constant/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/constant/package-info.java new file mode 100644 index 0000000..f0e9d89 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/constant/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的常量类 (Constant Class)。 + *

+ * 常量类用于定义在应用程序中广泛使用的静态常量值,例如配置键名、默认参数、固定字符串等。 + * 将常量集中管理有助于提高代码的可维护性和一致性。 + *

+ */ +package com.trade.strategy.constant; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/StrategyBacktestController.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/StrategyBacktestController.java new file mode 100644 index 0000000..911bd1b --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/StrategyBacktestController.java @@ -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 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 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 updateBacktestResult( + @PathVariable Long backtestId, + @RequestBody Map 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 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> getBacktestsByStrategyDefinitionId(@PathVariable Long strategyDefinitionId) { + List backtests = strategyBacktestService.getBacktestsByStrategyDefinitionId(strategyDefinitionId); + return ResponseEntity.ok(backtests); + } + + /** + * 查询所有策略回测记录 + * + * @return 所有回测记录列表 + */ + @GetMapping + @Operation(summary = "查询所有策略回测记录") + public ResponseEntity> getAllBacktests() { + List backtests = strategyBacktestService.getAllBacktests(); + return ResponseEntity.ok(backtests); + } + + /** + * 根据ID删除策略回测记录 + * + * @param backtestId 回测任务ID + * @return 操作结果 + */ + @DeleteMapping("/{backtestId}") + @Operation(summary = "根据ID删除策略回测记录") + public ResponseEntity 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 + } + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/StrategyDefinitionController.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/StrategyDefinitionController.java new file mode 100644 index 0000000..cd01870 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/StrategyDefinitionController.java @@ -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 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 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 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 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> getAllStrategies() { + List strategies = strategyDefinitionService.getAllStrategies(); + return ResponseEntity.ok(strategies); + } + + /** + * 根据策略名称查询交易策略定义 + * + * @param name 策略名称 + * @return 策略定义 + */ + @GetMapping("/by-name") + @Operation(summary = "根据策略名称查询交易策略定义") + public ResponseEntity 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> getStrategiesByType(@RequestParam String type) { + List strategies = strategyDefinitionService.getStrategiesByType(type); + return ResponseEntity.ok(strategies); + } + + /** + * 根据策略状态查询交易策略定义 + * + * @param status 策略状态 + * @return 策略定义列表 + */ + @GetMapping("/by-status") + @Operation(summary = "根据策略状态查询交易策略定义") + public ResponseEntity> getStrategiesByStatus(@RequestParam String status) { + List strategies = strategyDefinitionService.getStrategiesByStatus(status); + return ResponseEntity.ok(strategies); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/package-info.java new file mode 100644 index 0000000..5af4b49 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/controller/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的Controller层,负责处理HTTP请求和响应。 + */ +package com.trade.strategy.controller; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/dto/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/dto/package-info.java new file mode 100644 index 0000000..69fc98f --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的数据传输对象 (DTO)。 + */ +package com.trade.strategy.dto; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/engine/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/engine/package-info.java new file mode 100644 index 0000000..22ef8ff --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/engine/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略执行引擎相关的类,包括策略的加载、执行、事件处理等。 + */ +package com.trade.strategy.engine; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/StrategyBacktest.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/StrategyBacktest.java new file mode 100644 index 0000000..d9e333e --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/StrategyBacktest.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/StrategyDefinition.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/StrategyDefinition.java new file mode 100644 index 0000000..e963a43 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/StrategyDefinition.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/package-info.java new file mode 100644 index 0000000..a586a93 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/entity/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的数据库实体类。 + */ +package com.trade.strategy.entity; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/enums/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/enums/package-info.java new file mode 100644 index 0000000..165a45d --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/enums/package-info.java @@ -0,0 +1,5 @@ +/** + * 此包存放策略服务模块相关的枚举类。 + * 例如:策略状态枚举、信号类型枚举等。 + */ +package com.trade.strategy.enums; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/filter/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/filter/package-info.java new file mode 100644 index 0000000..fa2cc4b --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/filter/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的过滤器类 (Filter Class)。 + *

+ * 过滤器类用于在 Servlet 容器处理请求之前或之后执行某些操作,例如请求参数校验、字符编码转换、安全过滤等。 + * 它们是 Servlet规范的一部分。 + *

+ */ +package com.trade.strategy.filter; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/handler/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/handler/package-info.java new file mode 100644 index 0000000..527695d --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/handler/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的处理器类 (Handler Class)。 + *

+ * 处理器类通常用于处理特定类型的事件、消息或请求,例如消息队列的消费者、WebSocket 处理器、定时任务处理器等。 + * 它们封装了对特定输入的响应逻辑。 + *

+ */ +package com.trade.strategy.handler; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/indicator/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/indicator/package-info.java new file mode 100644 index 0000000..f1ddfed --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/indicator/package-info.java @@ -0,0 +1,5 @@ +/** + * 此包存放策略中可能用到的技术指标计算相关的类。 + * 考虑是否将通用指标计算放到独立的 trade-indicator 模块。 + */ +package com.trade.strategy.indicator; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/interceptor/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/interceptor/package-info.java new file mode 100644 index 0000000..7fc2a6a --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/interceptor/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的拦截器类 (Interceptor Class)。 + *

+ * 拦截器类用于在 Spring MVC 处理请求的过程中拦截和处理请求,例如权限验证、日志记录、性能监控等。 + * 它们提供了比过滤器更细粒度的控制,并且可以访问 Spring MVC 的上下文信息。 + *

+ */ +package com.trade.strategy.interceptor; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/job/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/job/package-info.java new file mode 100644 index 0000000..2635617 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/job/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的定时任务类 (Job Class)。 + *

+ * 定时任务类用于执行周期性或预定时间的后台任务,例如数据同步、报表生成、缓存刷新等。 + * 通常使用 Spring Task 或 Quartz 等框架来实现。 + *

+ */ +package com.trade.strategy.job; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/listener/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/listener/package-info.java new file mode 100644 index 0000000..637b6ab --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/listener/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放事件监听器,例如监听行情事件、订单回报事件等,并根据事件驱动策略执行。 + */ +package com.trade.strategy.listener; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/StrategyBacktestMapper.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/StrategyBacktestMapper.java new file mode 100644 index 0000000..a50a9bd --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/StrategyBacktestMapper.java @@ -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 { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/StrategyDefinitionMapper.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/StrategyDefinitionMapper.java new file mode 100644 index 0000000..5ea2b61 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/StrategyDefinitionMapper.java @@ -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 { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/package-info.java new file mode 100644 index 0000000..5afb554 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的Mapper接口,用于数据库操作(例如存储策略配置、回测结果等)。 + */ +package com.trade.strategy.mapper; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/model/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/model/package-info.java new file mode 100644 index 0000000..a6bef3a --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/model/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的领域模型 (Domain Model) 或实体类 (Entity)。 + *

+ * 这些类通常映射到数据库表结构,代表了系统中的核心业务对象。 + * 它们包含了对象的属性以及可能的操作方法。 + *

+ */ +package com.trade.strategy.model; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/package-info.java new file mode 100644 index 0000000..4db9640 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/package-info.java @@ -0,0 +1,4 @@ +/** + * 策略服务模块主包。 + */ +package com.trade.strategy; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/risk/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/risk/package-info.java new file mode 100644 index 0000000..d6a3bde --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/risk/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放风险管理相关的类,例如仓位管理、止盈止损逻辑等。 + */ +package com.trade.strategy.risk; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/scheduler/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/scheduler/package-info.java new file mode 100644 index 0000000..e472324 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/scheduler/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放定时任务相关的类,例如定时执行策略、数据同步等。 + */ +package com.trade.strategy.scheduler; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/StrategyBacktestService.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/StrategyBacktestService.java new file mode 100644 index 0000000..da5647a --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/StrategyBacktestService.java @@ -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 { + + /** + * 创建并初始化一个新的策略回测任务 + * + * @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 getBacktestsByStrategyDefinitionId(Long strategyDefinitionId); + + /** + * 查询所有策略回测记录 + * + * @return 所有回测记录列表 + */ + List getAllBacktests(); + + /** + * 根据ID删除策略回测记录 + * (通常只允许删除未开始或已失败的回测) + * + * @param backtestId 回测任务ID + */ + void deleteBacktest(Long backtestId); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/StrategyDefinitionService.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/StrategyDefinitionService.java new file mode 100644 index 0000000..6b75e33 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/StrategyDefinitionService.java @@ -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 { + + /** + * 创建新的交易策略定义 + * + * @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 getAllStrategies(); + + /** + * 根据策略名称查询交易策略定义 + * + * @param name 策略名称 + * @return 策略定义对象,如果不存在则返回null + */ + StrategyDefinition getStrategyByName(String name); + + /** + * 根据策略类型查询交易策略定义 + * + * @param type 策略类型 + * @return 符合类型的策略定义列表 + */ + List getStrategiesByType(String type); + + /** + * 根据策略状态查询交易策略定义 + * + * @param status 策略状态 + * @return 符合状态的策略定义列表 + */ + List getStrategiesByStatus(String status); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/StrategyBacktestServiceImpl.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/StrategyBacktestServiceImpl.java new file mode 100644 index 0000000..3cde5be --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/StrategyBacktestServiceImpl.java @@ -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 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 getBacktestsByStrategyDefinitionId(Long strategyDefinitionId) { + if (strategyDefinitionId == null) { + return List.of(); + } + return this.list(new QueryWrapper().eq("strategy_definition_id", strategyDefinitionId) + .orderByDesc("create_time")); // 按创建时间降序 + } + + /** + * 查询所有策略回测记录 + * + * @return 所有回测记录列表 + */ + @Override + public List getAllBacktests() { + return this.list(new QueryWrapper().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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/StrategyDefinitionServiceImpl.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/StrategyDefinitionServiceImpl.java new file mode 100644 index 0000000..82432bb --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/StrategyDefinitionServiceImpl.java @@ -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 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 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().eq("name", name)); + } + + /** + * 根据策略类型查询交易策略定义 + * + * @param type 策略类型 + * @return 符合类型的策略定义列表 + */ + @Override + public List getStrategiesByType(String type) { + if (!StringUtils.hasText(type)) { + return this.list(); // 如果类型为空,返回所有 + } + return this.list(new QueryWrapper().eq("type", type)); + } + + /** + * 根据策略状态查询交易策略定义 + * + * @param status 策略状态 + * @return 符合状态的策略定义列表 + */ + @Override + public List getStrategiesByStatus(String status) { + if (!StringUtils.hasText(status)) { + return this.list(); // 如果状态为空,返回所有 + } + return this.list(new QueryWrapper().eq("status", status)); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/package-info.java new file mode 100644 index 0000000..8761d0e --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/impl/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块Service接口的实现类。 + */ +package com.trade.strategy.service.impl; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/package-info.java new file mode 100644 index 0000000..868ccaf --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的Service层,负责业务逻辑处理,例如策略的创建、执行、回测等。 + */ +package com.trade.strategy.service; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/util/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/util/package-info.java new file mode 100644 index 0000000..0ab406c --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/util/package-info.java @@ -0,0 +1,8 @@ +/** + * 此包包含策略服务模块的工具类 (Utility Class)。 + *

+ * 工具类提供各种通用的辅助方法,例如日期时间处理、字符串操作、加解密、文件操作等。 + * 它们通常包含静态方法,方便在应用程序的各个部分调用。 + *

+ */ +package com.trade.strategy.util; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/utils/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/utils/package-info.java new file mode 100644 index 0000000..6ee1550 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的工具类。 + */ +package com.trade.strategy.utils; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/vo/package-info.java b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/vo/package-info.java new file mode 100644 index 0000000..4570d46 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/trade/strategy/vo/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放策略服务模块的视图对象 (VO)。 + */ +package com.trade.strategy.vo; \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/StrategyExecutionController.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/StrategyExecutionController.java new file mode 100644 index 0000000..8010afd --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/StrategyExecutionController.java @@ -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 createStrategyExecution(@Valid @RequestBody StrategyExecutionCreateReqVO createReqVO) { + return success(strategyExecutionService.createStrategyExecution(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新策略执行") + @PreAuthorize("@ss.hasPermission('strategy:execution:update')") + public CommonResult 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 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 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 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 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 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> getStrategyExecutionList(@RequestParam("ids") Collection ids) { + List list = strategyExecutionService.getStrategyExecutionList(ids); + return success(StrategyExecutionConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得策略执行分页") + @PreAuthorize("@ss.hasPermission('strategy:execution:query')") + public CommonResult> getStrategyExecutionPage(@Valid StrategyExecutionPageReqVO pageVO) { + PageResult 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> getStrategyExecutionsByDefinitionId(@RequestParam("strategyDefinitionId") Long strategyDefinitionId) { + List list = strategyExecutionService.getStrategyExecutionsByDefinitionId(strategyDefinitionId); + return success(StrategyExecutionConvert.INSTANCE.convertList(list)); + } + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionCreateReqVO.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionCreateReqVO.java new file mode 100644 index 0000000..77e8563 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionCreateReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionPageReqVO.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionPageReqVO.java new file mode 100644 index 0000000..3b125dc --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionPageReqVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionRespVO.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionRespVO.java new file mode 100644 index 0000000..4a1805f --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionRespVO.java @@ -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; + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionUpdateReqVO.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionUpdateReqVO.java new file mode 100644 index 0000000..0c5241d --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/controller/admin/execution/vo/StrategyExecutionUpdateReqVO.java @@ -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中添加并谨慎处理 +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/convert/execution/StrategyExecutionConvert.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/convert/execution/StrategyExecutionConvert.java new file mode 100644 index 0000000..4306f38 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/convert/execution/StrategyExecutionConvert.java @@ -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 convertList(List list); + + /** + * 分页结果转换 + */ + PageResult convertPage(PageResult page); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/dal/dataobject/execution/StrategyExecution.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/dal/dataobject/execution/StrategyExecution.java new file mode 100644 index 0000000..28f7310 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/dal/dataobject/execution/StrategyExecution.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/dal/mysql/execution/StrategyExecutionMapper.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/dal/mysql/execution/StrategyExecutionMapper.java new file mode 100644 index 0000000..3db43ef --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/dal/mysql/execution/StrategyExecutionMapper.java @@ -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 { + + // 可以在此定义特定的查询方法,例如根据策略定义ID查询执行记录等 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/service/execution/StrategyExecutionService.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/service/execution/StrategyExecutionService.java new file mode 100644 index 0000000..a98d8e1 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/service/execution/StrategyExecutionService.java @@ -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 getStrategyExecutionList(Collection ids); + + /** + * 获得策略执行记录分页 + * + * @param pageReqVO 分页查询 + * @return 策略执行记录分页 + */ + PageResult getStrategyExecutionPage(StrategyExecutionPageReqVO pageReqVO); + + /** + * 根据策略定义ID查询执行记录列表 + * + * @param strategyDefinitionId 策略定义ID + * @return 策略执行记录列表 + */ + List getStrategyExecutionsByDefinitionId(Long strategyDefinitionId); + +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/service/execution/StrategyExecutionServiceImpl.java b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/service/execution/StrategyExecutionServiceImpl.java new file mode 100644 index 0000000..2ad6d41 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/java/com/yudao/module/strategy/service/execution/StrategyExecutionServiceImpl.java @@ -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 getStrategyExecutionList(Collection ids) { + return strategyExecutionMapper.selectBatchIds(ids); + } + + /** + * 获得策略执行记录分页 + * + * @param pageReqVO 分页查询 + * @return 策略执行记录分页 + */ + @Override + public PageResult getStrategyExecutionPage(StrategyExecutionPageReqVO pageReqVO) { + return strategyExecutionMapper.selectPage(pageReqVO, StrategyExecutionConvert.INSTANCE.convert(pageReqVO)); + } + + /** + * 根据策略定义ID查询执行记录列表 + * + * @param strategyDefinitionId 策略定义ID + * @return 策略执行记录列表 + */ + @Override + public List getStrategyExecutionsByDefinitionId(Long strategyDefinitionId) { + return strategyExecutionMapper.selectList(StrategyExecution::getStrategyDefinitionId, strategyDefinitionId); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/resources/application-dev.yml b/samples/existing-system/trade-strategy/src/main/resources/application-dev.yml new file mode 100644 index 0000000..92dabb9 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/resources/application-prod.yml b/samples/existing-system/trade-strategy/src/main/resources/application-prod.yml new file mode 100644 index 0000000..6a200fd --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/resources/application-test.yml b/samples/existing-system/trade-strategy/src/main/resources/application-test.yml new file mode 100644 index 0000000..b66dabd --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/resources/application.yml b/samples/existing-system/trade-strategy/src/main/resources/application.yml new file mode 100644 index 0000000..f5fb0b4 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/resources/bootstrap.yml b/samples/existing-system/trade-strategy/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..d0f801d --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/resources/bootstrap.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-strategy/src/main/resources/logback-spring.xml b/samples/existing-system/trade-strategy/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..ee6e519 --- /dev/null +++ b/samples/existing-system/trade-strategy/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/pom.xml b/samples/existing-system/trade-user/pom.xml new file mode 100644 index 0000000..c416c0a --- /dev/null +++ b/samples/existing-system/trade-user/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + com.stock + stock-trade-system + 1.0.0 + ../pom.xml + + com.stock + trade-user + 1.0.0 + trade-user + trade-user + + + + + org.springframework.cloud + spring-cloud-dependencies + 2021.0.8 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + com.baomidou + mybatis-plus-boot-starter + 3.5.1 + + + com.stock + trade-common + 1.0.0 + + + org.projectlombok + lombok + true + + + io.swagger + swagger-annotations + 1.6.0 + + + javax.validation + validation-api + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springdoc + springdoc-openapi-ui + 1.6.14 + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/controller/UmsAdminController.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/controller/UmsAdminController.java new file mode 100644 index 0000000..a814d61 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/controller/UmsAdminController.java @@ -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 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> login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam) { + String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword()); + if (token == null) { + return CommonResult.validateFailed("用户名或密码错误"); + } + Map tokenMap = new HashMap<>(); + tokenMap.put("token", token); + tokenMap.put("tokenHead", tokenHead); + return CommonResult.success(tokenMap); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/dto/UmsAdminLoginParam.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/dto/UmsAdminLoginParam.java new file mode 100644 index 0000000..922c42c --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/dto/UmsAdminLoginParam.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/dto/UmsAdminParam.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/dto/UmsAdminParam.java new file mode 100644 index 0000000..5c90ccb --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/dto/UmsAdminParam.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/entity/UmsAdmin.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/entity/UmsAdmin.java new file mode 100644 index 0000000..4cf2514 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/entity/UmsAdmin.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/mapper/UmsAdminMapper.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/mapper/UmsAdminMapper.java new file mode 100644 index 0000000..7b9db60 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/mapper/UmsAdminMapper.java @@ -0,0 +1,16 @@ +package com.stock.trading.user.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.stock.trading.user.model.UmsAdmin; + +/** + *

+ * 后台用户表 Mapper 接口 + *

+ * + * @author macro + * @since 2020-08-21 + */ +public interface UmsAdminMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/model/UmsAdmin.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/model/UmsAdmin.java new file mode 100644 index 0000000..8fe3592 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/model/UmsAdmin.java @@ -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; + +/** + *

+ * 后台用户表 + *

+ * + * @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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/service/UmsAdminService.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/service/UmsAdminService.java new file mode 100644 index 0000000..af5510e --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/service/UmsAdminService.java @@ -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 getAdminByUsername(String username); + + /** + * 注册功能 + */ + UmsAdmin register(UmsAdminParam umsAdminParam); + + /** + * 登录功能 + * @param username 用户名 + * @param password 密码 + * @return 生成的JWT的token + */ + String login(String username, String password); +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/service/impl/UmsAdminServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/service/impl/UmsAdminServiceImpl.java new file mode 100644 index 0000000..d673fdd --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/trading/user/service/impl/UmsAdminServiceImpl.java @@ -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 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 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 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; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/user/controller/UserController.java b/samples/existing-system/trade-user/src/main/java/com/stock/user/controller/UserController.java new file mode 100644 index 0000000..7c9faa3 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/user/controller/UserController.java @@ -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: 实现用户相关接口 +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/user/dal/dataobject/UserDO.java b/samples/existing-system/trade-user/src/main/java/com/stock/user/dal/dataobject/UserDO.java new file mode 100644 index 0000000..b8a370b --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/user/dal/dataobject/UserDO.java @@ -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: 根据实际需求添加更多字段 + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/user/mapper/UserMapper.java b/samples/existing-system/trade-user/src/main/java/com/stock/user/mapper/UserMapper.java new file mode 100644 index 0000000..a2afa38 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/user/mapper/UserMapper.java @@ -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 { + // TODO: 定义用户相关的数据库操作方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/user/service/UserService.java b/samples/existing-system/trade-user/src/main/java/com/stock/user/service/UserService.java new file mode 100644 index 0000000..16b89bf --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/user/service/UserService.java @@ -0,0 +1,10 @@ +package com.stock.user.service; + +/** + * 用户服务接口 + * + * @author TraeAI + */ +public interface UserService { + // TODO: 定义用户服务相关方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/stock/user/service/impl/UserServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/stock/user/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..3f579d2 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/stock/user/service/impl/UserServiceImpl.java @@ -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: 实现用户服务相关方法 +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/TradeUserApplication.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/TradeUserApplication.java new file mode 100644 index 0000000..2ef3801 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/TradeUserApplication.java @@ -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); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/UserApplication.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/UserApplication.java new file mode 100644 index 0000000..a726e13 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/UserApplication.java @@ -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" + + " ''-' `'-' `-..-' "); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/config/MyBatisPlusConfig.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/MyBatisPlusConfig.java new file mode 100644 index 0000000..b5bfd8f --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/MyBatisPlusConfig.java @@ -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 配置类 + *

+ * 用于配置MyBatis Plus的相关特性,如分页插件、Mapper接口扫描等。 + *

+ * + * @author Trade Team + */ +@Configuration +@EnableTransactionManagement // 开启声明式事务管理 +@MapperScan("com.trade.user.mapper") // 扫描Mapper接口,UserApplication中已有,这里可省略或保留以明确 +public class MyBatisPlusConfig { + + /** + * 配置MyBatis Plus拦截器,用于支持分页等功能。 + *

+ * 新版MyBatis Plus(3.4.0+)推荐使用 {@link MybatisPlusInterceptor}。 + * 旧版的分页插件是 {@code PaginationInterceptor}。 + *

+ * + * @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(); + // } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/config/SecurityConfig.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/SecurityConfig.java new file mode 100644 index 0000000..e83ffd4 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/SecurityConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/config/SwaggerConfig.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/SwaggerConfig.java new file mode 100644 index 0000000..cb5c71b --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/SwaggerConfig.java @@ -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"))); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/config/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/package-info.java new file mode 100644 index 0000000..5b8f0dd --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/config/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户服务模块的配置类,例如 MyBatis Plus 配置、Swagger 配置等。 + */ +package com.trade.user.config; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/constant/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/constant/package-info.java new file mode 100644 index 0000000..0edce49 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/constant/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户模块相关的常量定义。 + */ +package com.trade.user.constant; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsAdminController.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsAdminController.java new file mode 100644 index 0000000..49bd3ce --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsAdminController.java @@ -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 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 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 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 data = new HashMap<>(); + data.put("username", umsAdmin.getUsername()); + data.put("menus", ""); + data.put("icon", umsAdmin.getIcon()); + List 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(); + } + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsPermissionController.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsPermissionController.java new file mode 100644 index 0000000..8d703d6 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsPermissionController.java @@ -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 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() { + List permissionList = permissionService.list(); + return CommonResult.success(permissionList); + } + + @ApiOperation("以层级结构返回所有权限") + @RequestMapping(value = "/treeList", method = RequestMethod.GET) + @ResponseBody + public CommonResult> treeList() { + List permissionList = permissionService.treeList(); + return CommonResult.success(permissionList); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsRoleController.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsRoleController.java new file mode 100644 index 0000000..c952fa2 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/UmsRoleController.java @@ -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 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> listAll() { + List roleList = roleService.list(); + return CommonResult.success(roleList); + } + + @ApiOperation("根据角色名称分页获取角色列表") + @RequestMapping(value = "/list", method = RequestMethod.GET) + @ResponseBody + public CommonResult> list(@RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "pageSize", defaultValue = "5") Integer pageSize, + @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum) { + Page page = new Page<>(pageNum, pageSize); + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + if (keyword!=null && keyword.length()>0){ + lambdaQueryWrapper.like(UmsRole::getName, keyword); + } + Page rolePage = roleService.page(page, lambdaQueryWrapper); + return CommonResult.success(CommonPage.restPage(rolePage)); + } + + @ApiOperation("获取角色相关权限") + @RequestMapping(value = "/listPermission/{roleId}", method = RequestMethod.GET) + @ResponseBody + public CommonResult> listPermission(@PathVariable Long roleId) { + List permissionList = roleService.getPermissionList(roleId); + return CommonResult.success(permissionList); + } + + @ApiOperation("修改角色相关权限") + @RequestMapping(value = "/updatePermission", method = RequestMethod.POST) + @ResponseBody + public CommonResult updatePermission(@RequestParam Long roleId, + @RequestParam List 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> listRoleByAdminId(@PathVariable Long adminId) { + List roleList = adminService.getRoleList(adminId); + return CommonResult.success(roleList); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/package-info.java new file mode 100644 index 0000000..7c0b667 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/controller/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户模块的Controller层代码,负责接收前端请求、参数校验、调用Service层处理业务逻辑,并返回响应。 + */ +package com.trade.user.controller; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminLoginParam.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminLoginParam.java new file mode 100644 index 0000000..adc76a3 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminLoginParam.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminParam.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminParam.java new file mode 100644 index 0000000..88d9ff9 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminParam.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminUpdatePasswordParam.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminUpdatePasswordParam.java new file mode 100644 index 0000000..5f7864b --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/UmsAdminUpdatePasswordParam.java @@ -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; +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/package-info.java new file mode 100644 index 0000000..3e3e048 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/dto/package-info.java @@ -0,0 +1,6 @@ +/** + * 此包存放用户模块的DTO (Data Transfer Object) 代码。 + * DTO用于在不同层之间传递数据,例如Controller接收的请求参数、Service返回给Controller的数据等。 + * 它们有助于解耦各层,并可以根据需要对数据进行裁剪或聚合。 + */ +package com.trade.user.dto; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/entity/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/entity/package-info.java new file mode 100644 index 0000000..d464f08 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/entity/package-info.java @@ -0,0 +1,5 @@ +/** + * 此包存放用户模块的Entity层代码,即数据库表对应的Java实体类。 + * 通常会使用Lombok注解简化代码,并可能包含JPA或MyBatis Plus的注解。 + */ +package com.trade.user.entity; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/enums/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/enums/package-info.java new file mode 100644 index 0000000..6011721 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/enums/package-info.java @@ -0,0 +1,5 @@ +/** + * 此包存放用户模块相关的枚举类。 + * 例如:用户状态枚举、性别枚举等。 + */ +package com.trade.user.enums; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/handler/packge-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/handler/packge-info.java new file mode 100644 index 0000000..2c4caaa --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/handler/packge-info.java @@ -0,0 +1 @@ +package com.trade.user.handler; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminMapper.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminMapper.java new file mode 100644 index 0000000..d188378 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminMapper.java @@ -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; + +/** + *

+ * 后台用户表 Mapper 接口 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsAdminMapper extends BaseMapper { + + /** + * 获取用户所有权限(包括角色权限和+-权限) + */ + List getPermissionList(@Param("adminId") Long adminId); +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminPermissionRelationMapper.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminPermissionRelationMapper.java new file mode 100644 index 0000000..f8e17ba --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminPermissionRelationMapper.java @@ -0,0 +1,16 @@ +package com.trade.user.mapper; + +import com.trade.user.model.UmsAdminPermissionRelation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 后台用户和权限关系表(除角色外的额外权限) Mapper 接口 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsAdminPermissionRelationMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminRoleRelationMapper.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminRoleRelationMapper.java new file mode 100644 index 0000000..c242959 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsAdminRoleRelationMapper.java @@ -0,0 +1,16 @@ +package com.trade.user.mapper; + +import com.trade.user.model.UmsAdminRoleRelation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 后台用户和角色关系表 Mapper 接口 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsAdminRoleRelationMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsPermissionMapper.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsPermissionMapper.java new file mode 100644 index 0000000..171fd34 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsPermissionMapper.java @@ -0,0 +1,16 @@ +package com.trade.user.mapper; + +import com.trade.user.model.UmsPermission; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 后台用户权限表 Mapper 接口 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsPermissionMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsRoleMapper.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsRoleMapper.java new file mode 100644 index 0000000..45dbc5f --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsRoleMapper.java @@ -0,0 +1,16 @@ +package com.trade.user.mapper; + +import com.trade.user.model.UmsRole; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 后台用户角色表 Mapper 接口 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsRoleMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsRolePermissionRelationMapper.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsRolePermissionRelationMapper.java new file mode 100644 index 0000000..6111ea2 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/UmsRolePermissionRelationMapper.java @@ -0,0 +1,16 @@ +package com.trade.user.mapper; + +import com.trade.user.model.UmsRolePermissionRelation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 后台用户角色和权限关系表 Mapper 接口 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsRolePermissionRelationMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/package-info.java new file mode 100644 index 0000000..45566af --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户模块的Mapper层代码,定义与数据库表对应的DAO接口,继承自MyBatis Plus的BaseMapper。 + */ +package com.trade.user.mapper; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdmin.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdmin.java new file mode 100644 index 0000000..a621014 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdmin.java @@ -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; + +/** + *

+ * 后台用户表 + *

+ * + * @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; + + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdminPermissionRelation.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdminPermissionRelation.java new file mode 100644 index 0000000..b261229 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdminPermissionRelation.java @@ -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; + +/** + *

+ * 后台用户和权限关系表(除角色外的额外权限) + *

+ * + * @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; + + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdminRoleRelation.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdminRoleRelation.java new file mode 100644 index 0000000..aaac983 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsAdminRoleRelation.java @@ -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; + +/** + *

+ * 后台用户和角色关系表 + *

+ * + * @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; + + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsPermission.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsPermission.java new file mode 100644 index 0000000..49661c9 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsPermission.java @@ -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; + +/** + *

+ * 后台用户权限表 + *

+ * + * @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; + + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsRole.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsRole.java new file mode 100644 index 0000000..211a030 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsRole.java @@ -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; + +/** + *

+ * 后台用户角色表 + *

+ * + * @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; + + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsRolePermissionRelation.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsRolePermissionRelation.java new file mode 100644 index 0000000..e89fdea --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/UmsRolePermissionRelation.java @@ -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; + +/** + *

+ * 后台用户角色和权限关系表 + *

+ * + * @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; + + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/model/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/package-info.java new file mode 100644 index 0000000..c05a476 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/model/package-info.java @@ -0,0 +1,7 @@ +/** + * 此包包含用户服务模块的领域模型 (Domain Model) 或实体类 (Entity)。 + *

+ * 这些类通常映射到用户相关的数据库表结构,例如用户表、角色表、权限表等。 + *

+ */ +package com.trade.user.model; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminPermissionRelationService.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminPermissionRelationService.java new file mode 100644 index 0000000..f9a3d11 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminPermissionRelationService.java @@ -0,0 +1,16 @@ +package com.trade.user.service; + +import com.trade.user.model.UmsAdminPermissionRelation; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 后台用户和权限关系表(除角色外的额外权限) 服务类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsAdminPermissionRelationService extends IService { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminRoleRelationService.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminRoleRelationService.java new file mode 100644 index 0000000..904996b --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminRoleRelationService.java @@ -0,0 +1,16 @@ +package com.trade.user.service; + +import com.trade.user.model.UmsAdminRoleRelation; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 后台用户和角色关系表 服务类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsAdminRoleRelationService extends IService { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminService.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminService.java new file mode 100644 index 0000000..10f1444 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsAdminService.java @@ -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 getAdminByUsername(String username); + + /** + * 注册功能 + */ + UmsAdmin register(UmsAdminParam umsAdminParam); + + /** + * 登录功能 + * @param username 用户名 + * @param password 密码 + * @return 生成的JWT的token + */ + String login(String username, String password); + + /** + * 根据用户id获取用户所有权限(角色权限和+-权限) + */ + List getPermissionList(Long adminId); + + /** + * 修改密码 + */ + int updatePassword(UmsAdminUpdatePasswordParam umsAdminUpdatePasswordParam); + + /** + * 获取用户信息 + */ + UserDto loadUserByUsername(String username); + + /** + * 获取用户对应角色 + */ + List getRoleList(Long adminId); + + /** + * 刷新token的功能 + * @param oldToken 旧的token + */ + String refreshToken(String oldToken); +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsPermissionService.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsPermissionService.java new file mode 100644 index 0000000..1277e97 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsPermissionService.java @@ -0,0 +1,21 @@ +package com.trade.user.service; + +import com.trade.user.model.UmsPermission; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 后台用户权限表 服务类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +import java.util.List; + +public interface UmsPermissionService extends IService { + /** + * 以层级结构返回所有权限 + */ + List treeList(); +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsRolePermissionRelationService.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsRolePermissionRelationService.java new file mode 100644 index 0000000..2c0d66d --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsRolePermissionRelationService.java @@ -0,0 +1,16 @@ +package com.trade.user.service; + +import com.trade.user.model.UmsRolePermissionRelation; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 后台用户角色和权限关系表 服务类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +public interface UmsRolePermissionRelationService extends IService { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsRoleService.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsRoleService.java new file mode 100644 index 0000000..6295748 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/UmsRoleService.java @@ -0,0 +1,28 @@ +package com.trade.user.service; + +import com.trade.user.model.UmsRole; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 后台用户角色表 服务类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +import com.trade.user.model.UmsPermission; + +import java.util.List; + +public interface UmsRoleService extends IService { + /** + * 获取角色相关权限 + */ + List getPermissionList(Long roleId); + + /** + * 修改角色权限 + */ + int updatePermission(Long roleId, List permissionIds); +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminPermissionRelationServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminPermissionRelationServiceImpl.java new file mode 100644 index 0000000..aa6c219 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminPermissionRelationServiceImpl.java @@ -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; + +/** + *

+ * 后台用户和权限关系表(除角色外的额外权限) 服务实现类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +@Service +public class UmsAdminPermissionRelationServiceImpl extends ServiceImpl implements UmsAdminPermissionRelationService { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminRoleRelationServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminRoleRelationServiceImpl.java new file mode 100644 index 0000000..d54dba4 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminRoleRelationServiceImpl.java @@ -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; + +/** + *

+ * 后台用户和角色关系表 服务实现类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +@Service +public class UmsAdminRoleRelationServiceImpl extends ServiceImpl implements UmsAdminRoleRelationService { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminServiceImpl.java new file mode 100644 index 0000000..0a566cc --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsAdminServiceImpl.java @@ -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 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 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 lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(UmsAdmin::getUsername, umsAdmin.getUsername()); + List 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 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 lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(UmsAdmin::getUsername, param.getUsername()); + List 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 permissionList = getPermissionList(admin.getId()); + List 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 getRoleList(Long adminId) { + return adminMapper.getRoleList(adminId); + } + + @Override + public String refreshToken(String oldToken) { + return jwtTokenUtil.refreshHeadToken(oldToken); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsPermissionServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsPermissionServiceImpl.java new file mode 100644 index 0000000..3a07809 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsPermissionServiceImpl.java @@ -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; + +/** + *

+ * 后台用户权限表 服务实现类 + *

+ * + * @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 implements UmsPermissionService { + + @Override + public List treeList() { + List permissionList = list(); + List 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 permissionList) { + permission.setChildren(permissionList.stream() + .filter(item -> item.getParentId().equals(permission.getId())) + .map(item -> covert(item, permissionList)) + .collect(Collectors.toList())); + return permission; + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsRolePermissionRelationServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsRolePermissionRelationServiceImpl.java new file mode 100644 index 0000000..fa09cea --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsRolePermissionRelationServiceImpl.java @@ -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; + +/** + *

+ * 后台用户角色和权限关系表 服务实现类 + *

+ * + * @author Trade Team + * @since 2024-03-20 + */ +@Service +public class UmsRolePermissionRelationServiceImpl extends ServiceImpl implements UmsRolePermissionRelationService { + +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsRoleServiceImpl.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsRoleServiceImpl.java new file mode 100644 index 0000000..ad2f17b --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/UmsRoleServiceImpl.java @@ -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; + +/** + *

+ * 后台用户角色表 服务实现类 + *

+ * + * @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 implements UmsRoleService { + @Autowired + private UmsRolePermissionRelationMapper rolePermissionRelationMapper; + + @Override + public List getPermissionList(Long roleId) { + return rolePermissionRelationMapper.getPermissionList(roleId); + } + + @Override + public int updatePermission(Long roleId, List permissionIds) { + //删除之前绑定的关系 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UmsRolePermissionRelation::getRoleId, roleId); + rolePermissionRelationMapper.delete(wrapper); + //批量插入新的关系 + List 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(); + } +} \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/package-info.java new file mode 100644 index 0000000..8f0e3e5 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/impl/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户模块Service层接口的实现类。 + */ +package com.trade.user.service.impl; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/service/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/package-info.java new file mode 100644 index 0000000..5c752fb --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/service/package-info.java @@ -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; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/util/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/util/package-info.java new file mode 100644 index 0000000..ae85c85 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/util/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户模块相关的工具类。 + */ +package com.trade.user.util; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/utils/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/utils/package-info.java new file mode 100644 index 0000000..2bbcca9 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/utils/package-info.java @@ -0,0 +1,4 @@ +/** + * 此包存放用户服务模块的工具类。 + */ +package com.trade.user.utils; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/java/com/trade/user/vo/package-info.java b/samples/existing-system/trade-user/src/main/java/com/trade/user/vo/package-info.java new file mode 100644 index 0000000..46701c4 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/java/com/trade/user/vo/package-info.java @@ -0,0 +1,6 @@ +/** + * 此包存放用户模块的VO (View Object) 代码。 + * VO是专门用于前端展示的对象,Controller层会将业务处理结果封装成VO返回给客户端。 + * VO的设计应贴合前端页面的展示需求。 + */ +package com.trade.user.vo; \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/application-dev.yml b/samples/existing-system/trade-user/src/main/resources/application-dev.yml new file mode 100644 index 0000000..6339599 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/application-prod.yml b/samples/existing-system/trade-user/src/main/resources/application-prod.yml new file mode 100644 index 0000000..cf30d4c --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/application-prod.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/application-test.yml b/samples/existing-system/trade-user/src/main/resources/application-test.yml new file mode 100644 index 0000000..2046100 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/application-test.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/application.yml b/samples/existing-system/trade-user/src/main/resources/application.yml new file mode 100644 index 0000000..846df93 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/bootstrap.yml b/samples/existing-system/trade-user/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..9ec581f --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/bootstrap.yml @@ -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 # 指定日志配置文件 \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/generator.properties b/samples/existing-system/trade-user/src/main/resources/generator.properties new file mode 100644 index 0000000..a162bfe --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/generator.properties @@ -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 \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/logback-spring.xml b/samples/existing-system/trade-user/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b2758eb --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/logback-spring.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminMapper.xml new file mode 100644 index 0000000..9fb5619 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminPermissionRelationMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminPermissionRelationMapper.xml new file mode 100644 index 0000000..6d2282e --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminPermissionRelationMapper.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminRoleRelationMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminRoleRelationMapper.xml new file mode 100644 index 0000000..1309584 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsAdminRoleRelationMapper.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsPermissionMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsPermissionMapper.xml new file mode 100644 index 0000000..1a150ad --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsPermissionMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsResourceMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsResourceMapper.xml new file mode 100644 index 0000000..e540e20 --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsResourceMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsRoleMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsRoleMapper.xml new file mode 100644 index 0000000..ebe9abf --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsRoleMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/existing-system/trade-user/src/main/resources/mapper/UmsRolePermissionRelationMapper.xml b/samples/existing-system/trade-user/src/main/resources/mapper/UmsRolePermissionRelationMapper.xml new file mode 100644 index 0000000..5287b6f --- /dev/null +++ b/samples/existing-system/trade-user/src/main/resources/mapper/UmsRolePermissionRelationMapper.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/要件定義_追加改修_股票.xlsx b/samples/要件定義_追加改修_股票.xlsx new file mode 100644 index 0000000..d8b88ed Binary files /dev/null and b/samples/要件定義_追加改修_股票.xlsx differ diff --git a/src/genesis/data_models.py b/src/genesis/data_models.py index 6033767..123c549 100644 --- a/src/genesis/data_models.py +++ b/src/genesis/data_models.py @@ -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 diff --git a/src/genesis/impact/code_parser.py b/src/genesis/impact/code_parser.py new file mode 100644 index 0000000..8efbfdd --- /dev/null +++ b/src/genesis/impact/code_parser.py @@ -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) diff --git a/src/genesis/impact/existing_system_explorer.py b/src/genesis/impact/existing_system_explorer.py new file mode 100644 index 0000000..3a32068 --- /dev/null +++ b/src/genesis/impact/existing_system_explorer.py @@ -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, + ) diff --git a/src/genesis/impact/impact_agent.py b/src/genesis/impact/impact_agent.py new file mode 100644 index 0000000..5f42eca --- /dev/null +++ b/src/genesis/impact/impact_agent.py @@ -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) diff --git a/src/genesis/parsers/source_aggregator.py b/src/genesis/parsers/source_aggregator.py index a610f8a..7f8bdaa 100644 --- a/src/genesis/parsers/source_aggregator.py +++ b/src/genesis/parsers/source_aggregator.py @@ -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, ) diff --git a/src/genesis/writer/context_builder.py b/src/genesis/writer/context_builder.py index 27bb7cb..f95e65a 100644 --- a/src/genesis/writer/context_builder.py +++ b/src/genesis/writer/context_builder.py @@ -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 diff --git a/src/genesis/writer/models.py b/src/genesis/writer/models.py index e05cf74..dac602b 100644 --- a/src/genesis/writer/models.py +++ b/src/genesis/writer/models.py @@ -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) diff --git a/src/genesis/writer/orchestrator.py b/src/genesis/writer/orchestrator.py index 1502158..1995407 100644 --- a/src/genesis/writer/orchestrator.py +++ b/src/genesis/writer/orchestrator.py @@ -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 diff --git a/src/genesis/writer/writer_agent.py b/src/genesis/writer/writer_agent.py index 007cf23..07d51c4 100644 --- a/src/genesis/writer/writer_agent.py +++ b/src/genesis/writer/writer_agent.py @@ -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 字段)所使用的自然语言," diff --git a/tests/test_code_parser.py b/tests/test_code_parser.py new file mode 100644 index 0000000..ef94caa --- /dev/null +++ b/tests/test_code_parser.py @@ -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 类" diff --git a/tests/test_data_models.py b/tests/test_data_models.py index 10e1d97..385da8a 100644 --- a/tests/test_data_models.py +++ b/tests/test_data_models.py @@ -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 diff --git a/tests/test_existing_system_explorer.py b/tests/test_existing_system_explorer.py new file mode 100644 index 0000000..458429c --- /dev/null +++ b/tests/test_existing_system_explorer.py @@ -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 == [] diff --git a/tests/test_impact_agent.py b/tests/test_impact_agent.py new file mode 100644 index 0000000..f26b3da --- /dev/null +++ b/tests/test_impact_agent.py @@ -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 diff --git a/tests/test_phase5_context_builder.py b/tests/test_phase5_context_builder.py index f77eb28..ad49560 100644 --- a/tests/test_phase5_context_builder.py +++ b/tests/test_phase5_context_builder.py @@ -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"] != "" diff --git a/tests/test_phase5_models.py b/tests/test_phase5_models.py index e11d121..ebdf8ab 100644 --- a/tests/test_phase5_models.py +++ b/tests/test_phase5_models.py @@ -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(): diff --git a/tests/test_phase5_writer_agent.py b/tests/test_phase5_writer_agent.py index bf671ba..7e6e336 100644 --- a/tests/test_phase5_writer_agent.py +++ b/tests/test_phase5_writer_agent.py @@ -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): diff --git a/tests/test_phase5_writer_orchestrator.py b/tests/test_phase5_writer_orchestrator.py index 18aa618..4aaae33 100644 --- a/tests/test_phase5_writer_orchestrator.py +++ b/tests/test_phase5_writer_orchestrator.py @@ -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(), + ) diff --git a/tests/test_source_aggregator.py b/tests/test_source_aggregator.py index 14f0a02..f116682 100644 --- a/tests/test_source_aggregator.py +++ b/tests/test_source_aggregator.py @@ -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)