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.platform.utils.exception.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.context.ApplicationEventPublisher; 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 com.yunhai.userbase.event.ApplicationInfoCacheChangedEvent; 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; @Autowired private ApplicationEventPublisher eventPublisher; /** * 查询缓存: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) { ApplicationInfo saved = applicationInfoRepository.save(info); eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved)); return saved; } /** * 删除缓存:删除数据时同步清除对应缓存项。 */ @Override @CacheEvict(value = "appInfo", key = "#id") public void deleteById(Integer id) { ApplicationInfo existing = applicationInfoRepository.findById(id).orElse(null); applicationInfoRepository.deleteById(id); if (existing != null && existing.getAppKey() != null) { eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.delete(this, existing.getAppKey())); } } @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); eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, 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); eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, 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 modules = appModuleRepository.findByAppIdIn(java.util.Collections.singletonList(id)); if (!modules.isEmpty()) { List moduleIds = new ArrayList(); for (AppModule m : modules) { moduleIds.add(m.getId()); } sysGroupModulePermRepository.deleteByModuleIdInBulk(moduleIds); } sysGroupAppRelRepository.deleteByAppIdBulk(id); // AI-FIX(角色授权): 删除应用前级联清理该应用下的业务角色及其授权关系(逻辑外键无物理 FK) // 仅清理「普通角色」(appId=本应用);全应用角色(appId=null)不受应用删除影响,保留不动 List appRoles = sysAppRoleRepository.findByAppId(id); if (!appRoles.isEmpty()) { List roleIds = new ArrayList(); for (SysAppRole r : appRoles) { roleIds.add(r.getId()); } sysAppRoleModuleRelRepository.deleteByRoleIdInBulk(roleIds); sysUserAppRoleRelRepository.deleteByRoleIdInBulk(roleIds); sysAppRoleRepository.deleteAll(appRoles); } // 先删模块(整棵子树随 appId 一并清除),再删应用,避免残留孤儿数据 appModuleRepository.deleteByAppId(id); applicationInfoRepository.deleteById(id); eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.delete(this, appKey)); } @Override public Page list(ApplicationInfoQuery query, Pageable pageable) { Objects.requireNonNull(query, "应用查询条件不能为空"); Objects.requireNonNull(pageable, "分页参数不能为空"); Page page = applicationInfoRepository.findAll(select(query), pageable); List content = new ArrayList(); for (ApplicationInfo entity : page.getContent()) { content.add(convertToVo(entity)); } return new PageImpl<>(content, pageable, page.getTotalElements()); } /** * 构建应用列表的动态查询条件。
* name 关键词:按应用名称模糊匹配,或按 appKey(appId)精确匹配;categoryId:按应用类别精确匹配。
* * @param query 前端传入的查询条件 * @return 动态查询 Specification */ private Specification select(ApplicationInfoQuery query) { Specification specification = new Specification() { @Override public Predicate toPredicate(Root root, CriteriaQuery cq, CriteriaBuilder cb) { List predicates = new ArrayList(); 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); eventPublisher.publishEvent(ApplicationInfoCacheChangedEvent.upsert(this, saved)); } @Override @Transactional public void sort(List 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 findListByCategoryId(Integer categoryId) { return applicationInfoRepository.findSortVoByCategoryId(categoryId); } }