ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TPlatformConfigController.java
New file @@ -0,0 +1,50 @@ package com.ruoyi.other.controller; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.common.log.annotation.Log; import com.ruoyi.common.log.enums.BusinessType; import com.ruoyi.common.security.annotation.RequiresPermissions; import com.ruoyi.other.domain.TPlatformConfig; import com.ruoyi.other.dto.TPlatformConfigBatchSaveDTO; import com.ruoyi.other.service.TPlatformConfigService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import java.util.List; /** 光伏储能配置接口。 */ @Api(tags = "后台-光伏储能配置") @RestController @RequestMapping("/platform-config") public class TPlatformConfigController { private final TPlatformConfigService platformConfigService; public TPlatformConfigController(TPlatformConfigService platformConfigService) { this.platformConfigService = platformConfigService; } @RequiresPermissions("/platformConfig") @ApiOperation("查询所有未删除的平台配置(不分页)") @GetMapping("/listAll") public AjaxResult<List<TPlatformConfig>> listAll() { return AjaxResult.ok(platformConfigService.listAll()); } @RequiresPermissions("/platformConfig") @ApiOperation(value = "批量保存平台配置(无ID新增,有ID修改)", notes = "请求体为JSON对象,configs字段为配置数组") @PostMapping("/saveBatch") @Log(title = "批量保存光伏储能配置", businessType = BusinessType.UPDATE, isSaveRequestData = false, isSaveResponseData = false) public AjaxResult<Void> saveBatch(@Valid @RequestBody TPlatformConfigBatchSaveDTO request) { platformConfigService.saveBatchConfigs(request.getConfigs()); return AjaxResult.ok(null); } } ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/domain/PlatformEnum.java
New file @@ -0,0 +1,39 @@ package com.ruoyi.other.domain; public enum PlatformEnum { KSTAR(0, "科士达"), HUAWEI(1, "华为"), SUNGROW(2, "阳光"), GROWATT(3, "古瑞瓦特"), OULITE(4, "欧力特");; private final Integer code; private final String name; PlatformEnum(Integer code, String name) { this.code = code; this.name = name; } public Integer getCode() { return code; } public String getName() { return name; } /** * 根据code获取枚举 */ public static PlatformEnum getByCode(Integer code) { for (PlatformEnum item : values()) { if (item.code.equals(code)) { return item; } } return null; } } ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/domain/TPlatformConfig.java
New file @@ -0,0 +1,84 @@ package com.ruoyi.other.domain; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.ToString; import java.io.Serializable; import java.time.LocalDateTime; /** 光伏储能平台配置。 */ @Data @TableName("t_platform_config") @ApiModel(value = "TPlatformConfig", description = "光伏储能配置表") public class TPlatformConfig implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) @ApiModelProperty("主键ID,新增时不传,修改时传入") private Long id; @TableField("type") @ApiModelProperty("类型(1:光伏,2:储能)") private Integer type; @TableField("platform_name") @ApiModelProperty("所属平台(利普芯、欧力特)") private String platformName; @TableField("platform_id") @ApiModelProperty("所属平台ID") private Integer platformId; @TableField("account") @ApiModelProperty("账号") private String account; @ToString.Exclude @TableField("password") @ApiModelProperty("密码") private String password; @ToString.Exclude @TableField("app_key") @ApiModelProperty("平台KEY") private String appKey; @ToString.Exclude @TableField("app_secret") @ApiModelProperty("平台密钥") private String appSecret; @TableField("app_path") @ApiModelProperty("平台请求地址") private String appPath; @TableField("remark") @ApiModelProperty("备注") private String remark; @TableField(value = "create_time", fill = FieldFill.INSERT, updateStrategy = FieldStrategy.NEVER) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ApiModelProperty(value = "创建时间,由服务端维护", readOnly = true) private LocalDateTime createTime; @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ApiModelProperty(value = "更新时间,由服务端维护", readOnly = true) private LocalDateTime updateTime; @TableField("del_flag") @TableLogic(value = "0", delval = "2") @ApiModelProperty(value = "删除标志(0存在,2删除),由服务端维护", readOnly = true) private String delFlag; } ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/dto/TPlatformConfigBatchSaveDTO.java
New file @@ -0,0 +1,19 @@ package com.ruoyi.other.dto; import com.ruoyi.other.domain.TPlatformConfig; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import javax.validation.constraints.NotEmpty; import java.util.List; /** 使用顶层 JSON 对象承载配置列表,兼容网关的签名参数解析。 */ @Data @ApiModel(value = "TPlatformConfigBatchSaveDTO", description = "批量保存光伏储能配置请求") public class TPlatformConfigBatchSaveDTO { @NotEmpty(message = "保存的配置列表不能为空") @ApiModelProperty(value = "配置列表,无ID新增,有ID修改", required = true) private List<TPlatformConfig> configs; } ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TPlatformConfigMapper.java
New file @@ -0,0 +1,8 @@ package com.ruoyi.other.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.other.domain.TPlatformConfig; /** 光伏储能配置 Mapper。 */ public interface TPlatformConfigMapper extends BaseMapper<TPlatformConfig> { } ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TPlatformConfigService.java
New file @@ -0,0 +1,16 @@ package com.ruoyi.other.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.other.domain.TPlatformConfig; import java.util.List; /** 光伏储能配置服务。 */ public interface TPlatformConfigService extends IService<TPlatformConfig> { /** 不分页查询所有未删除配置,按主键升序排列。 */ List<TPlatformConfig> listAll(); /** 批量保存:无 ID 新增,有 ID 修改;任一失败整批回滚。 */ void saveBatchConfigs(List<TPlatformConfig> configs); } ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TPlatformConfigServiceImpl.java
New file @@ -0,0 +1,57 @@ package com.ruoyi.other.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.other.domain.TPlatformConfig; import com.ruoyi.other.mapper.TPlatformConfigMapper; import com.ruoyi.other.service.TPlatformConfigService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashSet; import java.util.List; import java.util.Set; /** 光伏储能配置服务实现。 */ @Service public class TPlatformConfigServiceImpl extends ServiceImpl<TPlatformConfigMapper, TPlatformConfig> implements TPlatformConfigService { @Override public List<TPlatformConfig> listAll() { return list(Wrappers.lambdaQuery(TPlatformConfig.class).orderByAsc(TPlatformConfig::getId)); } @Override @Transactional(rollbackFor = Exception.class) public void saveBatchConfigs(List<TPlatformConfig> configs) { if (configs == null || configs.isEmpty()) { throw new ServiceException("保存的配置列表不能为空"); } Set<Long> ids = new HashSet<>(); for (TPlatformConfig config : configs) { if (config == null) { throw new ServiceException("配置列表不能包含空数据"); } if (config.getType() != null && config.getType() != 1 && config.getType() != 2) { throw new ServiceException("配置类型只能为1(光伏)或2(储能)"); } if (config.getId() != null && (config.getId() <= 0 || !ids.add(config.getId()))) { throw new ServiceException("配置ID必须为正数且不能重复"); } } // 传入 ID 必须对应有效配置,避免误将已删除或不存在的记录作为新增保存。 if (!ids.isEmpty() && listByIds(ids).size() != ids.size()) { throw new ServiceException("待修改的配置不存在或已删除"); } for (TPlatformConfig config : configs) { config.setDelFlag("0"); config.setCreateTime(null); config.setUpdateTime(null); } if (!saveOrUpdateBatch(configs)) { throw new ServiceException("批量保存平台配置失败"); } } } ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TPlatformConfigMapper.xml
New file @@ -0,0 +1,24 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.other.mapper.TPlatformConfigMapper"> <resultMap id="BaseResultMap" type="com.ruoyi.other.domain.TPlatformConfig"> <id column="id" property="id" /> <result column="type" property="type" /> <result column="platform_name" property="platformName" /> <result column="platform_id" property="platformId" /> <result column="account" property="account" /> <result column="password" property="password" /> <result column="app_key" property="appKey" /> <result column="app_secret" property="appSecret" /> <result column="app_path" property="appPath" /> <result column="remark" property="remark" /> <result column="create_time" property="createTime" /> <result column="update_time" property="updateTime" /> <result column="del_flag" property="delFlag" /> </resultMap> <sql id="Base_Column_List"> id, type, platform_name, platform_id, account, password, app_key, app_secret, app_path, remark, create_time, update_time, del_flag </sql> </mapper> ruoyi-service/ruoyi-other/src/main/resources/sql/t_platform_config.sql
New file @@ -0,0 +1,21 @@ -- ---------------------------- -- 增加各平台交互配置表 create table t_platform_config ( id bigint auto_increment comment '主键ID' primary key, type integer NULL COMMENT '类型(1:光伏,2:储能)', platform_name varchar(100) null comment '所属平台(利普芯、欧力特)', platform_id Integer COMMENT '所属平台ID', account VARCHAR(200) COMMENT '账号', password VARCHAR(500) COMMENT '密码', app_key VARCHAR(500) COMMENT '平台KEY', app_secret VARCHAR(500) COMMENT '平台密钥', app_path VARCHAR(500) COMMENT '平台请求地址', remark VARCHAR(500) COMMENT '备注', create_time datetime null comment '创建时间', update_time datetime null comment '更新时间', del_flag char default '0' not null comment '删除标志(0存在 2删除)' ) comment '光伏储能配置表' collate = utf8mb4_bin; ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/controller/TPlatformConfigControllerTest.java
New file @@ -0,0 +1,75 @@ package com.ruoyi.other.controller; import com.ruoyi.other.domain.TPlatformConfig; import com.ruoyi.other.service.TPlatformConfigService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class TPlatformConfigControllerTest { private TPlatformConfigService service; private MockMvc mvc; @BeforeEach void setUp() { service = Mockito.mock(TPlatformConfigService.class); mvc = MockMvcBuilders.standaloneSetup(new TPlatformConfigController(service)).build(); } @Test @SuppressWarnings({"rawtypes", "unchecked"}) void bindsConfigArrayInsideObject() throws Exception { mvc.perform(post("/platform-config/saveBatch") .contentType(MediaType.APPLICATION_JSON) .content("{\"configs\":[{\"id\":null,\"type\":1,\"platformName\":\"科士达\",\"account\":\"demo\"}," + "{\"id\":8,\"type\":2,\"platformName\":\"华为\"}]}")) .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200)); ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class); Mockito.verify(service).saveBatchConfigs(captor.capture()); List<TPlatformConfig> configs = captor.getValue(); assertEquals(2, configs.size()); assertEquals("科士达", configs.get(0).getPlatformName()); assertEquals("demo", configs.get(0).getAccount()); assertEquals(Long.valueOf(8), configs.get(1).getId()); assertEquals(Integer.valueOf(2), configs.get(1).getType()); } @Test void rejectsMissingOrNullConfigs() throws Exception { for (String body : new String[]{"{}", "{\"configs\":null}"}) { mvc.perform(post("/platform-config/saveBatch") .contentType(MediaType.APPLICATION_JSON).content(body)) .andExpect(status().isBadRequest()); } Mockito.verifyNoInteractions(service); } @Test void rejectsEmptyConfigs() throws Exception { mvc.perform(post("/platform-config/saveBatch") .contentType(MediaType.APPLICATION_JSON).content("{\"configs\":[]}")) .andExpect(status().isBadRequest()); Mockito.verifyNoInteractions(service); } @Test void rejectsBareArrayForObjectContract() throws Exception { mvc.perform(post("/platform-config/saveBatch") .contentType(MediaType.APPLICATION_JSON).content("[{\"type\":1}]")) .andExpect(status().isBadRequest()); Mockito.verifyNoInteractions(service); } } ruoyi-service/ruoyi-other/src/test/java/com/ruoyi/other/service/impl/TPlatformConfigServiceImplTest.java
New file @@ -0,0 +1,166 @@ package com.ruoyi.other.service.impl; import com.baomidou.mybatisplus.annotation.FieldStrategy; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.TableInfo; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.other.domain.TPlatformConfig; import com.ruoyi.other.mapper.TPlatformConfigMapper; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.anyInt; /** 配置保存边界及表映射测试,不启动外部服务。 */ class TPlatformConfigServiceImplTest { @BeforeAll static void initTableInfo() { TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), TPlatformConfig.class); } @Test void mapsLogicalDeletionAndProtectsCreationTime() { TableInfo table = TableInfoHelper.getTableInfo(TPlatformConfig.class); assertEquals("t_platform_config", table.getTableName()); assertEquals(Long.class, table.getKeyType()); assertEquals("0", table.getLogicDeleteFieldInfo().getLogicNotDeleteValue()); assertEquals("2", table.getLogicDeleteFieldInfo().getLogicDeleteValue()); assertEquals(FieldStrategy.NEVER, table.getFieldList().stream() .filter(field -> "create_time".equals(field.getColumn())) .findFirst().get().getUpdateStrategy()); } @Test @SuppressWarnings({"rawtypes", "unchecked"}) void listsAllWithoutPaginationInIdOrder() { TPlatformConfigMapper mapper = Mockito.mock(TPlatformConfigMapper.class); List<TPlatformConfig> rows = Arrays.asList(config(1L), config(2L)); Mockito.when(mapper.selectList(Mockito.any())).thenReturn(rows); TPlatformConfigServiceImpl service = service(mapper); assertSame(rows, service.listAll()); ArgumentCaptor<Wrapper> query = ArgumentCaptor.forClass(Wrapper.class); Mockito.verify(mapper).selectList(query.capture()); assertEquals("ORDER BY id ASC", query.getValue().getSqlSegment().trim()); } @Test void rejectsEmptyOrNullItemsBeforeWriting() { TPlatformConfigServiceImpl service = Mockito.spy(new TPlatformConfigServiceImpl()); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(null)); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Collections.emptyList())); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Arrays.asList(config(null), null))); Mockito.verify(service, Mockito.never()).saveOrUpdateBatch(anyCollection(), anyInt()); } @Test void rejectsInvalidTypeAndDuplicateIdsBeforeWriting() { TPlatformConfigServiceImpl service = Mockito.spy(new TPlatformConfigServiceImpl()); TPlatformConfig invalid = config(null); invalid.setType(3); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Collections.singletonList(invalid))); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Arrays.asList(config(1L), config(1L)))); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Collections.singletonList(config(0L)))); Mockito.verify(service, Mockito.never()).saveOrUpdateBatch(anyCollection(), anyInt()); } @Test void rejectsMissingOrDeletedIdBeforeWritingAnyNewRows() { TPlatformConfigServiceImpl service = Mockito.spy(new TPlatformConfigServiceImpl()); Mockito.doReturn(Collections.emptyList()).when(service).listByIds(anyCollection()); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Arrays.asList(config(null), config(5L)))); Mockito.verify(service, Mockito.never()).saveOrUpdateBatch(anyCollection(), anyInt()); } @Test void savesMixedBatchAndIgnoresClientManagedMetadata() { TPlatformConfigServiceImpl service = Mockito.spy(new TPlatformConfigServiceImpl()); Mockito.doReturn(Collections.singletonList(config(5L))).when(service).listByIds(anyCollection()); Mockito.doReturn(true).when(service).saveOrUpdateBatch(anyCollection(), anyInt()); TPlatformConfig created = config(null); TPlatformConfig updated = config(5L); updated.setCreateTime(LocalDateTime.of(2000, 1, 1, 0, 0)); updated.setUpdateTime(updated.getCreateTime()); updated.setDelFlag("2"); List<TPlatformConfig> rows = Arrays.asList(created, updated); service.saveBatchConfigs(rows); Mockito.verify(service).saveOrUpdateBatch(Mockito.same(rows), anyInt()); assertNull(created.getId()); assertEquals(Long.valueOf(5), updated.getId()); assertEquals("0", created.getDelFlag()); assertEquals("0", updated.getDelFlag()); assertNull(updated.getCreateTime()); assertNull(updated.getUpdateTime()); } @Test void acceptsNullableTypeAsDefinedBySchema() { TPlatformConfigServiceImpl service = Mockito.spy(new TPlatformConfigServiceImpl()); Mockito.doReturn(true).when(service).saveOrUpdateBatch(anyCollection(), anyInt()); TPlatformConfig row = config(null); row.setType(null); service.saveBatchConfigs(Collections.singletonList(row)); Mockito.verify(service).saveOrUpdateBatch(anyCollection(), anyInt()); } @Test void propagatesBatchFailuresSoTransactionCanRollBack() { TPlatformConfigServiceImpl service = Mockito.spy(new TPlatformConfigServiceImpl()); Mockito.doReturn(false).when(service).saveOrUpdateBatch(anyCollection(), anyInt()); assertThrows(ServiceException.class, () -> service.saveBatchConfigs(Collections.singletonList(config(null)))); IllegalStateException failure = new IllegalStateException("database failure"); Mockito.doThrow(failure).when(service).saveOrUpdateBatch(anyCollection(), anyInt()); assertSame(failure, assertThrows(IllegalStateException.class, () -> service.saveBatchConfigs(Collections.singletonList(config(null))))); } @Test void doesNotIncludeSecretsInToString() { TPlatformConfig row = config(null); row.setPassword("private-password"); row.setAppKey("private-key"); row.setAppSecret("private-secret"); assertTrue(!row.toString().contains("private-")); } private TPlatformConfigServiceImpl service(TPlatformConfigMapper mapper) { TPlatformConfigServiceImpl service = new TPlatformConfigServiceImpl(); ReflectionTestUtils.setField(service, "baseMapper", mapper); return service; } private TPlatformConfig config(Long id) { TPlatformConfig config = new TPlatformConfig(); config.setId(id); config.setType(1); config.setPlatformName("利普芯"); return config; } }