From 4ab8877b80ef709f92e5e8d6e9fd7f4d57221a54 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Wed, 19 Nov 2025 13:25:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8F=91=E5=B8=83=E6=96=87?= =?UTF-8?q?=E7=AB=A0=EF=BC=8C=E6=9D=83=E9=99=90=E6=8F=92=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crontab/DataCollectionItemService.java | 4 +- .../api/news/resource/ResourceService.java | 21 +- schoolNewsServ/crontab/pom.xml | 5 + .../impl/DataCollectionItemServiceImpl.java | 23 +- .../task/newsTask/NewsCrawlerTask.java | 218 ++++++++++++++++-- .../service/impl/NCResourceServiceImpl.java | 80 ++++++- .../main/resources/mapper/ResourceMapper.xml | 10 +- 7 files changed, 307 insertions(+), 54 deletions(-) diff --git a/schoolNewsServ/api/api-crontab/src/main/java/org/xyzh/api/crontab/DataCollectionItemService.java b/schoolNewsServ/api/api-crontab/src/main/java/org/xyzh/api/crontab/DataCollectionItemService.java index 6b28c3b..c6a9c3d 100644 --- a/schoolNewsServ/api/api-crontab/src/main/java/org/xyzh/api/crontab/DataCollectionItemService.java +++ b/schoolNewsServ/api/api-crontab/src/main/java/org/xyzh/api/crontab/DataCollectionItemService.java @@ -28,11 +28,11 @@ public interface DataCollectionItemService { /** * @description 批量创建采集项 * @param itemList 采集项列表 - * @return ResultDomain 创建数量 + * @return ResultDomain 成功创建的采集项列表(dataList) * @author yslg * @since 2025-11-08 */ - ResultDomain batchCreateItems(List itemList); + ResultDomain batchCreateItems(List itemList); /** * @description 更新采集项 diff --git a/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java b/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java index 7e8f930..dffc8a2 100644 --- a/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java +++ b/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java @@ -7,6 +7,7 @@ import org.xyzh.common.dto.resource.TbResource; import org.xyzh.common.dto.usercenter.TbUserCollection; import org.xyzh.common.vo.ResourceVO; import org.xyzh.common.vo.TaskItemVO; +import org.xyzh.common.vo.UserDeptRoleVO; import java.util.List; @@ -56,14 +57,24 @@ public interface ResourceService { ResultDomain getResourceById(String resourceID); /** - * @description 创建资源 - * @param resource 资源信息 - * @return ResultDomain 创建结果 - * @author yslg - * @since 2025-10-15 + * @description 创建资源(使用当前登录用户部门角色创建权限) */ ResultDomain createResource(ResourceVO resource); + /** + * @description 创建资源(显式指定创建者部门角色,适用于定时任务等无登录用户场景) + * @param resource 资源信息 + * @param userDeptRoles 创建者部门角色列表 + */ + ResultDomain createResource(ResourceVO resource, java.util.List userDeptRoles); + + /** + * @description 批量创建资源 + * @param resources 资源列表 + * @return ResultDomain 成功创建的数量 + */ + ResultDomain batchCreateResources(List resources); + /** * @description 更新资源 * @param resource 资源信息 diff --git a/schoolNewsServ/crontab/pom.xml b/schoolNewsServ/crontab/pom.xml index f4e9230..7135da9 100644 --- a/schoolNewsServ/crontab/pom.xml +++ b/schoolNewsServ/crontab/pom.xml @@ -30,6 +30,11 @@ api-news 1.0.0 + + org.xyzh + api-system + 1.0.0 + diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/DataCollectionItemServiceImpl.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/DataCollectionItemServiceImpl.java index 812dd9b..f445887 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/DataCollectionItemServiceImpl.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/DataCollectionItemServiceImpl.java @@ -96,8 +96,8 @@ public class DataCollectionItemServiceImpl implements DataCollectionItemService @Override @Transactional(rollbackFor = Exception.class) - public ResultDomain batchCreateItems(List itemList) { - ResultDomain resultDomain = new ResultDomain<>(); + public ResultDomain batchCreateItems(List itemList) { + ResultDomain resultDomain = new ResultDomain<>(); try { if (itemList == null || itemList.isEmpty()) { resultDomain.fail("采集项列表为空"); @@ -105,18 +105,17 @@ public class DataCollectionItemServiceImpl implements DataCollectionItemService } int successCount = 0; - Date now = new Date(); - List newItems = new ArrayList<>(); + List createdItems = new ArrayList<>(); + // 改为逐条创建,避免单条失败导致整批回滚 for (TbDataCollectionItem it : itemList) { - TbDataCollectionItem existing = itemMapper.selectBySourceUrl(it.getSourceUrl()); - if (existing == null) { - newItems.add(it); + ResultDomain createResult = createItem(it); + if (createResult.isSuccess()) { + successCount++; + if (createResult.getData() != null) { + createdItems.add(createResult.getData()); + } } } - - if (!newItems.isEmpty()) { - successCount = itemMapper.batchInsertItems(newItems); - } String logId = itemList.get(0).getLogId(); TbCrontabLog log = new TbCrontabLog(); log.setID(logId); @@ -125,7 +124,7 @@ public class DataCollectionItemServiceImpl implements DataCollectionItemService int i = logMapper.updateLog(log); logger.info("批量创建采集项成功,共{}条,成功{}条", itemList.size(), successCount); - resultDomain.success("批量创建采集项成功", successCount); + resultDomain.success("批量创建采集项成功", createdItems); } catch (Exception e) { logger.error("批量创建采集项异常: ", e); resultDomain.fail("批量创建采集项异常: " + e.getMessage()); diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/newsTask/NewsCrawlerTask.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/newsTask/NewsCrawlerTask.java index d9473ff..36a6e68 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/newsTask/NewsCrawlerTask.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/task/newsTask/NewsCrawlerTask.java @@ -9,14 +9,21 @@ import org.xyzh.api.crontab.DataCollectionItemService; import org.xyzh.api.crontab.EmailDefaultService; import org.xyzh.api.crontab.EmailRecipientService; import org.xyzh.api.crontab.TaskMetaService; +import org.xyzh.api.news.resource.ResourceService; +import org.xyzh.api.system.role.RoleService; import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.dto.crontab.TbCrontabEmailDefault; import org.xyzh.common.dto.crontab.TbCrontabEmailRecipient; import org.xyzh.common.dto.crontab.TbCrontabTask; import org.xyzh.common.dto.crontab.TbCrontabTaskMeta; import org.xyzh.common.dto.crontab.TbDataCollectionItem; +import org.xyzh.common.dto.resource.TbResource; import org.xyzh.common.utils.EmailUtils; import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.NonUtils; +import org.xyzh.common.vo.DataCollectionItemVO; +import org.xyzh.common.vo.ResourceVO; +import org.xyzh.common.vo.UserDeptRoleVO; import org.xyzh.crontab.pojo.TaskParams; import org.xyzh.crontab.task.PythonCommandTask; @@ -25,9 +32,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -58,6 +69,12 @@ public class NewsCrawlerTask extends PythonCommandTask { @Autowired private EmailUtils emailUtils; + @Autowired + private ResourceService resourceService; + + @Autowired + private RoleService roleService; + /** * 构建Python脚本参数 */ @@ -165,9 +182,7 @@ public class NewsCrawlerTask extends PythonCommandTask { ResultDomain taskResult = crontabService.getTaskById(taskId); saveNewsToDatabase(newsList, taskResult.getData(), logId); - - // 发送邮件通知 - sendEmailNotification(taskId, taskResult.getData(), newsList); + } else { logger.warn("未提供任务ID或日志ID,跳过数据保存"); } @@ -218,11 +233,22 @@ public class NewsCrawlerTask extends PythonCommandTask { item.setSourceUrl(news.getUrl()); item.setAuthor(news.getAuthor()); - // 发布时间 + // 发布时间,兼容多种格式 String publishTimeStr = news.getPublishTime(); if (publishTimeStr != null && !publishTimeStr.isEmpty()) { try { - item.setPublishTime(dateFormat.parse(dateFormat.format(parser.parse(publishTimeStr)))); + Date parsed; + if (publishTimeStr.contains("年")) { + // 原始格式:yyyy年MM月dd日HH:mm + parsed = parser.parse(publishTimeStr); + } else if (publishTimeStr.length() == 10 && publishTimeStr.charAt(4) == '-' && publishTimeStr.charAt(7) == '-') { + // 仅日期:yyyy-MM-dd + parsed = new SimpleDateFormat("yyyy-MM-dd").parse(publishTimeStr); + } else { + // 其它情况按原parser尝试 + parsed = parser.parse(publishTimeStr); + } + item.setPublishTime(dateFormat.parse(dateFormat.format(parsed))); } catch (Exception e) { logger.warn("解析发布时间失败: {}", publishTimeStr); item.setPublishTime(now); @@ -242,20 +268,28 @@ public class NewsCrawlerTask extends PythonCommandTask { } // 批量保存 + Set insertedUrls = new HashSet<>(); + ResultDomain dataResult = new ResultDomain<>(); if (!itemList.isEmpty()) { - ResultDomain result = itemService.batchCreateItems(itemList); - if (result.isSuccess()) { - logger.info("成功保存 {} 条新闻到数据库", result.getData()); + dataResult = itemService.batchCreateItems(itemList); + if (dataResult.isSuccess()) { + logger.info("成功保存 {} 条新闻到数据库", itemList.size()); + insertedUrls.addAll(dataResult.getDataList().stream().map(TbDataCollectionItem::getSourceUrl).toList()); } else { - logger.error("保存新闻到数据库失败: {}", result.getMessage()); + logger.error("保存新闻到数据库失败: {}", dataResult.getMessage()); } } else { logger.warn("没有有效的新闻数据需要保存"); } - if (taskMeta.getAutoPublish()){ - publishNewsToArticle(newsList, task, logId); + // 自动发布并记录成功发布的 URL 集合 + Set publishedUrls = new HashSet<>(); + if (taskMeta.getAutoPublish().booleanValue()){ + publishedUrls = publishNewsToArticle(dataResult.getDataList(), task, logId); } + // 发送邮件通知,包含自动发布与新增信息 + sendEmailNotification(task.getTaskId(), task, newsList, insertedUrls, publishedUrls); + } catch (Exception e) { logger.error("保存新闻数据到数据库异常: ", e); } @@ -264,7 +298,9 @@ public class NewsCrawlerTask extends PythonCommandTask { /** * 发送邮件通知 */ - private void sendEmailNotification(String taskId, TbCrontabTask task, List newsList) { + private void sendEmailNotification(String taskId, TbCrontabTask task, List newsList, + Set insertedUrls, + Set publishedUrls) { try { List recipients = new ArrayList<>(); @@ -300,7 +336,7 @@ public class NewsCrawlerTask extends PythonCommandTask { // 5. 构建邮件内容 String subject = "【新闻爬虫通知】" + task.getTaskName() + " 执行完成"; - String content = buildEmailContent(task.getTaskName(), newsList); + String content = buildEmailContent(task.getTaskName(), newsList, insertedUrls, publishedUrls); // 6. 发送邮件 int successCount = 0; @@ -320,7 +356,9 @@ public class NewsCrawlerTask extends PythonCommandTask { /** * 构建邮件HTML内容 */ - private String buildEmailContent(String taskName, List newsList) { + private String buildEmailContent(String taskName, List newsList, + java.util.Set insertedUrls, + java.util.Set publishedUrls) { StringBuilder html = new StringBuilder(); html.append("") .append("") @@ -350,17 +388,21 @@ public class NewsCrawlerTask extends PythonCommandTask { .append("
"); // 摘要信息 + int insertedCount = (insertedUrls != null) ? insertedUrls.size() : 0; + int totalCount = newsList.size(); html.append("
") .append("

任务名称:").append(taskName).append("

") .append("

执行时间:").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("

") - .append("

爬取数量:").append(newsList.size()).append(" 条

") + .append("

爬取数量:").append(totalCount).append(" 条

") + .append("

入库情况:") + .append("本次新增 ").append(insertedCount).append(" 条,已有历史记录的新闻不会重复入库。

") .append("
"); - - // 新闻列表 + + // 新闻列表(包含自动发布标记) html.append("
") .append("

爬取内容:

"); - int count = Math.min(newsList.size(), 10); // 最多显示10条 + int count = newsList.size(); // 最多显示10条 for (int i = 0; i < count; i++) { ArticleStruct news = newsList.get(i); html.append("
") @@ -369,11 +411,26 @@ public class NewsCrawlerTask extends PythonCommandTask { .append("来源:").append(news.getSource() != null ? news.getSource() : "未知") .append(" | ") .append("发布时间:").append(news.getPublishTime() != null ? news.getPublishTime() : "未知"); - + if (news.getUrl() != null && !news.getUrl().isEmpty()) { html.append(" | 查看原文"); } - + + // 入库标记(新增 / 历史已存在) + if (news.getUrl() != null && !news.getUrl().isEmpty() && insertedUrls != null) { + if (insertedUrls.contains(news.getUrl())) { + html.append(" | 【本次新增】"); + } else { + html.append(" | 【历史已存在,未重复入库】"); + } + } + + // 如果该新闻已自动发布,追加标记 + if (publishedUrls != null && !publishedUrls.isEmpty() + && news.getUrl() != null && publishedUrls.contains(news.getUrl())) { + html.append(" | 【已自动发布】"); + } + html.append("
") .append("
"); } @@ -399,8 +456,123 @@ public class NewsCrawlerTask extends PythonCommandTask { return html.toString(); } - // TODO 自动发布功能,把采集的数据发布到文章表 - private void publishNewsToArticle(List newsList, TbCrontabTask task, String logId) { - + // 自动发布功能,把采集的数据发布到文章资源表 + private Set publishNewsToArticle(List itemList, TbCrontabTask task, String logId) { + if (itemList == null || itemList.isEmpty()) { + logger.info("自动发布:itemList 为空,跳过发布,任务ID: {},日志ID: {}", task.getTaskId(), logId); + return new HashSet<>(); + } + + logger.info("自动发布开始,共 {} 条,任务ID: {},日志ID: {}", itemList.size(), task.getTaskId(), logId); + List userDeptRoleVOs = null; + try { + ResultDomain roleResult = roleService.getDeptRolesByUserId(task.getCreator()); + if (roleResult != null && roleResult.isSuccess()) { + userDeptRoleVOs = roleResult.getDataList(); + } + } catch (Exception e) { + logger.warn("根据任务创建者获取部门角色失败,taskId: {},creator: {},原因: {}", task.getTaskId(), task.getCreator(), e.getMessage()); + } + Date now = new Date(); + int successCount = 0; + // 记录 url -> resourceId,用于后续更新采集表 + Map urlToResourceId = new HashMap<>(); + + for (TbDataCollectionItem item : itemList) { + if(item.getContent().isEmpty()) continue; + try { + TbResource resource = new TbResource(); + resource.setID(IDUtils.generateID()); + resource.setResourceID(IDUtils.generateID()); + resource.setTitle(item.getTitle()); + // 创建者使用任务创建者 + resource.setCreator(task.getCreator()); + + // 内容直接使用采集表中的 HTML + resource.setContent(item.getContent()); + + resource.setSummary(null); + resource.setCoverImage(null); + // 统一打标签为专题报告(tag_article_005) + resource.setTagID("tag_article_005"); + resource.setAuthor(item.getAuthor()); + resource.setSource(item.getSource()); + resource.setSourceUrl(item.getSourceUrl()); + + // 发布时间:优先使用采集表中的时间 + Date publishTime = item.getPublishTime() != null ? item.getPublishTime() : now; + resource.setPublishTime(publishTime); + + // 状态:已发布 + resource.setStatus(1); + resource.setViewCount(0); + resource.setLikeCount(0); + resource.setCollectCount(0); + resource.setIsRecommend(false); + resource.setIsBanner(false); + resource.setCreateTime(now); + resource.setUpdateTime(now); + resource.setDeleted(false); + + // 逐条创建资源,避免整批回滚 + ResourceVO vo = new ResourceVO(); + vo.setResource(resource); + + ResultDomain createResult = resourceService.createResource(vo, userDeptRoleVOs); + if (createResult.isSuccess()) { + successCount++; + // 记录 url -> resourceId,用于回写采集表 + if (item.getSourceUrl() != null && !item.getSourceUrl().isEmpty()) { + urlToResourceId.put(item.getSourceUrl(), resource.getResourceID()); + } + } else { + logger.warn("自动发布:创建资源失败,标题: {},原因: {}", item.getTitle(), createResult.getMessage()); + } + } catch (Exception e) { + logger.error("自动发布单条新闻异常,标题: {}", item.getTitle(), e); + } + } + + logger.info("自动发布完成,共 {} 条,成功 {} 条,任务ID: {},日志ID: {}", itemList.size(), successCount, task.getTaskId(), logId); + + // 成功创建资源后,同步更新采集表状态 + try { + TbDataCollectionItem filter = new TbDataCollectionItem(); + filter.setTaskId(task.getTaskId()); + filter.setLogId(logId); + + ResultDomain itemResult = itemService.getItemList(filter); + if (itemResult.isSuccess() && itemResult.getDataList() != null) { + for (DataCollectionItemVO itemVO : itemResult.getDataList()) { + // 只处理未转换的采集项 + if (itemVO.getStatus() != null && itemVO.getStatus() != 0) { + continue; + } + + String sourceUrl = itemVO.getSourceUrl(); + if (sourceUrl == null || sourceUrl.isEmpty()) { + continue; + } + + String resourceId = urlToResourceId.get(sourceUrl); + if (resourceId == null) { + continue; + } + + TbDataCollectionItem update = new TbDataCollectionItem(); + update.setID(itemVO.getId()); + update.setStatus(1); // 已转换为资源 + update.setResourceId(resourceId); + update.setProcessTime(now); + update.setProcessor(task.getCreator()); + + itemService.updateItem(update); + } + } + } catch (Exception e) { + logger.error("自动发布后更新采集表状态异常,任务ID: {},日志ID: {}", task.getTaskId(), logId, e); + } + + return urlToResourceId.keySet(); } } diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java index 7a23504..0ee9201 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java @@ -73,6 +73,65 @@ public class NCResourceServiceImpl implements ResourceService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain batchCreateResources(List resources) { + ResultDomain resultDomain = new ResultDomain<>(); + try { + if (resources == null || resources.isEmpty()) { + resultDomain.fail("资源列表不能为空"); + return resultDomain; + } + + Date now = new Date(); + // 填充必要的默认值 + for (TbResource res : resources) { + if (res.getID() == null) { + res.setID(IDUtils.generateID()); + } + if (res.getResourceID() == null) { + res.setResourceID(IDUtils.generateID()); + } + if (res.getCreateTime() == null) { + res.setCreateTime(now); + } + if (res.getUpdateTime() == null) { + res.setUpdateTime(now); + } + if (res.getStatus() == null) { + res.setStatus(0); + } + if (res.getViewCount() == null) { + res.setViewCount(0); + } + if (res.getLikeCount() == null) { + res.setLikeCount(0); + } + if (res.getCollectCount() == null) { + res.setCollectCount(0); + } + if (res.getIsRecommend() == null) { + res.setIsRecommend(false); + } + if (res.getIsBanner() == null) { + res.setIsBanner(false); + } + } + + int count = resourceMapper.batchInsertResources(resources); + if (count > 0) { + logger.info("批量创建资源成功,共 {} 条", count); + resultDomain.success("批量创建资源成功", count); + } else { + resultDomain.fail("批量创建资源失败"); + } + } catch (Exception e) { + logger.error("批量创建资源异常: {}", e.getMessage(), e); + resultDomain.fail("批量创建资源失败: " + e.getMessage()); + } + return resultDomain; + } + @Override public ResultDomain getResourcePage(TbResource filter, PageParam pageParam) { ResultDomain resultDomain = new ResultDomain<>(); @@ -161,6 +220,19 @@ public class NCResourceServiceImpl implements ResourceService { @Override @Transactional(rollbackFor = Exception.class) public ResultDomain createResource(ResourceVO resourceVO) { + // 前端/管理端调用:使用当前登录用户的部门角色 + List userDeptRoles = null; + try { + userDeptRoles = LoginUtil.getCurrentDeptRole(); + } catch (Exception e) { + logger.warn("获取当前用户部门角色失败: {}", e.getMessage()); + } + return createResource(resourceVO, userDeptRoles); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain createResource(ResourceVO resourceVO, List userDeptRoles) { ResultDomain resultDomain = new ResultDomain<>(); try { // 参数验证 @@ -169,14 +241,6 @@ public class NCResourceServiceImpl implements ResourceService { return resultDomain; } - // 检查标题是否已存在 - List userDeptRoles = LoginUtil.getCurrentDeptRole(); - int count = resourceMapper.countByTitle(resourceVO.getResource().getTitle(), null, userDeptRoles); - if (count > 0) { - resultDomain.fail("资源标题已存在"); - return resultDomain; - } - // 设置默认值 if (resourceVO.getResource().getID() == null) { resourceVO.getResource().setID(IDUtils.generateID()); diff --git a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml index 464ac83..ffaff9c 100644 --- a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml +++ b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml @@ -288,13 +288,15 @@ INSERT INTO tb_resource ( id, resource_id, title, content, summary, cover_image, tag_id, author, source, - source_url, creator,create_time + source_url, status, is_recommend, is_banner, publish_time, + creator, updater, create_time, update_time, deleted ) VALUES ( - #{item.id}, #{item.resourceID}, #{item.title}, #{item.content}, #{item.summary}, #{item.coverImage}, - #{item.tagID}, #{item.author}, #{item.source}, #{item.sourceUrl}, - #{item.creator}, #{item.createTime} + #{item.id}, #{item.resourceID}, #{item.title}, #{item.content}, #{item.summary}, #{item.coverImage}, + #{item.tagID}, #{item.author}, #{item.source}, #{item.sourceUrl}, + #{item.status}, #{item.isRecommend}, #{item.isBanner}, #{item.publishTime}, + #{item.creator}, #{item.updater}, #{item.createTime}, #{item.updateTime}, #{item.deleted} )