Files
UserBaseService/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java
T
chenxing aa4ae32ac5
ci-userbase-service / build-and-push (push) Successful in 2m9s
8月10 版本修改
2026-08-10 18:53:04 +08:00

260 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.userbase.service.impl;
import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.entity.SysAppRole;
import com.yunhai.userbase.repository.AppModuleRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
import com.yunhai.userbase.repository.SysAppRoleRepository;
import com.yunhai.userbase.repository.SysGroupAppRelRepository;
import com.yunhai.userbase.repository.SysGroupModulePermRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.service.IApplicationInfoService;
import com.yunhai.userbase.service.SignKeyCacheService;
import com.bestsch.utils.DException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.yunhai.userbase.model.application.ApplicationInfoQuery;
import com.yunhai.userbase.model.application.ApplicationInfoRequest;
import com.yunhai.userbase.model.application.ApplicationInfoSortVo;
import com.yunhai.userbase.model.application.ApplicationInfoVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Service
public class ApplicationInfoService implements IApplicationInfoService {
@Autowired
private ApplicationInfoRepository applicationInfoRepository;
@Autowired
private AppModuleRepository appModuleRepository;
@Autowired
private SysGroupAppRelRepository sysGroupAppRelRepository;
@Autowired
private SysGroupModulePermRepository sysGroupModulePermRepository;
@Autowired
private SysAppRoleRepository sysAppRoleRepository;
@Autowired
private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository;
@Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Autowired
private SignKeyCacheService signKeyCacheService;
/**
* 查询缓存:sync = true 防击穿;底层优先 Redis,Redis 故障时自动降级 Caffeine。
*/
@Override
@Cacheable(value = "appInfo", key = "#id", sync = true)
public ApplicationInfo getById(Integer id) {
return applicationInfoRepository.findById(id).orElse(null);
}
/**
* 更新缓存:方法返回即写入缓存(与 @Cacheable 同一 cacheName/key,保证一致)。
*/
@Override
@CachePut(value = "appInfo", key = "#info.id")
public ApplicationInfo save(ApplicationInfo info) {
return applicationInfoRepository.save(info);
}
/**
* 删除缓存:删除数据时同步清除对应缓存项。
*/
@Override
@CacheEvict(value = "appInfo", key = "#id")
public void deleteById(Integer id) {
applicationInfoRepository.deleteById(id);
}
@Override
@Transactional
public void add(ApplicationInfoRequest request) {
Objects.requireNonNull(request, "应用新增入参不能为空");
// 重名校验:新增应用名称不允许重复
if (applicationInfoRepository.existsByName(request.getName())) {
throw new DException("应用名称已存在");
}
ApplicationInfo entity = new ApplicationInfo();
BeanUtils.copyProperties(request, entity);
entity.setId(null);
// appKey:整型,从 100000 起,取数据库现有最大 appKey + 1
Integer maxAppKey = applicationInfoRepository.findMaxAppKey();
Integer appKey = (maxAppKey == null) ? 100000 : maxAppKey + 1;
entity.setAppKey(appKey);
// appSecret:32 位 UUID(去除连字符)
entity.setAppSecret(UUID.randomUUID().toString().replace("-", ""));
ApplicationInfo saved = applicationInfoRepository.save(entity);
signKeyCacheService.syncOne(saved);
}
@Override
@Transactional
public void edit(Integer id, ApplicationInfoRequest request) {
Objects.requireNonNull(id, "应用ID不能为空");
Objects.requireNonNull(request, "应用编辑入参不能为空");
// 重名校验:编辑时排除自身
if (applicationInfoRepository.existsByNameAndIdNot(request.getName(), id)) {
throw new DException("应用名称已存在");
}
ApplicationInfo entity = applicationInfoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
BeanUtils.copyProperties(request, entity, "id", "appKey", "appSecret");
ApplicationInfo saved = applicationInfoRepository.save(entity);
signKeyCacheService.syncOne(saved);
}
@Override
@Transactional
// AI-FIX: 删除应用同时级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理),并清除 appInfo 缓存
@CacheEvict(value = "appInfo", key = "#id")
public void delete(Integer id) {
Objects.requireNonNull(id, "应用ID不能为空");
ApplicationInfo existing = applicationInfoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
Integer appKey = existing.getAppKey();
// AI-FIX: 删除应用前级联清理组权限关联(逻辑外键无物理 FK):先清该应用模块的组权限记录,再清组开通记录
List<AppModule> modules = appModuleRepository.findByAppIdIn(java.util.Collections.singletonList(id));
if (!modules.isEmpty()) {
List<Integer> moduleIds = new ArrayList<Integer>();
for (AppModule m : modules) {
moduleIds.add(m.getId());
}
sysGroupModulePermRepository.deleteByModuleIdInBulk(moduleIds);
}
sysGroupAppRelRepository.deleteByAppIdBulk(id);
// AI-FIX(角色授权): 删除应用前级联清理该应用下的业务角色及其授权关系(逻辑外键无物理 FK)
// 仅清理「普通角色」(appId=本应用);全应用角色(appId=null)不受应用删除影响,保留不动
List<SysAppRole> appRoles = sysAppRoleRepository.findByAppId(id);
if (!appRoles.isEmpty()) {
List<Integer> roleIds = new ArrayList<Integer>();
for (SysAppRole r : appRoles) {
roleIds.add(r.getId());
}
sysAppRoleModuleRelRepository.deleteByRoleIdInBulk(roleIds);
sysUserAppRoleRelRepository.deleteByRoleIdInBulk(roleIds);
sysAppRoleRepository.deleteAll(appRoles);
}
// 先删模块(整棵子树随 appId 一并清除),再删应用,避免残留孤儿数据
appModuleRepository.deleteByAppId(id);
applicationInfoRepository.deleteById(id);
signKeyCacheService.removeOne(appKey);
}
@Override
public Page<ApplicationInfoVo> list(ApplicationInfoQuery query, Pageable pageable) {
Objects.requireNonNull(query, "应用查询条件不能为空");
Objects.requireNonNull(pageable, "分页参数不能为空");
Page<ApplicationInfo> page = applicationInfoRepository.findAll(select(query), pageable);
List<ApplicationInfoVo> content = new ArrayList<ApplicationInfoVo>();
for (ApplicationInfo entity : page.getContent()) {
content.add(convertToVo(entity));
}
return new PageImpl<>(content, pageable, page.getTotalElements());
}
/**
* 构建应用列表的动态查询条件。<br>
* name 关键词:按应用名称模糊匹配,或按 appKey(appId)精确匹配;categoryId:按应用类别精确匹配。<br>
*
* @param query 前端传入的查询条件
* @return 动态查询 Specification
*/
private Specification select(ApplicationInfoQuery query) {
Specification<ApplicationInfo> specification = new Specification<ApplicationInfo>() {
@Override
public Predicate toPredicate(Root<ApplicationInfo> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
predicates.add(cb.greaterThan(root.get("appKey"), 100000));
if (query != null) {
if (StringUtils.isNotEmpty(query.getName())) {
String keyword = query.getName();
Predicate byName = cb.like(root.get("name"), "%" + keyword + "%");
// 关键词为纯数字时,同时按 appKey(appId)精确匹配
if (keyword.matches("\\d+")) {
predicates.add(cb.or(byName, cb.equal(root.get("appKey"), Integer.parseInt(keyword))));
} else {
predicates.add(byName);
}
}
if (query.getCategoryId() != null) {
predicates.add(cb.equal(root.get("categoryId"), query.getCategoryId()));
}
}
return cq.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
};
return specification;
}
@Override
@Transactional
// AI-FIX: 重置密钥后必须清除 appInfo 缓存,否则控制器随后 getById 命中旧缓存、返回旧密钥
@CacheEvict(value = "appInfo", key = "#id")
public void resetAppSecret(Integer id) {
Objects.requireNonNull(id, "应用ID不能为空");
ApplicationInfo entity = applicationInfoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
entity.setAppSecret(UUID.randomUUID().toString().replace("-", ""));
ApplicationInfo saved = applicationInfoRepository.save(entity);
signKeyCacheService.syncOne(saved);
}
@Override
@Transactional
public void sort(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
throw new DException("排序应用ID列表不能为空");
}
Integer categoryId = null;
for (int i = 0; i < ids.size(); i++) {
Integer id = ids.get(i);
ApplicationInfo entity = applicationInfoRepository.findById(id)
.orElseThrow(() -> new DException("应用不存在: " + id));
// 额外校验:排序必须限定在同一个应用类型内,避免跨类型串号导致排序错乱
if (categoryId == null) {
categoryId = entity.getCategoryId();
} else if (!Objects.equals(categoryId, entity.getCategoryId())) {
throw new DException("排序应用不属于同一应用类型,请检查入参");
}
applicationInfoRepository.updateSortNoById(id, i + 1);
}
}
private ApplicationInfoVo convertToVo(ApplicationInfo entity) {
ApplicationInfoVo vo = new ApplicationInfoVo();
BeanUtils.copyProperties(entity, vo);
return vo;
}
@Override
public List<ApplicationInfoSortVo> findListByCategoryId(Integer categoryId) {
return applicationInfoRepository.findSortVoByCategoryId(categoryId);
}
}