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
|
* @description
|
||||||
*/
|
*/
|
||||||
public interface ApplicationInfoRepository extends JpaRepository<ApplicationInfo, Integer>, JpaSpecificationExecutor<ApplicationInfo> {
|
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。
|
// 基石应用排除 = AppNature.isBaseApp(appKey<=100000 ∪ app_nature='BASE')的 SQL 形态,常量拼接、语义单点见 AppNature。
|
||||||
// app_nature 过渡期可空:`<> 'BASE'` 遇 NULL 求值为 NULL(非 TRUE)会整行漏掉,必须带 IS NULL 分支。
|
// app_nature 过渡期可空:`<> 'BASE'` 遇 NULL 求值为 NULL(非 TRUE)会整行漏掉,必须带 IS NULL 分支。
|
||||||
@Query("SELECT COUNT(a.id) FROM ApplicationInfo a WHERE a.categoryId = :categoryId AND a.appKey > 100000 " +
|
@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
|
@Override
|
||||||
public ApplicationInfo save(ApplicationInfo info) {
|
public ApplicationInfo save(ApplicationInfo info) {
|
||||||
|
if (info.getId() != null) {
|
||||||
|
requireUnchangedAppCode(applicationInfoRepository.findPersistedAppCodeById(info.getId()),
|
||||||
|
info.getAppCode());
|
||||||
|
}
|
||||||
ApplicationInfo saved = applicationInfoRepository.save(info);
|
ApplicationInfo saved = applicationInfoRepository.save(info);
|
||||||
eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved));
|
eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved));
|
||||||
return saved;
|
return saved;
|
||||||
@@ -199,6 +203,10 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
|||||||
}
|
}
|
||||||
ApplicationInfo entity = applicationInfoRepository.findById(id)
|
ApplicationInfo entity = applicationInfoRepository.findById(id)
|
||||||
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
|
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
|
||||||
|
// 空值仍按既有部分提交语义处理;非空已登记标识不可真正改名。
|
||||||
|
if (StringUtils.isNotBlank(request.getAppCode())) {
|
||||||
|
requireUnchangedAppCode(entity.getAppCode(), request.getAppCode());
|
||||||
|
}
|
||||||
if (StringUtils.isNotBlank(request.getAppCode())
|
if (StringUtils.isNotBlank(request.getAppCode())
|
||||||
&& applicationInfoRepository.existsByAppCodeAndIdNot(request.getAppCode(), id)) {
|
&& applicationInfoRepository.existsByAppCodeAndIdNot(request.getAppCode(), id)) {
|
||||||
throw new DException("技术标识已存在");
|
throw new DException("技术标识已存在");
|
||||||
@@ -225,6 +233,12 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
|||||||
eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved));
|
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
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
// AI-FIX: 删除应用同时级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理);
|
// AI-FIX: 删除应用同时级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理);
|
||||||
|
|||||||
@@ -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