From 427f66bce5bb816c785804a8970ee97a5fbe2236 Mon Sep 17 00:00:00 2001 From: jianghao <332515344@qq.com> Date: Mon, 16 Mar 2026 19:31:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=95=85=E4=BA=8B=E5=88=86=E4=BA=AB):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=95=85=E4=BA=8B=E5=88=86=E4=BA=AB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现故事分享功能,包括分享配置、发布、取消发布及公开访问接口 新增故事分享相关VO类及数据库表结构 扩展故事和故事项实体类以支持分享功能 添加故事分享Mapper及XML映射文件 更新故事服务和控制器以支持分享操作 --- deploy/update_story_share.sql | 18 + .../story/controller/AnalyticsController.java | 29 + .../story/controller/StoryItemController.java | 131 ++- .../controller/StoryPublicController.java | 19 +- .../timeline/story/dao/StoryItemMapper.java | 39 +- .../com/timeline/story/dao/StoryMapper.java | 16 +- .../timeline/story/dao/StoryShareMapper.java | 25 + .../java/com/timeline/story/entity/Story.java | 5 +- .../com/timeline/story/entity/StoryItem.java | 7 +- .../com/timeline/story/entity/StoryShare.java | 22 + .../story/service/AnalyticsService.java | 6 + .../story/service/StoryItemService.java | 38 +- .../service/impl/AnalyticsServiceImpl.java | 789 +++++++++++++++--- .../service/impl/StoryItemServiceImpl.java | 373 +++++++-- .../story/service/impl/StoryServiceImpl.java | 80 +- .../timeline/story/vo/StoryItemShareVo.java | 13 + .../timeline/story/vo/StoryShareConfigVo.java | 19 + .../timeline/story/vo/StoryShareMomentVo.java | 16 + .../story/vo/StorySharePublishRequest.java | 15 + .../story/vo/StorySharePublishVo.java | 14 + .../story/vo/TimelineArchiveBucketVo.java | 17 + .../story/vo/TimelineArchiveExploreVo.java | 13 + .../story/vo/TimelineArchiveMomentVo.java | 25 + .../story/vo/TimelineArchiveSummaryVo.java | 14 + .../timeline/story/dao/StoryItemMapper.xml | 151 ++-- .../com/timeline/story/dao/StoryMapper.xml | 77 +- .../timeline/story/dao/StoryShareMapper.xml | 84 ++ 27 files changed, 1629 insertions(+), 426 deletions(-) create mode 100644 deploy/update_story_share.sql create mode 100644 timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareMapper.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareMomentVo.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveExploreVo.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java create mode 100644 timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveSummaryVo.java create mode 100644 timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml diff --git a/deploy/update_story_share.sql b/deploy/update_story_share.sql new file mode 100644 index 0000000..df8a6d2 --- /dev/null +++ b/deploy/update_story_share.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS `story_share` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `share_id` varchar(64) NOT NULL COMMENT 'Public share ID', + `story_instance_id` varchar(32) NOT NULL COMMENT 'Story instance ID', + `hero_moment_id` varchar(32) DEFAULT NULL COMMENT 'Primary public moment ID', + `share_title` varchar(255) DEFAULT NULL COMMENT 'Curated public title', + `share_description` text COMMENT 'Curated public description', + `share_quote` text COMMENT 'Curated story note', + `featured_moment_ids` text COMMENT 'Comma separated featured moment IDs', + `create_id` varchar(32) DEFAULT NULL, + `update_id` varchar(32) DEFAULT NULL, + `create_time` datetime DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_delete` tinyint(1) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_story_share_share_id` (`share_id`), + KEY `idx_story_share_story_id` (`story_instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/timeline-story-service/src/main/java/com/timeline/story/controller/AnalyticsController.java b/timeline-story-service/src/main/java/com/timeline/story/controller/AnalyticsController.java index 61c7ccc..c51dec9 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/controller/AnalyticsController.java +++ b/timeline-story-service/src/main/java/com/timeline/story/controller/AnalyticsController.java @@ -4,6 +4,8 @@ import com.timeline.common.response.ResponseEntity; import com.timeline.common.utils.UserContextUtils; import com.timeline.story.service.AnalyticsService; import com.timeline.story.vo.TimelineAnalyticsVo; +import com.timeline.story.vo.TimelineArchiveExploreVo; +import com.timeline.story.vo.TimelineArchiveSummaryVo; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -124,6 +126,33 @@ public class AnalyticsController { * @param year 年份(可选,默认去年) * @return 年度报告 */ + @GetMapping("/archive/summary") + public ResponseEntity getArchiveSummary( + @RequestParam(defaultValue = "8") int locationLimit, + @RequestParam(defaultValue = "12") int tagLimit) { + String userId = UserContextUtils.getCurrentUserId(); + log.info( + "Get archive summary: userId={}, locationLimit={}, tagLimit={}", + userId, + locationLimit, + tagLimit); + + TimelineArchiveSummaryVo summary = analyticsService.getArchiveSummary(userId, locationLimit, tagLimit); + return ResponseEntity.success(summary); + } + + @GetMapping("/archive/explore") + public ResponseEntity getArchiveExplore( + @RequestParam String type, + @RequestParam(required = false) String value, + @RequestParam(defaultValue = "12") int limit) { + String userId = UserContextUtils.getCurrentUserId(); + log.info("Get archive explore: userId={}, type={}, value={}, limit={}", userId, type, value, limit); + + TimelineArchiveExploreVo explore = analyticsService.getArchiveExplore(userId, type, value, limit); + return ResponseEntity.success(explore); + } + @GetMapping("/yearly-report") public com.timeline.common.response.ResponseEntity getYearlyReport( @RequestParam(required = false) Integer year) { diff --git a/timeline-story-service/src/main/java/com/timeline/story/controller/StoryItemController.java b/timeline-story-service/src/main/java/com/timeline/story/controller/StoryItemController.java index 39f4dbc..dce9f82 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/controller/StoryItemController.java +++ b/timeline-story-service/src/main/java/com/timeline/story/controller/StoryItemController.java @@ -5,12 +5,12 @@ import com.timeline.common.response.ResponseEntity; import com.timeline.common.response.ResponseEnum; import com.timeline.story.entity.StoryItem; import com.timeline.story.service.StoryItemService; -import com.timeline.story.service.StoryService; import com.timeline.story.vo.StoryItemAddVo; import com.timeline.story.vo.StoryItemShareVo; import com.timeline.story.vo.StoryItemVo; -import com.timeline.story.vo.StoryVo; - +import com.timeline.story.vo.StoryShareConfigVo; +import com.timeline.story.vo.StorySharePublishRequest; +import com.timeline.story.vo.StorySharePublishVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.openfeign.SpringQueryMap; @@ -29,129 +29,126 @@ public class StoryItemController { private StoryItemService storyItemService; @PostMapping() - public ResponseEntity createItem(@RequestParam("storyItem") String storyItemVoString, + public ResponseEntity createItem( + @RequestParam("storyItem") String storyItemVoString, @RequestParam(value = "images", required = false) List images) { - log.info("创建 StoryItem,{}", storyItemVoString); + log.info("Create StoryItem: {}", storyItemVoString); storyItemService.createStoryItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images); - return ResponseEntity.success("StoryItem 创建成功"); + return ResponseEntity.success("StoryItem created"); } @PutMapping("") - public ResponseEntity updateItem(@RequestParam("storyItem") String storyItemVoString, + public ResponseEntity updateItem( + @RequestParam("storyItem") String storyItemVoString, @RequestParam(value = "images", required = false) List images) { - log.info("更新 StoryItem: {}", storyItemVoString); + log.info("Update StoryItem: {}", storyItemVoString); storyItemService.updateItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images); - return ResponseEntity.success("StoryItem 更新成功"); + return ResponseEntity.success("StoryItem updated"); } @DeleteMapping("/{itemId}") public ResponseEntity deleteItem(@PathVariable String itemId) { - log.info("删除 StoryItem: {}", itemId); + log.info("Delete StoryItem: {}", itemId); storyItemService.deleteItem(itemId); - return ResponseEntity.success("StoryItem 删除成功"); + return ResponseEntity.success("StoryItem deleted"); } @GetMapping("/{itemId}") public ResponseEntity getItemById(@PathVariable String itemId) { - log.info("获取 StoryItem 详情: {}", itemId); + log.info("Get StoryItem detail: {}", itemId); StoryItem item = storyItemService.getItemById(itemId); return ResponseEntity.success(item); } @GetMapping("/list") public ResponseEntity getItemsByMasterItem(@SpringQueryMap StoryItemVo storyItemVo) { - log.info("查询 StoryItem 列表,storyInstanceId: {}, current: {}, pageSize:{}", storyItemVo.getStoryInstanceId(), - storyItemVo.getCurrent(), storyItemVo.getPageSize()); + log.info( + "Query StoryItem list: storyInstanceId={}, current={}, pageSize={}", + storyItemVo.getStoryInstanceId(), + storyItemVo.getCurrent(), + storyItemVo.getPageSize()); Map items = storyItemService.getItemsByMasterItem(storyItemVo); return ResponseEntity.success(items); } @GetMapping("/images/{itemId}") public ResponseEntity> getStoryItemImages(@PathVariable String itemId) { - log.info("获取 StoryItem 图片列表,itemId: {}", itemId); + log.info("Get StoryItem images: itemId={}", itemId); List images = storyItemService.getStoryItemImages(itemId); return ResponseEntity.success(images); } @GetMapping("/count/{storyInstanceId}") public ResponseEntity getStoryItemCount(@PathVariable String storyInstanceId) { - log.info("获取 StoryItem 子项数量,storyInstanceId: {}", storyInstanceId); + log.info("Get StoryItem count: storyInstanceId={}", storyInstanceId); Integer count = storyItemService.getStoryItemCount(storyInstanceId); return ResponseEntity.success(count); } - /* - * @GetMapping("/public/story/item/{shareId}") - * public ResponseEntity getStoryItemByShareId(@PathVariable - * String shareId) { - * log.info("获取分享的 StoryItem,shareId: {}", shareId); - * StoryItemShareVo item = storyItemService.getItemByShareId(shareId); - * return ResponseEntity.success(item); - * } - */ + @GetMapping("/public/story/item/{shareId}") + public ResponseEntity getStoryItemByShareId(@PathVariable String shareId) { + log.info("Get public StoryItem by shareId: {}", shareId); + StoryItemShareVo item = storyItemService.getItemByShareId(shareId); + return ResponseEntity.success(item); + } + + @GetMapping("/share/{storyId}") + public ResponseEntity getStoryShareConfig(@PathVariable String storyId) { + log.info("Get Story share config: storyId={}", storyId); + StoryShareConfigVo config = storyItemService.getStoryShareConfig(storyId); + return ResponseEntity.success(config); + } + + @PostMapping("/share/publish") + public ResponseEntity publishStoryShare(@RequestBody StorySharePublishRequest request) { + log.info( + "Publish Story share: storyId={}, heroMomentId={}", + request.getStoryId(), + request.getHeroMomentId()); + StorySharePublishVo result = storyItemService.publishStoryShare(request); + return ResponseEntity.success(result); + } + + @DeleteMapping("/share/{storyId}") + public ResponseEntity unpublishStoryShare(@PathVariable String storyId) { + log.info("Unpublish Story share: storyId={}", storyId); + storyItemService.unpublishStoryShare(storyId); + return ResponseEntity.success("Story share unpublished"); + } @GetMapping("/search") - public ResponseEntity searchItems(@RequestParam String keyword, + public ResponseEntity searchItems( + @RequestParam String keyword, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { - log.info("搜索 StoryItem,keyword: {}, pageNum: {}, pageSize: {}", keyword, pageNum, pageSize); + log.info("Search StoryItem: keyword={}, pageNum={}, pageSize={}", keyword, pageNum, pageSize); Map result = storyItemService.searchItems(keyword, pageNum, pageSize); return ResponseEntity.success(result); } - /** - * 批量更新时间线节点排序 - * - * 功能描述: - * 接收前端拖拽排序后的节点顺序,批量更新各节点的 sortOrder 字段。 - * 用于保存用户手动调整的时间线节点顺序。 - * - * @param request 包含 items 数组,每个元素有 instanceId 和 sortOrder - * @return 操作结果 - */ @PutMapping("/order") public ResponseEntity updateItemsOrder(@RequestBody Map>> request) { List> items = request.get("items"); if (items == null || items.isEmpty()) { - return ResponseEntity.error("排序数据不能为空"); + return ResponseEntity.error("Sort payload cannot be empty"); } - log.info("批量更新 StoryItem 排序,共 {} 项", items.size()); + log.info("Update StoryItem order: {} items", items.size()); storyItemService.updateItemsOrder(items); - return ResponseEntity.success("排序更新成功"); + return ResponseEntity.success("Order updated"); } - /** - * 批量删除时间线节点 - * - * 功能描述: - * 根据节点ID列表批量软删除时间线节点。 - * 软删除仅标记 is_delete 字段,数据可恢复。 - * - * @param request 包含 instanceIds 数组 - * @return 操作结果 - */ @PostMapping("/batch-delete") public ResponseEntity batchDeleteItems(@RequestBody Map> request) { List instanceIds = request.get("instanceIds"); if (instanceIds == null || instanceIds.isEmpty()) { - return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "删除列表不能为空"); + return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "Delete list cannot be empty"); } - log.info("批量删除 StoryItem,共 {} 项", instanceIds.size()); + log.info("Batch delete StoryItem: {} items", instanceIds.size()); storyItemService.batchDeleteItems(instanceIds); - return ResponseEntity.success("批量删除成功"); + return ResponseEntity.success("Batch delete completed"); } - /** - * 批量修改时间线节点时间 - * - * 功能描述: - * 批量修改多个节点的 storyItemTime 字段。 - * 用于批量调整节点的时间信息。 - * - * @param request 包含 instanceIds 数组和 storyItemTime 字符串 - * @return 操作结果 - */ @PutMapping("/batch-time") public ResponseEntity batchUpdateItemTime(@RequestBody Map request) { @SuppressWarnings("unchecked") @@ -159,14 +156,14 @@ public class StoryItemController { String storyItemTime = (String) request.get("storyItemTime"); if (instanceIds == null || instanceIds.isEmpty()) { - return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "修改列表不能为空"); + return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "Update list cannot be empty"); } if (storyItemTime == null || storyItemTime.isEmpty()) { - return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "时间不能为空"); + return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "Time cannot be empty"); } - log.info("批量修改 StoryItem 时间,共 {} 项,新时间: {}", instanceIds.size(), storyItemTime); + log.info("Batch update StoryItem time: {} items, newTime={}", instanceIds.size(), storyItemTime); storyItemService.batchUpdateItemTime(instanceIds, storyItemTime); - return ResponseEntity.success("批量修改时间成功"); + return ResponseEntity.success("Batch time update completed"); } } diff --git a/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java b/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java index cdc7565..670543c 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java +++ b/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java @@ -1,8 +1,8 @@ package com.timeline.story.controller; import com.timeline.common.response.ResponseEntity; -import com.timeline.story.entity.StoryItem; import com.timeline.story.service.StoryItemService; +import com.timeline.story.vo.StoryItemShareVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -18,13 +18,10 @@ public class StoryPublicController { @Autowired private StoryItemService storyItemService; - /* - * @GetMapping("/{shareId}") - * public ResponseEntity getStoryItemByShareId(@PathVariable String - * shareId) { - * log.info("根据 shareId 获取 StoryItem: {}", shareId); - * StoryItem storyItem = storyItemService.getItemByShareId(shareId); - * return ResponseEntity.success(storyItem); - * } - */ -} + @GetMapping("/{shareId}") + public ResponseEntity getStoryItemByShareId(@PathVariable String shareId) { + log.info("Get public story by shareId: {}", shareId); + StoryItemShareVo storyItem = storyItemService.getItemByShareId(shareId); + return ResponseEntity.success(storyItem); + } +} \ No newline at end of file diff --git a/timeline-story-service/src/main/java/com/timeline/story/dao/StoryItemMapper.java b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryItemMapper.java index 2e331e6..c0f5bc3 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/dao/StoryItemMapper.java +++ b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryItemMapper.java @@ -3,10 +3,10 @@ package com.timeline.story.dao; import com.timeline.story.entity.StoryItem; import com.timeline.story.vo.StoryItemShareVo; import com.timeline.story.vo.StoryItemVo; - import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -16,35 +16,38 @@ public interface StoryItemMapper { void update(StoryItem storyItem); - void deleteByItemId(String itemId); + void deleteByItemId(@Param("instanceId") String itemId); - StoryItem selectById(String itemId); + StoryItem selectById(@Param("instanceId") String itemId); - List selectByMasterItem(String masterItemId); + List selectByMasterItem(@Param("masterItemId") String masterItemId); - List selectImagesByItemId(String itemId); + List selectImagesByItemId(@Param("instanceId") String itemId); List selectStoryItemByStoryInstanceId(Map map); - int countByStoryId(String storyInstanceId); + int countByStoryId(@Param("storyInstanceId") String storyInstanceId); - // StoryItem selectByShareId(String shareId); + StoryItem selectByShareId(@Param("shareId") String shareId); - // StoryItemShareVo selectByShareIdWithAuthor(String shareId); + StoryItemShareVo selectByShareIdWithAuthor(@Param("shareId") String shareId); + + StoryItem selectPublishedByStoryId(@Param("storyInstanceId") String storyInstanceId); + + void clearShareIdByStoryId( + @Param("storyInstanceId") String storyInstanceId, + @Param("updateId") String updateId, + @Param("updateTime") LocalDateTime updateTime); + + void updateShareId( + @Param("instanceId") String instanceId, + @Param("shareId") String shareId, + @Param("updateId") String updateId, + @Param("updateTime") LocalDateTime updateTime); List searchItems(@Param("keyword") String keyword); - /** - * 更新节点排序值 - * - * @param params 包含 instanceId, sortOrder, updateId, updateTime - */ void updateOrder(Map params); - /** - * 更新节点时间 - * - * @param params 包含 instanceId, storyItemTime, updateId, updateTime - */ void updateItemTime(Map params); } diff --git a/timeline-story-service/src/main/java/com/timeline/story/dao/StoryMapper.java b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryMapper.java index 2614dde..953e1dd 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/dao/StoryMapper.java +++ b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryMapper.java @@ -3,15 +3,21 @@ package com.timeline.story.dao; import com.timeline.story.entity.Story; import com.timeline.story.vo.StoryDetailVo; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface StoryMapper { void insert(Story story); + void update(Story story); - void deleteByInstanceId(String instanceId); - StoryDetailVo selectByInstanceId(String instanceId, String userId); - List selectByOwnerId(String ownerId); - void touchUpdate(String instanceId, String updateId); -} + + void deleteByInstanceId(@Param("instanceId") String instanceId); + + StoryDetailVo selectByInstanceId(@Param("instanceId") String instanceId, @Param("userId") String userId); + + List selectByOwnerId(@Param("ownerId") String ownerId); + + void touchUpdate(@Param("instanceId") String instanceId, @Param("updateId") String updateId); +} \ No newline at end of file diff --git a/timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareMapper.java b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareMapper.java new file mode 100644 index 0000000..411c6b6 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareMapper.java @@ -0,0 +1,25 @@ +package com.timeline.story.dao; + +import com.timeline.story.entity.StoryShare; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; + +@Mapper +public interface StoryShareMapper { + StoryShare selectActiveByStoryId(@Param("storyInstanceId") String storyInstanceId); + + StoryShare selectLatestByStoryId(@Param("storyInstanceId") String storyInstanceId); + + StoryShare selectByShareId(@Param("shareId") String shareId); + + void insert(StoryShare storyShare); + + void update(StoryShare storyShare); + + void softDeleteByStoryId( + @Param("storyInstanceId") String storyInstanceId, + @Param("updateId") String updateId, + @Param("updateTime") LocalDateTime updateTime); +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/entity/Story.java b/timeline-story-service/src/main/java/com/timeline/story/entity/Story.java index 0c4488c..a82a543 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/entity/Story.java +++ b/timeline-story-service/src/main/java/com/timeline/story/entity/Story.java @@ -9,6 +9,7 @@ import java.time.LocalDateTime; @Data public class Story { private String instanceId; + private String shareId; private String title; private String description; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @@ -17,9 +18,9 @@ public class Story { private LocalDate storyTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; - private String updateId; + private String updateId; private Integer isDelete; private String ownerId; private String status; private String logo; -} +} \ No newline at end of file diff --git a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryItem.java b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryItem.java index dca05a0..de0638a 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryItem.java +++ b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryItem.java @@ -10,6 +10,7 @@ public class StoryItem { private String instanceId; private String storyInstanceId; private String masterItemId; + private String shareId; private String title; private String description; private String location; @@ -22,9 +23,5 @@ public class StoryItem { private LocalDateTime createTime; private LocalDateTime updateTime; private Integer isDelete; - /** - * 排序值 - 用于拖拽排序 - * 数值越小越靠前,默认为0 - */ private Integer sortOrder; -} +} \ No newline at end of file diff --git a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java new file mode 100644 index 0000000..1f7df7e --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java @@ -0,0 +1,22 @@ +package com.timeline.story.entity; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class StoryShare { + private Long id; + private String shareId; + private String storyInstanceId; + private String heroMomentId; + private String shareTitle; + private String shareDescription; + private String shareQuote; + private String featuredMomentIds; + private String createId; + private String updateId; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Integer isDelete; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/AnalyticsService.java b/timeline-story-service/src/main/java/com/timeline/story/service/AnalyticsService.java index bb10ecb..80563b4 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/AnalyticsService.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/AnalyticsService.java @@ -1,6 +1,8 @@ package com.timeline.story.service; import com.timeline.story.vo.TimelineAnalyticsVo; +import com.timeline.story.vo.TimelineArchiveExploreVo; +import com.timeline.story.vo.TimelineArchiveSummaryVo; /** * AnalyticsService - 数据分析服务接口 @@ -63,6 +65,10 @@ public interface AnalyticsService { */ TimelineAnalyticsVo getTopTags(String userId, int limit); + TimelineArchiveSummaryVo getArchiveSummary(String userId, int locationLimit, int tagLimit); + + TimelineArchiveExploreVo getArchiveExplore(String userId, String type, String value, int limit); + /** * 生成年度报告 * diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/StoryItemService.java b/timeline-story-service/src/main/java/com/timeline/story/service/StoryItemService.java index 3ea8db1..b634c63 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/StoryItemService.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/StoryItemService.java @@ -4,6 +4,9 @@ import com.timeline.story.entity.StoryItem; import com.timeline.story.vo.StoryItemAddVo; import com.timeline.story.vo.StoryItemShareVo; import com.timeline.story.vo.StoryItemVo; +import com.timeline.story.vo.StoryShareConfigVo; +import com.timeline.story.vo.StorySharePublishRequest; +import com.timeline.story.vo.StorySharePublishVo; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -24,40 +27,19 @@ public interface StoryItemService { Integer getStoryItemCount(String storyInstanceId); - // StoryItemShareVo getItemByShareId(String shareId); + StoryItemShareVo getItemByShareId(String shareId); + + StoryShareConfigVo getStoryShareConfig(String storyId); + + StorySharePublishVo publishStoryShare(StorySharePublishRequest request); + + void unpublishStoryShare(String storyId); Map searchItems(String keyword, Integer pageNum, Integer pageSize); - /** - * 批量更新时间线节点排序 - * - * 功能描述: - * 根据前端传递的排序数据,批量更新各节点的 sortOrder 字段。 - * 该方法支持事务处理,确保所有排序更新原子性完成。 - * - * @param items 排序数据列表,每个元素包含 instanceId 和 sortOrder - */ void updateItemsOrder(List> items); - /** - * 批量删除时间线节点 - * - * 功能描述: - * 根据节点ID列表批量软删除时间线节点。 - * 软删除仅标记 is_delete 字段,数据可恢复。 - * - * @param instanceIds 要删除的节点ID列表 - */ void batchDeleteItems(List instanceIds); - /** - * 批量修改时间线节点时间 - * - * 功能描述: - * 批量修改多个节点的 storyItemTime 字段。 - * - * @param instanceIds 要修改的节点ID列表 - * @param storyItemTime 新的时间值 - */ void batchUpdateItemTime(List instanceIds, String storyItemTime); } diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java index c850b94..5749ffb 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java @@ -1,99 +1,538 @@ package com.timeline.story.service.impl; -import com.timeline.story.dao.StoryMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.timeline.story.dao.StoryItemMapper; +import com.timeline.story.dao.StoryMapper; import com.timeline.story.service.AnalyticsService; import com.timeline.story.vo.StoryDetailVo; +import com.timeline.story.vo.TimelineArchiveBucketVo; +import com.timeline.story.vo.TimelineArchiveExploreVo; +import com.timeline.story.vo.TimelineArchiveMomentVo; +import com.timeline.story.vo.TimelineArchiveSummaryVo; import com.timeline.story.vo.StoryItemVo; import com.timeline.story.vo.TimelineAnalyticsVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Year; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -/** - * AnalyticsServiceImpl - 数据分析服务实现类 - */ @Slf4j @Service public class AnalyticsServiceImpl implements AnalyticsService { + private static final Pattern HASHTAG_PATTERN = Pattern.compile("#([\\p{L}\\p{N}_-]+)"); + private static final String[] TAG_COLORS = { + "blue", "green", "gold", "magenta", "purple", "cyan", "orange", "volcano" + }; + @Autowired private StoryMapper storyMapper; @Autowired private StoryItemMapper storyItemMapper; + private final ObjectMapper objectMapper = new ObjectMapper(); + @Override public TimelineAnalyticsVo getOverallStats(String userId) { - log.info("获取用户总体统计: userId={}", userId); + log.info("Get overall timeline stats: userId={}", userId); + List storyData = loadStoryData(userId); + List itemData = flattenItemData(storyData); + TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - - // 基础统计数据 - 使用现有方法查询 - List stories = storyMapper.selectByOwnerId(userId); - vo.setTotalStories((long) (stories != null ? stories.size() : 0)); - - // 统计所有故事中的时刻数量 - long totalMoments = 0; - long imageCount = 0; - long videoCount = 0; - - if (stories != null) { - for (StoryDetailVo story : stories) { - Map params = new HashMap<>(); - params.put("storyInstanceId", story.getInstanceId()); - List items = storyItemMapper.selectStoryItemByStoryInstanceId(params); - if (items != null) { - totalMoments += items.size(); - for (StoryItemVo item : items) { - // 根据 video_url 判断类型:有 video_url 是视频,否则是图片 - if (item.getVideoUrl() != null && !item.getVideoUrl().isEmpty()) { - videoCount++; - } else { - imageCount++; - } - } - } - } - } - - vo.setTotalMoments(totalMoments); - vo.setTotalMedia(totalMoments); - vo.setImageCount(imageCount); - vo.setVideoCount(videoCount); - vo.setCollaborationCount(0L); + vo.setTotalStories((long) storyData.size()); + vo.setTotalMoments((long) itemData.size()); + vo.setTotalMedia(itemData.stream().mapToLong(ItemData::mediaCount).sum()); + vo.setImageCount(itemData.stream().mapToLong(ItemData::imageCount).sum()); + vo.setVideoCount(itemData.stream().mapToLong(item -> hasVideo(item.item()) ? 1 : 0).sum()); + vo.setCollaborationCount( + storyData.stream().filter(data -> data.story().getPermissionType() != null && data.story().getPermissionType() > 1).count()); vo.setTotalComments(0L); vo.setTotalLikes(0L); vo.setTotalFavorites(0L); - vo.setConsecutiveDays(0); - vo.setMaxConsecutiveDays(0); + vo.setMonthlyTrend(buildMonthlyTrend(itemData, Year.now().getValue())); + vo.setTopLocations(buildTopLocations(itemData, 10)); + vo.setTopTags(buildTopTags(itemData, 10)); + vo.setWeeklyDistribution(buildWeeklyDistribution(itemData)); + vo.setHourlyDistribution(buildHourlyDistribution(itemData)); + ActivitySummary activitySummary = buildActivitySummary(itemData); + vo.setLastActiveDate(activitySummary.lastActiveDate()); + vo.setConsecutiveDays(activitySummary.currentStreak()); + vo.setMaxConsecutiveDays(activitySummary.maxStreak()); return vo; } @Override public TimelineAnalyticsVo getStoryStats(String storyInstanceId) { - log.info("获取故事统计: storyInstanceId={}", storyInstanceId); + log.info("Get story stats: storyInstanceId={}", storyInstanceId); + StoryDetailVo story = storyMapper.selectByInstanceId(storyInstanceId, null); + List items = loadItemsForStory(storyInstanceId); + List itemData = new ArrayList<>(); + for (StoryItemVo item : items) { + itemData.add(buildItemData(story, item)); + } + TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - // TODO: 实现故事统计逻辑 - vo.setTotalMoments(0L); - vo.setTotalMedia(0L); + vo.setTotalStories(1L); + vo.setTotalMoments((long) itemData.size()); + vo.setTotalMedia(itemData.stream().mapToLong(ItemData::mediaCount).sum()); + vo.setImageCount(itemData.stream().mapToLong(ItemData::imageCount).sum()); + vo.setVideoCount(itemData.stream().mapToLong(item -> hasVideo(item.item()) ? 1 : 0).sum()); + vo.setTopLocations(buildTopLocations(itemData, 5)); + vo.setTopTags(buildTopTags(itemData, 5)); + vo.setWeeklyDistribution(buildWeeklyDistribution(itemData)); + vo.setHourlyDistribution(buildHourlyDistribution(itemData)); + vo.setMonthlyTrend(buildMonthlyTrend(itemData, resolveReportYear(itemData, Year.now().getValue()))); return vo; } @Override public TimelineAnalyticsVo getMonthlyTrend(String userId, int year) { - log.info("获取月度趋势: userId={}, year={}", userId, year); + log.info("Get monthly trend: userId={}, year={}", userId, year); + List itemData = flattenItemData(loadStoryData(userId)); TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - List monthlyTrend = new ArrayList<>(); + vo.setMonthlyTrend(buildMonthlyTrend(itemData, year)); + return vo; + } - // 生成12个月的默认数据 + @Override + public TimelineAnalyticsVo getTopLocations(String userId, int limit) { + log.info("Get top locations: userId={}, limit={}", userId, limit); + List itemData = flattenItemData(loadStoryData(userId)); + TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); + vo.setTopLocations(buildTopLocations(itemData, limit)); + return vo; + } + + @Override + public TimelineAnalyticsVo getTopTags(String userId, int limit) { + log.info("Get top tags: userId={}, limit={}", userId, limit); + List itemData = flattenItemData(loadStoryData(userId)); + TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); + vo.setTopTags(buildTopTags(itemData, limit)); + return vo; + } + + @Override + public TimelineArchiveSummaryVo getArchiveSummary(String userId, int locationLimit, int tagLimit) { + log.info("Get archive summary: userId={}, locationLimit={}, tagLimit={}", userId, locationLimit, tagLimit); + ArchiveDataset dataset = buildArchiveDataset(userId); + + TimelineArchiveSummaryVo summary = new TimelineArchiveSummaryVo(); + summary.setStoriesIndexed(dataset.storyCount()); + summary.setShareableStories(dataset.shareableStoryCount()); + summary.setVideoMoments(dataset.videoMomentCount()); + summary.setLocations(limitBuckets(dataset.locationBuckets(), locationLimit)); + summary.setTags(limitBuckets(dataset.tagBuckets(), tagLimit)); + return summary; + } + + @Override + public TimelineArchiveExploreVo getArchiveExplore(String userId, String type, String value, int limit) { + log.info("Get archive explore: userId={}, type={}, value={}, limit={}", userId, type, value, limit); + ArchiveDataset dataset = buildArchiveDataset(userId); + String resolvedType = resolveArchiveType(type, dataset); + List buckets = + "tag".equals(resolvedType) ? dataset.tagBuckets() : dataset.locationBuckets(); + + TimelineArchiveBucketVo bucket = findArchiveBucket(buckets, value); + List matchedMoments = bucket == null + ? Collections.emptyList() + : dataset.moments().stream() + .filter(moment -> matchesBucket(moment, resolvedType, bucket.getKey())) + .sorted((left, right) -> Long.compare( + right.getSortValue() == null ? 0L : right.getSortValue(), + left.getSortValue() == null ? 0L : left.getSortValue())) + .limit(limit) + .toList(); + + TimelineArchiveExploreVo explore = new TimelineArchiveExploreVo(); + explore.setType(resolvedType); + explore.setValue(bucket == null ? null : bucket.getKey()); + explore.setBucket(bucket); + explore.setMoments(matchedMoments); + return explore; + } + + @Override + public TimelineAnalyticsVo.YearlyReport generateYearlyReport(String userId, int year) { + log.info("Generate yearly report: userId={}, year={}", userId, year); + List itemData = flattenItemData(loadStoryData(userId)); + List inYear = itemData.stream() + .filter(item -> item.eventTime() != null && item.eventTime().getYear() == year) + .toList(); + + TimelineAnalyticsVo.YearlyReport report = new TimelineAnalyticsVo.YearlyReport(); + report.setYear(year); + report.setTotalMoments((long) inYear.size()); + report.setTotalMedia(inYear.stream().mapToLong(ItemData::mediaCount).sum()); + report.setMonthlyBreakdown(buildMonthlyTrend(inYear, year)); + report.setMostActiveMonth(pickMostActiveMonth(report.getMonthlyBreakdown())); + report.setMostActiveDay(pickMostActiveDay(inYear)); + + List topLocations = buildTopLocations(inYear, 1); + report.setTopLocation(topLocations.isEmpty() ? "-" : topLocations.get(0).getLocation()); + + List topTags = buildTopTags(inYear, 1); + report.setTopTag(topTags.isEmpty() ? "-" : topTags.get(0).getTag()); + return report; + } + + @Override + public TimelineAnalyticsVo getActivityStats(String userId) { + log.info("Get activity stats: userId={}", userId); + List itemData = flattenItemData(loadStoryData(userId)); + ActivitySummary summary = buildActivitySummary(itemData); + + TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); + vo.setConsecutiveDays(summary.currentStreak()); + vo.setMaxConsecutiveDays(summary.maxStreak()); + vo.setLastActiveDate(summary.lastActiveDate()); + return vo; + } + + @Override + public int calculateConsecutiveDays(String userId) { + log.info("Calculate consecutive days: userId={}", userId); + List itemData = flattenItemData(loadStoryData(userId)); + return buildActivitySummary(itemData).currentStreak(); + } + + @Override + public TimelineAnalyticsVo getTimeDistribution(String userId) { + log.info("Get time distribution: userId={}", userId); + List itemData = flattenItemData(loadStoryData(userId)); + + TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); + vo.setWeeklyDistribution(buildWeeklyDistribution(itemData)); + vo.setHourlyDistribution(buildHourlyDistribution(itemData)); + return vo; + } + + @Override + public byte[] exportStats(String userId, String format) { + log.info("Export stats: userId={}, format={}", userId, format); + TimelineAnalyticsVo overview = getOverallStats(userId); + TimelineAnalyticsVo.YearlyReport report = generateYearlyReport(userId, Year.now().getValue()); + + if ("csv".equalsIgnoreCase(format)) { + StringBuilder builder = new StringBuilder(); + builder.append("metric,value\n"); + builder.append("totalStories,").append(valueOrZero(overview.getTotalStories())).append("\n"); + builder.append("totalMoments,").append(valueOrZero(overview.getTotalMoments())).append("\n"); + builder.append("totalMedia,").append(valueOrZero(overview.getTotalMedia())).append("\n"); + builder.append("imageCount,").append(valueOrZero(overview.getImageCount())).append("\n"); + builder.append("videoCount,").append(valueOrZero(overview.getVideoCount())).append("\n"); + builder.append("consecutiveDays,").append(valueOrZero(overview.getConsecutiveDays())).append("\n"); + builder.append("maxConsecutiveDays,").append(valueOrZero(overview.getMaxConsecutiveDays())).append("\n"); + builder.append("lastActiveDate,").append(orDash(overview.getLastActiveDate())).append("\n"); + builder.append("yearlyReportYear,").append(valueOrZero(report.getYear())).append("\n"); + builder.append("yearlyReportMoments,").append(valueOrZero(report.getTotalMoments())).append("\n"); + builder.append("yearlyReportMedia,").append(valueOrZero(report.getTotalMedia())).append("\n"); + builder.append("topLocation,").append(orDash(report.getTopLocation())).append("\n"); + builder.append("topTag,").append(orDash(report.getTopTag())).append("\n"); + return builder.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + Map export = new LinkedHashMap<>(); + export.put("overview", overview); + export.put("yearlyReport", report); + try { + return objectMapper.writeValueAsBytes(export); + } catch (JsonProcessingException exception) { + log.error("Failed to export stats as JSON", exception); + return "{}".getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + } + + private List loadStoryData(String userId) { + List stories = storyMapper.selectByOwnerId(userId); + if (stories == null || stories.isEmpty()) { + return Collections.emptyList(); + } + + List data = new ArrayList<>(); + for (StoryDetailVo story : stories) { + data.add(new StoryData(story, loadItemsForStory(story.getInstanceId()))); + } + return data; + } + + private List loadItemsForStory(String storyInstanceId) { + Map params = new HashMap<>(); + params.put("storyInstanceId", storyInstanceId); + List items = storyItemMapper.selectStoryItemByStoryInstanceId(params); + return items == null ? Collections.emptyList() : items; + } + + private List flattenItemData(List storyData) { + List itemData = new ArrayList<>(); + for (StoryData data : storyData) { + for (StoryItemVo item : data.items()) { + itemData.add(buildItemData(data.story(), item)); + } + } + return itemData; + } + + private ArchiveDataset buildArchiveDataset(String userId) { + List storyData = loadStoryData(userId); + List itemData = flattenItemData(storyData); + Map locationBuckets = new LinkedHashMap<>(); + Map tagBuckets = new LinkedHashMap<>(); + List moments = new ArrayList<>(); + + for (ItemData item : itemData) { + TimelineArchiveMomentVo moment = buildArchiveMoment(item); + moments.add(moment); + + if (StringUtils.hasText(moment.getLocation())) { + mergeArchiveBucket(locationBuckets, moment.getLocation(), moment, item.story()); + } + for (String tag : item.tags()) { + mergeArchiveBucket(tagBuckets, tag, moment, item.story()); + } + } + + return new ArchiveDataset( + storyData.size(), + (int) storyData.stream().filter(data -> StringUtils.hasText(data.story().getShareId())).count(), + (int) itemData.stream().filter(data -> hasVideo(data.item())).count(), + sortArchiveBuckets(locationBuckets), + sortArchiveBuckets(tagBuckets), + moments); + } + + private ItemData buildItemData(StoryDetailVo story, StoryItemVo item) { + List images = item.getInstanceId() == null + ? Collections.emptyList() + : safeImages(storyItemMapper.selectImagesByItemId(item.getInstanceId())); + long imageCount = images.size(); + long mediaCount = imageCount + (hasVideo(item) ? 1 : 0); + LocalDateTime eventTime = resolveEventTime(story, item); + List tags = inferTags(item); + return new ItemData(story, item, images, imageCount, mediaCount, tags, eventTime); + } + + private List safeImages(List images) { + return images == null ? Collections.emptyList() : images; + } + + private LocalDateTime resolveEventTime(StoryDetailVo story, StoryItemVo item) { + if (item.getStoryItemTime() != null) { + return item.getStoryItemTime(); + } + if (story != null && story.getStoryTime() != null) { + return story.getStoryTime().atStartOfDay(); + } + if (story != null && story.getUpdateTime() != null) { + return story.getUpdateTime(); + } + return story != null ? story.getCreateTime() : null; + } + + private boolean hasVideo(StoryItemVo item) { + return StringUtils.hasText(item.getVideoUrl()); + } + + private List inferTags(StoryItemVo item) { + Set tags = new LinkedHashSet<>(); + String raw = ((item.getTitle() == null ? "" : item.getTitle()) + " " + (item.getDescription() == null ? "" : item.getDescription())).trim(); + String text = raw.toLowerCase(); + + Matcher matcher = HASHTAG_PATTERN.matcher(raw); + while (matcher.find()) { + tags.add(matcher.group(1)); + } + + if (containsAny(text, "travel", "trip", "journey", "\u65c5\u884c", "\u51fa\u6e38")) { + tags.add("Travel"); + } + if (containsAny(text, "family", "home", "\u5bb6", "\u7236\u6bcd", "\u5b69\u5b50")) { + tags.add("Family"); + } + if (containsAny(text, "friend", "party", "gathering", "\u670b\u53cb", "\u805a\u4f1a")) { + tags.add("Friends"); + } + if (containsAny(text, "birthday", "festival", "celebration", "\u751f\u65e5", "\u8282\u65e5", "\u65b0\u5e74")) { + tags.add("Celebration"); + } + if (containsAny(text, "work", "project", "office", "\u5de5\u4f5c", "\u9879\u76ee")) { + tags.add("Work"); + } + if (hasVideo(item)) { + tags.add("Video"); + } + if (tags.isEmpty()) { + tags.add("Everyday"); + } + + return new ArrayList<>(tags); + } + + private boolean containsAny(String text, String... keywords) { + for (String keyword : keywords) { + if (text.contains(keyword.toLowerCase())) { + return true; + } + } + return false; + } + + private TimelineArchiveMomentVo buildArchiveMoment(ItemData itemData) { + StoryDetailVo story = itemData.story(); + StoryItemVo item = itemData.item(); + TimelineArchiveMomentVo moment = new TimelineArchiveMomentVo(); + moment.setKey(StringUtils.hasText(item.getInstanceId()) + ? item.getInstanceId() + : (story.getInstanceId() + "-" + Math.abs((item.getTitle() == null ? "" : item.getTitle()).hashCode()))); + moment.setStoryInstanceId(story.getInstanceId()); + moment.setStoryShareId(story.getShareId()); + moment.setStoryTitle(StringUtils.hasText(story.getTitle()) ? story.getTitle() : "Untitled story"); + moment.setStoryTime(formatArchiveDateTime(itemData.eventTime())); + moment.setItemInstanceId(item.getInstanceId()); + moment.setItemTitle(StringUtils.hasText(item.getTitle()) ? item.getTitle() : "Untitled moment"); + moment.setItemDescription(StringUtils.hasText(item.getDescription()) + ? item.getDescription() + : "Add one clear sentence here and this memory becomes much easier to revisit."); + moment.setItemTime(formatArchiveDateTime(itemData.eventTime())); + moment.setLocation(item.getLocation()); + moment.setTags(itemData.tags()); + moment.setCoverInstanceId(resolveCoverInstanceId(itemData)); + moment.setCoverSrc(resolveCoverSrc(item)); + moment.setMediaCount((int) itemData.mediaCount()); + moment.setHasVideo(hasVideo(item)); + moment.setSortValue(itemData.eventTime() == null ? 0L : itemData.eventTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()); + return moment; + } + + private void mergeArchiveBucket( + Map buckets, + String bucketKey, + TimelineArchiveMomentVo moment, + StoryDetailVo story) { + TimelineArchiveBucketVo bucket = buckets.get(bucketKey); + if (bucket == null) { + bucket = new TimelineArchiveBucketVo(); + bucket.setKey(bucketKey); + bucket.setTitle(bucketKey); + bucket.setCount(0L); + bucket.setSubtitle(moment.getStoryTitle()); + bucket.setStoryInstanceId(moment.getStoryInstanceId()); + bucket.setStoryShareId(moment.getStoryShareId()); + bucket.setCoverInstanceId(moment.getCoverInstanceId()); + bucket.setCoverSrc(moment.getCoverSrc()); + bucket.setSampleMomentTitle(moment.getItemTitle()); + bucket.setSortValue(moment.getSortValue()); + buckets.put(bucketKey, bucket); + } + + bucket.setCount((bucket.getCount() == null ? 0L : bucket.getCount()) + 1L); + long bucketSortValue = bucket.getSortValue() == null ? 0L : bucket.getSortValue(); + long momentSortValue = moment.getSortValue() == null ? 0L : moment.getSortValue(); + if (momentSortValue >= bucketSortValue) { + bucket.setSubtitle(moment.getStoryTitle()); + bucket.setStoryInstanceId(story.getInstanceId()); + bucket.setStoryShareId(story.getShareId()); + bucket.setCoverInstanceId(moment.getCoverInstanceId()); + bucket.setCoverSrc(moment.getCoverSrc()); + bucket.setSampleMomentTitle(moment.getItemTitle()); + bucket.setSortValue(moment.getSortValue()); + } + } + + private List sortArchiveBuckets(Map bucketMap) { + return bucketMap.values().stream() + .sorted((left, right) -> { + int countCompare = Long.compare( + right.getCount() == null ? 0L : right.getCount(), + left.getCount() == null ? 0L : left.getCount()); + if (countCompare != 0) { + return countCompare; + } + return Long.compare( + right.getSortValue() == null ? 0L : right.getSortValue(), + left.getSortValue() == null ? 0L : left.getSortValue()); + }) + .toList(); + } + + private List limitBuckets(List buckets, int limit) { + return buckets.stream().limit(Math.max(limit, 0)).toList(); + } + + private String resolveArchiveType(String requestedType, ArchiveDataset dataset) { + if ("tag".equalsIgnoreCase(requestedType) && !dataset.tagBuckets().isEmpty()) { + return "tag"; + } + if (!dataset.locationBuckets().isEmpty()) { + return "location"; + } + return dataset.tagBuckets().isEmpty() ? "location" : "tag"; + } + + private TimelineArchiveBucketVo findArchiveBucket(List buckets, String value) { + if (buckets.isEmpty()) { + return null; + } + if (StringUtils.hasText(value)) { + for (TimelineArchiveBucketVo bucket : buckets) { + if (value.equals(bucket.getKey())) { + return bucket; + } + } + } + return buckets.get(0); + } + + private boolean matchesBucket(TimelineArchiveMomentVo moment, String type, String key) { + if (!StringUtils.hasText(key)) { + return false; + } + if ("tag".equals(type)) { + return moment.getTags() != null && moment.getTags().contains(key); + } + return key.equals(moment.getLocation()); + } + + private String resolveCoverInstanceId(ItemData itemData) { + if (!itemData.images().isEmpty()) { + return itemData.images().get(0); + } + return null; + } + + private String resolveCoverSrc(StoryItemVo item) { + return StringUtils.hasText(item.getThumbnailUrl()) ? item.getThumbnailUrl() : null; + } + + private String formatArchiveDateTime(LocalDateTime value) { + if (value == null) { + return "Pending date"; + } + return value.format(DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm")); + } + + private List buildMonthlyTrend(List itemData, int year) { + List monthlyTrend = new ArrayList<>(); for (int month = 1; month <= 12; month++) { TimelineAnalyticsVo.MonthlyStats stats = new TimelineAnalyticsVo.MonthlyStats(); stats.setMonth(String.format("%d-%02d", year, month)); @@ -102,83 +541,213 @@ public class AnalyticsServiceImpl implements AnalyticsService { monthlyTrend.add(stats); } - vo.setMonthlyTrend(monthlyTrend); - return vo; + for (ItemData item : itemData) { + if (item.eventTime() == null || item.eventTime().getYear() != year) { + continue; + } + TimelineAnalyticsVo.MonthlyStats stats = monthlyTrend.get(item.eventTime().getMonthValue() - 1); + stats.setCount(stats.getCount() + 1); + stats.setMediaCount(stats.getMediaCount() + item.mediaCount()); + } + + return monthlyTrend; } - @Override - public TimelineAnalyticsVo getTopLocations(String userId, int limit) { - log.info("获取热门地点: userId={}, limit={}", userId, limit); - TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - vo.setTopLocations(new ArrayList<>()); - return vo; + private List buildTopLocations(List itemData, int limit) { + Map counts = new HashMap<>(); + long total = 0L; + + for (ItemData item : itemData) { + String location = item.item().getLocation(); + if (!StringUtils.hasText(location)) { + continue; + } + counts.merge(location, 1L, Long::sum); + total++; + } + + List result = new ArrayList<>(); + final long totalCount = total; + counts.entrySet().stream() + .sorted((left, right) -> Long.compare(right.getValue(), left.getValue())) + .limit(limit) + .forEach(entry -> { + TimelineAnalyticsVo.LocationStats stats = new TimelineAnalyticsVo.LocationStats(); + stats.setLocation(entry.getKey()); + stats.setCount(entry.getValue()); + stats.setPercentage(totalCount == 0 ? 0D : roundPercentage(entry.getValue(), totalCount)); + result.add(stats); + }); + return result; } - @Override - public TimelineAnalyticsVo getTopTags(String userId, int limit) { - log.info("获取热门标签: userId={}, limit={}", userId, limit); - TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - vo.setTopTags(new ArrayList<>()); - return vo; + private List buildTopTags(List itemData, int limit) { + Map counts = new HashMap<>(); + long total = 0L; + + for (ItemData item : itemData) { + for (String tag : item.tags()) { + counts.merge(tag, 1L, Long::sum); + total++; + } + } + + List result = new ArrayList<>(); + final long totalCount = total; + counts.entrySet().stream() + .sorted((left, right) -> Long.compare(right.getValue(), left.getValue())) + .limit(limit) + .forEach(entry -> { + TimelineAnalyticsVo.TagStats stats = new TimelineAnalyticsVo.TagStats(); + stats.setTag(entry.getKey()); + stats.setColor(TAG_COLORS[Math.abs(entry.getKey().hashCode()) % TAG_COLORS.length]); + stats.setCount(entry.getValue()); + stats.setPercentage(totalCount == 0 ? 0D : roundPercentage(entry.getValue(), totalCount)); + result.add(stats); + }); + return result; } - @Override - public TimelineAnalyticsVo.YearlyReport generateYearlyReport(String userId, int year) { - log.info("生成年度报告: userId={}, year={}", userId, year); - TimelineAnalyticsVo.YearlyReport report = new TimelineAnalyticsVo.YearlyReport(); - report.setYear(year); - report.setTotalMoments(0L); - report.setTotalMedia(0L); - report.setMostActiveMonth("-"); - report.setMostActiveDay("-"); - report.setTopLocation("-"); - report.setTopTag("-"); - report.setMonthlyBreakdown(new ArrayList<>()); - return report; - } - - @Override - public TimelineAnalyticsVo getActivityStats(String userId) { - log.info("获取活跃度统计: userId={}", userId); - TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - vo.setConsecutiveDays(0); - vo.setMaxConsecutiveDays(0); - vo.setLastActiveDate(LocalDate.now().format(DateTimeFormatter.ISO_DATE)); - return vo; - } - - @Override - public int calculateConsecutiveDays(String userId) { - log.info("计算连续记录天数: userId={}", userId); - return 0; - } - - @Override - public TimelineAnalyticsVo getTimeDistribution(String userId) { - log.info("获取时间分布: userId={}", userId); - TimelineAnalyticsVo vo = new TimelineAnalyticsVo(); - - // 初始化周分布 - Map weeklyDistribution = new HashMap<>(); + private Map buildWeeklyDistribution(List itemData) { + Map weeklyDistribution = new LinkedHashMap<>(); for (int i = 1; i <= 7; i++) { weeklyDistribution.put(i, 0L); } - vo.setWeeklyDistribution(weeklyDistribution); - // 初始化小时分布 - Map hourlyDistribution = new HashMap<>(); + for (ItemData item : itemData) { + if (item.eventTime() == null) { + continue; + } + int day = item.eventTime().getDayOfWeek().getValue(); + weeklyDistribution.put(day, weeklyDistribution.get(day) + 1); + } + return weeklyDistribution; + } + + private Map buildHourlyDistribution(List itemData) { + Map hourlyDistribution = new LinkedHashMap<>(); for (int i = 0; i < 24; i++) { hourlyDistribution.put(i, 0L); } - vo.setHourlyDistribution(hourlyDistribution); - return vo; + for (ItemData item : itemData) { + if (item.eventTime() == null) { + continue; + } + int hour = item.eventTime().getHour(); + hourlyDistribution.put(hour, hourlyDistribution.get(hour) + 1); + } + return hourlyDistribution; } - @Override - public byte[] exportStats(String userId, String format) { - log.info("导出统计数据: userId={}, format={}", userId, format); - // 返回空数据 - return new byte[0]; + private ActivitySummary buildActivitySummary(List itemData) { + Set distinctDates = new LinkedHashSet<>(); + for (ItemData item : itemData) { + if (item.eventTime() != null) { + distinctDates.add(item.eventTime().toLocalDate()); + } + } + + List sortedDates = new ArrayList<>(distinctDates); + sortedDates.sort(Comparator.reverseOrder()); + if (sortedDates.isEmpty()) { + return new ActivitySummary(null, 0, 0); + } + + int currentStreak = 1; + for (int i = 1; i < sortedDates.size(); i++) { + if (sortedDates.get(i - 1).minusDays(1).equals(sortedDates.get(i))) { + currentStreak++; + } else { + break; + } + } + + List ascending = new ArrayList<>(sortedDates); + ascending.sort(Comparator.naturalOrder()); + int maxStreak = 1; + int runningStreak = 1; + for (int i = 1; i < ascending.size(); i++) { + if (ascending.get(i - 1).plusDays(1).equals(ascending.get(i))) { + runningStreak++; + maxStreak = Math.max(maxStreak, runningStreak); + } else { + runningStreak = 1; + } + } + + return new ActivitySummary(sortedDates.get(0).format(DateTimeFormatter.ISO_DATE), currentStreak, maxStreak); + } + + private int resolveReportYear(List itemData, int fallbackYear) { + return itemData.stream() + .map(ItemData::eventTime) + .filter(java.util.Objects::nonNull) + .map(LocalDateTime::getYear) + .max(Integer::compareTo) + .orElse(fallbackYear); + } + + private String pickMostActiveMonth(List monthlyBreakdown) { + return monthlyBreakdown.stream() + .max(Comparator.comparingLong(stat -> stat.getCount() == null ? 0L : stat.getCount())) + .filter(stat -> stat.getCount() != null && stat.getCount() > 0) + .map(TimelineAnalyticsVo.MonthlyStats::getMonth) + .orElse("-"); + } + + private String pickMostActiveDay(List itemData) { + Map dailyCounts = new HashMap<>(); + for (ItemData item : itemData) { + if (item.eventTime() == null) { + continue; + } + dailyCounts.merge(item.eventTime().toLocalDate().format(DateTimeFormatter.ISO_DATE), 1L, Long::sum); + } + + return dailyCounts.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("-"); + } + + private double roundPercentage(long count, long total) { + if (total == 0) { + return 0D; + } + return Math.round((count * 10000.0) / total) / 100.0; + } + + private String orDash(String value) { + return StringUtils.hasText(value) ? value : "-"; + } + + private long valueOrZero(Number value) { + return value == null ? 0L : value.longValue(); + } + + private record StoryData(StoryDetailVo story, List items) { + } + + private record ItemData( + StoryDetailVo story, + StoryItemVo item, + List images, + long imageCount, + long mediaCount, + List tags, + LocalDateTime eventTime) { + } + + private record ArchiveDataset( + int storyCount, + int shareableStoryCount, + int videoMomentCount, + List locationBuckets, + List tagBuckets, + List moments) { + } + + private record ActivitySummary(String lastActiveDate, int currentStreak, int maxStreak) { } } diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java index 1b35521..9ae403b 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java @@ -2,25 +2,42 @@ package com.timeline.story.service.impl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; +import com.timeline.common.constants.CommonConstants; +import com.timeline.common.exception.CustomException; +import com.timeline.common.response.ResponseEnum; import com.timeline.common.utils.IdUtils; import com.timeline.common.utils.UserContextUtils; +import com.timeline.story.dao.StoryMapper; import com.timeline.story.dao.StoryItemMapper; +import com.timeline.story.dao.StoryShareMapper; import com.timeline.story.entity.StoryItem; +import com.timeline.story.entity.StoryShare; +import com.timeline.story.service.StoryPermissionService; import com.timeline.story.service.StoryItemService; +import com.timeline.story.vo.StoryDetailVo; import com.timeline.story.vo.StoryItemAddVo; import com.timeline.story.vo.StoryItemShareVo; import com.timeline.story.vo.StoryItemVo; +import com.timeline.story.vo.StoryShareConfigVo; +import com.timeline.story.vo.StoryShareMomentVo; +import com.timeline.story.vo.StorySharePublishRequest; +import com.timeline.story.vo.StorySharePublishVo; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; @Slf4j @Service @@ -29,15 +46,51 @@ public class StoryItemServiceImpl implements StoryItemService { @Autowired private StoryItemMapper storyItemMapper; + @Autowired + private StoryMapper storyMapper; + + @Autowired + private StoryPermissionService storyPermissionService; + + @Autowired + private StoryShareMapper storyShareMapper; + + private String getCurrentUserId() { + String currentUserId = UserContextUtils.getCurrentUserId(); + if (!StringUtils.hasText(currentUserId)) { + throw new CustomException(ResponseEnum.UNAUTHORIZED, "User context is missing"); + } + return currentUserId; + } + + private StoryDetailVo getAccessibleStory(String storyId, String currentUserId) { + StoryDetailVo story = storyMapper.selectByInstanceId(storyId, currentUserId); + if (story == null) { + throw new CustomException(ResponseEnum.NOT_FOUND, "Story not found"); + } + if (!storyPermissionService.checkUserPermission( + storyId, + currentUserId, + CommonConstants.STORY_PERMISSION_TYPE_WRITE)) { + throw new CustomException(ResponseEnum.FORBIDDEN, "No permission to publish this story"); + } + return story; + } + @Override public Map getItemsByMasterItem(StoryItemVo storyItemVo) { - PageHelper.startPage(storyItemVo.getCurrent() != null ? storyItemVo.getCurrent() : 1, + PageHelper.startPage( + storyItemVo.getCurrent() != null ? storyItemVo.getCurrent() : 1, storyItemVo.getPageSize() != null ? storyItemVo.getPageSize() : 10); + Map params = new HashMap<>(); params.put("storyInstanceId", storyItemVo.getStoryInstanceId()); if (storyItemVo.getAfterTime() != null) { params.put("afterTime", storyItemVo.getAfterTime()); } + if (storyItemVo.getBeforeTime() != null) { + params.put("beforeTime", storyItemVo.getBeforeTime()); + } List list = storyItemMapper.selectStoryItemByStoryInstanceId(params); PageInfo pageInfo = new PageInfo<>(list); @@ -61,7 +114,7 @@ public class StoryItemServiceImpl implements StoryItemService { } storyItemMapper.insert(storyItemAddVo); - // Images handling to be implemented + // Images handling to be implemented separately. } @Override @@ -70,7 +123,7 @@ public class StoryItemServiceImpl implements StoryItemService { storyItemAddVo.setUpdateId(UserContextUtils.getCurrentUserId()); storyItemAddVo.setUpdateTime(LocalDateTime.now()); storyItemMapper.update(storyItemAddVo); - // Images handling to be implemented + // Images handling to be implemented separately. } @Override @@ -86,7 +139,8 @@ public class StoryItemServiceImpl implements StoryItemService { @Override public List getStoryItemImages(String itemId) { - return storyItemMapper.selectImagesByItemId(itemId); + List images = storyItemMapper.selectImagesByItemId(itemId); + return images == null ? Collections.emptyList() : images; } @Override @@ -94,12 +148,127 @@ public class StoryItemServiceImpl implements StoryItemService { return storyItemMapper.countByStoryId(storyInstanceId); } - /* - * @Override - * public StoryItemShareVo getItemByShareId(String shareId) { - * return storyItemMapper.selectByShareIdWithAuthor(shareId); - * } - */ + @Override + public StoryItemShareVo getItemByShareId(String shareId) { + StoryItemShareVo shareVo = storyItemMapper.selectByShareIdWithAuthor(shareId); + if (shareVo == null) { + throw new CustomException(ResponseEnum.NOT_FOUND, "Public share not found"); + } + + hydrateShareItem(shareVo); + + StoryShare storyShare = storyShareMapper.selectByShareId(shareId); + if (storyShare != null) { + shareVo.setShareTitle(trimToNull(storyShare.getShareTitle())); + shareVo.setShareDescription(trimToNull(storyShare.getShareDescription())); + shareVo.setShareQuote(trimToNull(storyShare.getShareQuote())); + shareVo.setHeroMomentId(storyShare.getHeroMomentId()); + List featuredMomentIds = parseFeaturedMomentIds(storyShare.getFeaturedMomentIds()); + shareVo.setFeaturedMomentIds(featuredMomentIds); + shareVo.setFeaturedMoments(loadFeaturedMoments(shareVo.getStoryInstanceId(), featuredMomentIds)); + } + + if ((shareVo.getFeaturedMoments() == null || shareVo.getFeaturedMoments().isEmpty()) + && StringUtils.hasText(shareVo.getInstanceId())) { + shareVo.setFeaturedMomentIds(Collections.singletonList(shareVo.getInstanceId())); + shareVo.setFeaturedMoments(Collections.singletonList(buildShareMomentVo(shareVo))); + } + + return shareVo; + } + + @Override + public StoryShareConfigVo getStoryShareConfig(String storyId) { + if (!StringUtils.hasText(storyId)) { + throw new CustomException(ResponseEnum.BAD_REQUEST, "Story id is required"); + } + + String currentUserId = getCurrentUserId(); + StoryDetailVo story = getAccessibleStory(storyId, currentUserId); + StoryShare storyShare = storyShareMapper.selectLatestByStoryId(storyId); + StoryItem publishedItem = storyItemMapper.selectPublishedByStoryId(storyId); + + StoryShareConfigVo config = new StoryShareConfigVo(); + config.setStoryId(storyId); + config.setShareId(StringUtils.hasText(story.getShareId()) ? story.getShareId() : storyShare == null ? null : storyShare.getShareId()); + config.setHeroMomentId(storyShare != null && StringUtils.hasText(storyShare.getHeroMomentId()) + ? storyShare.getHeroMomentId() + : publishedItem == null ? null : publishedItem.getInstanceId()); + config.setTitle(storyShare == null ? null : trimToNull(storyShare.getShareTitle())); + config.setDescription(storyShare == null ? null : trimToNull(storyShare.getShareDescription())); + config.setQuote(storyShare == null ? null : trimToNull(storyShare.getShareQuote())); + config.setFeaturedMomentIds(storyShare == null + ? Collections.emptyList() + : parseFeaturedMomentIds(storyShare.getFeaturedMomentIds())); + config.setPublished(StringUtils.hasText(story.getShareId())); + config.setUpdatedAt(storyShare == null ? null : storyShare.getUpdateTime()); + return config; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public StorySharePublishVo publishStoryShare(StorySharePublishRequest request) { + if (request == null || !StringUtils.hasText(request.getStoryId())) { + throw new CustomException(ResponseEnum.BAD_REQUEST, "Story id is required"); + } + if (!StringUtils.hasText(request.getHeroMomentId())) { + throw new CustomException(ResponseEnum.BAD_REQUEST, "Hero moment id is required"); + } + + String currentUserId = getCurrentUserId(); + StoryDetailVo story = getAccessibleStory(request.getStoryId(), currentUserId); + StoryItem heroItem = storyItemMapper.selectById(request.getHeroMomentId()); + if (heroItem == null || (heroItem.getIsDelete() != null && heroItem.getIsDelete() == 1)) { + throw new CustomException(ResponseEnum.NOT_FOUND, "Hero moment not found"); + } + if (!request.getStoryId().equals(heroItem.getStoryInstanceId())) { + throw new CustomException(ResponseEnum.BAD_REQUEST, "Hero moment does not belong to this story"); + } + + StoryItem existingPublished = storyItemMapper.selectPublishedByStoryId(request.getStoryId()); + StoryShare existingShareConfig = storyShareMapper.selectLatestByStoryId(request.getStoryId()); + String shareId = existingPublished != null && StringUtils.hasText(existingPublished.getShareId()) + ? existingPublished.getShareId() + : existingShareConfig != null && StringUtils.hasText(existingShareConfig.getShareId()) + ? existingShareConfig.getShareId() + : IdUtils.randomUuid(); + LocalDateTime now = LocalDateTime.now(); + + storyItemMapper.clearShareIdByStoryId(request.getStoryId(), currentUserId, now); + storyItemMapper.updateShareId(heroItem.getInstanceId(), shareId, currentUserId, now); + upsertStoryShareConfig( + existingShareConfig, + request, + shareId, + currentUserId, + now, + heroItem.getInstanceId()); + storyMapper.touchUpdate(request.getStoryId(), currentUserId); + + StorySharePublishVo result = new StorySharePublishVo(); + result.setStoryId(story.getInstanceId()); + result.setShareId(shareId); + result.setHeroMomentId(heroItem.getInstanceId()); + result.setPublicPath("/share/" + shareId); + result.setPublishedAt(now); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void unpublishStoryShare(String storyId) { + if (!StringUtils.hasText(storyId)) { + throw new CustomException(ResponseEnum.BAD_REQUEST, "Story id is required"); + } + + String currentUserId = getCurrentUserId(); + getAccessibleStory(storyId, currentUserId); + LocalDateTime now = LocalDateTime.now(); + + storyItemMapper.clearShareIdByStoryId(storyId, currentUserId, now); + storyShareMapper.softDeleteByStoryId(storyId, currentUserId, now); + storyMapper.touchUpdate(storyId, currentUserId); + } @Override public Map searchItems(String keyword, Integer pageNum, Integer pageSize) { @@ -113,20 +282,6 @@ public class StoryItemServiceImpl implements StoryItemService { return result; } - /** - * 批量更新时间线节点排序 - * - * 实现思路: - * 1. 使用事务确保批量更新的原子性 - * 2. 遍历排序数据,逐个更新节点的 sortOrder 字段 - * 3. 同时更新 updateTime 时间戳 - * - * 注意事项: - * - 该方法需要在事务中执行,确保数据一致性 - * - 如果任一更新失败,整个事务将回滚 - * - * @param items 排序数据列表 - */ @Override @Transactional(rollbackFor = Exception.class) public void updateItemsOrder(List> items) { @@ -137,7 +292,6 @@ public class StoryItemServiceImpl implements StoryItemService { String instanceId = (String) item.get("instanceId"); Integer sortOrder = ((Number) item.get("sortOrder")).intValue(); - // 构建更新参数 Map params = new HashMap<>(); params.put("instanceId", instanceId); params.put("sortOrder", sortOrder); @@ -145,54 +299,27 @@ public class StoryItemServiceImpl implements StoryItemService { params.put("updateTime", now); storyItemMapper.updateOrder(params); - log.debug("更新节点排序: instanceId={}, sortOrder={}", instanceId, sortOrder); + log.debug("Updated story item order: instanceId={}, sortOrder={}", instanceId, sortOrder); } - log.info("批量更新排序完成,共更新 {} 个节点", items.size()); + log.info("Updated order for {} story items", items.size()); } - /** - * 批量删除时间线节点 - * - * 实现思路: - * 1. 使用事务确保批量删除的原子性 - * 2. 遍历节点ID列表,逐个执行软删除 - * 3. 软删除仅标记 is_delete = 1,数据可恢复 - * - * 注意事项: - * - 该方法需要在事务中执行 - * - 如果任一删除失败,整个事务将回滚 - * - * @param instanceIds 要删除的节点ID列表 - */ @Override @Transactional(rollbackFor = Exception.class) public void batchDeleteItems(List instanceIds) { for (String instanceId : instanceIds) { storyItemMapper.deleteByItemId(instanceId); - log.debug("软删除节点: instanceId={}", instanceId); + log.debug("Soft deleted story item: {}", instanceId); } - log.info("批量删除完成,共删除 {} 个节点", instanceIds.size()); + log.info("Soft deleted {} story items", instanceIds.size()); } - /** - * 批量修改时间线节点时间 - * - * 实现思路: - * 1. 使用事务确保批量修改的原子性 - * 2. 遍历节点ID列表,逐个更新时间字段 - * 3. 同时更新 updateTime 时间戳 - * - * @param instanceIds 要修改的节点ID列表 - * @param storyItemTime 新的时间值 - */ @Override @Transactional(rollbackFor = Exception.class) public void batchUpdateItemTime(List instanceIds, String storyItemTime) { String currentUserId = UserContextUtils.getCurrentUserId(); LocalDateTime now = LocalDateTime.now(); - - // 解析时间字符串为 LocalDateTime LocalDateTime parsedTime = LocalDateTime.parse(storyItemTime.replace(" ", "T")); for (String instanceId : instanceIds) { @@ -203,9 +330,139 @@ public class StoryItemServiceImpl implements StoryItemService { params.put("updateTime", now); storyItemMapper.updateItemTime(params); - log.debug("更新节点时间: instanceId={}, storyItemTime={}", instanceId, storyItemTime); + log.debug("Updated story item time: instanceId={}, storyItemTime={}", instanceId, storyItemTime); } - log.info("批量修改时间完成,共修改 {} 个节点", instanceIds.size()); + log.info("Updated time for {} story items", instanceIds.size()); + } + + private void hydrateShareItem(StoryItemShareVo shareVo) { + List images = getStoryItemImages(shareVo.getInstanceId()); + shareVo.setImages(images); + shareVo.setRelatedImageInstanceIds(images); + + if (!StringUtils.hasText(shareVo.getCoverInstanceId()) && !images.isEmpty()) { + shareVo.setCoverInstanceId(images.get(0)); + } + if (!StringUtils.hasText(shareVo.getThumbnailInstanceId())) { + shareVo.setThumbnailInstanceId(shareVo.getThumbnailUrl()); + } + if (!StringUtils.hasText(shareVo.getOwnerName())) { + shareVo.setOwnerName(shareVo.getAuthorName()); + } + } + + private List parseFeaturedMomentIds(String featuredMomentIds) { + if (!StringUtils.hasText(featuredMomentIds)) { + return Collections.emptyList(); + } + return Arrays.stream(featuredMomentIds.split(",")) + .map(String::trim) + .filter(StringUtils::hasText) + .distinct() + .toList(); + } + + private String serializeFeaturedMomentIds(List featuredMomentIds, String heroMomentId) { + Set result = new LinkedHashSet<>(); + if (StringUtils.hasText(heroMomentId)) { + result.add(heroMomentId); + } + if (featuredMomentIds != null) { + for (String featuredMomentId : featuredMomentIds) { + if (StringUtils.hasText(featuredMomentId) && result.size() < 6) { + result.add(featuredMomentId.trim()); + } + } + } + return String.join(",", result); + } + + private void upsertStoryShareConfig( + StoryShare existingShareConfig, + StorySharePublishRequest request, + String shareId, + String currentUserId, + LocalDateTime now, + String heroMomentId) { + StoryShare storyShare = existingShareConfig == null ? new StoryShare() : existingShareConfig; + storyShare.setShareId(shareId); + storyShare.setStoryInstanceId(request.getStoryId()); + storyShare.setHeroMomentId(heroMomentId); + storyShare.setShareTitle(trimToNull(request.getTitle())); + storyShare.setShareDescription(trimToNull(request.getDescription())); + storyShare.setShareQuote(trimToNull(request.getQuote())); + storyShare.setFeaturedMomentIds(serializeFeaturedMomentIds(request.getFeaturedMomentIds(), heroMomentId)); + storyShare.setUpdateId(currentUserId); + storyShare.setUpdateTime(now); + storyShare.setIsDelete(0); + + if (storyShare.getId() == null) { + storyShare.setCreateId(currentUserId); + storyShare.setCreateTime(now); + storyShareMapper.insert(storyShare); + return; + } + + storyShareMapper.update(storyShare); + } + + private List loadFeaturedMoments(String storyId, List featuredMomentIds) { + if (!StringUtils.hasText(storyId) || featuredMomentIds == null || featuredMomentIds.isEmpty()) { + return Collections.emptyList(); + } + + List moments = new ArrayList<>(); + for (String featuredMomentId : featuredMomentIds) { + StoryItem item = storyItemMapper.selectById(featuredMomentId); + if (item == null || (item.getIsDelete() != null && item.getIsDelete() == 1)) { + continue; + } + if (!storyId.equals(item.getStoryInstanceId())) { + continue; + } + moments.add(buildShareMomentVo(item)); + } + return moments; + } + + private StoryShareMomentVo buildShareMomentVo(StoryItem item) { + StoryShareMomentVo moment = new StoryShareMomentVo(); + moment.setId(item.getId()); + moment.setInstanceId(item.getInstanceId()); + moment.setStoryInstanceId(item.getStoryInstanceId()); + moment.setMasterItemId(item.getMasterItemId()); + moment.setShareId(item.getShareId()); + moment.setTitle(item.getTitle()); + moment.setDescription(item.getDescription()); + moment.setLocation(item.getLocation()); + moment.setStoryItemTime(item.getStoryItemTime()); + moment.setVideoUrl(item.getVideoUrl()); + moment.setDuration(item.getDuration()); + moment.setThumbnailUrl(item.getThumbnailUrl()); + moment.setCreateId(item.getCreateId()); + moment.setUpdateId(item.getUpdateId()); + moment.setCreateTime(item.getCreateTime()); + moment.setUpdateTime(item.getUpdateTime()); + moment.setIsDelete(item.getIsDelete()); + moment.setSortOrder(item.getSortOrder()); + + List images = getStoryItemImages(item.getInstanceId()); + moment.setImages(images); + moment.setRelatedImageInstanceIds(images); + if (!images.isEmpty()) { + moment.setCoverInstanceId(images.get(0)); + } + if (!StringUtils.hasText(moment.getThumbnailInstanceId())) { + moment.setThumbnailInstanceId(moment.getThumbnailUrl()); + } + return moment; + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); } } diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java index 2682ae2..e63f8fd 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java @@ -3,29 +3,28 @@ package com.timeline.story.service.impl; import com.timeline.common.constants.CommonConstants; import com.timeline.common.exception.CustomException; import com.timeline.common.response.ResponseEnum; +import com.timeline.common.utils.IdUtils; +import com.timeline.common.utils.UserContextUtils; +import com.timeline.story.dao.StoryMapper; import com.timeline.story.entity.Story; import com.timeline.story.entity.StoryActivity; import com.timeline.story.entity.StoryItem; -import com.timeline.story.dao.StoryMapper; +import com.timeline.story.mq.ActivityLogProducer; import com.timeline.story.service.StoryItemService; import com.timeline.story.service.StoryPermissionService; import com.timeline.story.service.StoryService; -import com.timeline.story.mq.ActivityLogProducer; import com.timeline.story.vo.StoryDetailVo; import com.timeline.story.vo.StoryDetailWithItemsVo; import com.timeline.story.vo.StoryItemVo; import com.timeline.story.vo.StoryPermissionVo; import com.timeline.story.vo.StoryVo; -import com.timeline.common.utils.IdUtils; -import com.timeline.common.utils.UserContextUtils; - import lombok.extern.slf4j.Slf4j; -import lombok.val; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -48,7 +47,7 @@ public class StoryServiceImpl implements StoryService { private String getCurrentUserId() { String currentUserId = UserContextUtils.getCurrentUserId(); if (currentUserId == null || currentUserId.isEmpty()) { - throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份"); + throw new CustomException(ResponseEnum.UNAUTHORIZED, "User context is missing"); } return currentUserId; } @@ -57,10 +56,8 @@ public class StoryServiceImpl implements StoryService { @Transactional(rollbackFor = Exception.class) public void createStory(StoryVo storyVo) { try { - String currentUserId = UserContextUtils.getCurrentUserId(); - if (currentUserId == null || currentUserId.isEmpty()) { - throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份"); - } + String currentUserId = getCurrentUserId(); + Story story = new Story(); story.setOwnerId(currentUserId); story.setUpdateId(currentUserId); @@ -74,11 +71,10 @@ public class StoryServiceImpl implements StoryService { story.setLogo(storyVo.getLogo()); story.setIsDelete(0); storyMapper.insert(story); - // 自动添加创建者权限 + StoryPermissionVo permissionVo = new StoryPermissionVo(); permissionVo.setStoryInstanceId(story.getInstanceId()); permissionVo.setUserId(currentUserId); - // 创建者权限 permissionVo.setPermissionType(CommonConstants.STORY_PERMISSION_TYPE_OWNER); storyPermissionService.createPermission(permissionVo); @@ -88,9 +84,9 @@ public class StoryServiceImpl implements StoryService { activity.setStoryInstanceId(story.getInstanceId()); activity.setRemark(CommonConstants.ACTION_REMARK_STORY_CREATE); activityLogProducer.sendLog("story.activity.create", activity); - } catch (Exception e) { - log.error("创建故事失败", e); - throw new CustomException(500, "创建故事失败: " + e.toString()); + } catch (Exception exception) { + log.error("Create story failed", exception); + throw new CustomException(500, "Create story failed: " + exception.getMessage()); } } @@ -103,17 +99,19 @@ public class StoryServiceImpl implements StoryService { if (story == null) { throw new CustomException(ResponseEnum.NOT_FOUND); } - if (!storyPermissionService.checkUserPermission(storyId, currentUserId, + if (!storyPermissionService.checkUserPermission( + storyId, + currentUserId, CommonConstants.STORY_PERMISSION_TYPE_WRITE)) { - throw new CustomException(ResponseEnum.FORBIDDEN, "无权限修改故事"); + throw new CustomException(ResponseEnum.FORBIDDEN, "No permission to update story"); } + story.setTitle(storyVo.getTitle()); story.setDescription(storyVo.getDescription()); story.setStatus(storyVo.getStatus()); story.setStoryTime(storyVo.getStoryTime()); story.setUpdateTime(LocalDateTime.now()); story.setLogo(storyVo.getLogo()); - storyMapper.update(story); StoryActivity activity = new StoryActivity(); @@ -122,7 +120,6 @@ public class StoryServiceImpl implements StoryService { activity.setStoryInstanceId(storyId); activity.setRemark(CommonConstants.ACTION_REMARK_STORY_UPDATE); activityLogProducer.sendLog("story.activity.update", activity); - } @Override @@ -133,15 +130,16 @@ public class StoryServiceImpl implements StoryService { if (story == null) { throw new CustomException(ResponseEnum.NOT_FOUND); } - if (!storyPermissionService.checkUserPermission(storyId, currentUserId, + if (!storyPermissionService.checkUserPermission( + storyId, + currentUserId, CommonConstants.STORY_PERMISSION_TYPE_ADMIN)) { - throw new CustomException(ResponseEnum.FORBIDDEN, "无权限删除故事"); + throw new CustomException(ResponseEnum.FORBIDDEN, "No permission to delete story"); } - // delete story + storyMapper.deleteByInstanceId(storyId); - // delete permission storyPermissionService.deletePermission(storyId); - // delete activity + StoryActivity activity = new StoryActivity(); activity.setActorId(currentUserId); activity.setAction(CommonConstants.ACTION_TYPE_STORY_DELETE); @@ -152,21 +150,32 @@ public class StoryServiceImpl implements StoryService { @Override public StoryDetailWithItemsVo getStoryByInstanceId(String storyId) { - val userId = getCurrentUserId(); + String userId = getCurrentUserId(); StoryDetailVo story = storyMapper.selectByInstanceId(storyId, userId); if (story == null) { throw new CustomException(ResponseEnum.NOT_FOUND); } + StoryItemVo storyItemVo = new StoryItemVo(); storyItemVo.setStoryInstanceId(storyId); storyItemVo.setCurrent(1); storyItemVo.setPageSize(10); - Map itemsMap = storyItemService.getItemsByMasterItem(storyItemVo); - List items = (List) itemsMap.get("list"); + Map itemsMap = storyItemService.getItemsByMasterItem(storyItemVo); + + List items = new ArrayList<>(); + Object rawItems = itemsMap.get("list"); + if (rawItems instanceof List rawList) { + for (Object item : rawList) { + if (item instanceof StoryItem storyItem) { + items.add(storyItem); + } + } + } StoryDetailWithItemsVo result = new StoryDetailWithItemsVo(); result.setItems(items); result.setInstanceId(story.getInstanceId()); + result.setShareId(story.getShareId()); result.setOwnerId(story.getOwnerId()); result.setUpdateId(story.getUpdateId()); result.setTitle(story.getTitle()); @@ -181,7 +190,6 @@ public class StoryServiceImpl implements StoryService { result.setUpdateName(story.getUpdateName()); result.setPermissionType(story.getPermissionType()); result.setItemCount(story.getItemCount()); - return result; } @@ -189,9 +197,9 @@ public class StoryServiceImpl implements StoryService { public List getStoriesByOwnerId(String ownerId) { try { return storyMapper.selectByOwnerId(ownerId); - } catch (Exception e) { - log.error("查询用户故事列表失败", e); - throw new CustomException(500, "查询用户故事列表失败"); + } catch (Exception exception) { + log.error("Query user stories failed", exception); + throw new CustomException(500, "Query user stories failed"); } } @@ -200,9 +208,9 @@ public class StoryServiceImpl implements StoryService { try { String currentUserId = getCurrentUserId(); return storyMapper.selectByOwnerId(currentUserId); - } catch (Exception e) { - log.error("查询用户故事列表失败", e); - throw new CustomException(500, "查询用户故事列表失败"); + } catch (Exception exception) { + log.error("Query stories failed", exception); + throw new CustomException(500, "Query stories failed"); } } -} +} \ No newline at end of file diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java index c44c135..69ed745 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java @@ -4,9 +4,22 @@ import com.timeline.story.entity.StoryItem; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + @Data @EqualsAndHashCode(callSuper = true) public class StoryItemShareVo extends StoryItem { private String authorName; private String authorAvatar; + private String ownerName; + private String coverInstanceId; + private String thumbnailInstanceId; + private String shareTitle; + private String shareDescription; + private String shareQuote; + private String heroMomentId; + private List images; + private List relatedImageInstanceIds; + private List featuredMomentIds; + private List featuredMoments; } diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java new file mode 100644 index 0000000..bc697ef --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java @@ -0,0 +1,19 @@ +package com.timeline.story.vo; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class StoryShareConfigVo { + private String shareId; + private String storyId; + private String heroMomentId; + private String title; + private String description; + private String quote; + private List featuredMomentIds; + private Boolean published; + private LocalDateTime updatedAt; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareMomentVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareMomentVo.java new file mode 100644 index 0000000..94c2a95 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareMomentVo.java @@ -0,0 +1,16 @@ +package com.timeline.story.vo; + +import com.timeline.story.entity.StoryItem; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class StoryShareMomentVo extends StoryItem { + private String coverInstanceId; + private String thumbnailInstanceId; + private List images; + private List relatedImageInstanceIds; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java new file mode 100644 index 0000000..28763f2 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java @@ -0,0 +1,15 @@ +package com.timeline.story.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class StorySharePublishRequest { + private String storyId; + private String heroMomentId; + private String title; + private String description; + private String quote; + private List featuredMomentIds; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java new file mode 100644 index 0000000..85566a9 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java @@ -0,0 +1,14 @@ +package com.timeline.story.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class StorySharePublishVo { + private String storyId; + private String shareId; + private String heroMomentId; + private String publicPath; + private LocalDateTime publishedAt; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java new file mode 100644 index 0000000..1e2c04e --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java @@ -0,0 +1,17 @@ +package com.timeline.story.vo; + +import lombok.Data; + +@Data +public class TimelineArchiveBucketVo { + private String key; + private String title; + private Long count; + private String subtitle; + private String storyInstanceId; + private String storyShareId; + private String coverInstanceId; + private String coverSrc; + private String sampleMomentTitle; + private Long sortValue; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveExploreVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveExploreVo.java new file mode 100644 index 0000000..a36e5fe --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveExploreVo.java @@ -0,0 +1,13 @@ +package com.timeline.story.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class TimelineArchiveExploreVo { + private String type; + private String value; + private TimelineArchiveBucketVo bucket; + private List moments; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java new file mode 100644 index 0000000..5c8bae3 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java @@ -0,0 +1,25 @@ +package com.timeline.story.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class TimelineArchiveMomentVo { + private String key; + private String storyInstanceId; + private String storyShareId; + private String storyTitle; + private String storyTime; + private String itemInstanceId; + private String itemTitle; + private String itemDescription; + private String itemTime; + private String location; + private List tags; + private String coverInstanceId; + private String coverSrc; + private Integer mediaCount; + private Boolean hasVideo; + private Long sortValue; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveSummaryVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveSummaryVo.java new file mode 100644 index 0000000..ba83b53 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveSummaryVo.java @@ -0,0 +1,14 @@ +package com.timeline.story.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class TimelineArchiveSummaryVo { + private Integer storiesIndexed; + private Integer shareableStories; + private Integer videoMoments; + private List locations; + private List tags; +} diff --git a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryItemMapper.xml b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryItemMapper.xml index 18f6d67..ff8484d 100644 --- a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryItemMapper.xml +++ b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryItemMapper.xml @@ -6,7 +6,7 @@ INSERT INTO story_item (instance_id, master_item_id, description, location, title, create_id, story_instance_id, is_delete, story_item_time, update_id, video_url, duration, thumbnail_url) - VALUES (#{instanceId}, #{masterItemId}, #{description}, #{location}, #{title},#{createId}, #{storyInstanceId}, #{isDelete}, #{storyItemTime}, #{updateId}, #{videoUrl}, #{duration}, #{thumbnailUrl}) + VALUES (#{instanceId}, #{masterItemId}, #{description}, #{location}, #{title}, #{createId}, #{storyInstanceId}, #{isDelete}, #{storyItemTime}, #{updateId}, #{videoUrl}, #{duration}, #{thumbnailUrl}) @@ -30,16 +30,26 @@ + - - + + UPDATE story_item + SET share_id = NULL, + update_id = #{updateId}, + update_time = #{updateTime} + WHERE story_instance_id = #{storyInstanceId} + AND is_delete = 0 + AND share_id IS NOT NULL + + + + UPDATE story_item + SET share_id = #{shareId}, + update_id = #{updateId}, + update_time = #{updateTime} + WHERE instance_id = #{instanceId} + AND is_delete = 0 + + UPDATE story_item SET sort_order = #{sortOrder}, @@ -134,19 +172,6 @@ WHERE instance_id = #{instanceId} - UPDATE story_item SET story_item_time = #{storyItemTime}, diff --git a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml index 918fb1b..59c46d1 100644 --- a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml +++ b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml @@ -22,43 +22,74 @@ - UPDATE story SET story.is_delete = 1, update_time = NOW() WHERE instance_id = #{instanceId} + UPDATE story + SET is_delete = 1, + update_time = NOW() + WHERE instance_id = #{instanceId} @@ -68,4 +99,4 @@ WHERE instance_id = #{instanceId} - + \ No newline at end of file diff --git a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml new file mode 100644 index 0000000..0944597 --- /dev/null +++ b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + INSERT INTO story_share ( + share_id, + story_instance_id, + hero_moment_id, + share_title, + share_description, + share_quote, + featured_moment_ids, + create_id, + update_id, + create_time, + update_time, + is_delete + ) VALUES ( + #{shareId}, + #{storyInstanceId}, + #{heroMomentId}, + #{shareTitle}, + #{shareDescription}, + #{shareQuote}, + #{featuredMomentIds}, + #{createId}, + #{updateId}, + #{createTime}, + #{updateTime}, + #{isDelete} + ) + + + + UPDATE story_share + SET share_id = #{shareId}, + hero_moment_id = #{heroMomentId}, + share_title = #{shareTitle}, + share_description = #{shareDescription}, + share_quote = #{shareQuote}, + featured_moment_ids = #{featuredMomentIds}, + update_id = #{updateId}, + update_time = #{updateTime}, + is_delete = #{isDelete} + WHERE id = #{id} + + + + UPDATE story_share + SET is_delete = 1, + update_id = #{updateId}, + update_time = #{updateTime} + WHERE story_instance_id = #{storyInstanceId} + AND is_delete = 0 + +