feat(app): application_info app_code 写入口守卫(已登记拒改名/拒清空,初次登记唯一性保持)
build / build-push (push) Successful in 1m26s
build / build-push (push) Successful in 1m26s
This commit is contained in:
@@ -17,6 +17,12 @@ import java.util.List;
|
||||
* @description
|
||||
*/
|
||||
public interface ApplicationInfoRepository extends JpaRepository<ApplicationInfo, Integer>, JpaSpecificationExecutor<ApplicationInfo> {
|
||||
/** 读取持久化标识快照;禁止查询前自动 flush 掉待校验的实体改名。 */
|
||||
@Query("SELECT a.appCode FROM ApplicationInfo a WHERE a.id = :id")
|
||||
@org.springframework.data.jpa.repository.QueryHints(
|
||||
@javax.persistence.QueryHint(name = "org.hibernate.flushMode", value = "COMMIT"))
|
||||
String findPersistedAppCodeById(@Param("id") Integer id);
|
||||
|
||||
// 基石应用排除 = AppNature.isBaseApp(appKey<=100000 ∪ app_nature='BASE')的 SQL 形态,常量拼接、语义单点见 AppNature。
|
||||
// app_nature 过渡期可空:`<> 'BASE'` 遇 NULL 求值为 NULL(非 TRUE)会整行漏掉,必须带 IS NULL 分支。
|
||||
@Query("SELECT COUNT(a.id) FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000 " +
|
||||
|
||||
@@ -145,6 +145,10 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
||||
*/
|
||||
@Override
|
||||
public ApplicationInfo save(ApplicationInfo info) {
|
||||
if (info.getId() != null) {
|
||||
requireUnchangedAppCode(applicationInfoRepository.findPersistedAppCodeById(info.getId()),
|
||||
info.getAppCode());
|
||||
}
|
||||
ApplicationInfo saved = applicationInfoRepository.save(info);
|
||||
eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved));
|
||||
return saved;
|
||||
@@ -199,6 +203,10 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
||||
}
|
||||
ApplicationInfo entity = applicationInfoRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
|
||||
// 空值仍按既有部分提交语义处理;非空已登记标识不可真正改名。
|
||||
if (StringUtils.isNotBlank(request.getAppCode())) {
|
||||
requireUnchangedAppCode(entity.getAppCode(), request.getAppCode());
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.getAppCode())
|
||||
&& applicationInfoRepository.existsByAppCodeAndIdNot(request.getAppCode(), id)) {
|
||||
throw new DException("技术标识已存在");
|
||||
@@ -225,6 +233,12 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
||||
eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved));
|
||||
}
|
||||
|
||||
private static void requireUnchangedAppCode(String persisted, String proposed) {
|
||||
if (StringUtils.isNotBlank(persisted) && !Objects.equals(persisted, proposed)) {
|
||||
throw new DException("已登记的应用技术标识 appCode 不可修改");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
// AI-FIX: 删除应用同时级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理);
|
||||
@@ -433,4 +447,4 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.yunhai.userbase.service.impl;
|
||||
|
||||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||||
import com.yunhai.userbase.model.application.ApplicationInfoRequest;
|
||||
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.Optional;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class ApplicationInfoAppCodeTest {
|
||||
private ApplicationInfoService service;
|
||||
private ApplicationInfoRepository repository;
|
||||
private ApplicationInfo entity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ApplicationInfoService();
|
||||
repository = mock(ApplicationInfoRepository.class);
|
||||
ReflectionTestUtils.setField(service, "applicationInfoRepository", repository);
|
||||
ReflectionTestUtils.setField(service, "eventPublisher", mock(ApplicationEventPublisher.class));
|
||||
entity = new ApplicationInfo();
|
||||
entity.setId(1); entity.setAppCode("svc-hall");
|
||||
when(repository.findById(1)).thenReturn(Optional.of(entity));
|
||||
when(repository.save(any(ApplicationInfo.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
private ApplicationInfoRequest request(String code) {
|
||||
ApplicationInfoRequest r = new ApplicationInfoRequest();
|
||||
r.setName("display-name"); r.setAppCode(code);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Test
|
||||
void editRejectsRenameBeforeMutatingEntity() {
|
||||
for (String code : new String[]{"new-name", "SVC-HALL", "svc-hall "}) {
|
||||
assertThrows(DException.class, () -> service.edit(1, request(code)));
|
||||
}
|
||||
assertEquals("svc-hall", entity.getAppCode());
|
||||
verify(repository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameCodeOrOmittedCodeCanEditOtherFields() {
|
||||
for (String code : new String[]{"svc-hall", null, "", " "}) {
|
||||
service.edit(1, request(code));
|
||||
assertEquals("svc-hall", entity.getAppCode());
|
||||
assertEquals("display-name", entity.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unregisteredCodeMayBeFilledButUniquenessStillApplies() {
|
||||
entity.setAppCode(null);
|
||||
when(repository.existsByAppCodeAndIdNot("taken", 1)).thenReturn(true);
|
||||
assertThrows(DException.class, () -> service.edit(1, request("taken")));
|
||||
service.edit(1, request("svc-hall"));
|
||||
assertEquals("svc-hall", entity.getAppCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void genericSaveCannotRenameOrClearPersistedCode() {
|
||||
when(repository.findPersistedAppCodeById(1)).thenReturn("svc-hall");
|
||||
for (String code : new String[]{"new-name", null, ""}) {
|
||||
entity.setAppCode(code);
|
||||
assertThrows(DException.class, () -> service.save(entity));
|
||||
}
|
||||
entity.setAppCode("svc-hall");
|
||||
assertSame(entity, service.save(entity));
|
||||
}
|
||||
|
||||
@Test
|
||||
void genericSaveAllowsInitialRegistration() {
|
||||
when(repository.findPersistedAppCodeById(1)).thenReturn(null);
|
||||
assertSame(entity, service.save(entity));
|
||||
entity.setId(null);
|
||||
assertSame(entity, service.save(entity));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user