推送订阅添加批量删除功能

This commit is contained in:
chenxing
2026-08-19 20:38:00 +08:00
parent a91dacd442
commit f3656057d4
9 changed files with 160 additions and 8 deletions
@@ -12,6 +12,8 @@ public class DWebMvcConfig implements WebMvcConfigurer
{
@Autowired
private BoaUserContextInterceptor boaUserContextInterceptor;
@Autowired
private OapiCallerAppInterceptor oapiCallerAppInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry)
@@ -25,6 +27,9 @@ public class DWebMvcConfig implements WebMvcConfigurer
@Override
public void addInterceptors(InterceptorRegistry registry) {
// oapi 调用方来源注入:仅命中 /oapi/**,供推送来源排除使用
registry.addInterceptor(oapiCallerAppInterceptor)
.addPathPatterns("/oapi/**");
registry.addInterceptor(boaUserContextInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login", "/logout", "/static/**", "/error", "/upload/**");
@@ -87,11 +87,12 @@ public class GroupController {
}
/**
* 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。无返回值。
* 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。<br>
* 新增 {@code schoolId} 边界:服务端强制 id 属于该校子树且不等于 schoolId,避免删学校根组/跨校删组。
*/
@PostMapping("delete/{id}")
public void delete(@PathVariable Integer id) {
sysGroupService.delete(id);
@PostMapping("delete/{id}/{schoolId}")
public void delete(@PathVariable Integer id, @PathVariable Integer schoolId) {
sysGroupService.delete(id, schoolId);
}
/**
@@ -8,6 +8,7 @@ import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userbase.common.validation.ValidationGroups;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
import com.yunhai.userbase.model.sysuser.SysUserMoveRequest;
import com.yunhai.userbase.model.sysuser.SysUserRequest;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
@@ -168,11 +169,20 @@ public class UserController {
* 删除用户(按组解绑,语义同管理端「从组中删除用户」):仅解除该用户与指定组的关联;<br>
* 仅当用户已无任何组关联时,才级联删除 sys_user 及其角色绑定。<br>
* 这样多校用户只会被退出指定学校,而不会被整体误删。<br>
* 若用户仅归属这一个组,调用后该用户将被真正删除(组关联清空即触发级联删)。
* 新增 {@code schoolId} 边界:服务端强制 groupId 属于该校子树且不等于 schoolId,避免跨校/整校移除。
*/
@PostMapping("delete/{id}/{groupId}")
public void delete(@PathVariable Integer id, @PathVariable Integer groupId) {
sysUserService.deleteFromGroup(id, groupId);
@PostMapping("delete/{id}/{groupId}/{schoolId}")
public void delete(@PathVariable Integer id, @PathVariable Integer groupId, @PathVariable Integer schoolId) {
sysUserService.deleteFromGroup(id, groupId, schoolId);
}
/**
* 批量删除用户-组关系:请求体为 {@code SysUserGroupDeleteItem} 数组,每条携带自身 (userId, groupId, schoolId);<br>
* 服务端逐条校验学校边界后解绑,语义与单条 delete 一致。
*/
@PostMapping("deleteUsers")
public void deleteUsers(@RequestBody List<SysUserGroupDeleteItem> items) {
sysUserService.deleteUsers(items);
}
/**
@@ -24,4 +24,7 @@ public class DataSyncEvent {
private List<Integer> userIds;
/** 组维度的关联事件(如删除用户=解除组关系)携带的组 id */
private Integer groupId;
/** 触发本次变更的 oapi 调用方 appKey;非 oapi 来源(如管理后台)为 null。
* 推送分发器据此回推时排除来源应用自身,避免收到自己的回声。 */
private Integer sourceAppKey;
}
@@ -23,6 +23,18 @@ public interface ISysGroupService {
void delete(Integer id);
/**
* 开放接口专用:在学校(组)范围内删除组。<br>
* 与 {@link #delete(Integer)} 删除逻辑一致,但额外强制边界:<br>
* ① id 不能等于 schoolId(禁止删学校根组);<br>
* ② id 必须属于 schoolId 子树(禁止跨校删组)。<br>
* 任意一条不满足即抛异常。
*
* @param id 待删除组ID
* @param schoolId 学校组ID(限定删除范围)
*/
void delete(Integer id, Integer schoolId);
List<SysGroupVo> list(SysGroupQuery query);
/** 获取组织架构树;表为空时自动创建根节点「教育云中心」(pid=0)。 */
@@ -1,6 +1,7 @@
package com.yunhai.userbase.service;
import com.yunhai.userbase.model.sysuser.BatchImportResult;
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
import com.yunhai.userbase.model.sysuser.SysUserQuery;
import com.yunhai.userbase.model.sysuser.SysUserRequest;
import com.yunhai.userbase.model.sysuser.SysUserVo;
@@ -46,6 +47,26 @@ public interface ISysUserService {
*/
void batchDeleteFromGroup(List<Integer> userIds, Integer groupId);
/**
* 开放接口专用:在学校(组)范围内按组解绑用户。<br>
* 与 {@link #deleteFromGroup(Integer, Integer)} 语义一致,但额外强制边界:<br>
* ① groupId 不能等于 schoolId(禁止对学校根组解绑,避免整校移除);<br>
* ② groupId 必须属于 schoolId 子树(禁止跨校解绑)。<br>
* 任意一条不满足即抛异常。
*
* @param userId 用户ID
* @param groupId 当前组ID
* @param schoolId 学校组ID(限定解绑范围)
*/
void deleteFromGroup(Integer userId, Integer groupId, Integer schoolId);
/**
* 开放接口专用:批量删除用户-组关系;每条携带自身 (userId, groupId, schoolId),逐条做上述学校边界校验后解绑。
*
* @param items 待删除项列表({@link SysUserGroupDeleteItem})
*/
void deleteUsers(List<SysUserGroupDeleteItem> items);
/**
* 启用/禁用用户。<br>
*
@@ -6,6 +6,7 @@ import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.userbase.enums.GroupTypeEnum;
import com.yunhai.userbase.enums.IEnum;
import com.yunhai.userbase.event.DataSyncEvent;
import com.yunhai.userbase.common.context.CallerAppContext;
import com.yunhai.userbase.model.push.PushInterfaceType;
import org.springframework.context.ApplicationEventPublisher;
import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest;
@@ -199,6 +200,18 @@ public class SysGroupService implements ISysGroupService {
@Override
@Transactional
public void delete(Integer id) {
deleteInternal(id);
}
@Override
@Transactional
public void delete(Integer id, Integer schoolId) {
// 开放接口边界:id 必须属于 schoolId 子树且不能等于 schoolId(禁止删学校根组/跨校删组)
assertWithinSchool(id, schoolId);
deleteInternal(id);
}
private void deleteInternal(Integer id) {
SysGroup entity = sysGroupRepository.findById(id)
.orElseThrow(() -> new DException("系统组不存在"));
// 根节点不可删除
@@ -218,6 +231,31 @@ public class SysGroupService implements ISysGroupService {
publishGroupEvent(PushInterfaceType.DELETE_GROUP, id);
}
/**
* 学校边界校验:确保待删除的组属于指定学校范围,且不是学校根组本身。
*
* @param groupId 待删除组ID
* @param schoolId 学校组ID(限定范围)
*/
private void assertWithinSchool(Integer groupId, Integer schoolId) {
if (schoolId == null) {
throw new DException("学校ID不能为空");
}
if (groupId == null) {
throw new DException("组ID不能为空");
}
if (!sysGroupRepository.existsById(schoolId)) {
throw new DException("学校分组不存在");
}
if (groupId.equals(schoolId)) {
throw new DException("禁止删除学校根组,请指定具体子组");
}
List<Integer> schoolScope = listSelfAndDescendantIds(schoolId);
if (!schoolScope.contains(groupId)) {
throw new DException("组不属于该学校范围,禁止跨校操作");
}
}
@Override
public List<SysGroupVo> list(SysGroupQuery query) {
List<SysGroup> list;
@@ -653,6 +691,7 @@ public class SysGroupService implements ISysGroupService {
DataSyncEvent e = new DataSyncEvent();
e.setType(type);
e.setGroupIds(Collections.singletonList(groupId));
e.setSourceAppKey(CallerAppContext.getAppKey());
eventPublisher.publishEvent(e);
}
@@ -663,6 +702,7 @@ public class SysGroupService implements ISysGroupService {
DataSyncEvent e = new DataSyncEvent();
e.setType(type);
e.setGroupIds(groupIds);
e.setSourceAppKey(CallerAppContext.getAppKey());
eventPublisher.publishEvent(e);
}
}
@@ -9,6 +9,7 @@ import com.yunhai.userbase.enums.UserTypeEnum;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.model.sysuser.BatchImportResult;
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
import com.yunhai.userbase.model.sysuser.SysUserQuery;
import com.yunhai.userbase.model.sysuser.SysUserRequest;
import com.yunhai.userbase.model.sysuser.SysUserVo;
@@ -19,6 +20,7 @@ import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.ISysGroupService;
import com.yunhai.userbase.service.ISysUserService;
import com.yunhai.userbase.event.DataSyncEvent;
import com.yunhai.userbase.common.context.CallerAppContext;
import com.yunhai.userbase.model.push.PushInterfaceType;
import org.springframework.context.ApplicationEventPublisher;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
@@ -243,6 +245,53 @@ public class SysUserService implements ISysUserService {
publishUserEvent(PushInterfaceType.DELETE_USER, userIds, groupId);
}
@Override
@Transactional
public void deleteFromGroup(Integer userId, Integer groupId, Integer schoolId) {
// 开放接口边界:groupId 必须属于 schoolId 子树且不能等于 schoolId(禁止跨校/整校移除)
assertGroupWithinSchool(groupId, schoolId);
batchDeleteFromGroup(Collections.singletonList(userId), groupId);
}
@Override
@Transactional
public void deleteUsers(List<SysUserGroupDeleteItem> items) {
if (items == null || items.isEmpty()) {
throw new DException("批量删除用户-组关系列表不能为空");
}
for (SysUserGroupDeleteItem item : items) {
deleteFromGroup(item.getUserId(), item.getGroupId(), item.getSchoolId());
}
}
/**
* 学校边界校验:确保待操作的组属于指定学校范围,且不是学校根组本身。<br>
* 与 {@link #moveUserWithinSchool} 的「学校子树范围」判定一致,复用 {@code listSelfAndDescendantIds}。
*
* @param groupId 待操作组ID(解绑/删除目标)
* @param schoolId 学校组ID(限定范围)
*/
private void assertGroupWithinSchool(Integer groupId, Integer schoolId) {
if (schoolId == null) {
throw new DException("学校ID不能为空");
}
if (groupId == null) {
throw new DException("组ID不能为空");
}
if (!sysGroupRepository.existsById(schoolId)) {
throw new DException("学校分组不存在");
}
// 拿学校根组当 groupId = 把用户从整所学校移除,禁止
if (groupId.equals(schoolId)) {
throw new DException("禁止对学校根组执行该操作,请指定具体子组");
}
// groupId 必须落在 schoolId 子树(含子孙,不含学校根组自身——上面已拦截)
List<Integer> schoolScope = sysGroupService.listSelfAndDescendantIds(schoolId);
if (!schoolScope.contains(groupId)) {
throw new DException("组不属于该学校范围,禁止跨校操作");
}
}
@Override
@Transactional
public void changeStatus(Integer id, boolean disabled) {
@@ -1008,6 +1057,7 @@ public class SysUserService implements ISysUserService {
DataSyncEvent e = new DataSyncEvent();
e.setType(type);
e.setUserIds(Collections.singletonList(userId));
e.setSourceAppKey(CallerAppContext.getAppKey());
eventPublisher.publishEvent(e);
}
@@ -1018,6 +1068,7 @@ public class SysUserService implements ISysUserService {
DataSyncEvent e = new DataSyncEvent();
e.setType(type);
e.setUserIds(userIds);
e.setSourceAppKey(CallerAppContext.getAppKey());
eventPublisher.publishEvent(e);
}
@@ -1030,6 +1081,7 @@ public class SysUserService implements ISysUserService {
e.setType(type);
e.setUserIds(userIds);
e.setGroupId(groupId);
e.setSourceAppKey(CallerAppContext.getAppKey());
eventPublisher.publishEvent(e);
}
}
@@ -72,6 +72,10 @@ public class PushDispatcher {
? event.getGroupIds() : event.getUserIds();
Integer groupId = event.getGroupId();
for (ApplicationInfo app : applicationInfoRepository.findByPushEnabled(1)) {
// 来源排除:应用自己触发的变更(如 oapi 写接口)不回推给自己,避免收到自己的回声
if (event.getSourceAppKey() != null && event.getSourceAppKey().equals(app.getAppKey())) {
continue;
}
dispatchDeleteToOne(app, type, ids, groupId, "auto");
}
return;
@@ -88,6 +92,10 @@ public class PushDispatcher {
users = sysUserRepository.findAll();
}
for (ApplicationInfo app : applicationInfoRepository.findByPushEnabled(1)) {
// 来源排除:应用自己触发的变更(如 oapi 写接口)不回推给自己,避免收到自己的回声
if (event.getSourceAppKey() != null && event.getSourceAppKey().equals(app.getAppKey())) {
continue;
}
dispatchToOne(app, type, groups, users, "auto", false);
}
} catch (Exception e) {