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.dto.PushFilterDTO;
import com.renkang.emergency.entity.FirstAidResource; import com.renkang.emergency.entity.FirstAidResource;
import java.util.Map;
/** /**
* @Description: 急救宣教推送Service * @Description: 急救宣教推送Service
* @Author: jeecg-boot * @Author: jeecg-boot
@@ -24,7 +26,7 @@ public interface IFirstAidPushService {
void send(String resourceId, String orderId); 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.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@@ -81,17 +80,15 @@ public class FirstAidPushController {
* 一键推送(批量) * 一键推送(批量)
*/ */
@AutoLog(value = "急救宣教资源-批量推送") @AutoLog(value = "急救宣教资源-批量推送")
@Operation(summary = "一键推送", description = "批量推送资源,支持指定ID列表或按筛选条件全量推送") @Operation(summary = "一键推送", description = "批量推送资源,支持指定ID列表或按筛选条件全量推送,返回成功/失败明细")
@PostMapping(value = "/batchSend") @PostMapping(value = "/batchSend")
public Result<Map<String, Object>> batchSend(@RequestBody PushBatchSendDTO dto) { public Result<Map<String, Object>> batchSend(@RequestBody PushBatchSendDTO dto) {
if (dto.getOrderId() == null || dto.getOrderId().isEmpty()) { if (dto.getOrderId() == null || dto.getOrderId().isEmpty()) {
return Result.error("工单ID不能为空"); return Result.error("工单ID不能为空");
} }
try { try {
int count = pushService.batchSend(dto); Map<String, Object> result = pushService.batchSend(dto);
Map<String, Object> result = new HashMap<>(); return Result.OK(result);
result.put("count", count);
return Result.OK("已推送 " + count + "", result);
} catch (RuntimeException e) { } catch (RuntimeException e) {
return Result.error(e.getMessage()); return Result.error(e.getMessage());
} }
@@ -58,13 +58,13 @@ public class FirstAidResourceCategoryController extends JeecgController<FirstAid
return Result.OK(categoryService.listCategoriesWithTags()); return Result.OK(categoryService.listCategoriesWithTags());
} }
@Operation(summary = "急救宣教资源分类-分类列表") @Operation(summary = "急救宣教资源分类-分类列表")
@GetMapping(value = "/listRoot") @GetMapping(value = "/listRoot")
public Result<List<FirstAidResourceCategory>> listRoot() { public Result<List<FirstAidResourceCategory>> listRoot() {
return Result.OK(categoryService.listRootCategories()); return Result.OK(categoryService.listRootCategories());
} }
@Operation(summary = "急救宣教资源分类-子分类列表") @Operation(summary = "急救宣教资源分类-标签列表(按分类)")
@GetMapping(value = "/listChildren") @GetMapping(value = "/listChildren")
public Result<List<FirstAidResourceCategory>> listChildren(@RequestParam(name = "parentId") String parentId) { public Result<List<FirstAidResourceCategory>> listChildren(@RequestParam(name = "parentId") String parentId) {
return Result.OK(categoryService.listChildrenByParentId(parentId)); return Result.OK(categoryService.listChildrenByParentId(parentId));
@@ -128,13 +128,10 @@ public class FirstAidResourceCategoryController extends JeecgController<FirstAid
@AutoLog(value = "急救宣教资源分类-批量删除") @AutoLog(value = "急救宣教资源分类-批量删除")
//@RequiresPermissions("emergency:firstAidCategory:deleteBatch") //@RequiresPermissions("emergency:firstAidCategory:deleteBatch")
@PostMapping(value = "/deleteBatch") @PostMapping(value = "/deleteBatch")
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public Result<String> deleteBatch(@RequestBody List<String> ids) { public Result<String> deleteBatch(@RequestBody List<String> ids) {
for (String id : ids) { for (String id : ids) {
try {
categoryService.deleteWithCheck(id); categoryService.deleteWithCheck(id);
} catch (RuntimeException e) {
return Result.error("ID[" + id + "]" + e.getMessage());
}
} }
return Result.OK("批量删除成功!"); return Result.OK("批量删除成功!");
} }
@@ -61,7 +61,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
entity.setStatus(status); entity.setStatus(status);
entity.setTags(tags); entity.setTags(tags);
Page<FirstAidResource> page = new Page<>(pageNo, pageSize); 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)); return Result.OK(resourceService.queryPageList(entity, page, userId));
} }
@@ -91,7 +92,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-删除") @AutoLog(value = "急救宣教资源-删除")
@PostMapping(value = "/delete") @PostMapping(value = "/delete")
public Result<String> delete(@RequestBody IdDTO dto) { 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("删除成功!"); return Result.OK("删除成功!");
} }
@@ -99,7 +104,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量删除") @AutoLog(value = "急救宣教资源-批量删除")
@PostMapping(value = "/deleteBatch") @PostMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestBody List<String> ids) { 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("批量删除成功!"); return Result.OK("批量删除成功!");
} }
@@ -107,7 +116,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量发布") @AutoLog(value = "急救宣教资源-批量发布")
@PostMapping(value = "/batchPublish") @PostMapping(value = "/batchPublish")
public Result<String> batchPublish(@RequestBody List<String> ids) { public Result<String> batchPublish(@RequestBody List<String> ids) {
try {
resourceService.batchPublish(ids); resourceService.batchPublish(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量发布成功!"); return Result.OK("批量发布成功!");
} }
@@ -115,7 +128,11 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-批量下架") @AutoLog(value = "急救宣教资源-批量下架")
@PostMapping(value = "/batchTakeDown") @PostMapping(value = "/batchTakeDown")
public Result<String> batchTakeDown(@RequestBody List<String> ids) { public Result<String> batchTakeDown(@RequestBody List<String> ids) {
try {
resourceService.batchTakeDown(ids); resourceService.batchTakeDown(ids);
} catch (RuntimeException e) {
return Result.error(e.getMessage());
}
return Result.OK("批量下架成功!"); return Result.OK("批量下架成功!");
} }
@@ -158,7 +175,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@Operation(summary = "急救宣教资源-通过id查询详情") @Operation(summary = "急救宣教资源-通过id查询详情")
@GetMapping(value = "/queryById") @GetMapping(value = "/queryById")
public Result<FirstAidResource> queryById(@RequestParam(name = "id") String id) { 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); FirstAidResource entity = resourceService.getDetail(id, userId);
return entity == null ? Result.error("未找到对应数据") : Result.OK(entity); return entity == null ? Result.error("未找到对应数据") : Result.OK(entity);
} }
@@ -167,7 +185,8 @@ public class FirstAidResourceController extends JeecgController<FirstAidResource
@AutoLog(value = "急救宣教资源-记录阅读") @AutoLog(value = "急救宣教资源-记录阅读")
@PostMapping(value = "/incrementView") @PostMapping(value = "/incrementView")
public Result<String> incrementView(@RequestBody IdDTO dto) { 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); boolean firstRead = resourceService.incrementViewCount(dto.getId(), userId);
return firstRead ? Result.OK("阅读记录成功") : Result.OK("已阅读过"); return firstRead ? Result.OK("阅读记录成功") : Result.OK("已阅读过");
} }
@@ -128,13 +128,10 @@ public class FirstAidResourceTagController extends JeecgController<FirstAidResou
@AutoLog(value = "急救宣教资源标签-批量删除") @AutoLog(value = "急救宣教资源标签-批量删除")
//@RequiresPermissions("emergency:firstAidTag:deleteBatch") //@RequiresPermissions("emergency:firstAidTag:deleteBatch")
@PostMapping(value = "/deleteBatch") @PostMapping(value = "/deleteBatch")
@org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
public Result<String> deleteBatch(@RequestBody List<String> ids) { public Result<String> deleteBatch(@RequestBody List<String> ids) {
for (String id : ids) { for (String id : ids) {
try {
categoryService.deleteWithCheck(id); categoryService.deleteWithCheck(id);
} catch (RuntimeException e) {
return Result.error("ID[" + id + "]" + e.getMessage());
}
} }
return Result.OK("批量删除成功!"); return Result.OK("批量删除成功!");
} }
@@ -18,9 +18,9 @@ import java.util.List;
public interface FirstAidResourceCategoryMapper extends BaseMapper<FirstAidResourceCategory> { 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); 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); 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") @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> 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")
List<FirstAidResourceCategory> selectByParentId(@Param("parentId") String parentId); List<FirstAidResourceCategory> selectByParentId(@Param("parentId") String parentId);
/** /**
@@ -63,12 +57,6 @@ public interface FirstAidResourceCategoryMapper extends BaseMapper<FirstAidResou
"</script>") "</script>")
List<FirstAidResourceCategory> selectByTypeAndParent(@Param("type") Integer type, @Param("parentId") String parentId); 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 * 查询所有根分类(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") @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); 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> { public interface IFirstAidResourceCategoryService extends IService<FirstAidResourceCategory> {
/** /**
* 查询所有有效分类(扁平,一级分类 * 按父分类ID查询标签列表(TYPE=2
*/
List<FirstAidResourceCategory> listCategories();
/**
* 查询所有分类和标签(扁平,通过parentId组装树)
*/
List<FirstAidResourceCategory> listAllCategoriesAndTags();
/**
* 按父ID查询子节点(含子分类和标签,树形展开用)
*/ */
List<FirstAidResourceCategory> listByParentId(String parentId); List<FirstAidResourceCategory> listByParentId(String parentId);
/** /**
* 查询根分类列表(树形结构顶层 * 查询所有分类(扁平,无子分类
*/ */
List<FirstAidResourceCategory> listRootCategories(); List<FirstAidResourceCategory> listRootCategories();
/** /**
* 按父分类ID查询子分类 * 按父分类ID查询标签(树形展开用)
*/ */
List<FirstAidResourceCategory> listChildrenByParentId(String parentId); List<FirstAidResourceCategory> listChildrenByParentId(String parentId);
@@ -37,10 +37,10 @@ public interface IFirstAidResourceService extends IService<FirstAidResource> {
/** 审核驳回 */ /** 审核驳回 */
void reject(String id, String rejectReason); void reject(String id, String rejectReason);
/** 批量发布 */ /** 批量发布(前置校验:全部审核通过才执行,否则抛异常) */
void batchPublish(List<String> ids); void batchPublish(List<String> ids);
/** 批量下架 */ /** 批量下架(前置校验:全部已发布才执行,否则抛异常) */
void batchTakeDown(List<String> ids); void batchTakeDown(List<String> ids);
IPage<FirstAidResource> queryPageList(FirstAidResource resource, Page<FirstAidResource> page, String userId); 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); FirstAidResource getDetail(String id, String userId);
/** 删除资源(已发布状态不可删除) */
void deleteResource(String id);
/** 批量删除资源(前置校验:全部非已发布才执行,否则抛异常) */
void batchDeleteResources(List<String> ids);
/** 批量转换实体为导出VO(含分类、标签翻译) */ /** 批量转换实体为导出VO(含分类、标签翻译) */
List<FirstAidResourceExportVO> convertToExportVO(List<FirstAidResource> resources); List<FirstAidResourceExportVO> convertToExportVO(List<FirstAidResource> resources);
} }
@@ -19,10 +19,12 @@ import com.renkang.emergency.mapper.OrderMapper;
import com.renkang.emergency.service.IFirstAidPushService; import com.renkang.emergency.service.IFirstAidPushService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.global.GlobalUtils; import org.jeecg.global.GlobalUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.*;
@@ -63,14 +65,22 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
// 查询当前工单已推送的资源ID集合 // 查询当前工单已推送的资源ID集合
Set<String> pushedIds = getPushedResourceIds(orderId); 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()) { for (FirstAidResource resource : result.getRecords()) {
resource.setPushed(pushedIds.contains(resource.getId())); resource.setPushed(pushedIds.contains(resource.getId()));
if (StrUtil.isNotBlank(resource.getCategoryId())) { if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId()); resource.setCategoryName(categoryNameMap.get(resource.getCategoryId()));
if (category != null) {
resource.setCategoryName(category.getName());
}
} }
} }
@@ -78,7 +88,7 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void send(String resourceId, String orderId) { public void send(String resourceId, String orderId) {
// 1. 校验资源 // 1. 校验资源
FirstAidResource resource = resourceMapper.selectById(resourceId); FirstAidResource resource = resourceMapper.selectById(resourceId);
@@ -108,25 +118,29 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
} }
// 4. 保存推送记录 // 4. 保存推送记录
LoginUser loginUser = GlobalUtils.getLoginUser();
FirstAidPushRecord record = new FirstAidPushRecord(); FirstAidPushRecord record = new FirstAidPushRecord();
record.setResourceId(resourceId); record.setResourceId(resourceId);
record.setResourceTitle(resource.getTitle()); record.setResourceTitle(resource.getTitle());
record.setOrderId(orderId); record.setOrderId(orderId);
record.setTargetUserId(targetUserId); record.setTargetUserId(targetUserId);
record.setPushBy(GlobalUtils.getLoginUser().getRealname()); if (loginUser != null) {
record.setPushById(GlobalUtils.getLoginUser().getId()); record.setPushBy(loginUser.getRealname());
record.setPushById(loginUser.getId());
}
record.setPushTime(new Date()); record.setPushTime(new Date());
pushRecordMapper.insert(record); pushRecordMapper.insert(record);
// 5. 推送通知(预留,当前记录日志) // 5. 推送通知(预留,当前记录日志)
log.info("急救宣教推送: resourceId={}, title={}, orderId={}, targetUserId={}, pushById={}, pushByName={}", log.info("急救宣教推送: resourceId={}, title={}, orderId={}, targetUserId={}, pushById={}, pushByName={}",
resourceId, resource.getTitle(), orderId, targetUserId, resourceId, resource.getTitle(), orderId, targetUserId,
GlobalUtils.getLoginUser().getId(), GlobalUtils.getLoginUser().getRealname()); loginUser != null ? loginUser.getId() : null,
loginUser != null ? loginUser.getRealname() : null);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int batchSend(PushBatchSendDTO dto) { public Map<String, Object> batchSend(PushBatchSendDTO dto) {
List<String> resourceIds = dto.getResourceIds(); List<String> resourceIds = dto.getResourceIds();
// 方式二:按筛选条件查询 // 方式二:按筛选条件查询
@@ -141,7 +155,7 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
} }
int successCount = 0; int successCount = 0;
List<String> errors = new ArrayList<>(); List<Map<String, String>> errors = new ArrayList<>();
for (String resourceId : resourceIds) { for (String resourceId : resourceIds) {
try { try {
@@ -149,19 +163,29 @@ public class FirstAidPushServiceImpl implements IFirstAidPushService {
self.send(resourceId, dto.getOrderId()); self.send(resourceId, dto.getOrderId());
successCount++; successCount++;
} catch (RuntimeException e) { } 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()) { if (!errors.isEmpty()) {
log.warn("批量推送部分失败: {}/{} 条, 失败原因: {}", 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) { private Set<String> getPushedResourceIds(String orderId) {
if (StrUtil.isBlank(orderId)) {
return Collections.emptySet();
}
LambdaQueryWrapper<FirstAidPushRecord> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<FirstAidPushRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(FirstAidPushRecord::getOrderId, orderId) wrapper.eq(FirstAidPushRecord::getOrderId, orderId)
.select(FirstAidPushRecord::getResourceId); .select(FirstAidPushRecord::getResourceId);
@@ -2,6 +2,7 @@ package com.renkang.emergency.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.renkang.emergency.constant.FirstAidConstants; import com.renkang.emergency.constant.FirstAidConstants;
import org.springframework.transaction.annotation.Transactional;
import com.renkang.emergency.entity.FirstAidResourceCategory; import com.renkang.emergency.entity.FirstAidResourceCategory;
import com.renkang.emergency.mapper.FirstAidResourceCategoryMapper; import com.renkang.emergency.mapper.FirstAidResourceCategoryMapper;
import com.renkang.emergency.mapper.FirstAidResourceMapper; import com.renkang.emergency.mapper.FirstAidResourceMapper;
@@ -26,39 +27,13 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
@Autowired @Autowired
private FirstAidResourceMapper resourceMapper; 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 @Override
public List<FirstAidResourceCategory> listByParentId(String parentId) { public List<FirstAidResourceCategory> listByParentId(String parentId) {
// selectByParentId 仅查 TYPE=2(标签),无子分类
List<FirstAidResourceCategory> list = categoryMapper.selectByParentId(parentId); List<FirstAidResourceCategory> list = categoryMapper.selectByParentId(parentId);
for (FirstAidResourceCategory item : list) { for (FirstAidResourceCategory tag : list) {
if (FirstAidConstants.TYPE_CATEGORY.equals(item.getType())) { Integer count = categoryMapper.countResourcesByTagId(tag.getId());
Integer count = resourceMapper.countByCategoryId(item.getId()); tag.setResourceCount(count != null ? count : 0);
item.setResourceCount(count != null ? count : 0);
} else {
Integer count = categoryMapper.countResourcesByTagId(item.getId());
item.setResourceCount(count != null ? count : 0);
}
} }
return list; return list;
} }
@@ -117,6 +92,7 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void deleteWithCheck(String id) { public void deleteWithCheck(String id) {
FirstAidResourceCategory record = getById(id); FirstAidResourceCategory record = getById(id);
if (record == null) { if (record == null) {
@@ -125,8 +101,8 @@ public class FirstAidResourceCategoryServiceImpl extends ServiceImpl<FirstAidRes
if (FirstAidConstants.TYPE_CATEGORY.equals(record.getType())) { if (FirstAidConstants.TYPE_CATEGORY.equals(record.getType())) {
// 分类:检查是否有标签 // 分类:检查是否有标签
Integer childCount = categoryMapper.countChildrenByParentId(id); Integer tagCount = categoryMapper.countChildrenByParentId(id);
if (childCount != null && childCount > 0) { if (tagCount != null && tagCount > 0) {
throw new RuntimeException("该分类下存在标签,无法删除"); throw new RuntimeException("该分类下存在标签,无法删除");
} }
// 分类:检查是否有资源 // 分类:检查是否有资源
@@ -17,15 +17,19 @@ import com.renkang.emergency.service.IFirstAidResourceUserActionService;
import com.renkang.emergency.service.IFirstAidResourceVersionService; import com.renkang.emergency.service.IFirstAidResourceVersionService;
import com.renkang.emergency.vo.FirstAidResourceExportVO; import com.renkang.emergency.vo.FirstAidResourceExportVO;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.global.GlobalUtils; import org.jeecg.global.GlobalUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -63,6 +67,7 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
} }
resource.setStatus(FirstAidConstants.STATUS_DRAFT); resource.setStatus(FirstAidConstants.STATUS_DRAFT);
resource.setAuditStatus(null); resource.setAuditStatus(null);
validateResource(resource);
save(resource); save(resource);
// 保存初始版本,标记为当前版本 // 保存初始版本,标记为当前版本
versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "初始版本", true); 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.setStatus(FirstAidConstants.STATUS_DRAFT);
resource.setAuditStatus(null); resource.setAuditStatus(null);
resource.setAuditRejectReason(null); resource.setAuditRejectReason(null);
validateResource(resource);
updateById(resource); updateById(resource);
// 保存编辑后的新内容为当前版本 // 保存编辑后的新内容为当前版本
versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "编辑更新", true); versionService.saveVersion(resource.getId(), resource.getTitle(), resource.getContent(), "编辑更新", true);
@@ -103,11 +109,16 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
copy.setContent(source.getContent()); copy.setContent(source.getContent());
copy.setCoverImage(source.getCoverImage()); copy.setCoverImage(source.getCoverImage());
copy.setFileUrl(source.getFileUrl()); copy.setFileUrl(source.getFileUrl());
copy.setEffectiveTime(source.getEffectiveTime());
copy.setExpiryTime(source.getExpiryTime());
copy.setStatus(FirstAidConstants.STATUS_DRAFT); copy.setStatus(FirstAidConstants.STATUS_DRAFT);
copy.setAuditStatus(null); copy.setAuditStatus(null);
copy.setViewCount(0); copy.setViewCount(0);
copy.setFavoriteCount(0); copy.setFavoriteCount(0);
validateResource(copy);
save(copy); save(copy);
// 保存初始版本
versionService.saveVersion(copy.getId(), copy.getTitle(), copy.getContent(), "复制自「" + source.getTitle() + "", true);
return copy; return copy;
} }
@@ -118,6 +129,12 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
if (resource == null) { if (resource == null) {
throw new RuntimeException("资源不存在"); 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.setAuditStatus(FirstAidConstants.AUDIT_STATUS_PENDING);
resource.setAuditRejectReason(null); resource.setAuditRejectReason(null);
updateById(resource); updateById(resource);
@@ -130,6 +147,9 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
if (resource == null) { if (resource == null) {
throw new RuntimeException("资源不存在"); throw new RuntimeException("资源不存在");
} }
if (!FirstAidConstants.STATUS_PUBLISHED.equals(resource.getStatus())) {
throw new RuntimeException("仅已发布状态的资源可下架");
}
resource.setStatus(FirstAidConstants.STATUS_DOWN); resource.setStatus(FirstAidConstants.STATUS_DOWN);
updateById(resource); updateById(resource);
} }
@@ -157,8 +177,11 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
} }
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_APPROVED); resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_APPROVED);
resource.setAuditRejectReason(null); resource.setAuditRejectReason(null);
resource.setAuditBy(GlobalUtils.getLoginUser().getRealname()); LoginUser loginUser = GlobalUtils.getLoginUser();
resource.setAuditById(GlobalUtils.getLoginUser().getId()); if (loginUser != null) {
resource.setAuditBy(loginUser.getRealname());
resource.setAuditById(loginUser.getId());
}
resource.setAuditTime(new Date()); resource.setAuditTime(new Date());
updateById(resource); updateById(resource);
// 记录审核日志 // 记录审核日志
@@ -174,8 +197,11 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
} }
resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_REJECTED); resource.setAuditStatus(FirstAidConstants.AUDIT_STATUS_REJECTED);
resource.setAuditRejectReason(rejectReason); resource.setAuditRejectReason(rejectReason);
resource.setAuditBy(GlobalUtils.getLoginUser().getRealname()); LoginUser loginUser = GlobalUtils.getLoginUser();
resource.setAuditById(GlobalUtils.getLoginUser().getId()); if (loginUser != null) {
resource.setAuditBy(loginUser.getRealname());
resource.setAuditById(loginUser.getId());
}
resource.setAuditTime(new Date()); resource.setAuditTime(new Date());
updateById(resource); updateById(resource);
// 记录审核日志 // 记录审核日志
@@ -185,14 +211,18 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void batchPublish(List<String> ids) { public void batchPublish(List<String> ids) {
// 前置校验:全部审核通过才允许批量发布
for (String id : ids) { for (String id : ids) {
FirstAidResource resource = getById(id); FirstAidResource resource = getById(id);
if (resource == null) { if (resource == null) {
continue; throw new RuntimeException("资源[" + id + "]不存在");
} }
if (!FirstAidConstants.AUDIT_STATUS_APPROVED.equals(resource.getAuditStatus())) { 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); resource.setStatus(FirstAidConstants.STATUS_PUBLISHED);
updateById(resource); updateById(resource);
} }
@@ -201,13 +231,52 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void batchTakeDown(List<String> ids) { 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) { for (String id : ids) {
FirstAidResource resource = getById(id); FirstAidResource resource = getById(id);
if (resource != null) {
resource.setStatus(FirstAidConstants.STATUS_DOWN); resource.setStatus(FirstAidConstants.STATUS_DOWN);
updateById(resource); 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 @Override
@@ -240,12 +309,12 @@ public class FirstAidResourceServiceImpl extends ServiceImpl<FirstAidResourceMap
queryWrapper.eq(FirstAidResource::getAuditStatus, resource.getAuditStatus()); queryWrapper.eq(FirstAidResource::getAuditStatus, resource.getAuditStatus());
} }
} }
// 标签过滤 // 标签过滤(边界精确匹配,避免 id=2 误匹配 id=12/22
if (StrUtil.isNotBlank(resource.getTags())) { if (StrUtil.isNotBlank(resource.getTags())) {
String[] tagIds = resource.getTags().split(","); String[] tagIds = resource.getTags().split(",");
for (String tagId : tagIds) { for (String tagId : tagIds) {
if (StrUtil.isNotBlank(tagId)) { 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) ? userActionService.getFavoritedResourceIds(userId, resourceIds)
: Collections.emptyList(); : Collections.emptyList();
// 批量收集所有分类ID和标签ID
List<String> allCategoryIds = new ArrayList<>();
for (FirstAidResource resource : resources) { for (FirstAidResource resource : resources) {
if (StrUtil.isNotBlank(resource.getCategoryId())) { if (StrUtil.isNotBlank(resource.getCategoryId())) {
FirstAidResourceCategory category = categoryMapper.selectById(resource.getCategoryId()); allCategoryIds.add(resource.getCategoryId());
if (category != null) {
resource.setCategoryName(category.getName());
} }
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())) { if (StrUtil.isNotBlank(resource.getTags())) {
List<String> tagNames = new ArrayList<>(); List<String> tagNames = new ArrayList<>();
String[] tagIds = resource.getTags().split(","); for (String tagId : resource.getTags().split(",")) {
for (String tagId : tagIds) { String trimmed = tagId.trim();
String trimmedId = tagId.trim(); if (StrUtil.isNotBlank(trimmed)) {
if (StrUtil.isNotBlank(trimmedId)) { String tagName = nameMap.get(trimmed);
FirstAidResourceCategory tag = categoryMapper.selectById(trimmedId); if (tagName != null) {
if (tag != null) { tagNames.add(tagName);
tagNames.add(tag.getName());
} }
} }
} }
@@ -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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean recordRead(String resourceId, String userId) { 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(); FirstAidResourceUserAction action = new FirstAidResourceUserAction();
action.setResourceId(resourceId); action.setResourceId(resourceId);
action.setUserId(userId); action.setUserId(userId);
action.setType(FirstAidConstants.ACTION_TYPE_READ); action.setType(FirstAidConstants.ACTION_TYPE_READ);
try {
save(action); save(action);
} catch (org.springframework.dao.DuplicateKeyException e) {
// 浏览计数+1 return false; // 已有阅读记录
}
resourceMapper.incrementViewCount(resourceId); resourceMapper.incrementViewCount(resourceId);
return true; return true;
} }
@@ -61,27 +53,23 @@ public class FirstAidResourceUserActionServiceImpl extends ServiceImpl<FirstAidR
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean toggleFavorite(String resourceId, String userId) { 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(); FirstAidResourceUserAction action = new FirstAidResourceUserAction();
action.setResourceId(resourceId); action.setResourceId(resourceId);
action.setUserId(userId); action.setUserId(userId);
action.setType(FirstAidConstants.ACTION_TYPE_FAVORITE); action.setType(FirstAidConstants.ACTION_TYPE_FAVORITE);
try {
save(action); save(action);
resourceMapper.incrementFavoriteCount(resourceId); resourceMapper.incrementFavoriteCount(resourceId);
return true; 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; package com.renkang.emergency.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.renkang.emergency.constant.FirstAidConstants; import com.renkang.emergency.constant.FirstAidConstants;
@@ -23,6 +24,9 @@ import java.util.List;
@Service @Service
public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidResourceVersionMapper, FirstAidResourceVersion> implements IFirstAidResourceVersionService { public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidResourceVersionMapper, FirstAidResourceVersion> implements IFirstAidResourceVersionService {
/** 每个资源最多保留的版本数(含当前版本) */
private static final int MAX_VERSIONS = 20;
@Autowired @Autowired
private FirstAidResourceVersionMapper versionMapper; private FirstAidResourceVersionMapper versionMapper;
@@ -52,6 +56,24 @@ public class FirstAidResourceVersionServiceImpl extends ServiceImpl<FirstAidReso
version.setChangeDescription(changeDescription); version.setChangeDescription(changeDescription);
version.setIsCurrent(isCurrent); version.setIsCurrent(isCurrent);
save(version); 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 @Override