260 lines
12 KiB
Java
260 lines
12 KiB
Java
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);
|
||
}
|
||
} |