添加代码注释,去掉可能有安全问题代码

This commit is contained in:
wanxiaoli 2026-08-07 09:56:04 +08:00
parent 43e820fd3b
commit 4bafdf81da
55 changed files with 1465 additions and 500 deletions

View File

@ -0,0 +1,22 @@
/**
* 仿真数据构建Build相关能力
* <p>
* 该包用于将项目拓扑/设备配置/事件/材料/关键参数等多源输入组织成推演/计算可消费的结构化数据
* 侧重于流程编排与数据组装而非对外协议处理
* </p>
* <ul>
* <li>输入项目ID场景配置拓扑结构设备与材料数据等</li>
* <li>输出推演上下文推演请求体结构化数据包等</li>
* <li>约束构建过程应保持确定性避免引入与时间相关的隐式状态</li>
* <li>健壮性对缺失字段非法引用拓扑不一致等情况给出可定位错误</li>
* </ul>
* <p>
* 设计原则
* </p>
* <ul>
* <li>分阶段构建先解析与归一化再校验与补全最后生成输出</li>
* <li>只读输入避免在构建阶段修改持久化实体对象减少副作用</li>
* <li>可测试关键构建步骤保持可单元测试输入输出可预测</li>
* </ul>
*/
package com.yfd.business.css.build;

View File

@ -0,0 +1,21 @@
/**
* 业务异常定义
* <p>
* 用于表达业务处理过程中的可预期错误区分于系统异常NPE网络故障数据库不可用等
* 业务异常应具备明确语义便于前端提示与测试用例覆盖
* </p>
* <ul>
* <li>参数类错误入参缺失格式非法越界引用不存在等</li>
* <li>状态类错误对象状态不允许当前操作例如任务状态机不允许跳转</li>
* <li>推演类错误场景推演前置条件不满足数据不一致外部推演服务拒绝等</li>
* <li>权限类错误项目不可访问资源无权限读写等如需与安全框架集成</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>异常消息面向用户/测试可读避免泄露敏感信息</li>
* <li>异常类型按场景区分类型避免一个异常承担全部语义</li>
* </ul>
*/
package com.yfd.business.css.common.exception;

View File

@ -0,0 +1,21 @@
/**
* Spring Boot 配置
* <p>
* 该包集中放置与运行时装配相关的配置类例如
* </p>
* <ul>
* <li>MyBatis/MyBatis-Plus 配置分页类型处理器拦截器等</li>
* <li>OpenAPI/Swagger 配置接口文档分组鉴权头部描述等</li>
* <li>RestTemplate/WebClient 配置超时拦截器序列化策略等</li>
* <li>WebSocket 配置消息代理端点跨域策略等</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>配置类仅负责 Bean 装配不承载业务逻辑</li>
* <li>外部可变参数地址密钥阈值等通过配置注入不硬编码在源码</li>
* <li>默认值要保守避免因配置缺失导致系统启动后产生危险行为</li>
* </ul>
*/
package com.yfd.business.css.config;

View File

