refactor(emergency): 宣教模块代码整理与问题修复

- 修复 GlobalUtils.getLoginUser() NPE 风险(send/approve/reject)
- 批量操作改为前置校验+全量执行(batchPublish/batchTakeDown/batchDeleteResources)
- 新增 deleteResource/batchDeleteResources,已发布资源不可删除
- 标签过滤 SQL 修复边界精确匹配(避免 id=2 误匹配 12/22)
- 收藏/阅读改用 DuplicateKeyException 消除并发竞态
- fillResourceExtras/queryResourceList 批量查询分类名称,避免 N+1
- 新增 validateResource 校验内容类型、分类、标签合法性
- copyResource 补全 effectiveTime/expiryTime、版本记录、校验
- 审核下发/下架/删除增加状态机保护
- send() 使用 REQUIRES_NEW 事务隔离,确保批量推送单条失败互不影响
- 版本历史上限控制 MAX_VERSIONS=20
- 分类/标签树形重构:分类不再嵌套子分类,统一为分类+标签两级
- 清理废弃方法(listCategories/listAllCategoriesAndTags 等)
- 批量删除分类/标签增加 @Transactional 事务保护

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:23:57 +08:00
co-authored by Claude
parent 3d8b930d1a
commit 5612fe0752
14 changed files with 301 additions and 156 deletions
@@ -6,6 +6,8 @@ import com.renkang.emergency.dto.PushBatchSendDTO;
import com.renkang.emergency.dto.PushFilterDTO;
import com.renkang.emergency.entity.FirstAidResource;
import java.util.Map;
/**
* @Description: 急救宣教推送Service
* @Author: jeecg-boot
@@ -24,7 +26,7 @@ public interface IFirstAidPushService {
void send(String resourceId, String orderId);
/**
* 批量推送
* 批量推送,返回 {total, success, failed} 结构
*/
int batchSend(PushBatchSendDTO dto);
Map<String, Object> batchSend(PushBatchSendDTO dto);
}
@@ -14,7 +14,6 @@ import org.jeecg.common.aspect.annotation.AutoLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
@@ -81,17 +80,15 @@ public class FirstAidPushController {
* 一键推送(批量)
*/
@AutoLog(value = "急救宣教资源-批量推送")
@Operation(summary = "一键推送", description = "批量推送资源,支持指定ID列表或按筛选条件全量推送")
@Operation(summary = "一键推送", description = "批量推送资源,支持指定ID列表或按筛选条件全量推送,返回成功/失败明细")
@PostMapping(value = "/batchSend")
public Result<Map<String, Object>> batchSend(@RequestBody PushBatchSendDTO dto) {
if (dto.getOrderId() == null || dto.getOrderId().isEmpty()) {
return Result.error("工单ID不能为空");
}
try {
int count = pushService.batchSend(dto);
Map<String, Object> result = new HashMap<>();
result.put("count", count);
return Result.OK("已推送 " + count + "", result);
Map<String, Object> result = pushService.batchSend(dto);
return Result.OK(result);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
@@ -58,13 +58,13 @@ public class FirstAidResourceCategoryController extends JeecgController<FirstAid
return Result.OK(categoryService.listCategoriesWithTags());
}
@Operation(summary = "急救宣教资源分类-分类列表")
@Operation(summary = "急救宣教资源分类-分类列表")
@GetMapping(value = "/listRoot")
public Result<List<FirstAidResourceCategory>> listRoot() {
return Result.OK(categoryService.listRootCategories());
}
@Operation(summary = "急救宣教资源分类-子分类列表")
@Operation(summary = "急救宣教资源分类-标签列表(按分类)")
@GetMapping(value = "/listChildren")
public Result<List<FirstAidResourceCategory>> listChildren(@RequestParam(name = "parentId") String parentId) {
return Result.OK(categoryService.listChildrenByParentId(parentId));
@@ -128,13 +128,10 @@ public class FirstAidResourceCategoryController extends JeecgController<FirstAid
@AutoLog(value = "急救宣教资源分类-批量删除")
//@RequiresPermissions("emergency:firstAidCategory:deleteBatch")
@PostMapping(value = "/deleteBatch")
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public Result<String> deleteBatch(@RequestBody List<String> ids) {
for (String id : ids) {
try {
categoryService.deleteWithCheck(id);
} catch (RuntimeException e) {
return Result.error("ID[" + id + "]" + e.getMessage());
}
}
return Result.OK("批量删除成功!");
}
@@ -61,7 +61,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
entity.setStatus(status);
entity.setTags(tags);
Page<FirstAidResource> page = new Page<>(pageNo, pageSize);
String userId = GlobalUtils.getLoginUser().getId();
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
return Result.OK(resourceService.queryPageList(entity, page, userId));
}
@@ -91,7 +92,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-删除")
@PostMapping(value = "/delete")
public Result<String> delete(@RequestBody IdDTO dto) {
resourceService.removeById(dto.getId());
try {
resourceService.deleteResource(dto.getId());
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("删除成功!");
}
@@ -99,7 +104,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量删除")
@PostMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestBody List<String> ids) {
resourceService.removeByIds(ids);
try {
resourceService.batchDeleteResources(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量删除成功!");
}
@@ -107,7 +116,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量发布")
@PostMapping(value = "/batchPublish")
public Result<String> batchPublish(@RequestBody List<String> ids) {
try {
resourceService.batchPublish(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量发布成功!");
}
@@ -115,7 +128,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量下架")
@PostMapping(value = "/batchTakeDown")
public Result<String> batchTakeDown(@RequestBody List<String> ids) {
try {
resourceService.batchTakeDown(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量下架成功!");
}
@@ -158,7 +175,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@Operation(summary = "急救宣教资源-通过id查询详情")
@GetMapping(value = "/queryById")
public Result<FirstAidResource> queryById(@RequestParam(name = "id") String id) {
String userId = GlobalUtils.getLoginUser().getId();
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
FirstAidResource entity = resourceService.getDetail(id, userId);
return entity == null ? Result.error("未找到对应数据") : Result.OK(entity);
}
@@ -167,7 +185,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-记录阅读")
@PostMapping(value = "/incrementView")
public Result<String> incrementView(@RequestBody IdDTO dto) {
String userId = GlobalUtils.getLoginUser().getId();
LoginUser loginUser = GlobalUtils.getLoginUser();
String userId = loginUser != null ? loginUser.getId() : null;
boolean firstRead = resourceService.incrementViewCount(dto.getId(), userId);
return firstRead ? Result.OK("阅读记录成功") : Result.OK("已阅读过");
}
@@ -128,13 +128,10 @@ public class FirstAidResourceTagController extends JeecgController<FirstAidResou
@AutoLog(value = "急救宣教资源标签-批量删除")
//@RequiresPermissions("emergency:firstAidTag:deleteBatch")
@PostMapping(value = "/deleteBatch")
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public Result<String> deleteBatch(@RequestBody List<String> ids) {
for (String id : ids) {
try {
categoryService.deleteWithCheck(id);
} catch (RuntimeException e) {
return Result.error("ID[" + id + "]" + e.getMessage());
}
}
return Result.OK("批量删除成功!");
}
@@ -18,9 +18,9 @@ import java.util.List;
public interface FirstAidResourceCategoryMapper extends BaseMapper<FirstAidResourceCategory> {
/**
* 查询某分类下的子分类/标签数量(级联删除校验用,含TYPE=1子分类和TYPE=2标签
* 查询某分类下的标签数量(级联删除校验用,TYPE=2)
*/
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND DEL_FLAG = 0")
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND TYPE = 2 AND DEL_FLAG = 0")
Integer countChildrenByParentId(@Param("parentId") String parentId);
/**
@@ -32,19 +32,13 @@ public interface FirstAidResourceCategoryMapper extends BaseMapper<FirstAidResou
/**
* 查询引用某标签的资源数量(标签删除校验用)
*/
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE WHERE DEL_FLAG = 0 AND TAGS LIKE CONCAT('%', #{tagId}, '%')")
@Select("SELECT COUNT(*) FROM QH_EME_FIRST_AID_RESOURCE WHERE DEL_FLAG = 0 AND CONCAT(',', TAGS, ',') LIKE CONCAT('%,', #{tagId}, ',%')")
Integer countResourcesByTagId(@Param("tagId") String tagId);
/**
* 查询所有有效分类和标签(树形组装用,含parentId
* 按父分类ID查询标签列表(TYPE=2
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE STATUS = 1 AND DEL_FLAG = 0 ORDER BY TYPE ASC, SORT_NO ASC")
List<FirstAidResourceCategory> selectAllCategoriesAndTags();
/**
* 按父ID查询子节点(含子分类TYPE=1和标签TYPE=2
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY TYPE ASC, SORT_NO ASC")
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND TYPE = 2 AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC")
List<FirstAidResourceCategory> selectByParentId(@Param("parentId") String parentId);
/**
@@ -63,12 +57,6 @@ public interface FirstAidResourceCategoryMapper extends BaseMapper<FirstAidResou
"</script>")
List<FirstAidResourceCategory> selectByTypeAndParent(@Param("type") Integer type, @Param("parentId") String parentId);
/**
* 按父分类ID查询子分类列表(树形结构用)
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_CATEGORY WHERE PARENT_ID = #{parentId} AND TYPE = 1 AND STATUS = 1 AND DEL_FLAG = 0 ORDER BY SORT_NO ASC")
List<FirstAidResourceCategory> selectChildrenByParentId(@Param("parentId") String parentId);
/**
* 查询所有根分类(PARENT_ID IS NULL
*/
@@ -28,4 +28,10 @@ public interface FirstAidResourceVersionMapper extends BaseMapper<FirstAidResour
*/
@Select("SELECT * FROM QH_EME_FIRST_AID_RESOURCE_VERSION WHERE RESOURCE_ID = #{resourceId} ORDER BY VERSION_NO DESC")
List<FirstAidResourceVersion> selectByResourceId(@Param("resourceId") String resourceId);
/**
* 查询某资源的所有非当前版本ID(按版本号降序),用于清理旧版本
*/
@Select("SELECT ID FROM QH_EME_FIRST_AID_RESOURCE_VERSION WHERE RESOURCE_ID = #{resourceId} AND IS_CURRENT = 0 ORDER BY VERSION_NO DESC")
List<String> selectNonCurrentVersionIds(@Param("resourceId") String resourceId);
}
@@ -14,27 +14,17 @@ import java.util.List;
public interface IFirstAidResourceCategoryService extends IService<FirstAidResourceCategory> {
/**
* 查询所有有效分类(扁平,一级分类
*/
List<FirstAidResourceCategory> listCategories();
/**
* 查询所有分类和标签(扁平,通过parentId组装树)
*/
List<FirstAidResourceCategory> listAllCategoriesAndTags();
/**
* 按父ID查询子节点(含子分类和标签,树形展开用)
* 按父分类ID查询标签列表(TYPE=2
*/
List<FirstAidResourceCategory> listByParentId(String parentId);
/**
* 查询根分类列表(树形结构顶层
* 查询所有分类(扁平,无子分类
*/
List<FirstAidResourceCategory> listRootCategories();
/**
* 按父分类ID查询子分类
* 按父分类ID查询标签(树形展开用)
*/
List<FirstAidResourceCategory> listChildrenByParentId(String parentId);
@@ -37,10 +37,10 @@ public interface IFirstAidResourceService extends IService<FirstAidResource> {
/** 审核驳回 */
void reject(String id, String rejectReason);
/** 批量发布 */
/** 批量发布(前置校验:全部审核通过才执行,否则抛异常) */
void batchPublish(List<String> ids);
/** 批量下架 */
/** 批量下架(前置校验:全部已发布才执行,否则抛异常) */
void batchTakeDown(List<String> ids);
IPage<FirstAidResource> queryPageList(FirstAidResource resource, Page<FirstAidResource> page, String userId);
@@ -49,6 +49,12 @@ public interface IFirstAidResourceService extends IService<FirstAidResource> {
FirstAidResource getDetail(String id, String userId);
/** 删除资源(已发布状态不可删除) */
void deleteResource(String id);
/** 批量删除资源(前置校验:全部非已发布才执行,否则抛异常) */
void batchDeleteResources(List<String> ids);
/** 批量转换实体为导出VO(含分类、标签翻译) */
List<FirstAidResourceExportVO> convertToExportVO(List<FirstAidResource> resources);
}
@@ -19,10 +19,12 @@ import com.renkang.emergency.mapper.OrderMapper;
import com.renkang.emergency.service.IFirstAidPushService;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.global.GlobalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@@ -63,14 +65,22 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
// 查询当前工单已推送的资源ID集合
Set<String> pushedIds = getPushedResourceIds(orderId);
// 批量查询分类名称,避免 N+1
List<String> categoryIds = result.getRecords().stream()
.map(FirstAidResource::getCategoryId)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
java.util.Map<String, String> categoryNameMap = CollectionUtil.isEmpty(categoryIds)
? Collections.emptyMap()
: categoryMapper.selectBatchIds(categoryIds).stream()
.collect(Collectors.toMap(FirstAidResourceCategory::getId, FirstAidResourceCategory::getName, (a, b) -> a));
// 填充推送状态 + 分类名称
for (FirstAidResource resource : result.getRecords()) {
resource.setPushed(pushedIds.contains(resource.getId()));
if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId());
if (category != null) {
resource.setCategoryName(category.getName());
}
resource.setCategoryName(categoryNameMap.get(resource.getCategoryId()));
}
}
@@ -78,7 +88,7 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
}
@Override
@Transactional(rollbackFor = Exception.class)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void send(String resourceId, String orderId) {
// 1. 校验资源
FirstAidResource resource = resourceMapper.selectById(resourceId);
@@ -108,25 +118,29 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
}
// 4. 保存推送记录
LoginUser loginUser = GlobalUtils.getLoginUser();
FirstAidPushRecord record = new FirstAidPushRecord();
record.setResourceId(resourceId);
record.setResourceTitle(resource.getTitle());
record.setOrderId(orderId);
record.setTargetUserId(targetUserId);
record.setPushBy(GlobalUtils.getLoginUser().getRealname());
record.setPushById(GlobalUtils.getLoginUser().getId());
if (loginUser != null) {
record.setPushBy(loginUser.getRealname());
record.setPushById(loginUser.getId());
}
record.setPushTime(new Date());
pushRecordMapper.insert(record);
// 5. 推送通知(预留,当前记录日志)
log.info("急救宣教推送: resourceId={}, title={}, orderId={}, targetUserId={}, pushById={}, pushByName={}",
resourceId, resource.getTitle(), orderId, targetUserId,
GlobalUtils.getLoginUser().getId(), GlobalUtils.getLoginUser().getRealname());
loginUser != null ? loginUser.getId() : null,
loginUser != null ? loginUser.getRealname() : null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int batchSend(PushBatchSendDTO dto) {
public Map<String, Object> batchSend(PushBatchSendDTO dto) {
List<String> resourceIds = dto.getResourceIds();
// 方式二:按筛选条件查询
@@ -141,7 +155,7 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
}
int successCount = 0;
List<String> errors = new ArrayList<>();
List<Map<String, String>> errors = new ArrayList<>();
for (String resourceId : resourceIds) {
try {
@@ -149,19 +163,29 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
self.send(resourceId, dto.getOrderId());
successCount++;
} catch (RuntimeException e) {
errors.add(resourceId + "" + e.getMessage());
Map<String, String> err = new HashMap<>();
err.put("resourceId", resourceId);
err.put("message", e.getMessage());
errors.add(err);
}
}
if (!errors.isEmpty()) {
log.warn("批量推送部分失败: {}/{} 条, 失败原因: {}",
successCount, resourceIds.size(), String.join("; ", errors));
successCount, resourceIds.size(), errors);
}
return successCount;
Map<String, Object> result = new HashMap<>();
result.put("total", resourceIds.size());
result.put("success", successCount);
result.put("failed", errors);
return result;
}
private Set<String> getPushedResourceIds(String orderId) {
if (StrUtil.isBlank(orderId)) {
return Collections.emptySet();
}
LambdaQueryWrapper<FirstAidPushRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FirstAidPushRecord::getOrderId, orderId)
.select(FirstAidPushRecord::getResourceId);
@@ -2,6 +2,7 @@ package com.renkang.emergency.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.renkang.emergency.constant.FirstAidConstants;
import org.springframework.transaction.annotation.Transactional;
import com.renkang.emergency.entity.FirstAidResourceCategory;
import com.renkang.emergency.mapper.FirstAidResourceCategoryMapper;
import com.renkang.emergency.mapper.FirstAidResourceMapper;
@@ -26,39 +27,13 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
@Autowired
private FirstAidResourceMapper resourceMapper;
@Override
public List<FirstAidResourceCategory> listCategories() {
List<FirstAidResourceCategory> list = categoryMapper.selectByType(FirstAidConstants.TYPE_CATEGORY);
fillResourceCounts(list);
return list;
}
@Override
public List<FirstAidResourceCategory> listAllCategoriesAndTags() {
List<FirstAidResourceCategory> list = categoryMapper.selectAllCategoriesAndTags();
for (FirstAidResourceCategory item : list) {
if (FirstAidConstants.TYPE_CATEGORY.equals(item.getType())) {
Integer count = resourceMapper.countByCategoryId(item.getId());
item.setResourceCount(count != null ? count : 0);
} else {
Integer count = categoryMapper.countResourcesByTagId(item.getId());
item.setResourceCount(count != null ? count : 0);
}
}
return list;
}
@Override
public List<FirstAidResourceCategory> listByParentId(String parentId) {
// selectByParentId 仅查 TYPE=2(标签),无子分类
List<FirstAidResourceCategory> list = categoryMapper.selectByParentId(parentId);
for (FirstAidResourceCategory item : list) {
if (FirstAidConstants.TYPE_CATEGORY.equals(item.getType())) {
Integer count = resourceMapper.countByCategoryId(item.getId());
item.setResourceCount(count != null ? count : 0);
} else {
Integer count = categoryMapper.countResourcesByTagId(item.getId());
item.setResourceCount(count != null ? count : 0);
}
for (FirstAidResourceCategory tag : list) {
Integer count = categoryMapper.countResourcesByTagId(tag.getId());
tag.setResourceCount(count != null ? count : 0);
}
return list;
}
@@ -117,6 +92,7 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteWithCheck(String id) {
FirstAidResourceCategory record = getById(id);
if (record == null) {
@@ -125,8 +101,8 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
if (FirstAidConstants.TYPE_CATEGORY.equals(record.getType())) {
// 分类:检查是否有标签
Integer childCount = categoryMapper.countChildrenByParentId(id);
if (childCount != null && childCount > 0) {
Integer tagCount = categoryMapper.countChildrenByParentId(id);
if (tagCount != null && tagCount > 0) {
throw new RuntimeException("该分类下存在标签,无法删除");
}
// 分类:检查是否有资源
@@ -17,15 +17,19 @@ import com.renkang.emergency.service.IFirstAidResourceUserActionService;
import com.renkang.emergency.service.IFirstAidResourceVersionService;
import com.renkang.emergency.vo.FirstAidResourceExportVO;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.global.GlobalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -63,6 +67,7 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
resource.setStatus(FirstAidConstants.STATUS_DRAFT);
resource.setAuditStatus(null);
validateResource(resource);
save(resource);
// 保存初始版本,标记为当前版本
versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "初始版本", true);
@@ -81,6 +86,7 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
resource.setStatus(FirstAidConstants.STATUS_DRAFT);
resource.setAuditStatus(null);
resource.setAuditRejectReason(null);
validateResource(resource);
updateById(resource);
// 保存编辑后的新内容为当前版本
versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "编辑更新", true);
@@ -103,11 +109,16 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
copy.setContent(source.getContent());
copy.setCoverImage(source.getCoverImage());
copy.setFileUrl(source.getFileUrl());
copy.setEffectiveTime(source.getEffectiveTime());
copy.setExpiryTime(source.getExpiryTime());
copy.setStatus(FirstAidConstants.STATUS_DRAFT);
copy.setAuditStatus(null);
copy.setViewCount(0);
copy.setFavoriteCount(0);
validateResource(copy);
save(copy);
// 保存初始版本
versionService.saveVersion(copy.getId(), copy.getTitle(), copy.getContent(), "复制自「" + source.getTitle() + "", true);
return copy;
}
@@ -118,6 +129,12 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
if (resource == null) {
throw new RuntimeException("资源不存在");
}
// 仅草稿或已驳回状态可提交审核
if (!FirstAidConstants.STATUS_DRAFT.equals(resource.getStatus())) {
if (!FirstAidConstants.AUDIT_STATUS_REJECTED.equals(resource.getAuditStatus())) {
throw new RuntimeException("当前状态不可提交审核");
}
}
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_PENDING);
resource.setAuditRejectReason(null);
updateById(resource);
@@ -130,6 +147,9 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
if (resource == null) {
throw new RuntimeException("资源不存在");
}
if (!FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("仅已发布状态的资源可下架");
}
resource.setStatus(FirstAidConstants.STATUS_DOWN);
updateById(resource);
}
@@ -157,8 +177,11 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_APPROVED);
resource.setAuditRejectReason(null);
resource.setAuditBy(GlobalUtils.getLoginUser().getRealname());
resource.setAuditById(GlobalUtils.getLoginUser().getId());
LoginUser loginUser = GlobalUtils.getLoginUser();
if (loginUser != null) {
resource.setAuditBy(loginUser.getRealname());
resource.setAuditById(loginUser.getId());
}
resource.setAuditTime(new Date());
updateById(resource);
// 记录审核日志
@@ -174,8 +197,11 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_REJECTED);
resource.setAuditRejectReason(rejectReason);
resource.setAuditBy(GlobalUtils.getLoginUser().getRealname());
resource.setAuditById(GlobalUtils.getLoginUser().getId());
LoginUser loginUser = GlobalUtils.getLoginUser();
if (loginUser != null) {
resource.setAuditBy(loginUser.getRealname());
resource.setAuditById(loginUser.getId());
}
resource.setAuditTime(new Date());
updateById(resource);
// 记录审核日志
@@ -185,14 +211,18 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
@Override
@Transactional(rollbackFor = Exception.class)
public void batchPublish(List<String> ids) {
// 前置校验:全部审核通过才允许批量发布
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource == null) {
continue;
throw new RuntimeException("资源[" + id + "]不存在");
}
if (!FirstAidConstants.AUDIT_STATUS_APPROVED.equals(resource.getAuditStatus())) {
continue;
throw new RuntimeException("资源[" + resource.getTitle() + "]尚未通过审核,无法发布");
}
}
for (String id : ids) {
FirstAidResource resource = getById(id);
resource.setStatus(FirstAidConstants.STATUS_PUBLISHED);
updateById(resource);
}
@@ -201,13 +231,52 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
@Override
@Transactional(rollbackFor = Exception.class)
public void batchTakeDown(List<String> ids) {
// 前置校验:全部已发布才允许批量下架
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource == null) {
throw new RuntimeException("资源[" + id + "]不存在");
}
if (!FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("资源[" + resource.getTitle() + "]非已发布状态,无法下架");
}
}
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource != null) {
resource.setStatus(FirstAidConstants.STATUS_DOWN);
updateById(resource);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteResource(String id) {
FirstAidResource resource = getById(id);
if (resource == null) {
throw new RuntimeException("资源不存在");
}
if (FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("已发布的资源不可删除,请先下架");
}
removeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDeleteResources(List<String> ids) {
// 前置校验:全部非已发布才允许删除
for (String id : ids) {
FirstAidResource resource = getById(id);
if (resource == null) {
throw new RuntimeException("资源[" + id + "]不存在");
}
if (FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("资源[" + resource.getTitle() + "]已发布,请先下架再删除");
}
}
for (String id : ids) {
removeById(id);
}
}
@Override
@@ -240,12 +309,12 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
queryWrapper.eq(FirstAidResource::getAuditStatus, resource.getAuditStatus());
}
}
// 标签过滤
// 标签过滤(边界精确匹配,避免 id=2 误匹配 id=12/22
if (StrUtil.isNotBlank(resource.getTags())) {
String[] tagIds = resource.getTags().split(",");
for (String tagId : tagIds) {
if (StrUtil.isNotBlank(tagId)) {
queryWrapper.like(FirstAidResource::getTags, tagId.trim());
queryWrapper.apply("CONCAT(',', TAGS, ',') LIKE CONCAT('%,', {0}, ',%')", tagId.trim());
}
}
}
@@ -302,22 +371,39 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
? userActionService.getFavoritedResourceIds(userId, resourceIds)
: Collections.emptyList();
// 批量收集所有分类ID和标签ID
List<String> allCategoryIds = new ArrayList<>();
for (FirstAidResource resource : resources) {
if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId());
if (category != null) {
resource.setCategoryName(category.getName());
allCategoryIds.add(resource.getCategoryId());
}
if (StrUtil.isNotBlank(resource.getTags())) {
for (String tagId : resource.getTags().split(",")) {
String trimmed = tagId.trim();
if (StrUtil.isNotBlank(trimmed)) {
allCategoryIds.add(trimmed);
}
}
}
}
// 批量查询,避免 N+1
java.util.Map<String, String> nameMap = CollectionUtil.isEmpty(allCategoryIds)
? Collections.emptyMap()
: categoryMapper.selectBatchIds(allCategoryIds).stream()
.collect(Collectors.toMap(FirstAidResourceCategory::getId, FirstAidResourceCategory::getName, (a, b) -> a));
for (FirstAidResource resource : resources) {
if (StrUtil.isNotBlank(resource.getCategoryId())) {
resource.setCategoryName(nameMap.get(resource.getCategoryId()));
}
if (StrUtil.isNotBlank(resource.getTags())) {
List<String> tagNames = new ArrayList<>();
String[] tagIds = resource.getTags().split(",");
for (String tagId : tagIds) {
String trimmedId = tagId.trim();
if (StrUtil.isNotBlank(trimmedId)) {
FirstAidResourceCategory tag = categoryMapper.selectById(trimmedId);
if (tag != null) {
tagNames.add(tag.getName());
for (String tagId : resource.getTags().split(",")) {
String trimmed = tagId.trim();
if (StrUtil.isNotBlank(trimmed)) {
String tagName = nameMap.get(trimmed);
if (tagName != null) {
tagNames.add(tagName);
}
}
}
@@ -327,6 +413,53 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
}
}
private static final Set<String> VALID_CONTENT_TYPES = new HashSet<>(Arrays.asList(
FirstAidConstants.CONTENT_TYPE_IMAGE_TEXT,
FirstAidConstants.CONTENT_TYPE_VIDEO,
FirstAidConstants.CONTENT_TYPE_AUDIO,
FirstAidConstants.CONTENT_TYPE_PDF
));
/**
* 校验资源的分类和标签引用合法性
*/
private void validateResource(FirstAidResource resource) {
// 校验内容类型
if (StrUtil.isNotBlank(resource.getContentType())
&& !VALID_CONTENT_TYPES.contains(resource.getContentType())) {
throw new RuntimeException("无效的内容类型: " + resource.getContentType());
}
// 校验分类存在且为分类类型
if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId());
if (category == null || !FirstAidConstants.TYPE_CATEGORY.equals(category.getType())) {
throw new RuntimeException("所选分类不存在");
}
}
// 校验标签存在且为标签类型
if (StrUtil.isNotBlank(resource.getTags())) {
String[] parts = resource.getTags().split(",");
Set<String> tagIdSet = new HashSet<>();
for (String p : parts) {
String trimmed = p.trim();
if (StrUtil.isNotBlank(trimmed)) {
tagIdSet.add(trimmed);
}
}
if (CollectionUtil.isNotEmpty(tagIdSet)) {
List<FirstAidResourceCategory> tags = categoryMapper.selectBatchIds(new ArrayList<>(tagIdSet));
if (tags.size() != tagIdSet.size()) {
throw new RuntimeException("所选标签不存在");
}
for (FirstAidResourceCategory tag : tags) {
if (!FirstAidConstants.TYPE_TAG.equals(tag.getType())) {
throw new RuntimeException("标签[" + tag.getName() + "]类型不正确");
}
}
}
}
}
/**
* 计算导出用的状态文本(审核状态优先于发布状态)
*/
@@ -37,23 +37,15 @@ public class FirstAidResourceUserActionServiceImpl extends ServiceImpl<FirstAidR
@Override
@Transactional(rollbackFor = Exception.class)
public boolean recordRead(String resourceId, String userId) {
// 检查是否已有阅读记录
LambdaQueryWrapper<FirstAidResourceUserAction> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FirstAidResourceUserAction::getResourceId, resourceId)
.eq(FirstAidResourceUserAction::getUserId, userId)
.eq(FirstAidResourceUserAction::getType, FirstAidConstants.ACTION_TYPE_READ);
if (ObjectUtil.isNotNull(getOne(queryWrapper, false))) {
return false;
}
// 插入阅读记录
FirstAidResourceUserAction action = new FirstAidResourceUserAction();
action.setResourceId(resourceId);
action.setUserId(userId);
action.setType(FirstAidConstants.ACTION_TYPE_READ);
try {
save(action);
// 浏览计数+1
} catch (org.springframework.dao.DuplicateKeyException e) {
return false; // 已有阅读记录
}
resourceMapper.incrementViewCount(resourceId);
return true;
}
@@ -61,27 +53,23 @@ public class FirstAidResourceUserActionServiceImpl extends ServiceImpl<FirstAidR
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleFavorite(String resourceId, String userId) {
// 查是否已收藏
LambdaQueryWrapper<FirstAidResourceUserAction> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FirstAidResourceUserAction::getResourceId, resourceId)
.eq(FirstAidResourceUserAction::getUserId, userId)
.eq(FirstAidResourceUserAction::getType, FirstAidConstants.ACTION_TYPE_FAVORITE);
FirstAidResourceUserAction existing = getOne(queryWrapper, false);
if (ObjectUtil.isNotNull(existing)) {
// 已收藏 → 取消收藏
removeById(existing.getId());
resourceMapper.decrementFavoriteCount(resourceId);
return false;
} else {
// 未收藏 → 收藏
FirstAidResourceUserAction action = new FirstAidResourceUserAction();
action.setResourceId(resourceId);
action.setUserId(userId);
action.setType(FirstAidConstants.ACTION_TYPE_FAVORITE);
try {
save(action);
resourceMapper.incrementFavoriteCount(resourceId);
return true;
} catch (org.springframework.dao.DuplicateKeyException e) {
// 已收藏 → 取消收藏
LambdaQueryWrapper<FirstAidResourceUserAction> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FirstAidResourceUserAction::getResourceId, resourceId)
.eq(FirstAidResourceUserAction::getUserId, userId)
.eq(FirstAidResourceUserAction::getType, FirstAidConstants.ACTION_TYPE_FAVORITE);
remove(queryWrapper);
resourceMapper.decrementFavoriteCount(resourceId);
return false;
}
}
@@ -1,5 +1,6 @@
package com.renkang.emergency.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.renkang.emergency.constant.FirstAidConstants;
@@ -23,6 +24,9 @@ import java.util.List;
@Service
public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidResourceVersionMapper, FirstAidResourceVersion> implements IFirstAidResourceVersionService {
/** 每个资源最多保留的版本数(含当前版本) */
private static final int MAX_VERSIONS = 20;
@Autowired
private FirstAidResourceVersionMapper versionMapper;
@@ -52,6 +56,24 @@ public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidReso
version.setChangeDescription(changeDescription);
version.setIsCurrent(isCurrent);
save(version);
// 清理超出上限的旧版本(当前版本 + 最近 MAX_VERSIONS-1 条非当前版本,其余删除)
cleanOldVersions(resourceId);
}
/**
* 清理超出保留上限的旧版本,保留当前版本 + 最近N条非当前版本
*/
private void cleanOldVersions(String resourceId) {
List<String> nonCurrentIds = versionMapper.selectNonCurrentVersionIds(resourceId);
if (nonCurrentIds.size() <= MAX_VERSIONS - 1) {
return; // 未超上限,无需清理
}
// 保留最近 MAX_VERSIONS-1 条非当前版本,删除其余
List<String> toDelete = nonCurrentIds.subList(MAX_VERSIONS - 1, nonCurrentIds.size());
if (CollUtil.isNotEmpty(toDelete)) {
removeByIds(toDelete);
}
}
@Override