init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s

This commit is contained in:
chenxing
2026-08-10 14:50:22 +08:00
commit 4ec19486e0
234 changed files with 15604 additions and 0 deletions
@@ -0,0 +1,259 @@
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>();
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);
}
}