@ -19,23 +19,58 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List; import java.util.List;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 算法字典管理接口
* <p>
* 提供算法字典的基础 CRUD按算法类型/名称查询以及算法启用/停用等管理能力
* </p>
* <p>
* 字段约定
* </p>
* <ul>
* <li>status算法状态标识"1" 表示启用"0" 表示停用</li>
* <li>modifier最后修改人用户名取自当前登录上下文</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/algorithms") @RequestMapping("/algorithms")
@Tag(name = "算法接口", description = "算法字典的增删改查与搜索") @Tag(name = "算法接口", description = "算法字典的增删改查与搜索")
public class AlgorithmController { public class AlgorithmController {
/**
* 算法字典服务MyBatis-Plus Service
*/
@Autowired @Autowired
private AlgorithmService algorithmService; private AlgorithmService algorithmService;
/**
* 当前用户信息服务用于记录 modifier 等审计字段
*/
@Autowired @Autowired
private IUserService userService; private IUserService userService;
/**
* 根据主键查询算法
*
* @param id 算法主键
* @return 算法对象不存在时返回 null
*/
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "根据算法ID获取算法", description = "路径参数传入算法ID返回算法对象") @Operation(summary = "根据算法ID获取算法", description = "路径参数传入算法ID返回算法对象")
public Algorithm getAlgorithmById(@PathVariable String id) { public Algorithm getAlgorithmById(@PathVariable String id) {
return algorithmService.getById(id); return algorithmService.getById(id);
} }
/**
* 根据算法类型查询算法
* <p>
* 当前实现返回满足条件的第一条记录LIMIT 1
* </p>
*
* @param type 算法类型 GPR
* @return 算法对象不存在时返回 null
*/
@GetMapping("/type/{type}") @GetMapping("/type/{type}")
@Operation(summary = "根据算法类型获取算法", description = "路径参数传入算法类型(如GPR),返回算法对象") @Operation(summary = "根据算法类型获取算法", description = "路径参数传入算法类型(如GPR),返回算法对象")
public Algorithm getAlgorithmByType(@PathVariable String type) { public Algorithm getAlgorithmByType(@PathVariable String type) {
@ -49,6 +84,15 @@ public class AlgorithmController {
@PreAuthorize("hasAuthority('algorithm:add')") @PreAuthorize("hasAuthority('algorithm:add')")
@PostMapping @PostMapping
@Operation(summary = "新增算法", description = "请求体传入算法对象,返回是否新增成功") @Operation(summary = "新增算法", description = "请求体传入算法对象,返回是否新增成功")
/**
* 新增算法
* <p>
* 写入审计字段modifiercreatedAtupdatedAt
* </p>
*
* @param algorithm 算法对象由请求体提供
* @return 是否新增成功
*/
public boolean createAlgorithm(@RequestBody Algorithm algorithm) { public boolean createAlgorithm(@RequestBody Algorithm algorithm) {
algorithm.setModifier(currentUsername()); algorithm.setModifier(currentUsername());
algorithm.setCreatedAt(LocalDateTime.now()); algorithm.setCreatedAt(LocalDateTime.now());
@ -60,6 +104,15 @@ public class AlgorithmController {
@PreAuthorize("hasAuthority('algorithm:update')") @PreAuthorize("hasAuthority('algorithm:update')")
@PutMapping @PutMapping
@Operation(summary = "修改算法", description = "请求体传入算法对象(需包含主键),返回是否修改成功") @Operation(summary = "修改算法", description = "请求体传入算法对象(需包含主键),返回是否修改成功")
/**
* 修改算法
* <p>
* 仅更新 updatedAt modifier其他字段由请求体携带并覆盖更新
* </p>
*
* @param algorithm 算法对象需包含主键
* @return 是否修改成功
*/
public boolean updateAlgorithm(@RequestBody Algorithm algorithm) { public boolean updateAlgorithm(@RequestBody Algorithm algorithm) {
algorithm.setModifier(currentUsername()); algorithm.setModifier(currentUsername());
algorithm.setUpdatedAt(LocalDateTime.now()); algorithm.setUpdatedAt(LocalDateTime.now());
@ -70,6 +123,12 @@ public class AlgorithmController {
@PreAuthorize("hasAuthority('algorithm:del')") @PreAuthorize("hasAuthority('algorithm:del')")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "删除算法(单条)", description = "根据算法ID删除算法") @Operation(summary = "删除算法(单条)", description = "根据算法ID删除算法")
/**
* 删除单条算法记录
*
* @param id 算法主键
* @return 是否删除成功
*/
public boolean deleteAlgorithm(@PathVariable String id) { public boolean deleteAlgorithm(@PathVariable String id) {
return algorithmService.removeById(id); return algorithmService.removeById(id);
} }
@ -78,6 +137,12 @@ public class AlgorithmController {
@PreAuthorize("hasAuthority('algorithm:del')") @PreAuthorize("hasAuthority('algorithm:del')")
@DeleteMapping @DeleteMapping
@Operation(summary = "删除算法(批量)", description = "请求体传入算法ID列表批量删除算法") @Operation(summary = "删除算法(批量)", description = "请求体传入算法ID列表批量删除算法")
/**
* 批量删除算法记录
*
* @param ids 算法主键列表
* @return 是否删除成功
*/
public boolean deleteAlgorithms(@RequestBody List<String> ids) { public boolean deleteAlgorithms(@RequestBody List<String> ids) {
return algorithmService.removeByIds(ids); return algorithmService.removeByIds(ids);
} }
@ -86,6 +151,12 @@ public class AlgorithmController {
// @PreAuthorize("hasAuthority('algorithm:activate')") // @PreAuthorize("hasAuthority('algorithm:activate')")
@PostMapping("/activate") @PostMapping("/activate")
@Operation(summary = "激活算法", description = "激活当前算法类型") @Operation(summary = "激活算法", description = "激活当前算法类型")
/**
* 启用算法status=1
*
* @param algorithmId 算法主键
* @return 是否更新成功算法不存在时返回 false
*/
public boolean activate(@RequestParam String algorithmId) { public boolean activate(@RequestParam String algorithmId) {
Algorithm algorithm = algorithmService.getById(algorithmId); Algorithm algorithm = algorithmService.getById(algorithmId);
if (algorithm == null) return false; if (algorithm == null) return false;
@ -100,6 +171,12 @@ public class AlgorithmController {
// @PreAuthorize("hasAuthority('algorithm:unactivate')") // @PreAuthorize("hasAuthority('algorithm:unactivate')")
@PostMapping("/unactivate") @PostMapping("/unactivate")
@Operation(summary = "关闭算法", description = "关闭当前算法类型") @Operation(summary = "关闭算法", description = "关闭当前算法类型")
/**
* 停用算法status=0
*
* @param algorithmId 算法主键
* @return 是否更新成功算法不存在时返回 false
*/
public boolean unactivate(@RequestParam String algorithmId) { public boolean unactivate(@RequestParam String algorithmId) {
Algorithm algorithm = algorithmService.getById(algorithmId); Algorithm algorithm = algorithmService.getById(algorithmId);
if (algorithm == null) return false; if (algorithm == null) return false;
@ -110,10 +187,9 @@ public class AlgorithmController {
return algorithmService.updateById(algorithm); return algorithmService.updateById(algorithm);
} }
//获取激活的算法类型
/** /**
* 获取所有激活的算法类型 * 获取所有启用状态的算法列表status=1
* 输出参数激活的算法类型列表 *
* @return 激活的算法类型列表 * @return 激活的算法类型列表
*/ */
@GetMapping("/getActiveAlgorithms") @GetMapping("/getActiveAlgorithms")
@ -125,12 +201,11 @@ public class AlgorithmController {
} }
/** /**
* 根据算法名称搜索并分页返回 * 根据算法名称搜索并分页返回
* 输入参数查询参数 name算法名称关键词可为空pageNum页码默认1pageSize每页条数默认10 *
* 输出参数算法分页列表
* @param name 算法名称关键词可为空 * @param name 算法名称关键词可为空
* @param pageNum 页码 * @param pageNum 页码默认 1
* @param pageSize 每页条数 * @param pageSize 每页条数默认 20
* @return 算法分页列表 * @return 算法分页列表
*/ */
@GetMapping("/search") @GetMapping("/search")
@ -147,6 +222,14 @@ public class AlgorithmController {
return algorithmService.page(page, qw); return algorithmService.page(page, qw);
} }
/**
* 获取当前登录用户名
* <p>
* 用于落库的 modifier 字段若用户未登录或上下文解析失败返回 "anonymous"
* </p>
*
* @return 当前用户名或 "anonymous"
*/
private String currentUsername() { private String currentUsername() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();

View File

@ -3,9 +3,7 @@ package com.yfd.business.css.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yfd.business.css.domain.AlgorithmModel; import com.yfd.business.css.domain.AlgorithmModel;
import com.yfd.business.css.domain.Algorithm;
import com.yfd.business.css.service.AlgorithmModelService; import com.yfd.business.css.service.AlgorithmModelService;
import com.yfd.business.css.service.AlgorithmService;
import com.yfd.platform.system.service.IUserService; import com.yfd.platform.system.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -18,35 +16,49 @@ import com.yfd.platform.annotation.Log;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/**
* 算法模型版本管理接口
* <p>
* 提供算法模型版本的查询分页筛选当前版本获取版本激活/切换以及删除等管理能力
* </p>
* <p>
* 关键约定
* </p>
* <ul>
* <li>模型分组维度algorithmType + deviceType + materialType</li>
* <li>当前版本标识isCurrent=1 表示当前激活版本</li>
* <li>materialType 兼容Mixed MIX 视为同一类</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/algorithm-models") @RequestMapping("/algorithm-models")
@Tag(name = "算法模型接口", description = "算法模型版本的增删改查、查询当前版本与在线训练") @Tag(name = "算法模型接口", description = "算法模型版本的增删改查、查询当前版本与在线训练")
public class AlgorithmModelController { public class AlgorithmModelController {
/**
* 算法模型服务MyBatis-Plus Service
*/
@Autowired @Autowired
private AlgorithmModelService algorithmModelService; private AlgorithmModelService algorithmModelService;
/**
* 当前用户信息服务用于记录 modifier 等审计字段
*/
@Autowired @Autowired
private IUserService userService; private IUserService userService;
@Autowired
private AlgorithmService algorithmService;
@Autowired
private ObjectMapper objectMapper;
/**
* 根据主键查询模型版本
*
* @param id 模型主键
* @return 模型版本对象不存在时返回 null
*/
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "根据模型ID获取模型版本", description = "路径参数传入模型ID返回模型版本对象") @Operation(summary = "根据模型ID获取模型版本", description = "路径参数传入模型ID返回模型版本对象")
public AlgorithmModel getById(@PathVariable String id) { public AlgorithmModel getById(@PathVariable String id) {
@ -57,6 +69,15 @@ public class AlgorithmModelController {
// @PreAuthorize("hasAuthority('algorithmModel:add')") // @PreAuthorize("hasAuthority('algorithmModel:add')")
// @PostMapping // @PostMapping
@Operation(summary = "新增模型版本", description = "请求体传入模型版本对象,返回是否新增成功") @Operation(summary = "新增模型版本", description = "请求体传入模型版本对象,返回是否新增成功")
/**
* 新增模型版本
* <p>
* 当前方法仅负责落库与审计字段写入接口映射若被关闭@PostMapping 注释则不会对外提供入口
* </p>
*
* @param model 模型版本对象
* @return 是否新增成功
*/
public boolean create(@RequestBody AlgorithmModel model) { public boolean create(@RequestBody AlgorithmModel model) {
model.setModifier(currentUsername()); model.setModifier(currentUsername());
model.setCreatedAt(LocalDateTime.now()); model.setCreatedAt(LocalDateTime.now());
@ -68,6 +89,15 @@ public class AlgorithmModelController {
// @PreAuthorize("hasAuthority('algorithmModel:update')") // @PreAuthorize("hasAuthority('algorithmModel:update')")
// @PutMapping // @PutMapping
@Operation(summary = "修改模型版本", description = "请求体传入模型版本对象(需包含主键),返回是否修改成功") @Operation(summary = "修改模型版本", description = "请求体传入模型版本对象(需包含主键),返回是否修改成功")
/**
* 修改模型版本
* <p>
* 当前方法仅负责落库与审计字段写入接口映射若被关闭@PutMapping 注释则不会对外提供入口
* </p>
*
* @param model 模型版本对象需包含主键
* @return 是否修改成功
*/
public boolean update(@RequestBody AlgorithmModel model) { public boolean update(@RequestBody AlgorithmModel model) {
model.setModifier(currentUsername()); model.setModifier(currentUsername());
model.setUpdatedAt(LocalDateTime.now()); model.setUpdatedAt(LocalDateTime.now());
@ -78,6 +108,12 @@ public class AlgorithmModelController {
@PreAuthorize("hasAuthority('algorithmModel:del')") @PreAuthorize("hasAuthority('algorithmModel:del')")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "删除模型版本(单条)", description = "根据模型ID删除模型版本") @Operation(summary = "删除模型版本(单条)", description = "根据模型ID删除模型版本")
/**
* 删除单条模型版本记录
*
* @param id 模型主键
* @return 是否删除成功
*/
public boolean delete(@PathVariable String id) { public boolean delete(@PathVariable String id) {
return algorithmModelService.removeById(id); return algorithmModelService.removeById(id);
} }
@ -86,11 +122,28 @@ public class AlgorithmModelController {
@PreAuthorize("hasAuthority('algorithmModel:del')") @PreAuthorize("hasAuthority('algorithmModel:del')")
@DeleteMapping @DeleteMapping
@Operation(summary = "删除模型版本(批量)", description = "请求体传入模型ID列表批量删除模型版本") @Operation(summary = "删除模型版本(批量)", description = "请求体传入模型ID列表批量删除模型版本")
/**
* 批量删除模型版本记录
*
* @param ids 模型主键列表
* @return 是否删除成功
*/
public boolean deleteBatch(@RequestBody List<String> ids) { public boolean deleteBatch(@RequestBody List<String> ids) {
return algorithmModelService.deleteBatchWithCheck(ids); return algorithmModelService.deleteBatchWithCheck(ids);
} }
//返回该算法+设备类型+材料类型的版本列表 /**
* 查询模型版本列表分页
*
* @param algorithmType 算法类型
* @param deviceType 设备类型
* @param materialType 材料类型支持 Mixed/MIX 兼容
* @param versionTag 版本号
* @param isCurrent 是否为当前版本1/0
* @param pageNum 页码默认 1
* @param pageSize 每页条数默认 20
* @return 模型版本分页结果
*/
@GetMapping("/search") @GetMapping("/search")
@Operation(summary = "查询模型版本列表", description = "按算法类型、设备类型与材料类型过滤并分页返回模型版本") @Operation(summary = "查询模型版本列表", description = "按算法类型、设备类型与材料类型过滤并分页返回模型版本")
public Page<AlgorithmModel> search(@RequestParam(required = false) String algorithmType, public Page<AlgorithmModel> search(@RequestParam(required = false) String algorithmType,
@ -118,6 +171,17 @@ public class AlgorithmModelController {
return algorithmModelService.page(page, qw); return algorithmModelService.page(page, qw);
} }
/**
* 获取模型版本下拉选项
* <p>
* 返回包含 optionsvalue=模型ID,label=版本号 currentModelId当前激活模型ID的结构便于前端直接使用
* </p>
*
* @param algorithmType 算法类型
* @param deviceType 设备类型
* @param materialType 材料类型可选
* @return 选项数据
*/
@GetMapping("/options") @GetMapping("/options")
@Operation(summary = "获取模型版本选项列表", description = "用于界面下拉选择按算法类型与设备类型返回模型版本列表value=模型ID,label=版本号并返回当前激活模型ID") @Operation(summary = "获取模型版本选项列表", description = "用于界面下拉选择按算法类型与设备类型返回模型版本列表value=模型ID,label=版本号并返回当前激活模型ID")
public Map<String, Object> options(@RequestParam String algorithmType, public Map<String, Object> options(@RequestParam String algorithmType,
@ -158,7 +222,14 @@ public class AlgorithmModelController {
return out; return out;
} }
//返回该算法+设备类型+材料类型的当前激活版本 /**
* 获取当前激活版本is_current=1
*
* @param algorithmType 算法类型
* @param deviceType 设备类型
* @param materialType 材料类型可选
* @return 当前激活模型版本不存在时返回 null
*/
@GetMapping("/current") @GetMapping("/current")
@Operation(summary = "获取当前激活版本", description = "根据算法类型、设备类型与材料类型,返回 is_current=1 的模型版本") @Operation(summary = "获取当前激活版本", description = "根据算法类型、设备类型与材料类型,返回 is_current=1 的模型版本")
public AlgorithmModel getCurrent(@RequestParam String algorithmType, public AlgorithmModel getCurrent(@RequestParam String algorithmType,
@ -184,6 +255,15 @@ public class AlgorithmModelController {
@PreAuthorize("hasAuthority('algorithmModel:activate')") @PreAuthorize("hasAuthority('algorithmModel:activate')")
@PostMapping("/activate") @PostMapping("/activate")
@Operation(summary = "激活模型版本", description = "将目标模型版本设为当前,并将同组(算法+设备+材料)其他版本设为非当前") @Operation(summary = "激活模型版本", description = "将目标模型版本设为当前,并将同组(算法+设备+材料)其他版本设为非当前")
/**
* 激活指定模型版本
* <p>
* 激活时会将同组algorithmType+deviceType+materialType其他版本置为非当前isCurrent=0再将指定版本置为当前
* </p>
*
* @param algorithmModelId 模型主键
* @return 是否更新成功模型不存在时返回 false
*/
public boolean activate(@RequestParam String algorithmModelId) { public boolean activate(@RequestParam String algorithmModelId) {
AlgorithmModel model = algorithmModelService.getById(algorithmModelId); AlgorithmModel model = algorithmModelService.getById(algorithmModelId);
if (model == null) return false; if (model == null) return false;
@ -212,148 +292,12 @@ public class AlgorithmModelController {
return algorithmModelService.updateById(model); return algorithmModelService.updateById(model);
} }
@Log(value = "在线训练(Excel)", module = "算法模型管理") /**
// 在线训练Excel 数据集 * 归一化材料类型取值
// @PostMapping("/train/excel") *
@Operation(summary = "在线训练Excel", description = "传入算法类型、设备类型与Excel路径训练完成新增模型版本记录可选激活") * @param raw 原始材料类型
public Map<String, Object> trainExcel(@RequestBody Map<String, Object> body) { * @return 归一化后的材料类型空白返回 null
String algorithmType = str(body.get("algorithm_type")); */
String deviceType = str(body.get("device_type"));
String materialType = normalizeMaterialType(str(body.getOrDefault("material_type", "")));
String datasetPath = str(body.get("dataset_path"));
String modelDir = str(body.getOrDefault("model_dir", ""));
boolean activate = bool(body.getOrDefault("activate", false));
String featureMapSnapshot = toJson(body.get("feature_map_snapshot"));
if (isBlank(algorithmType) || isBlank(deviceType) || isBlank(datasetPath)) {
return Map.of("code", 1, "msg", "algorithm_type/device_type/dataset_path 必填");
}
Algorithm algo = getAlgorithmByType(algorithmType);
if (algo == null || isBlank(algo.getTrainBaseUrl())) {
return Map.of("code", 1, "msg", "算法或训练URL未配置");
}
String baseUrl = algo.getTrainBaseUrl();
if (baseUrl != null && baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
String url = baseUrl + "/v1/train/" + deviceType;
Map<String, Object> payload = new HashMap<>();
payload.put("dataset_path", datasetPath);
if (!isBlank(modelDir)) payload.put("model_dir", modelDir);
Map<String, Object> resp = httpPostJson(url, payload);
if (resp == null) return Map.of("code", 1, "msg", "训练接口无响应");
int code = 0;
Object codeObj = resp.get("code");
if (codeObj instanceof Number) {
code = ((Number) codeObj).intValue();
} else if (codeObj instanceof String) {
try {
code = Integer.parseInt((String) codeObj);
} catch (NumberFormatException ignored) {
code = 1;
}
}
if (code != 0) return Map.of("code", 1, "msg", "训练失败: " + str(resp.get("msg")));
Map<String, Object> data = castMap(resp.get("data"));
String modelPath = str(data.get("model_path"));
String metrics = toJson(data.get("metrics"));
AlgorithmModel model = new AlgorithmModel();
model.setAlgorithmModelId(UUID.randomUUID().toString());
model.setAlgorithmType(algorithmType);
model.setDeviceType(deviceType);
model.setMaterialType(materialType);
model.setVersionTag(genVersionTag());
model.setModelPath(modelPath);
model.setFeatureMapSnapshot(isBlank(featureMapSnapshot) ? "{}" : featureMapSnapshot);
model.setMetrics(metrics);
model.setTrainedAt(LocalDateTime.now());
model.setIsCurrent(activate ? 1 : 0);
model.setCreatedAt(LocalDateTime.now());
model.setUpdatedAt(LocalDateTime.now());
model.setModifier(currentUsername());
if (activate) {
QueryWrapper<AlgorithmModel> qw = new QueryWrapper<>();
qw.eq("algorithm_type", algorithmType).eq("device_type", deviceType);
if (!isBlank(materialType)) {
if ("Mixed".equals(materialType)) {
qw.in("material_type", List.of("Mixed", "MIX"));
} else {
qw.eq("material_type", materialType);
}
} else {
qw.and(wrapper -> wrapper.isNull("material_type").or().eq("material_type", ""));
}
AlgorithmModel upd = new AlgorithmModel();
upd.setIsCurrent(0);
algorithmModelService.update(upd, qw);
}
algorithmModelService.save(model);
return Map.of("code", 0, "msg", "训练成功", "data", model);
}
@Log(value = "在线训练(样本)", module = "算法模型管理")
// 在线训练样本集合
// @PostMapping("/train/samples")
@Operation(summary = "在线训练(样本集合)", description = "传入算法类型、设备类型与样本集,训练完成新增模型版本记录,可选激活")
public Map<String, Object> trainSamples(@RequestBody Map<String, Object> body) {
String algorithmType = str(body.get("algorithm_type"));
String deviceType = str(body.get("device_type"));
String materialType = normalizeMaterialType(str(body.getOrDefault("material_type", "")));
Object samples = body.get("samples"); // 期望为 List<Map>由前端提供
String modelDir = str(body.getOrDefault("model_dir", ""));
boolean activate = bool(body.getOrDefault("activate", false));
String featureMapSnapshot = toJson(body.get("feature_map_snapshot"));
if (isBlank(algorithmType) || isBlank(deviceType) || samples == null) {
return Map.of("code", 1, "msg", "algorithm_type/device_type/samples 必填");
}
Algorithm algo = getAlgorithmByType(algorithmType);
if (algo == null || isBlank(algo.getTrainBaseUrl())) {
return Map.of("code", 1, "msg", "算法或训练URL未配置");
}
String url = "";//trimSlash(algo.getTrainBaseUrl()) + "/v1/train/" + deviceType + "/from-samples";
Map<String, Object> payload = new HashMap<>();
payload.put("samples", samples);
if (!isBlank(modelDir)) payload.put("model_dir", modelDir);
Map<String, Object> resp = httpPostJson(url, payload);
if (resp == null) return Map.of("code", 1, "msg", "训练接口无响应");
int code = 0;//intVal(resp.get("code"));
if (code != 0) return Map.of("code", 1, "msg", "训练失败: " + str(resp.get("msg")));
Map<String, Object> data = castMap(resp.get("data"));
String modelPath = str(data.get("model_path"));
String metrics = toJson(data.get("metrics"));
AlgorithmModel model = new AlgorithmModel();
model.setAlgorithmModelId(UUID.randomUUID().toString());
model.setAlgorithmType(algorithmType);
model.setDeviceType(deviceType);
model.setMaterialType(materialType);
model.setVersionTag(genVersionTag());
model.setModelPath(modelPath);
model.setFeatureMapSnapshot(isBlank(featureMapSnapshot) ? "{}" : featureMapSnapshot);
model.setMetrics(metrics);
model.setTrainedAt(LocalDateTime.now());
model.setIsCurrent(activate ? 1 : 0);
model.setCreatedAt(LocalDateTime.now());
model.setUpdatedAt(LocalDateTime.now());
model.setModifier(currentUsername());
if (activate) {
QueryWrapper<AlgorithmModel> qw = new QueryWrapper<>();
qw.eq("algorithm_type", algorithmType).eq("device_type", deviceType);
if (!isBlank(materialType)) {
if ("Mixed".equals(materialType)) {
qw.in("material_type", List.of("Mixed", "MIX"));
} else {
qw.eq("material_type", materialType);
}
} else {
qw.and(wrapper -> wrapper.isNull("material_type").or().eq("material_type", ""));
}
AlgorithmModel upd = new AlgorithmModel();
upd.setIsCurrent(0);
algorithmModelService.update(upd, qw);
}
algorithmModelService.save(model);
return Map.of("code", 0, "msg", "训练成功", "data", model);
}
private String normalizeMaterialType(String raw) { private String normalizeMaterialType(String raw) {
if (raw == null) return null; if (raw == null) return null;
String s = raw.trim(); String s = raw.trim();
@ -365,6 +309,14 @@ public class AlgorithmModelController {
} }
/**
* 获取当前登录用户名
* <p>
* 用于落库的 modifier 字段若用户未登录或上下文解析失败返回 "anonymous"
* </p>
*
* @return 当前用户名或 "anonymous"
*/
private String currentUsername() { private String currentUsername() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@ -385,55 +337,4 @@ public class AlgorithmModelController {
return "anonymous"; return "anonymous";
} }
} }
private Algorithm getAlgorithmByType(String algorithmType) {
QueryWrapper<Algorithm> qw = new QueryWrapper<>();
qw.eq("algorithm_type", algorithmType);
return algorithmService.getOne(qw);
}
private Map<String, Object> httpPostJson(String url, Object payload) {
try {
String json = objectMapper.writeValueAsString(payload);
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8))
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (res.statusCode() >= 200 && res.statusCode() < 300) {
return objectMapper.readValue(res.body(), new TypeReference<Map<String, Object>>() {});
}
return Map.of("code", 1, "msg", "HTTP " + res.statusCode());
} catch (Exception e) {
return Map.of("code", 1, "msg", e.getMessage());
}
}
private String genVersionTag() {
return "v" + LocalDateTime.now().toString().replace(":", "-");
}
private String str(Object v) { return v == null ? "" : String.valueOf(v); }
private boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }
private boolean bool(Object v) {
if (v instanceof Boolean) return (Boolean) v;
String s = str(v).toLowerCase();
return "true".equals(s) || "1".equals(s) || "yes".equals(s);
}
private String toJson(Object v) {
try {
if (v == null) return null;
if (v instanceof String) return (String) v;
return objectMapper.writeValueAsString(v);
} catch (Exception e) {
return null;
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> castMap(Object v) {
if (v instanceof Map) return (Map<String, Object>) v;
return new HashMap<>();
}
} }

View File

@ -24,12 +24,32 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 临界数据管理接口
* <p>
* 提供临界数据的新增修改删除按设备类型查询以及导入/导出等管理能力
* </p>
* <p>
* 字段约定
* </p>
* <ul>
* <li>extraFeaturesJSON 字段前端传空字符串时需归一化为 null避免 JSON 列写入失败</li>
* <li>modifier最后修改人用户名取自当前登录上下文</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/critical-data") @RequestMapping("/critical-data")
public class CriticalDataController { public class CriticalDataController {
/**
* 临界数据服务MyBatis-Plus Service
*/
@Resource @Resource
private CriticalDataService criticalDataService; private CriticalDataService criticalDataService;
/**
* 当前用户信息服务用于记录 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
@ -51,6 +71,14 @@ public class CriticalDataController {
return criticalDataService.save(data); return criticalDataService.save(data);
} }
/**
* 获取当前登录用户名
* <p>
* 用于落库的 modifier 字段若用户未登录或上下文解析失败返回 "anonymous"
* </p>
*
* @return 当前用户名或 "anonymous"
*/
private String currentUsername() { private String currentUsername() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
@ -185,6 +213,12 @@ public class CriticalDataController {
.body(bytes); .body(bytes);
} }
/**
* 导出临界数据模板V2
*
* @param deviceType 设备类型
* @return 模板文件字节流
*/
@PreAuthorize("hasAuthority('criticalData:import')") @PreAuthorize("hasAuthority('criticalData:import')")
@GetMapping("/v2/template") @GetMapping("/v2/template")
public ResponseEntity<byte[]> templateCriticalDataV2(@RequestParam String deviceType) { public ResponseEntity<byte[]> templateCriticalDataV2(@RequestParam String deviceType) {

View File

@ -12,19 +12,40 @@ import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.util.Map; import java.util.Map;
/**
* 设备元数据接口
* <p>
* 提供设备尺寸 schemasize-schema等元数据供前端动态渲染表单导入导出与校验使用
* </p>
*/
@RestController @RestController
@RequestMapping("/devices/v2") @RequestMapping("/devices/v2")
@Tag(name = "设备元数据接口", description = "提供设备尺寸 size-schema 元数据,供前端动态渲染与校验使用") @Tag(name = "设备元数据接口", description = "提供设备尺寸 size-schema 元数据,供前端动态渲染与校验使用")
public class DeviceMetaController { public class DeviceMetaController {
/**
* 设备尺寸 schema 注册表
*/
@Resource @Resource
private DeviceSizeSchemaRegistry registry; private DeviceSizeSchemaRegistry registry;
/**
* 获取指定设备类型的尺寸 schema
*
* @param deviceType 设备类型
* @return 尺寸字段元数据
*/
@GetMapping("/size-schema") @GetMapping("/size-schema")
@Operation(summary = "获取指定设备类型的尺寸 schema", description = "返回 deviceType 对应的字段元数据key/label/unit/required/order/min/max用于前端动态渲染与与导入导出/推理口径对齐") @Operation(summary = "获取指定设备类型的尺寸 schema", description = "返回 deviceType 对应的字段元数据key/label/unit/required/order/min/max用于前端动态渲染与与导入导出/推理口径对齐")
public DeviceSizeSchema schema(@RequestParam String deviceType) { public DeviceSizeSchema schema(@RequestParam String deviceType) {
return registry.getSchema(deviceType); return registry.getSchema(deviceType);
} }
/**
* 获取全部设备类型的尺寸 schema
*
* @return deviceType -> schema 的映射
*/
@GetMapping("/size-schema/all") @GetMapping("/size-schema/all")
@Operation(summary = "获取全部设备类型的尺寸 schema", description = "返回所有 deviceType 的 schema Map适合前端一次性缓存") @Operation(summary = "获取全部设备类型的尺寸 schema", description = "返回所有 deviceType 的 schema Map适合前端一次性缓存")
public Map<String, DeviceSizeSchema> all() { public Map<String, DeviceSizeSchema> all() {

View File

@ -33,18 +33,50 @@ import org.apache.poi.ss.usermodel.*;
import java.io.InputStream; import java.io.InputStream;
import java.util.Iterator; import java.util.Iterator;
/**
* 情景事件管理接口
* <p>
* 提供事件的新增修改批量保存查询与导入等能力并通过 Scenario -> Project 的关系校验项目读写权限
* </p>
* <p>
* 事件与权限
* </p>
* <ul>
* <li>事件属于情景scenarioId通过情景关联项目projectId进行权限控制</li>
* <li>写操作需要项目写权限读操作需要项目读权限</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/events") @RequestMapping("/events")
public class EventController { public class EventController {
/**
* 事件服务MyBatis-Plus Service
*/
@Resource @Resource
private EventService eventService; private EventService eventService;
/**
* JSON 解析与转换
*/
@Resource @Resource
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
/**
* 当前用户信息服务用于记录 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
/**
* 情景服务用于根据 scenarioId 获取 projectId 并执行权限校验
*/
@Resource @Resource
private ScenarioService scenarioService; private ScenarioService scenarioService;
/**
* 项目权限辅助类用于对 projectId 的读写操作进行鉴权校验
*/
@Resource @Resource
private ProjectAccessHelper projectAccessHelper; private ProjectAccessHelper projectAccessHelper;
@ -436,6 +468,9 @@ public class EventController {
} }
private double getCellValueAsDouble(Cell cell) { private double getCellValueAsDouble(Cell cell) {
if (cell == null) {
return 0.0;
}
if (cell.getCellType() == CellType.NUMERIC) { if (cell.getCellType() == CellType.NUMERIC) {
return cell.getNumericCellValue(); return cell.getNumericCellValue();
} else if (cell.getCellType() == CellType.STRING) { } else if (cell.getCellType() == CellType.STRING) {
@ -482,6 +517,14 @@ public class EventController {
projectAccessHelper.assertCanWriteProject(sc.getProjectId()); projectAccessHelper.assertCanWriteProject(sc.getProjectId());
} }
/**
* 获取当前登录用户名
* <p>
* 用于落库的 modifier 字段若用户未登录或上下文解析失败返回 "anonymous"
* </p>
*
* @return 当前用户名或 "anonymous"
*/
private String currentUsername() { private String currentUsername() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();

View File

@ -4,9 +4,20 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/**
* 健康检查接口
* <p>
* 用于探活与连通性测试不依赖业务数据与权限
* </p>
*/
@RestController @RestController
public class HealthController { public class HealthController {
/**
* Ping 接口返回固定字符串 "ok"
*
* @return ok
*/
@GetMapping("/ping") @GetMapping("/ping")
public ResponseEntity<String> ping() { public ResponseEntity<String> ping() {
return ResponseEntity.ok("ok"); return ResponseEntity.ok("ok");

View File

@ -23,14 +23,38 @@ import jakarta.annotation.Resource;
import java.util.List; import java.util.List;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 物料管理接口模板库与项目物料
* <p>
* 提供物料的基础增删改查以及 Excel/CSV 导入导出与模板下载能力
* </p>
* <p>
* 权限与数据域
* </p>
* <ul>
* <li>模板库数据project_id = -1</li>
* <li>项目数据project_id != -1 通过 ProjectAccessHelper 校验读写权限</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/materials") @RequestMapping("/materials")
public class MaterialController { public class MaterialController {
/**
* 物料服务MyBatis-Plus Service
*/
@Resource @Resource
private MaterialService materialService; private MaterialService materialService;
/**
* 当前用户信息服务用于记录 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
/**
* 项目权限辅助类用于对 projectId 的读写操作进行鉴权校验
*/
@Resource @Resource
private ProjectAccessHelper projectAccessHelper; private ProjectAccessHelper projectAccessHelper;
@ -162,6 +186,11 @@ public class MaterialController {
} }
@GetMapping("/v2/template") @GetMapping("/v2/template")
/**
* 下载物料导入模板V2
*
* @return xlsx 模板字节流
*/
public ResponseEntity<byte[]> templateMaterialsV2() { public ResponseEntity<byte[]> templateMaterialsV2() {
byte[] bytes = materialService.templateMaterialsV2(); byte[] bytes = materialService.templateMaterialsV2();
return ResponseEntity.ok() return ResponseEntity.ok()
@ -171,6 +200,15 @@ public class MaterialController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
/**
* 根据主键查询物料
* <p>
* projectId != -1 时会校验项目读权限
* </p>
*
* @param id 物料主键
* @return 物料对象不存在时返回 null
*/
public Material getById(@PathVariable String id) { public Material getById(@PathVariable String id) {
Material m = materialService.getById(id); Material m = materialService.getById(id);
if (m != null && m.getProjectId() != null && !m.getProjectId().isBlank() && !"-1".equals(m.getProjectId())) { if (m != null && m.getProjectId() != null && !m.getProjectId().isBlank() && !"-1".equals(m.getProjectId())) {
@ -211,6 +249,14 @@ public class MaterialController {
} }
@GetMapping("/by-project") @GetMapping("/by-project")
/**
* 按项目分页查询物料列表
*
* @param projectId 项目ID
* @param pageNum 页码默认 1
* @param pageSize 每页条数默认 20
* @return 物料分页列表
*/
public Page<Material> pageByProject(@RequestParam String projectId, public Page<Material> pageByProject(@RequestParam String projectId,
@RequestParam(defaultValue = "1") long pageNum, @RequestParam(defaultValue = "1") long pageNum,
@RequestParam(defaultValue = "20") long pageSize) { @RequestParam(defaultValue = "20") long pageSize) {
@ -232,6 +278,14 @@ public class MaterialController {
return materialService.page(page, qw); return materialService.page(page, qw);
} }
/**
* 获取当前登录用户名
* <p>
* 用于落库的 modifier 字段若用户未登录或上下文解析失败返回 "anonymous"
* </p>
*
* @return 当前用户名或 "anonymous"
*/
private String currentUsername() { private String currentUsername() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();

View File

@ -16,21 +16,48 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.Map; import java.util.Map;
/**
* 模型训练任务接口
* <p>
* 提供训练任务的容量预检任务提交任务列表与详情查询训练状态回调接入以及模型发布与任务删除等能力
* </p>
* <p>
* 回调链路
* </p>
* <ul>
* <li>Python 侧回调 {@code /train/internal/callback}后端更新任务状态后通过 WebSocket 广播到 {@code /topic/train-status/all}</li>
* <li>前端列表页订阅广播主题以更新任务状态详情页可通过轮询 {@code /train/status/{taskId}} 获取最新状态</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/train") @RequestMapping("/train")
public class ModelTrainController { public class ModelTrainController {
/**
* 模型训练任务服务
*/
@Autowired @Autowired
private ModelTrainService modelTrainService; private ModelTrainService modelTrainService;
/**
* WebSocket 推送服务用于将训练状态广播给前端
*/
@Autowired @Autowired
private TrainWebSocketService trainWebSocketService; private TrainWebSocketService trainWebSocketService;
/**
* JSON 序列化与请求参数解析
*/
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@PreAuthorize("hasAuthority('modelTrain:add')") @PreAuthorize("hasAuthority('modelTrain:add')")
@GetMapping("/capacity/check") @GetMapping("/capacity/check")
/**
* 提交训练任务前的容量预检
*
* @return 容量检查结果包含 canSubmitcountslimits
*/
public ResponseResult capacityCheck() { public ResponseResult capacityCheck() {
return ResponseResult.successData(modelTrainService.checkCapacity()); return ResponseResult.successData(modelTrainService.checkCapacity());
} }
@ -39,6 +66,15 @@ public class ModelTrainController {
* 接收 Python 端的训练状态回调 * 接收 Python 端的训练状态回调
*/ */
@PostMapping("/internal/callback") @PostMapping("/internal/callback")
/**
* Python 训练状态回调入口
* <p>
* 回调体需包含 taskId status 等字段后端更新任务状态并通过 WebSocket 推送给前端
* </p>
*
* @param callbackData 回调数据
* @return 标准响应
*/
public ResponseResult handleTrainCallback(@RequestBody Map<String, Object> callbackData) { public ResponseResult handleTrainCallback(@RequestBody Map<String, Object> callbackData) {
System.out.println("====== 收到 Python 端训练回调 ======"); System.out.println("====== 收到 Python 端训练回调 ======");
System.out.println("回调数据: " + callbackData); System.out.println("回调数据: " + callbackData);
@ -62,6 +98,12 @@ public class ModelTrainController {
*/ */
@Log(value = "上传训练数据集", module = "模型训练") @Log(value = "上传训练数据集", module = "模型训练")
@PostMapping("/upload") @PostMapping("/upload")
/**
* 上传训练数据集并执行基础预检解析列名返回告警等
*
* @param file 数据集文件
* @return 上传与预检结果
*/
public ResponseResult upload(@RequestParam("file") MultipartFile file) { public ResponseResult upload(@RequestParam("file") MultipartFile file) {
return ResponseResult.successData(modelTrainService.uploadAndInspectDataset(file)); return ResponseResult.successData(modelTrainService.uploadAndInspectDataset(file));
} }
@ -72,6 +114,16 @@ public class ModelTrainController {
@Log(value = "提交训练任务", module = "模型训练") @Log(value = "提交训练任务", module = "模型训练")
@PreAuthorize("hasAuthority('modelTrain:add')") @PreAuthorize("hasAuthority('modelTrain:add')")
@PostMapping("/submit") @PostMapping("/submit")
/**
* 提交训练任务
* <p>
* 采用 multipart/form-data 传参task JSON 字符串file 为可选数据集文件若上传了文件优先以文件路径作为 datasetPath
* </p>
*
* @param taskJson 任务 JSON字符串
* @param file 数据集文件可选
* @return 提交结果data taskId
*/
public ResponseResult submit(@RequestPart("task") String taskJson, public ResponseResult submit(@RequestPart("task") String taskJson,
@RequestPart(value = "file", required = false) MultipartFile file) { @RequestPart(value = "file", required = false) MultipartFile file) {
try { try {
@ -105,6 +157,18 @@ public class ModelTrainController {
* 查询任务列表 (支持条件查询) * 查询任务列表 (支持条件查询)
*/ */
@GetMapping("/list") @GetMapping("/list")
/**
* 查询训练任务列表分页支持条件过滤
*
* @param current 当前页默认 1
* @param size 每页条数默认 10
* @param algorithmType 算法类型兼容 algorithmType/algoType 两种参数
* @param algoType 算法类型兼容字段
* @param deviceType 设备类型
* @param status 状态过滤
* @param name 名称关键字模糊匹配
* @return 分页结果
*/
public ResponseResult list(@RequestParam(defaultValue = "1") Integer current, public ResponseResult list(@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size, @RequestParam(defaultValue = "10") Integer size,
@RequestParam(value = "algorithmType", required = false) String algorithmType, @RequestParam(value = "algorithmType", required = false) String algorithmType,
@ -140,6 +204,12 @@ public class ModelTrainController {
* 查询任务详情/状态 * 查询任务详情/状态
*/ */
@GetMapping("/status/{taskId}") @GetMapping("/status/{taskId}")
/**
* 查询任务详情/状态
*
* @param taskId 任务ID
* @return 任务对象
*/
public ResponseResult status(@PathVariable String taskId) { public ResponseResult status(@PathVariable String taskId) {
ModelTrainTask task = modelTrainService.syncTaskStatus(taskId); ModelTrainTask task = modelTrainService.syncTaskStatus(taskId);
return ResponseResult.successData(task); return ResponseResult.successData(task);
@ -151,6 +221,15 @@ public class ModelTrainController {
@Log(value = "发布训练模型", module = "模型训练") @Log(value = "发布训练模型", module = "模型训练")
@PreAuthorize("hasAuthority('modelTrain:publish')") @PreAuthorize("hasAuthority('modelTrain:publish')")
@PostMapping("/publish") @PostMapping("/publish")
/**
* 发布模型
* <p>
* 仅允许对成功完成的训练任务进行发布versionTag 需满足版本号格式约束
* </p>
*
* @param body 包含 taskId versionTag
* @return 发布结果
*/
public ResponseResult publish(@RequestBody Map<String, String> body) { public ResponseResult publish(@RequestBody Map<String, String> body) {
String taskId = body.get("taskId"); String taskId = body.get("taskId");
String versionTag = body.get("versionTag"); String versionTag = body.get("versionTag");
@ -165,6 +244,12 @@ public class ModelTrainController {
@PreAuthorize("hasAuthority('modelTrain:del')") @PreAuthorize("hasAuthority('modelTrain:del')")
//删除训练任务 //删除训练任务
@DeleteMapping("/{taskId}") @DeleteMapping("/{taskId}")
/**
* 删除训练任务
*
* @param taskId 任务ID
* @return 删除结果
*/
public ResponseResult delete(@PathVariable String taskId) { public ResponseResult delete(@PathVariable String taskId) {
boolean success = modelTrainService.removeById(taskId); boolean success = modelTrainService.removeById(taskId);
return success ? ResponseResult.success() : ResponseResult.error("删除失败"); return success ? ResponseResult.success() : ResponseResult.error("删除失败");

View File

@ -24,17 +24,45 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 项目管理接口
* <p>
* 提供项目的增删改查权限可见性管理工程数据导入导出拓扑解析与仿真初始化相关能力
* </p>
* <p>
* 权限与可见性
* </p>
* <ul>
* <li>visibilityPRIVATE / READONLY / PUBLIC ProjectAccessHelper 统一归一化与鉴权</li>
* <li>非管理员用户查询项目列表时会按可见性与所有者过滤</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/projects") @RequestMapping("/projects")
@Tag(name = "项目接口", description = "项目增删改查、拓扑解析与模拟初始化") @Tag(name = "项目接口", description = "项目增删改查、拓扑解析与模拟初始化")
public class ProjectController { public class ProjectController {
/**
* 项目服务MyBatis-Plus Service
*/
@Resource @Resource
private ProjectService projectService; private ProjectService projectService;
/**
* JSON 序列化与拓扑字段解析
*/
@Resource @Resource
private com.fasterxml.jackson.databind.ObjectMapper objectMapper; private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
/**
* 当前用户信息服务用于获取用户名等信息
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
/**
* 项目权限辅助类用于对项目读写可见性修改等操作进行鉴权校验
*/
@Resource @Resource
private ProjectAccessHelper projectAccessHelper; private ProjectAccessHelper projectAccessHelper;
@ -98,6 +126,14 @@ public class ProjectController {
return projectService.updateById(p); return projectService.updateById(p);
} }
/**
* 获取当前登录账号
* <p>
* 用于 creator/modifier 等审计字段未登录或取不到用户信息时返回 "anonymous"
* </p>
*
* @return 当前账号或 "anonymous"
*/
private String currentAccount() { private String currentAccount() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();

View File

@ -18,14 +18,37 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 情景管理接口
* <p>
* 提供情景的新增修改删除按项目查询等能力并在写操作时进行项目写权限校验
* </p>
* <p>
* 状态约定
* </p>
* <ul>
* <li>status="0"默认初始状态待运行/未运行</li>
* </ul>
*/
@RestController @RestController
@RequestMapping("/scenarios") @RequestMapping("/scenarios")
public class ScenarioController { public class ScenarioController {
/**
* 情景服务MyBatis-Plus Service
*/
@Resource @Resource
private ScenarioService scenarioService; private ScenarioService scenarioService;
/**
* 当前用户信息服务用于记录 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
/**
* 项目权限辅助类用于对 projectId 的读写操作进行鉴权校验
*/
@Resource @Resource
private ProjectAccessHelper projectAccessHelper; private ProjectAccessHelper projectAccessHelper;
@ -206,6 +229,14 @@ public class ScenarioController {
return scenarioService.page(page, qw); return scenarioService.page(page, qw);
} }
/**
* 获取当前登录用户名
* <p>
* 用于落库的 modifier 字段若用户未登录或上下文解析失败返回 "anonymous"
* </p>
*
* @return 当前用户名或 "anonymous"
*/
private String currentUsername() { private String currentUsername() {
try { try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();

View File

@ -28,16 +28,37 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* 情景结果查询与导出接口
* <p>
* 提供情景仿真结果的分页查询与按条件导出 Excel 能力并通过 Scenario -> Project 的关系校验项目读权限
* </p>
*/
@RestController @RestController
@RequestMapping("/scenario-results") @RequestMapping("/scenario-results")
public class ScenarioResultController { public class ScenarioResultController {
/**
* 情景结果服务MyBatis-Plus Service
*/
@Resource @Resource
private ScenarioResultService scenarioResultService; private ScenarioResultService scenarioResultService;
/**
* 情景服务用于获取 scenarioId 对应的 projectId 并执行权限校验
*/
@Resource @Resource
private ScenarioService scenarioService; private ScenarioService scenarioService;
/**
* 项目权限辅助类用于对 projectId 的读操作进行鉴权校验
*/
@Resource @Resource
private ProjectAccessHelper projectAccessHelper; private ProjectAccessHelper projectAccessHelper;
/**
* 设备服务用于补全结果中的设备名称/类型等展示字段
*/
@Resource @Resource
private DeviceService deviceService; private DeviceService deviceService;

View File

@ -0,0 +1,29 @@
/**
* REST API Controller
* <p>
* Controller 负责对外协议处理与请求路由重点关注
* </p>
* <ul>
* <li>入参来源Path/Query/Body 的解析与校验</li>
* <li>权限校验在安全框架基础上增加必要的业务级访问控制例如项目权限</li>
* <li>业务委托不堆叠复杂业务逻辑主要调用 Service 层完成业务处理</li>
* <li>返回统一接口返回结构与错误码语义一致便于前端与测试定位</li>
* </ul>
* <p>
* 常见接口类型
* </p>
* <ul>
* <li>基础 CRUD项目场景设备事件材料关键数据等</li>
* <li>解析与校验拓扑解析属性解析数据校验与问题汇总</li>
* <li>推演与训练推演提交训练提交状态查询结果查询等</li>
* </ul>
* <p>
* 编码约定
* </p>
* <ul>
* <li>避免直接操作 Mapper统一通过 Service 层访问数据</li>
* <li>避免在 Controller 中拼装复杂对象复杂拼装放到 build/facade/model 相关包</li>
* <li>异常处理依赖统一异常机制Controller 内部尽量不捕获吞异常</li>
* </ul>
*/
package com.yfd.business.css.controller;

View File

@ -0,0 +1,20 @@
/**
* 领域实体Domain
* <p>
* 该包主要承载与数据库表结构强相关的数据对象Entity/PO用于持久化与查询映射
* DTO/Model 不同Domain 更强调
* </p>
* <ul>
* <li>字段与表结构一致便于 Mapper 层直接映射</li>
* <li>以数据承载为主尽量避免包含跨聚合业务逻辑</li>
* <li>序列化字段 JSON需要在接口层做输入规范化避免落库类型不匹配</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>Domain 用于持久化不作为接口出参的最终结构避免字段泄露与耦合</li>
* <li>字段校验非空范围格式 Controller/Service 层完成</li>
* </ul>
*/
package com.yfd.business.css.domain;

View File

@ -0,0 +1,20 @@
/**
* DTOData Transfer Object
* <p>
* DTO 用于跨层传递与接口出入参承载典型场景包括
* </p>
* <ul>
* <li>拓扑解析结果推演计划节点/边结构等结构化返回</li>
* <li>事件属性解析结果时间线点位分段信息等解析型数据</li>
* <li>面向前端的聚合视图对象避免直接暴露 Domain 字段</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>字段命名与前端协议保持一致减少序列化配置复杂度</li>
* <li>DTO 内部尽量不包含持久化细节主键策略表字段等</li>
* <li>集合字段尽量返回空集合而非 null减少前端判空负担</li>
* </ul>
*/
package com.yfd.business.css.dto;

View File

@ -14,15 +14,43 @@ import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
/** /**
* 仿真数据门面 * 仿真数据门面
* 负责与各个业务Service交互获取仿真所需的原始数据 (Project, Events ) * <p>
* 负责与各业务 Service 交互获取仿真所需的原始数据项目设备事件等并组装为统一的数据包对象供仿真流程使用
* </p>
* <p>
* 当前实现的职责边界
* </p>
* <ul>
* <li>只做数据拉取与聚合不做仿真计算与业务写入</li>
* <li>数据查询维度projectId 关联项目与设备scenarioId 关联事件</li>
* </ul>
*/ */
@Component @Component
public class SimDataFacade { public class SimDataFacade {
/**
* 项目服务用于获取项目基础信息与拓扑数据等
*/
@Autowired private ProjectService projectService; @Autowired private ProjectService projectService;
/**
* 事件服务用于加载情景下的事件列表
*/
@Autowired private EventService eventService; @Autowired private EventService eventService;
/**
* 设备服务用于加载项目下设备列表静态属性解析与补全
*/
@Autowired private DeviceService deviceService; @Autowired private DeviceService deviceService;
/**
* 加载仿真所需的基础数据并组装为数据包
*
* @param projectId 项目ID
* @param scenarioId 情景ID
* @return 仿真数据包包含 ProjectDevice 列表与 Event 列表
*/
public SimDataPackage loadSimulationData(String projectId, String scenarioId) { public SimDataPackage loadSimulationData(String projectId, String scenarioId) {
// 1. 获取项目与拓扑 // 1. 获取项目与拓扑
Project project = projectService.getById(projectId); Project project = projectService.getById(projectId);

View File

@ -0,0 +1,19 @@
/**
* Facade门面
* <p>
* 该包用于封装对外部依赖或跨领域聚合的调用细节对上层Controller/Service提供更稳定的调用接口
* </p>
* <ul>
* <li>聚合多个数据源/服务的结果降低上层编排复杂度</li>
* <li>对外部系统返回做统一解析容错与错误语义转换</li>
* <li>对外部调用的超时重试限流策略提供集中入口如有需要</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>Facade 不应绕过 Service 的业务约束避免形成第二套业务入口</li>
* <li>对外部调用涉及敏感信息时必须避免日志泄露</li>
* </ul>
*/
package com.yfd.business.css.facade;

View File

@ -0,0 +1,20 @@
/**
* Mapper数据访问
* <p>
* 该包用于定义数据库访问接口负责将 Domain 实体与数据库表进行映射典型职责
* </p>
* <ul>
* <li>基础 CRUD按主键/条件增删改查</li>
* <li>复杂查询按业务维度做聚合/统计/分页查询如有</li>
* <li>尽量不在 Mapper 层加入业务判断保持其数据访问属性</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>事务边界由 Service 层控制Mapper 不负责事务语义</li>
* <li>外部输入参数需在上层完成校验Mapper 层不承担入参清洗</li>
* <li>对于可能返回 null 的聚合结果 max/sum上层应做判空兜底</li>
* </ul>
*/
package com.yfd.business.css.mapper;

View File

@ -0,0 +1,19 @@
/**
* 元信息Meta定义与注册
* <p>
* 用于描述与设备属性尺寸等相关的元数据结构包括
* </p>
* <ul>
* <li>字段定义字段名类型单位取值约束等</li>
* <li>Schema/Registry对元数据进行注册查询与按类型匹配</li>
* <li>与业务实体解耦元信息侧重描述业务数据侧重实例</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>元信息应具备向后兼容策略避免升级导致历史数据不可解析</li>
* <li>注册表初始化应可控避免在静态初始化中引入外部依赖</li>
* </ul>
*/
package com.yfd.business.css.meta;

View File

@ -0,0 +1,26 @@
/**
* 业务过程模型Model
* <p>
* 该包用于承载推演/仿真过程中的上下文请求与响应结构结果转换等过程型对象典型用途
* </p>
* <ul>
* <li>推演请求/响应面向推演服务的请求体与结果结构</li>
* <li>推演上下文在一次推演流程内贯穿使用的临时数据与中间结果</li>
* <li>结果转换将底层数据结构转换为前端可展示的结构如轨迹影响链路</li>
* </ul>
* <p>
* Domain/DTO 的区别
* </p>
* <ul>
* <li>Domain更偏持久化实体Model更偏过程与计算语义</li>
* <li>DTO更偏接口传输Model更偏内部流程编排与输出组织</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>Model 应可序列化必要时并避免引入不必要的框架依赖</li>
* <li>对外输出字段应稳定避免随内部实现频繁变化</li>
* </ul>
*/
package com.yfd.business.css.model;

View File

@ -0,0 +1,54 @@
/**
* business-css 后端主模块
* <p>
* 本模块围绕仿真/场景/项目/设备/事件/材料/关键数据/训练任务等核心对象提供后端服务能力
* 主要以 REST API 形式对外提供增删改查解析仿真推演模型训练提交与状态查询等能力
* </p>
* <p>
* 设计目标
* </p>
* <ul>
* <li>职责清晰Controller 仅做入参校验与协议转换业务规则集中在 Service </li>
* <li>最小副作用对外接口尽量保持幂等避免隐藏状态修改</li>
* <li>可追溯关键动作与异常路径保持可定位性便于测试与运维排障</li>
* <li>可演进核心对象与流程以可扩展方式建模避免跨层强耦合</li>
* </ul>
* <p>
* 目录结构约定对应子 package
* </p>
* <ul>
* <li>build仿真数据构建与组装流程面向推演输入的结构化转换</li>
* <li>controller对外 HTTP API 负责请求路由鉴权入口与参数校验</li>
* <li>service领域服务接口层定义业务能力边界与业务语义</li>
* <li>service.impl领域服务实现层封装业务规则事务语义与外部依赖调用</li>
* <li>domain领域实体与表结构/持久化对象强相关</li>
* <li>dto跨层传输对象接口返回解析结果拓扑结构等</li>
* <li>mapper数据访问层MyBatis/MyBatis-Plus Mapper</li>
* <li>model业务过程模型推演上下文推演结果请求/响应结构</li>
* <li>meta设备/属性等元信息定义与注册表</li>
* <li>facade面向外部依赖的聚合门面减少 Controller/ServiceImpl 直接依赖外部细节</li>
* <li>security与项目访问控制相关的辅助能力在现有安全框架内做业务级校验</li>
* <li>configSpring Boot 配置类MyBatisOpenAPIWebSocketRestTemplate </li>
* <li>utils业务内工具类解析转换拼装等与通用框架工具区分</li>
* <li>common.exception业务异常定义区分参数错误状态错误推演错误等</li>
* </ul>
* <p>
* 编码约定
* </p>
* <ul>
* <li>接口返回统一成功/失败结构统一错误信息可读且便于定位</li>
* <li>空值与边界对外输入必须显式校验对外输出避免返回 null 集合</li>
* <li>外部返回解析对外系统返回字段例如 code/msg/data需做健壮解析与容错</li>
* <li>序列化DTO/Model 中字段命名与前端约定一致避免隐式字段映射</li>
* <li>日志仅记录业务必要信息不记录密钥口令token 等敏感数据</li>
* </ul>
* <p>
* 运行期约束
* </p>
* <ul>
* <li>认证统一使用平台侧鉴权机制JWT + 在线状态校验</li>
* <li>权限项目为业务隔离边界读写操作需校验项目可访问性</li>
* <li>并发训练/推演等重任务需受容量控制与超时回收机制约束</li>
* </ul>
*/
package com.yfd.business.css;

View File

@ -0,0 +1,20 @@
/**
* 业务安全辅助能力
* <p>
* 在平台安全框架认证鉴权Token 校验等之上该包提供与业务对象相关的访问控制辅助能力
* 常见场景为按项目隔离的访问检查
* </p>
* <ul>
* <li>对外接口封装用户是否可访问某项目/资源的判断</li>
* <li>最小暴露不在此处扩散安全框架细节避免上层出现大量重复判断</li>
* <li>失败语义权限不足时给出明确的错误信息与统一的返回结构</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>安全校验为前置约束避免业务执行到中途再拒绝导致数据不一致</li>
* <li>不记录敏感信息token密钥口令等</li>
* </ul>
*/
package com.yfd.business.css.security;

View File

@ -5,6 +5,13 @@ import com.yfd.business.css.domain.AlgorithmModel;
import java.util.List; import java.util.List;
/**
* 算法模型版本服务接口
* <p>
* 提供模型版本查询当前激活版本解析以及批量删除前置校验等能力
* 默认 CRUD 能力由 MyBatis-Plus {@link IService} 提供
* </p>
*/
public interface AlgorithmModelService extends IService<AlgorithmModel> { public interface AlgorithmModelService extends IService<AlgorithmModel> {
/** /**
@ -36,6 +43,12 @@ public interface AlgorithmModelService extends IService<AlgorithmModel> {
*/ */
AlgorithmModel getCurrentModel(String algorithmType, String deviceType, String materialType); AlgorithmModel getCurrentModel(String algorithmType, String deviceType, String materialType);
/**
* 批量删除模型版本带业务校验
*
* @param ids 模型主键列表
* @return 是否删除成功
*/
boolean deleteBatchWithCheck(List<String> ids); boolean deleteBatchWithCheck(List<String> ids);
} }

View File

@ -3,5 +3,11 @@ package com.yfd.business.css.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.business.css.domain.Algorithm; import com.yfd.business.css.domain.Algorithm;
/**
* 算法字典服务接口
* <p>
* 定义算法字典Algorithm的基础数据访问与管理能力默认 CRUD 能力由 MyBatis-Plus {@link IService} 提供
* </p>
*/
public interface AlgorithmService extends IService<Algorithm> { public interface AlgorithmService extends IService<Algorithm> {
} }

View File

@ -6,20 +6,59 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 临界数据服务接口
* <p>
* 提供临界数据的导入导出校验清理等业务能力默认 CRUD 能力由 MyBatis-Plus {@link IService} 提供
* </p>
*/
public interface CriticalDataService extends IService<CriticalData> { public interface CriticalDataService extends IService<CriticalData> {
/** /**
* 导入临界数据 * 导入临界数据
*/ */
boolean importCriticalData(MultipartFile file, String deviceType); boolean importCriticalData(MultipartFile file, String deviceType);
/**
* 导入临界数据V2
*
* @param file Excel/CSV 文件
* @param deviceType 设备类型
* @return 是否导入成功
*/
boolean importCriticalDataV2(MultipartFile file, String deviceType); boolean importCriticalDataV2(MultipartFile file, String deviceType);
/**
* 校验临界数据导入文件V2
*
* @param file Excel/CSV 文件
* @param deviceType 设备类型
* @return 校验结果包含错误可导入行等
*/
Map<String, Object> validateCriticalDataV2(MultipartFile file, String deviceType); Map<String, Object> validateCriticalDataV2(MultipartFile file, String deviceType);
/**
* 导出临界数据V2
*
* @param deviceType 设备类型
* @param ids 指定导出记录ID列表可选
* @return xlsx 文件字节流
*/
byte[] exportCriticalDataV2(String deviceType, List<String> ids); byte[] exportCriticalDataV2(String deviceType, List<String> ids);
/**
* 下载临界数据导入模板V2
*
* @param deviceType 设备类型
* @return xlsx 模板字节流
*/
byte[] templateCriticalDataV2(String deviceType); byte[] templateCriticalDataV2(String deviceType);
/**
* 按设备类型清空临界数据
*
* @param deviceType 设备类型
* @return 删除条数
*/
int deleteByDeviceType(String deviceType); int deleteByDeviceType(String deviceType);
} }

View File

@ -28,6 +28,20 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.*; import java.util.*;
/**
* 设备推理服务
* <p>
* 将仿真计算得到的设备时序数据 deviceType/materialType 分组转换为推理请求
* 调用 Python 推理服务并将推理结果写入情景结果表
* </p>
* <p>
* 推理分组策略由业务约定驱动
* </p>
* <ul>
* <li>按设备类型分组 按算法类型分组全局/设备级覆盖 按材料类型分组 按模型ID分组可选</li>
* <li>若未指定模型ID则使用当前激活版本的模型</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class DeviceInferService { public class DeviceInferService {
@ -39,16 +53,34 @@ public class DeviceInferService {
@Value("${file-space.model-path}") @Value("${file-space.model-path}")
private String modelRootPath; private String modelRootPath;
/**
* 情景服务用于读取情景配置全局算法类型设备级算法配置等
*/
@Resource @Resource
private ScenarioService scenarioService; private ScenarioService scenarioService;
/**
* 模型版本服务用于解析当前激活模型或按 ID 定向加载模型版本
*/
@Resource @Resource
private AlgorithmModelService algorithmModelService; private AlgorithmModelService algorithmModelService;
/**
* 情景结果服务用于写入推理结果与失败信息
*/
@Resource @Resource
private ScenarioResultService scenarioResultService; private ScenarioResultService scenarioResultService;
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
/**
* 对项目某情景下的设备数据进行推理并落库
*
* @param projectId 项目ID
* @param scenarioId 情景ID
* @param groupedDevices deviceType -> 设备时间步数据列表
*/
public void processDeviceInference(String projectId, String scenarioId, public void processDeviceInference(String projectId, String scenarioId,
Map<String, List<DeviceStepInfo>> groupedDevices) { Map<String, List<DeviceStepInfo>> groupedDevices) {
// 增加标志位记录是否至少成功执行了一次推理 // 增加标志位记录是否至少成功执行了一次推理

View File

@ -5,21 +5,69 @@ import com.yfd.business.css.domain.Device;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 设备服务接口
* <p>
* 提供设备模板库/项目设备的业务操作能力包括新增更新以及 Excel 导入导出等
* </p>
*/
public interface DeviceService extends IService<Device> { public interface DeviceService extends IService<Device> {
/** /**
* 导入设备 * 导入设备
*/ */
boolean importDevices(MultipartFile file, String deviceType); boolean importDevices(MultipartFile file, String deviceType);
/**
* 导入设备V2
*
* @param file Excel/CSV 文件
* @param projectId 项目ID为空时一般表示模板库
* @param deviceType 设备类型
* @return 是否导入成功
*/
boolean importDevicesV2(MultipartFile file, String projectId, String deviceType); boolean importDevicesV2(MultipartFile file, String projectId, String deviceType);
/**
* 校验设备导入文件V2
*
* @param file Excel/CSV 文件
* @param projectId 项目ID
* @param deviceType 设备类型
* @return 校验结果包含错误可导入行等
*/
Map<String, Object> validateDevicesV2(MultipartFile file, String projectId, String deviceType); Map<String, Object> validateDevicesV2(MultipartFile file, String projectId, String deviceType);
/**
* 导出设备V2
*
* @param projectId 项目ID
* @param deviceType 设备类型
* @param ids 指定导出设备ID列表可选
* @return xlsx 文件字节流
*/
byte[] exportDevicesV2(String projectId, String deviceType, List<String> ids); byte[] exportDevicesV2(String projectId, String deviceType, List<String> ids);
/**
* 下载设备导入模板V2
*
* @param deviceType 设备类型
* @return xlsx 模板字节流
*/
byte[] templateDevicesV2(String deviceType); byte[] templateDevicesV2(String deviceType);
/**
* 新增设备包含业务侧字段补全与校验
*
* @param device 设备对象
* @return 是否新增成功
*/
boolean createDevice(Device device) ; boolean createDevice(Device device) ;
/**
* 保存或更新设备按业务主键/唯一键语义处理
*
* @param device 设备对象
* @return 是否保存/更新成功
*/
boolean saveOrUpdateByBusiness(Device device); boolean saveOrUpdateByBusiness(Device device);
} }

View File

@ -3,5 +3,11 @@ package com.yfd.business.css.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.business.css.domain.Event; import com.yfd.business.css.domain.Event;
/**
* 事件服务接口
* <p>
* 定义事件Event的基础数据访问与管理能力默认 CRUD 能力由 MyBatis-Plus {@link IService} 提供
* </p>
*/
public interface EventService extends IService<Event> { public interface EventService extends IService<Event> {
} }

View File

@ -6,6 +6,12 @@ import com.yfd.business.css.domain.Material;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
/**
* 物料服务接口
* <p>
* 提供物料模板库/项目物料的业务操作能力包括导入导出保存与业务维度的保存/更新等
* </p>
*/
public interface MaterialService extends IService<Material> { public interface MaterialService extends IService<Material> {
/** /**
* 导入物料 * 导入物料
@ -17,9 +23,28 @@ public interface MaterialService extends IService<Material> {
*/ */
boolean saveMaterial(Material material); boolean saveMaterial(Material material);
/**
* 保存或更新物料按业务主键/唯一键语义处理
*
* @param material 物料对象
* @return 是否保存/更新成功
*/
boolean saveOrUpdateByBusiness(Material material); boolean saveOrUpdateByBusiness(Material material);
/**
* 导出物料V2
*
* @param projectId 项目ID为空时一般表示模板库
* @param ids 指定导出记录ID列表可选
* @param nameLike 名称模糊筛选可选
* @return xlsx 文件字节流
*/
byte[] exportMaterialsV2(String projectId, List<String> ids, String nameLike); byte[] exportMaterialsV2(String projectId, List<String> ids, String nameLike);
/**
* 下载物料导入模板V2
*
* @return xlsx 模板字节流
*/
byte[] templateMaterialsV2(); byte[] templateMaterialsV2();
} }

View File

@ -7,6 +7,12 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 模型训练任务服务接口
* <p>
* 提供数据集上传与解析训练任务提交状态同步与回调更新以及模型发布等能力
* </p>
*/
public interface ModelTrainService extends IService<ModelTrainTask> { public interface ModelTrainService extends IService<ModelTrainTask> {
/** /**
* 上传数据集 * 上传数据集
@ -15,8 +21,20 @@ public interface ModelTrainService extends IService<ModelTrainTask> {
*/ */
String uploadDataset(MultipartFile file); String uploadDataset(MultipartFile file);
/**
* 解析数据集列名列表
*
* @param datasetPath 数据集文件路径
* @return 列名列表
*/
List<String> parseDatasetColumns(String datasetPath); List<String> parseDatasetColumns(String datasetPath);
/**
* 上传数据集并立即进行字段解析与基础检查
*
* @param file 上传文件
* @return 预检结果包含 path/columns/warnings
*/
Map<String, Object> uploadAndInspectDataset(MultipartFile file); Map<String, Object> uploadAndInspectDataset(MultipartFile file);
/** /**
@ -48,5 +66,10 @@ public interface ModelTrainService extends IService<ModelTrainTask> {
*/ */
boolean publishModel(String taskId, String versionTag); boolean publishModel(String taskId, String versionTag);
/**
* 查询训练任务提交容量Training/Pending 上限与当前计数
*
* @return 容量检查结果包含 canSubmitcountslimits
*/
Map<String, Object> checkCapacity(); Map<String, Object> checkCapacity();
} }

View File

@ -6,6 +6,12 @@ import com.yfd.business.css.model.SimInfluenceNode;
import java.util.List; import java.util.List;
/**
* 项目服务接口
* <p>
* 提供项目的基础 CRUD工程导入导出拓扑解析仿真初始化与运行等业务能力
* </p>
*/
public interface ProjectService extends IService<Project> { public interface ProjectService extends IService<Project> {
/** /**

View File

@ -3,5 +3,11 @@ package com.yfd.business.css.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.business.css.domain.ScenarioResult; import com.yfd.business.css.domain.ScenarioResult;
/**
* 情景结果服务接口
* <p>
* 定义情景仿真结果ScenarioResult的基础数据访问与管理能力默认 CRUD 能力由 MyBatis-Plus {@link IService} 提供
* </p>
*/
public interface ScenarioResultService extends IService<ScenarioResult> { public interface ScenarioResultService extends IService<ScenarioResult> {
} }

View File

@ -3,7 +3,18 @@ package com.yfd.business.css.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.business.css.domain.Scenario; import com.yfd.business.css.domain.Scenario;
/**
* 情景服务接口
* <p>
* 定义情景Scenario的基础数据访问与管理能力默认 CRUD 能力由 MyBatis-Plus {@link IService} 提供
* </p>
*/
public interface ScenarioService extends IService<Scenario> { public interface ScenarioService extends IService<Scenario> {
//根据场景id获取算法类型 /**
* 根据情景 ID 获取该情景配置的算法类型
*
* @param scenarioId 情景ID
* @return 算法类型
*/
String getAlgorithmType(String scenarioId); String getAlgorithmType(String scenarioId);
} }

View File

@ -3,6 +3,18 @@ package com.yfd.business.css.service;
import com.yfd.business.css.model.SimulationRequest; import com.yfd.business.css.model.SimulationRequest;
import com.yfd.business.css.model.SimulationResult; import com.yfd.business.css.model.SimulationResult;
/**
* 仿真服务接口
* <p>
* 定义仿真执行的统一入口具体实现可根据不同仿真引擎/算法进行扩展
* </p>
*/
public interface SimulationService { public interface SimulationService {
/**
* 执行一次仿真并返回结果
*
* @param request 仿真请求参数
* @return 仿真结果
*/
SimulationResult runSimulation(SimulationRequest request); SimulationResult runSimulation(SimulationRequest request);
} }

View File

@ -6,6 +6,16 @@ import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.Map; import java.util.Map;
/**
* 训练状态 WebSocket 推送服务
* <p>
* 将训练任务状态变化通过 STOMP 广播给前端支持
* </p>
* <ul>
* <li>单任务主题/topic/train-status/{taskId}详情页可用</li>
* <li>全局主题/topic/train-status/all列表页可用</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class TrainWebSocketService { public class TrainWebSocketService {

View File

@ -12,6 +12,21 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* 算法模型版本服务实现
* <p>
* 提供当前激活模型的查询 algorithmType/deviceType/materialType 维度以及模型版本的批量删除校验能力
* </p>
* <p>
* 业务约定
* </p>
* <ul>
* <li>当前版本is_current=1</li>
* <li>材料类型兼容Mixed MIX 视为同一类</li>
* <li>material_type 为空时优先匹配空/NULL再作为回退版本使用</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class AlgorithmModelServiceImpl extends ServiceImpl<AlgorithmModelMapper, AlgorithmModel> implements AlgorithmModelService { public class AlgorithmModelServiceImpl extends ServiceImpl<AlgorithmModelMapper, AlgorithmModel> implements AlgorithmModelService {

View File

@ -7,6 +7,12 @@ import com.yfd.business.css.service.AlgorithmService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/**
* 算法字典服务实现
* <p>
* 在保存/更新算法字典时对部分 JSON 字段进行归一化处理避免空字符串导致解析失败或前端渲染异常
* </p>
*/
@Slf4j @Slf4j
@Service @Service
public class AlgorithmServiceImpl extends ServiceImpl<AlgorithmMapper, Algorithm> implements AlgorithmService { public class AlgorithmServiceImpl extends ServiceImpl<AlgorithmMapper, Algorithm> implements AlgorithmService {
@ -22,6 +28,14 @@ public class AlgorithmServiceImpl extends ServiceImpl<AlgorithmMapper, Algorithm
return super.updateById(entity); return super.updateById(entity);
} }
/**
* 归一化算法字典中的 JSON 字段为空值
* <p>
* 将空/空白字符串替换为默认 JSON{} []保证字段在下游解析时始终为合法 JSON
* </p>
*
* @param a 算法对象
*/
private void normalizeJsonFields(Algorithm a) { private void normalizeJsonFields(Algorithm a) {
if (a == null) return; if (a == null) return;
a.setInputParams(blankToDefaultJson(a.getInputParams(), "{}")); a.setInputParams(blankToDefaultJson(a.getInputParams(), "{}"));

View File

@ -37,13 +37,36 @@ import java.util.Collections;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/**
* 临界数据服务实现
* <p>
* 提供临界数据的 Excel 导入校验导出与模板生成能力并在导入过程中做字段解析与数据规整
* </p>
* <p>
* 设计要点
* </p>
* <ul>
* <li>导入支持 xls/xlsxV2 提供先校验后导入的交互方式</li>
* <li>对数值/公式单元格使用 DataFormatter + FormulaEvaluator 进行兼容读取</li>
* <li>解析失败信息以结构化 errors 列表返回给前端便于定位问题行</li>
* </ul>
*/
@Service @Service
@Slf4j @Slf4j
public class CriticalDataServiceImpl public class CriticalDataServiceImpl
extends ServiceImpl<CriticalDataMapper, CriticalData> extends ServiceImpl<CriticalDataMapper, CriticalData>
implements CriticalDataService { implements CriticalDataService {
/**
* JSON 解析器用于处理扩展字段与自定义属性的解析
*/
@Resource @Resource
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
/**
* 当前用户信息服务用于写入 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
@Override @Override

View File

@ -37,15 +37,40 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
/**
* 设备服务实现
* <p>
* 提供设备的导入导出 V2 校验与模板以及设备新增/更新等能力并对设备尺寸等字段按 schema 做解析与规整
* </p>
* <p>
* 设计要点
* </p>
* <ul>
* <li>导入支持 xls/xlsxV2 提供先校验后导入的交互方式</li>
* <li>deviceSizeSchemaRegistry 用于按 deviceType 获取尺寸字段元数据以统一导入/导出/校验口径</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class DeviceServiceImpl public class DeviceServiceImpl
extends ServiceImpl<DeviceMapper, Device> extends ServiceImpl<DeviceMapper, Device>
implements DeviceService { implements DeviceService {
/**
* JSON 解析器用于处理扩展字段与自定义属性的解析
*/
@Resource @Resource
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
/**
* 当前用户信息服务用于写入 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
/**
* 设备尺寸 schema 注册表用于按设备类型解析/校验尺寸字段
*/
@Resource @Resource
private DeviceSizeSchemaRegistry deviceSizeSchemaRegistry; private DeviceSizeSchemaRegistry deviceSizeSchemaRegistry;
@Override @Override

View File

@ -7,6 +7,12 @@ import com.yfd.business.css.service.EventService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/**
* 事件服务实现
* <p>
* 事件数据主要由上层控制器按情景维度进行读写与权限校验此处提供基于 MyBatis-Plus 的基础 CRUD 能力
* </p>
*/
@Slf4j @Slf4j
@Service @Service
public class EventServiceImpl public class EventServiceImpl

View File

@ -35,13 +35,34 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/**
* 物料服务实现
* <p>
* 提供物料的 Excel 导入导出模板生成以及物料新增/更新的业务语义处理 materialId 自动生成
* </p>
* <p>
* 设计要点
* </p>
* <ul>
* <li>导入支持 xls/xlsx通过表头映射进行字段解析</li>
* <li>custom_attrs 字段以 JSON 形式保存需要做合法性校验与空值规整</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class MaterialServiceImpl public class MaterialServiceImpl
extends ServiceImpl<MaterialMapper, Material> extends ServiceImpl<MaterialMapper, Material>
implements MaterialService { implements MaterialService {
/**
* JSON 解析器用于处理自定义属性等 JSON 字段
*/
@Resource @Resource
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
/**
* 当前用户信息服务用于写入 modifier 等审计字段
*/
@Resource @Resource
private IUserService userService; private IUserService userService;
@Override @Override

View File

@ -15,6 +15,7 @@ import com.yfd.business.css.service.ModelTrainService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -23,6 +24,8 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -53,6 +56,31 @@ import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.regex.Matcher; import java.util.regex.Matcher;
/**
* 模型训练任务服务实现
* <p>
* 该类负责训练数据集上传与解析训练任务创建与提交与外部 Python 训练服务交互
* 以及训练任务状态流转与超时回收等能力
* </p>
* <p>
* 关键流程
* </p>
* <ul>
* <li>数据集上传接收 MultipartFile将文件落盘并返回可追溯路径</li>
* <li>数据集检查解析表头/字段并返回给前端用于配置训练参数</li>
* <li>任务提交创建训练任务初始化状态并触发异步训练调用</li>
* <li>容量控制限制 Training 并发上限与 Pending 排队上限</li>
* <li>超时回收定时扫描超时的 Training 任务并置为失败防止容量被长期占用</li>
* </ul>
* <p>
* 约束与注意事项
* </p>
* <ul>
* <li>路径通过配置注入避免环境差异导致不可写目录</li>
* <li>外部返回字段需要做健壮解析避免 null/空字符串/非数字导致异常</li>
* <li>回调更新需要具备幂等语义避免重复回调造成状态覆盖</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, ModelTrainTask> implements ModelTrainService { public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, ModelTrainTask> implements ModelTrainService {
@ -75,6 +103,10 @@ public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, Mod
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@Autowired
@Lazy
private ModelTrainServiceImpl self;
private static final Pattern VERSION_TAG_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$"); private static final Pattern VERSION_TAG_PATTERN = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$");
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
private static final Pattern DERIVED_EXPR_ALLOWED_PATTERN = Pattern.compile("^[A-Za-z0-9_+\\-*/()\\s]+$"); private static final Pattern DERIVED_EXPR_ALLOWED_PATTERN = Pattern.compile("^[A-Za-z0-9_+\\-*/()\\s]+$");
@ -82,6 +114,20 @@ public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, Mod
private static final int MAX_PENDING = 5; private static final int MAX_PENDING = 5;
private static final int TRAINING_TIMEOUT_MINUTES = 120; private static final int TRAINING_TIMEOUT_MINUTES = 120;
/**
* 上传训练数据集文件并返回保存后的绝对路径
* <p>
* 文件将按日期目录分组保存文件名使用 UUID避免并发上传冲突
* </p>
* <ul>
* <li>入参校验上传文件不能为空</li>
* <li>路径解析支持相对路径配置运行时转换为绝对路径</li>
* <li>目录创建目录不存在时创建目录并处理并发创建的情况</li>
* </ul>
*
* @param file 上传文件
* @return 保存后的绝对路径
*/
@Override @Override
public String uploadDataset(MultipartFile file) { public String uploadDataset(MultipartFile file) {
if (file.isEmpty()) { if (file.isEmpty()) {
@ -129,6 +175,16 @@ public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, Mod
} }
} }
/**
* 解析数据集列名列表
* <p>
* 该方法用于前端在配置训练参数时选择输入特征列/输出列等字段
* 当前支持 CSV Excelxls/xlsx两类格式并对不支持的格式返回明确错误语义
* </p>
*
* @param datasetPath 数据集文件路径绝对路径
* @return 列名列表
*/
@Override @Override
public List<String> parseDatasetColumns(String datasetPath) { public List<String> parseDatasetColumns(String datasetPath) {
if (datasetPath == null || datasetPath.isBlank()) { if (datasetPath == null || datasetPath.isBlank()) {
@ -148,6 +204,20 @@ public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, Mod
throw new BizException("不支持的数据集格式: " + p.getFileName()); throw new BizException("不支持的数据集格式: " + p.getFileName());
} }
/**
* 上传数据集并立即执行字段解析与基础检查
* <p>
* 适用于一步上传并预检的页面交互返回结构包含
* </p>
* <ul>
* <li>path保存后的数据集路径</li>
* <li>columns解析出的列名列表</li>
* <li>warnings字段与内容的告警信息用于提示潜在配置错误</li>
* </ul>
*
* @param file 上传文件
* @return 预检结果
*/
@Override @Override
public Map<String, Object> uploadAndInspectDataset(MultipartFile file) { public Map<String, Object> uploadAndInspectDataset(MultipartFile file) {
String path = uploadDataset(file); String path = uploadDataset(file);
@ -183,12 +253,25 @@ public class ModelTrainServiceImpl extends ServiceImpl<ModelTrainTaskMapper, Mod
this.save(task); this.save(task);
// 2. 异步调用 Python 训练 // 2. 调用 Python 训练
asyncCallTrain(task); runAfterCommit(() -> self.asyncCallTrain(task));
return task.getTaskId(); return task.getTaskId();
} }
private void runAfterCommit(Runnable runnable) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
runnable.run();
}
});
return;
}
runnable.run();
}
@Override @Override
public Map<String, Object> checkCapacity() { public Map<String, Object> checkCapacity() {
long training = countActiveTraining(); long training = countActiveTraining();

View File

@ -51,6 +51,30 @@ import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.math.BigDecimal; import java.math.BigDecimal;
/**
* 项目域核心服务实现
* <p>
* 该类聚合了项目相关的高频业务能力覆盖项目基础信息拓扑解析与校验设备执行顺序解析
* 画布视图数据组织推演初始化与运行工程数据 Excel 导入导出等场景
* </p>
* <p>
* 主要能力边界
* </p>
* <ul>
* <li>数据读取通过 Project/Device/Material/Scenario/Event 等服务与 Mapper 获取所需数据</li>
* <li>结构解析解析项目 topology JSON生成节点/边与线性执行计划并输出问题列表</li>
* <li>推演编排组织推演输入上下文计算设备顺序与材料注入信息输出推演可用的数据结构</li>
* <li>工程数据支持工程 Excel 的导入/导出用于项目初始化备份与批量维护</li>
* </ul>
* <p>
* 运行与质量约束
* </p>
* <ul>
* <li>健壮性 JSON 字段缺失空字符串非法引用等情况给出明确问题列表或异常语义</li>
* <li>一致性涉及级联删除/批量导入时需要保证项目内相关实体的一致性与可追溯性</li>
* <li>性能导入导出与解析逻辑避免一次性构造超大字符串必要处做长度截断与分段处理</li>
* </ul>
*/
@Slf4j @Slf4j
@Service @Service
public class ProjectServiceImpl public class ProjectServiceImpl
@ -78,6 +102,19 @@ public class ProjectServiceImpl
this.deviceDataParser = deviceDataParser; this.deviceDataParser = deviceDataParser;
} }
/**
* 导出全部项目数据到 Excelxlsx
* <p>
* 主要用于后台管理侧的项目清单导出字段包含项目编号名称描述拓扑文本与时间字段等
* </p>
* <ul>
* <li>排序按创建时间倒序输出</li>
* <li>长度限制拓扑字段受 Excel 单元格长度限制超长文本会被截断并追加省略号</li>
* <li>异常语义导出失败抛出运行时异常交由上层统一异常处理机制返回</li>
* </ul>
*
* @return Excel 二进制内容
*/
@Override @Override
public byte[] exportAllProjectsExcel() { public byte[] exportAllProjectsExcel() {
log.info("exportAllProjectsExcel start"); log.info("exportAllProjectsExcel start");
@ -113,16 +150,26 @@ public class ProjectServiceImpl
if (s == null) return ""; if (s == null) return "";
int max = 32767; int max = 32767;
if (s.length() <= max) return s; if (s.length() <= max) return s;
if (max <= 3) return s.substring(0, max);
return s.substring(0, max - 3) + "..."; return s.substring(0, max - 3) + "...";
} }
@Override
/** /**
* 解析指定项目的拓扑结构生成节点边与线性计算计划 * 解析指定项目的拓扑结构生成节点边与线性计算计划
* <p>
* topology 期望为 JSON 字符串内部包含设备列表与连接关系等结构
* 解析结果将包含
* </p>
* <ul>
* <li>nodes拓扑节点列表</li>
* <li>edges拓扑边列表</li>
* <li>plans线性执行计划用于后续计算顺序/推演编排</li>
* <li>issues解析过程中发现的问题列表例如字段缺失引用不存在等</li>
* </ul>
*
* @param projectId 项目ID * @param projectId 项目ID
* @return TopologyParseResult * @return TopologyParseResult
*/ */
@Override
public TopologyParseResult parseTopology(String projectId) { public TopologyParseResult parseTopology(String projectId) {
log.info("parseTopology start projectId={}", projectId); log.info("parseTopology start projectId={}", projectId);
try { try {
@ -386,6 +433,20 @@ public class ProjectServiceImpl
return t; return t;
} }
/**
* topology 中的设备出现顺序解析设备列表
* <p>
* 与直接按数据库字段排序不同该方法以 topology JSON devices 数组的顺序为准
* 并将其映射为对应的设备实体列表
* </p>
* <ul>
* <li>当项目不存在或 topology 为空时返回空列表</li>
* <li>当某些 deviceId 在数据库中不存在时会自动跳过缺失设备</li>
* </ul>
*
* @param projectId 项目ID
* @return 设备列表保持 topology 顺序
*/
@Override @Override
public List<Device> parseDeviceOrder(String projectId) { public List<Device> parseDeviceOrder(String projectId) {
log.info("parseDeviceOrder start projectId={}", projectId); log.info("parseDeviceOrder start projectId={}", projectId);
@ -419,12 +480,21 @@ public class ProjectServiceImpl
} }
} }
@Override
/** /**
* 提取画布视图所需数据设备管线边界显示配置 * 提取画布视图所需数据设备管线边界显示配置
* <p>
* 用于前端建模/编辑页面的初始化展示返回结构为 Map按前端协议组织多个子对象
* </p>
* <ul>
* <li>基础信息项目信息拓扑基础字段</li>
* <li>实体集合设备材料事件等按需要提供</li>
* <li>展示配置画布边界布局与扩展字段按拓扑内容解析</li>
* </ul>
*
* @param projectId 项目ID * @param projectId 项目ID
* @return Map 视图对象 * @return 画布视图对象
*/ */
@Override
public Map<String, Object> parseCanvasView(String projectId) { public Map<String, Object> parseCanvasView(String projectId) {
log.info("parseCanvasView start projectId={}", projectId); log.info("parseCanvasView start projectId={}", projectId);
try { try {

View File

@ -7,6 +7,12 @@ import com.yfd.business.css.service.ScenarioResultService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/**
* 情景结果服务实现
* <p>
* 结果数据由仿真/推理流程写入本类提供基于 MyBatis-Plus 的基础 CRUD 能力与统一的数据访问入口
* </p>
*/
@Slf4j @Slf4j
@Service @Service
public class ScenarioResultServiceImpl public class ScenarioResultServiceImpl

View File

@ -7,6 +7,12 @@ import com.yfd.business.css.service.ScenarioService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/**
* 情景服务实现
* <p>
* 提供情景相关的基础 CRUD 能力以及按情景 ID 获取算法类型等轻量查询能力
* </p>
*/
@Slf4j @Slf4j
@Service @Service
public class ScenarioServiceImpl public class ScenarioServiceImpl

View File

@ -8,9 +8,21 @@ import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
/**
* 仿真服务实现占位实现
* <p>
* 当前实现用于打通接口链路与日志观测尚未接入真实的仿真引擎计算逻辑
* </p>
*/
@Slf4j @Slf4j
@Service @Service
public class SimulationServiceImpl implements SimulationService { public class SimulationServiceImpl implements SimulationService {
/**
* 执行一次仿真并返回结果
*
* @param request 仿真请求参数
* @return 仿真结果
*/
@Override @Override
public SimulationResult runSimulation(SimulationRequest request) { public SimulationResult runSimulation(SimulationRequest request) {
log.info("SimulationServiceImpl runSimulation start request={}", request); log.info("SimulationServiceImpl runSimulation start request={}", request);

View File

@ -0,0 +1,29 @@
/**
* Service 实现层
* <p>
* 该包包含领域服务的具体实现承载主要业务规则与流程编排典型职责
* </p>
* <ul>
* <li>业务规则落地状态机容量控制参数校验对象关联关系处理等</li>
* <li>事务边界在必要处声明事务确保写操作一致性</li>
* <li>外部调用调用推演/训练等外部服务时负责请求组装返回解析与容错</li>
* <li>数据访问通过 Mapper 层完成持久化读写不直接拼接 SQL</li>
* </ul>
* <p>
* 质量与安全约束
* </p>
* <ul>
* <li>健壮性对空值空集合外部返回字段做防御性处理避免 NPE</li>
* <li>一致性写入前校验引用存在性避免产生孤儿数据与不一致关联</li>
* <li>幂等性对外部回调/重复提交等场景具备幂等保护如按任务ID去重</li>
* <li>敏感信息日志不记录口令token密钥等敏感字段</li>
* </ul>
* <p>
* 性能约定
* </p>
* <ul>
* <li>批量操作大数据量导出/查询优先使用分页与批处理避免一次性加载过多数据</li>
* <li>长耗时任务推演/训练等应走任务化机制并提供状态回调与超时回收</li>
* </ul>
*/
package com.yfd.business.css.service.impl;

View File

@ -0,0 +1,27 @@
/**
* Service领域服务接口层
* <p>
* 该包定义后端核心业务能力的接口边界 Controller 与数据访问层之间的业务语义层
* </p>
* <ul>
* <li>接口表达业务语义方法名与入参/出参体现领域含义而非表字段操作</li>
* <li>事务语义在实现层落地接口侧强调做什么实现侧强调如何做</li>
* <li>幂等与一致性对外接口尽量保持幂等与可预期错误语义</li>
* </ul>
* <p>
* 典型服务域
* </p>
* <ul>
* <li>项目/场景拓扑解析推演输入准备结果查询等</li>
* <li>设备/事件/材料基础数据维护解析与结构化转换</li>
* <li>训练任务提交容量控制状态流转与回调处理</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>接口不要依赖 Web 层对象HttpServletRequest </li>
* <li>接口返回值尽量明确避免 Object/Map 泛化导致契约不清</li>
* </ul>
*/
package com.yfd.business.css.service;

View File

@ -0,0 +1,20 @@
/**
* 业务工具类
* <p>
* 该包用于放置与 business-css 业务强相关的工具能力例如
* </p>
* <ul>
* <li>数据解析对设备数据属性字段拓扑结构等进行解析与归一化</li>
* <li>转换与拼装 Domain/DTO/Model 在不同层之间做结构化转换</li>
* <li>校验辅助对复杂字段格式引用关系一致性约束做统一校验</li>
* </ul>
* <p>
* 约定
* </p>
* <ul>
* <li>工具类不应持有全局可变状态避免并发与复用风险</li>
* <li>与框架通用工具framework 模块区分避免重复实现通用能力</li>
* <li>异常提示应可定位输入问题便于测试与排障</li>
* </ul>
*/
package com.yfd.business.css.utils;

View File

@ -73,20 +73,6 @@ public class LoginController {
user.getPassword()); user.getPassword());
// 是否需要验证码不需要改成false
boolean hascode = false;//true;
if (hascode) {
// 查询验证码
String code = webConfig.loginuserCache().get(user.getUuid());
// 清除验证码
webConfig.loginuserCache().remove(user.getUuid());
if (StrUtil.isBlank(code)) {
return ResponseResult.error("验证码不存在或已过期");
}
if (StrUtil.isBlank(user.getCode()) || !user.getCode().equalsIgnoreCase(code)) {
return ResponseResult.error("验证码错误");
}
}
//如果认证通过了使用userId生成token token存入ResponseResult返回 //如果认证通过了使用userId生成token token存入ResponseResult返回
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), new UsernamePasswordAuthenticationToken(user.getUsername(),

View File

@ -1,100 +0,0 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yfd.platform.utils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
/**
* 加密
* @author
* @date 2018-11-23
*/
public class EncryptUtils {
private static final String STR_PARAM = "Passw0rd";
private static Cipher cipher;
private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8));
private static DESKeySpec getDesKeySpec(String source) throws Exception {
if (source == null || source.length() == 0){
return null;
}
cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
String strKey = "Passw0rd";
return new DESKeySpec(strKey.getBytes(StandardCharsets.UTF_8));
}
/**
* 对称加密
*/
public static String desEncrypt(String source) throws Exception {
DESKeySpec desKeySpec = getDesKeySpec(source);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV);
return byte2hex(
cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase();
}
/**
* 对称解密
*/
public static String desDecrypt(String source) throws Exception {
byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8));
DESKeySpec desKeySpec = getDesKeySpec(source);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey secretKey = keyFactory.generateSecret(desKeySpec);
cipher.init(Cipher.DECRYPT_MODE, secretKey, IV);
byte[] retByte = cipher.doFinal(src);
return new String(retByte);
}
private static String byte2hex(byte[] inStr) {
String stmp;
StringBuilder out = new StringBuilder(inStr.length * 2);
for (byte b : inStr) {
stmp = Integer.toHexString(b & 0xFF);
if (stmp.length() == 1) {
// 如果是0至F的单位字符串则添加0
out.append("0").append(stmp);
} else {
out.append(stmp);
}
}
return out.toString();
}
private static byte[] hex2byte(byte[] b) {
int size = 2;
if ((b.length % size) != 0){
throw new IllegalArgumentException("长度不是偶数");
}
byte[] b2 = new byte[b.length / 2];
for (int n = 0; n < b.length; n += size) {
String item = new String(b, n, 2);
b2[n / 2] = (byte) Integer.parseInt(item, 16);
}
return b2;
}
}

View File

@ -38,7 +38,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.io.*; import java.io.*;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.security.MessageDigest;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
@ -285,61 +284,6 @@ public class FileUtil extends cn.hutool.core.io.FileUtil {
} }
} }
/**
* 判断两个文件是否相同
*/
public static boolean check(File file1, File file2) {
String img1Md5 = getMd5(file1);
String img2Md5 = getMd5(file2);
return img1Md5.equals(img2Md5);
}
/**
* 判断两个文件是否相同
*/
public static boolean check(String file1Md5, String file2Md5) {
return file1Md5.equals(file2Md5);
}
private static byte[] getByte(File file) {
// 得到文件长度
byte[] b = new byte[(int) file.length()];
try {
InputStream in = new FileInputStream(file);
try {
System.out.println(in.read(b));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
} catch (FileNotFoundException e) {
log.error(e.getMessage(), e);
return null;
}
return b;
}
private static String getMd5(byte[] bytes) {
// 16进制字符
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(bytes);
byte[] md = mdTemp.digest();
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
// 移位 输出字符串
for (byte byte0 : md) {
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
/** /**
* 下载文件 * 下载文件
* *
@ -391,8 +335,4 @@ public class FileUtil extends cn.hutool.core.io.FileUtil {
fis.close(); fis.close();
} }
public static String getMd5(File file) {
return getMd5(getByte(file));
}
} }

View File

@ -4,8 +4,6 @@ import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import java.security.*; import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
@ -16,57 +14,6 @@ import java.security.spec.X509EncodedKeySpec;
**/ **/
public class RsaUtils { public class RsaUtils {
private static final String SRC = "123456";
public static void main(String[] args) throws Exception {
System.out.println("\n");
RsaKeyPair keyPair = generateKeyPair();
System.out.println("公钥:" + keyPair.getPublicKey());
System.out.println("私钥:" + keyPair.getPrivateKey());
System.out.println("\n");
test1(keyPair);
System.out.println("\n");
test2(keyPair);
System.out.println("\n");
}
/**
* 公钥加密私钥解密
*/
private static void test1(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 公钥加密私钥解密开始 *****************");
String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtils.SRC);
String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1);
System.out.println("加密前:" + RsaUtils.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (RsaUtils.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 公钥加密私钥解密结束 *****************");
}
/**
* 私钥加密公钥解密
* @throws Exception /
*/
private static void test2(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 私钥加密公钥解密开始 *****************");
String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtils.SRC);
String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1);
System.out.println("加密前:" + RsaUtils.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (RsaUtils.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 私钥加密公钥解密结束 *****************");
}
/** /**
* 公钥解密 * 公钥解密
* *
@ -137,45 +84,4 @@ public class RsaUtils {
byte[] result = cipher.doFinal(text.getBytes()); byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result); return Base64.encodeBase64String(result);
} }
/**
* 构建RSA密钥对
*
* @return /
* @throws NoSuchAlgorithmException /
*/
public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RsaKeyPair(publicKeyString, privateKeyString);
}
/**
* RSA密钥对对象
*/
public static class RsaKeyPair {
private final String publicKey;
private final String privateKey;
public RsaKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
} }