feat(故事分享): 新增故事分享功能及相关接口
All checks were successful
test/timeline-server/pipeline/head This commit looks good
All checks were successful
test/timeline-server/pipeline/head This commit looks good
实现故事分享功能,包括分享配置、发布、取消发布及公开访问接口 新增故事分享相关VO类及数据库表结构 扩展故事和故事项实体类以支持分享功能 添加故事分享Mapper及XML映射文件 更新故事服务和控制器以支持分享操作
This commit is contained in:
18
deploy/update_story_share.sql
Normal file
18
deploy/update_story_share.sql
Normal file
@@ -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;
|
||||||
@@ -4,6 +4,8 @@ import com.timeline.common.response.ResponseEntity;
|
|||||||
import com.timeline.common.utils.UserContextUtils;
|
import com.timeline.common.utils.UserContextUtils;
|
||||||
import com.timeline.story.service.AnalyticsService;
|
import com.timeline.story.service.AnalyticsService;
|
||||||
import com.timeline.story.vo.TimelineAnalyticsVo;
|
import com.timeline.story.vo.TimelineAnalyticsVo;
|
||||||
|
import com.timeline.story.vo.TimelineArchiveExploreVo;
|
||||||
|
import com.timeline.story.vo.TimelineArchiveSummaryVo;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -124,6 +126,33 @@ public class AnalyticsController {
|
|||||||
* @param year 年份(可选,默认去年)
|
* @param year 年份(可选,默认去年)
|
||||||
* @return 年度报告
|
* @return 年度报告
|
||||||
*/
|
*/
|
||||||
|
@GetMapping("/archive/summary")
|
||||||
|
public ResponseEntity<TimelineArchiveSummaryVo> 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<TimelineArchiveExploreVo> 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")
|
@GetMapping("/yearly-report")
|
||||||
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo.YearlyReport> getYearlyReport(
|
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo.YearlyReport> getYearlyReport(
|
||||||
@RequestParam(required = false) Integer year) {
|
@RequestParam(required = false) Integer year) {
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import com.timeline.common.response.ResponseEntity;
|
|||||||
import com.timeline.common.response.ResponseEnum;
|
import com.timeline.common.response.ResponseEnum;
|
||||||
import com.timeline.story.entity.StoryItem;
|
import com.timeline.story.entity.StoryItem;
|
||||||
import com.timeline.story.service.StoryItemService;
|
import com.timeline.story.service.StoryItemService;
|
||||||
import com.timeline.story.service.StoryService;
|
|
||||||
import com.timeline.story.vo.StoryItemAddVo;
|
import com.timeline.story.vo.StoryItemAddVo;
|
||||||
import com.timeline.story.vo.StoryItemShareVo;
|
import com.timeline.story.vo.StoryItemShareVo;
|
||||||
import com.timeline.story.vo.StoryItemVo;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.cloud.openfeign.SpringQueryMap;
|
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||||
@@ -29,129 +29,126 @@ public class StoryItemController {
|
|||||||
private StoryItemService storyItemService;
|
private StoryItemService storyItemService;
|
||||||
|
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public ResponseEntity<String> createItem(@RequestParam("storyItem") String storyItemVoString,
|
public ResponseEntity<String> createItem(
|
||||||
|
@RequestParam("storyItem") String storyItemVoString,
|
||||||
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
|
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
|
||||||
|
|
||||||
log.info("创建 StoryItem,{}", storyItemVoString);
|
log.info("Create StoryItem: {}", storyItemVoString);
|
||||||
storyItemService.createStoryItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
|
storyItemService.createStoryItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
|
||||||
return ResponseEntity.success("StoryItem 创建成功");
|
return ResponseEntity.success("StoryItem created");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("")
|
@PutMapping("")
|
||||||
public ResponseEntity<String> updateItem(@RequestParam("storyItem") String storyItemVoString,
|
public ResponseEntity<String> updateItem(
|
||||||
|
@RequestParam("storyItem") String storyItemVoString,
|
||||||
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
|
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
|
||||||
log.info("更新 StoryItem: {}", storyItemVoString);
|
log.info("Update StoryItem: {}", storyItemVoString);
|
||||||
storyItemService.updateItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
|
storyItemService.updateItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
|
||||||
return ResponseEntity.success("StoryItem 更新成功");
|
return ResponseEntity.success("StoryItem updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{itemId}")
|
@DeleteMapping("/{itemId}")
|
||||||
public ResponseEntity<String> deleteItem(@PathVariable String itemId) {
|
public ResponseEntity<String> deleteItem(@PathVariable String itemId) {
|
||||||
log.info("删除 StoryItem: {}", itemId);
|
log.info("Delete StoryItem: {}", itemId);
|
||||||
storyItemService.deleteItem(itemId);
|
storyItemService.deleteItem(itemId);
|
||||||
return ResponseEntity.success("StoryItem 删除成功");
|
return ResponseEntity.success("StoryItem deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{itemId}")
|
@GetMapping("/{itemId}")
|
||||||
public ResponseEntity<StoryItem> getItemById(@PathVariable String itemId) {
|
public ResponseEntity<StoryItem> getItemById(@PathVariable String itemId) {
|
||||||
log.info("获取 StoryItem 详情: {}", itemId);
|
log.info("Get StoryItem detail: {}", itemId);
|
||||||
StoryItem item = storyItemService.getItemById(itemId);
|
StoryItem item = storyItemService.getItemById(itemId);
|
||||||
return ResponseEntity.success(item);
|
return ResponseEntity.success(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public ResponseEntity<Map> getItemsByMasterItem(@SpringQueryMap StoryItemVo storyItemVo) {
|
public ResponseEntity<Map> getItemsByMasterItem(@SpringQueryMap StoryItemVo storyItemVo) {
|
||||||
log.info("查询 StoryItem 列表,storyInstanceId: {}, current: {}, pageSize:{}", storyItemVo.getStoryInstanceId(),
|
log.info(
|
||||||
storyItemVo.getCurrent(), storyItemVo.getPageSize());
|
"Query StoryItem list: storyInstanceId={}, current={}, pageSize={}",
|
||||||
|
storyItemVo.getStoryInstanceId(),
|
||||||
|
storyItemVo.getCurrent(),
|
||||||
|
storyItemVo.getPageSize());
|
||||||
Map items = storyItemService.getItemsByMasterItem(storyItemVo);
|
Map items = storyItemService.getItemsByMasterItem(storyItemVo);
|
||||||
return ResponseEntity.success(items);
|
return ResponseEntity.success(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/images/{itemId}")
|
@GetMapping("/images/{itemId}")
|
||||||
public ResponseEntity<List<String>> getStoryItemImages(@PathVariable String itemId) {
|
public ResponseEntity<List<String>> getStoryItemImages(@PathVariable String itemId) {
|
||||||
log.info("获取 StoryItem 图片列表,itemId: {}", itemId);
|
log.info("Get StoryItem images: itemId={}", itemId);
|
||||||
List<String> images = storyItemService.getStoryItemImages(itemId);
|
List<String> images = storyItemService.getStoryItemImages(itemId);
|
||||||
return ResponseEntity.success(images);
|
return ResponseEntity.success(images);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/count/{storyInstanceId}")
|
@GetMapping("/count/{storyInstanceId}")
|
||||||
public ResponseEntity<Integer> getStoryItemCount(@PathVariable String storyInstanceId) {
|
public ResponseEntity<Integer> getStoryItemCount(@PathVariable String storyInstanceId) {
|
||||||
log.info("获取 StoryItem 子项数量,storyInstanceId: {}", storyInstanceId);
|
log.info("Get StoryItem count: storyInstanceId={}", storyInstanceId);
|
||||||
Integer count = storyItemService.getStoryItemCount(storyInstanceId);
|
Integer count = storyItemService.getStoryItemCount(storyInstanceId);
|
||||||
return ResponseEntity.success(count);
|
return ResponseEntity.success(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
@GetMapping("/public/story/item/{shareId}")
|
||||||
* @GetMapping("/public/story/item/{shareId}")
|
public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable String shareId) {
|
||||||
* public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable
|
log.info("Get public StoryItem by shareId: {}", shareId);
|
||||||
* String shareId) {
|
StoryItemShareVo item = storyItemService.getItemByShareId(shareId);
|
||||||
* log.info("获取分享的 StoryItem,shareId: {}", shareId);
|
return ResponseEntity.success(item);
|
||||||
* StoryItemShareVo item = storyItemService.getItemByShareId(shareId);
|
}
|
||||||
* return ResponseEntity.success(item);
|
|
||||||
* }
|
@GetMapping("/share/{storyId}")
|
||||||
*/
|
public ResponseEntity<StoryShareConfigVo> 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<StorySharePublishVo> 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<String> unpublishStoryShare(@PathVariable String storyId) {
|
||||||
|
log.info("Unpublish Story share: storyId={}", storyId);
|
||||||
|
storyItemService.unpublishStoryShare(storyId);
|
||||||
|
return ResponseEntity.success("Story share unpublished");
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/search")
|
@GetMapping("/search")
|
||||||
public ResponseEntity<Map> searchItems(@RequestParam String keyword,
|
public ResponseEntity<Map> searchItems(
|
||||||
|
@RequestParam String keyword,
|
||||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
@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);
|
Map result = storyItemService.searchItems(keyword, pageNum, pageSize);
|
||||||
return ResponseEntity.success(result);
|
return ResponseEntity.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量更新时间线节点排序
|
|
||||||
*
|
|
||||||
* 功能描述:
|
|
||||||
* 接收前端拖拽排序后的节点顺序,批量更新各节点的 sortOrder 字段。
|
|
||||||
* 用于保存用户手动调整的时间线节点顺序。
|
|
||||||
*
|
|
||||||
* @param request 包含 items 数组,每个元素有 instanceId 和 sortOrder
|
|
||||||
* @return 操作结果
|
|
||||||
*/
|
|
||||||
@PutMapping("/order")
|
@PutMapping("/order")
|
||||||
public ResponseEntity<String> updateItemsOrder(@RequestBody Map<String, List<Map<String, Object>>> request) {
|
public ResponseEntity<String> updateItemsOrder(@RequestBody Map<String, List<Map<String, Object>>> request) {
|
||||||
List<Map<String, Object>> items = request.get("items");
|
List<Map<String, Object>> items = request.get("items");
|
||||||
if (items == null || items.isEmpty()) {
|
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);
|
storyItemService.updateItemsOrder(items);
|
||||||
return ResponseEntity.success("排序更新成功");
|
return ResponseEntity.success("Order updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除时间线节点
|
|
||||||
*
|
|
||||||
* 功能描述:
|
|
||||||
* 根据节点ID列表批量软删除时间线节点。
|
|
||||||
* 软删除仅标记 is_delete 字段,数据可恢复。
|
|
||||||
*
|
|
||||||
* @param request 包含 instanceIds 数组
|
|
||||||
* @return 操作结果
|
|
||||||
*/
|
|
||||||
@PostMapping("/batch-delete")
|
@PostMapping("/batch-delete")
|
||||||
public ResponseEntity<String> batchDeleteItems(@RequestBody Map<String, List<String>> request) {
|
public ResponseEntity<String> batchDeleteItems(@RequestBody Map<String, List<String>> request) {
|
||||||
List<String> instanceIds = request.get("instanceIds");
|
List<String> instanceIds = request.get("instanceIds");
|
||||||
if (instanceIds == null || instanceIds.isEmpty()) {
|
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);
|
storyItemService.batchDeleteItems(instanceIds);
|
||||||
return ResponseEntity.success("批量删除成功");
|
return ResponseEntity.success("Batch delete completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量修改时间线节点时间
|
|
||||||
*
|
|
||||||
* 功能描述:
|
|
||||||
* 批量修改多个节点的 storyItemTime 字段。
|
|
||||||
* 用于批量调整节点的时间信息。
|
|
||||||
*
|
|
||||||
* @param request 包含 instanceIds 数组和 storyItemTime 字符串
|
|
||||||
* @return 操作结果
|
|
||||||
*/
|
|
||||||
@PutMapping("/batch-time")
|
@PutMapping("/batch-time")
|
||||||
public ResponseEntity<String> batchUpdateItemTime(@RequestBody Map<String, Object> request) {
|
public ResponseEntity<String> batchUpdateItemTime(@RequestBody Map<String, Object> request) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@@ -159,14 +156,14 @@ public class StoryItemController {
|
|||||||
String storyItemTime = (String) request.get("storyItemTime");
|
String storyItemTime = (String) request.get("storyItemTime");
|
||||||
|
|
||||||
if (instanceIds == null || instanceIds.isEmpty()) {
|
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()) {
|
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);
|
storyItemService.batchUpdateItemTime(instanceIds, storyItemTime);
|
||||||
return ResponseEntity.success("批量修改时间成功");
|
return ResponseEntity.success("Batch time update completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.timeline.story.controller;
|
package com.timeline.story.controller;
|
||||||
|
|
||||||
import com.timeline.common.response.ResponseEntity;
|
import com.timeline.common.response.ResponseEntity;
|
||||||
import com.timeline.story.entity.StoryItem;
|
|
||||||
import com.timeline.story.service.StoryItemService;
|
import com.timeline.story.service.StoryItemService;
|
||||||
|
import com.timeline.story.vo.StoryItemShareVo;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -18,13 +18,10 @@ public class StoryPublicController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private StoryItemService storyItemService;
|
private StoryItemService storyItemService;
|
||||||
|
|
||||||
/*
|
@GetMapping("/{shareId}")
|
||||||
* @GetMapping("/{shareId}")
|
public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable String shareId) {
|
||||||
* public ResponseEntity<StoryItem> getStoryItemByShareId(@PathVariable String
|
log.info("Get public story by shareId: {}", shareId);
|
||||||
* shareId) {
|
StoryItemShareVo storyItem = storyItemService.getItemByShareId(shareId);
|
||||||
* log.info("根据 shareId 获取 StoryItem: {}", shareId);
|
return ResponseEntity.success(storyItem);
|
||||||
* StoryItem storyItem = storyItemService.getItemByShareId(shareId);
|
}
|
||||||
* return ResponseEntity.success(storyItem);
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,10 @@ package com.timeline.story.dao;
|
|||||||
import com.timeline.story.entity.StoryItem;
|
import com.timeline.story.entity.StoryItem;
|
||||||
import com.timeline.story.vo.StoryItemShareVo;
|
import com.timeline.story.vo.StoryItemShareVo;
|
||||||
import com.timeline.story.vo.StoryItemVo;
|
import com.timeline.story.vo.StoryItemVo;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -16,35 +16,38 @@ public interface StoryItemMapper {
|
|||||||
|
|
||||||
void update(StoryItem storyItem);
|
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<StoryItem> selectByMasterItem(String masterItemId);
|
List<StoryItem> selectByMasterItem(@Param("masterItemId") String masterItemId);
|
||||||
|
|
||||||
List<String> selectImagesByItemId(String itemId);
|
List<String> selectImagesByItemId(@Param("instanceId") String itemId);
|
||||||
|
|
||||||
List<StoryItemVo> selectStoryItemByStoryInstanceId(Map map);
|
List<StoryItemVo> 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<StoryItemVo> searchItems(@Param("keyword") String keyword);
|
List<StoryItemVo> searchItems(@Param("keyword") String keyword);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新节点排序值
|
|
||||||
*
|
|
||||||
* @param params 包含 instanceId, sortOrder, updateId, updateTime
|
|
||||||
*/
|
|
||||||
void updateOrder(Map<String, Object> params);
|
void updateOrder(Map<String, Object> params);
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新节点时间
|
|
||||||
*
|
|
||||||
* @param params 包含 instanceId, storyItemTime, updateId, updateTime
|
|
||||||
*/
|
|
||||||
void updateItemTime(Map<String, Object> params);
|
void updateItemTime(Map<String, Object> params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ package com.timeline.story.dao;
|
|||||||
import com.timeline.story.entity.Story;
|
import com.timeline.story.entity.Story;
|
||||||
import com.timeline.story.vo.StoryDetailVo;
|
import com.timeline.story.vo.StoryDetailVo;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface StoryMapper {
|
public interface StoryMapper {
|
||||||
void insert(Story story);
|
void insert(Story story);
|
||||||
|
|
||||||
void update(Story story);
|
void update(Story story);
|
||||||
void deleteByInstanceId(String instanceId);
|
|
||||||
StoryDetailVo selectByInstanceId(String instanceId, String userId);
|
void deleteByInstanceId(@Param("instanceId") String instanceId);
|
||||||
List<StoryDetailVo> selectByOwnerId(String ownerId);
|
|
||||||
void touchUpdate(String instanceId, String updateId);
|
StoryDetailVo selectByInstanceId(@Param("instanceId") String instanceId, @Param("userId") String userId);
|
||||||
|
|
||||||
|
List<StoryDetailVo> selectByOwnerId(@Param("ownerId") String ownerId);
|
||||||
|
|
||||||
|
void touchUpdate(@Param("instanceId") String instanceId, @Param("updateId") String updateId);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import java.time.LocalDateTime;
|
|||||||
@Data
|
@Data
|
||||||
public class Story {
|
public class Story {
|
||||||
private String instanceId;
|
private String instanceId;
|
||||||
|
private String shareId;
|
||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class StoryItem {
|
|||||||
private String instanceId;
|
private String instanceId;
|
||||||
private String storyInstanceId;
|
private String storyInstanceId;
|
||||||
private String masterItemId;
|
private String masterItemId;
|
||||||
|
private String shareId;
|
||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
private String location;
|
private String location;
|
||||||
@@ -22,9 +23,5 @@ public class StoryItem {
|
|||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
private Integer isDelete;
|
private Integer isDelete;
|
||||||
/**
|
|
||||||
* 排序值 - 用于拖拽排序
|
|
||||||
* 数值越小越靠前,默认为0
|
|
||||||
*/
|
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.timeline.story.service;
|
package com.timeline.story.service;
|
||||||
|
|
||||||
import com.timeline.story.vo.TimelineAnalyticsVo;
|
import com.timeline.story.vo.TimelineAnalyticsVo;
|
||||||
|
import com.timeline.story.vo.TimelineArchiveExploreVo;
|
||||||
|
import com.timeline.story.vo.TimelineArchiveSummaryVo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AnalyticsService - 数据分析服务接口
|
* AnalyticsService - 数据分析服务接口
|
||||||
@@ -63,6 +65,10 @@ public interface AnalyticsService {
|
|||||||
*/
|
*/
|
||||||
TimelineAnalyticsVo getTopTags(String userId, int limit);
|
TimelineAnalyticsVo getTopTags(String userId, int limit);
|
||||||
|
|
||||||
|
TimelineArchiveSummaryVo getArchiveSummary(String userId, int locationLimit, int tagLimit);
|
||||||
|
|
||||||
|
TimelineArchiveExploreVo getArchiveExplore(String userId, String type, String value, int limit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成年度报告
|
* 生成年度报告
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import com.timeline.story.entity.StoryItem;
|
|||||||
import com.timeline.story.vo.StoryItemAddVo;
|
import com.timeline.story.vo.StoryItemAddVo;
|
||||||
import com.timeline.story.vo.StoryItemShareVo;
|
import com.timeline.story.vo.StoryItemShareVo;
|
||||||
import com.timeline.story.vo.StoryItemVo;
|
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 org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,40 +27,19 @@ public interface StoryItemService {
|
|||||||
|
|
||||||
Integer getStoryItemCount(String storyInstanceId);
|
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<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize);
|
Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize);
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量更新时间线节点排序
|
|
||||||
*
|
|
||||||
* 功能描述:
|
|
||||||
* 根据前端传递的排序数据,批量更新各节点的 sortOrder 字段。
|
|
||||||
* 该方法支持事务处理,确保所有排序更新原子性完成。
|
|
||||||
*
|
|
||||||
* @param items 排序数据列表,每个元素包含 instanceId 和 sortOrder
|
|
||||||
*/
|
|
||||||
void updateItemsOrder(List<Map<String, Object>> items);
|
void updateItemsOrder(List<Map<String, Object>> items);
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除时间线节点
|
|
||||||
*
|
|
||||||
* 功能描述:
|
|
||||||
* 根据节点ID列表批量软删除时间线节点。
|
|
||||||
* 软删除仅标记 is_delete 字段,数据可恢复。
|
|
||||||
*
|
|
||||||
* @param instanceIds 要删除的节点ID列表
|
|
||||||
*/
|
|
||||||
void batchDeleteItems(List<String> instanceIds);
|
void batchDeleteItems(List<String> instanceIds);
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量修改时间线节点时间
|
|
||||||
*
|
|
||||||
* 功能描述:
|
|
||||||
* 批量修改多个节点的 storyItemTime 字段。
|
|
||||||
*
|
|
||||||
* @param instanceIds 要修改的节点ID列表
|
|
||||||
* @param storyItemTime 新的时间值
|
|
||||||
*/
|
|
||||||
void batchUpdateItemTime(List<String> instanceIds, String storyItemTime);
|
void batchUpdateItemTime(List<String> instanceIds, String storyItemTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,538 @@
|
|||||||
package com.timeline.story.service.impl;
|
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.StoryItemMapper;
|
||||||
|
import com.timeline.story.dao.StoryMapper;
|
||||||
import com.timeline.story.service.AnalyticsService;
|
import com.timeline.story.service.AnalyticsService;
|
||||||
import com.timeline.story.vo.StoryDetailVo;
|
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.StoryItemVo;
|
||||||
import com.timeline.story.vo.TimelineAnalyticsVo;
|
import com.timeline.story.vo.TimelineAnalyticsVo;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.Year;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
|
||||||
* AnalyticsServiceImpl - 数据分析服务实现类
|
|
||||||
*/
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class AnalyticsServiceImpl implements AnalyticsService {
|
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
|
@Autowired
|
||||||
private StoryMapper storyMapper;
|
private StoryMapper storyMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private StoryItemMapper storyItemMapper;
|
private StoryItemMapper storyItemMapper;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TimelineAnalyticsVo getOverallStats(String userId) {
|
public TimelineAnalyticsVo getOverallStats(String userId) {
|
||||||
log.info("获取用户总体统计: userId={}", userId);
|
log.info("Get overall timeline stats: userId={}", userId);
|
||||||
|
List<StoryData> storyData = loadStoryData(userId);
|
||||||
|
List<ItemData> itemData = flattenItemData(storyData);
|
||||||
|
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||||
|
vo.setTotalStories((long) storyData.size());
|
||||||
// 基础统计数据 - 使用现有方法查询
|
vo.setTotalMoments((long) itemData.size());
|
||||||
List<StoryDetailVo> stories = storyMapper.selectByOwnerId(userId);
|
vo.setTotalMedia(itemData.stream().mapToLong(ItemData::mediaCount).sum());
|
||||||
vo.setTotalStories((long) (stories != null ? stories.size() : 0));
|
vo.setImageCount(itemData.stream().mapToLong(ItemData::imageCount).sum());
|
||||||
|
vo.setVideoCount(itemData.stream().mapToLong(item -> hasVideo(item.item()) ? 1 : 0).sum());
|
||||||
// 统计所有故事中的时刻数量
|
vo.setCollaborationCount(
|
||||||
long totalMoments = 0;
|
storyData.stream().filter(data -> data.story().getPermissionType() != null && data.story().getPermissionType() > 1).count());
|
||||||
long imageCount = 0;
|
|
||||||
long videoCount = 0;
|
|
||||||
|
|
||||||
if (stories != null) {
|
|
||||||
for (StoryDetailVo story : stories) {
|
|
||||||
Map<String, Object> params = new HashMap<>();
|
|
||||||
params.put("storyInstanceId", story.getInstanceId());
|
|
||||||
List<StoryItemVo> 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.setTotalComments(0L);
|
vo.setTotalComments(0L);
|
||||||
vo.setTotalLikes(0L);
|
vo.setTotalLikes(0L);
|
||||||
vo.setTotalFavorites(0L);
|
vo.setTotalFavorites(0L);
|
||||||
vo.setConsecutiveDays(0);
|
vo.setMonthlyTrend(buildMonthlyTrend(itemData, Year.now().getValue()));
|
||||||
vo.setMaxConsecutiveDays(0);
|
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;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TimelineAnalyticsVo getStoryStats(String storyInstanceId) {
|
public TimelineAnalyticsVo getStoryStats(String storyInstanceId) {
|
||||||
log.info("获取故事统计: storyInstanceId={}", storyInstanceId);
|
log.info("Get story stats: storyInstanceId={}", storyInstanceId);
|
||||||
|
StoryDetailVo story = storyMapper.selectByInstanceId(storyInstanceId, null);
|
||||||
|
List<StoryItemVo> items = loadItemsForStory(storyInstanceId);
|
||||||
|
List<ItemData> itemData = new ArrayList<>();
|
||||||
|
for (StoryItemVo item : items) {
|
||||||
|
itemData.add(buildItemData(story, item));
|
||||||
|
}
|
||||||
|
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||||
// TODO: 实现故事统计逻辑
|
vo.setTotalStories(1L);
|
||||||
vo.setTotalMoments(0L);
|
vo.setTotalMoments((long) itemData.size());
|
||||||
vo.setTotalMedia(0L);
|
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;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TimelineAnalyticsVo getMonthlyTrend(String userId, int year) {
|
public TimelineAnalyticsVo getMonthlyTrend(String userId, int year) {
|
||||||
log.info("获取月度趋势: userId={}, year={}", userId, year);
|
log.info("Get monthly trend: userId={}, year={}", userId, year);
|
||||||
|
List<ItemData> itemData = flattenItemData(loadStoryData(userId));
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||||
List<TimelineAnalyticsVo.MonthlyStats> 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> 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> 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<TimelineArchiveBucketVo> buckets =
|
||||||
|
"tag".equals(resolvedType) ? dataset.tagBuckets() : dataset.locationBuckets();
|
||||||
|
|
||||||
|
TimelineArchiveBucketVo bucket = findArchiveBucket(buckets, value);
|
||||||
|
List<TimelineArchiveMomentVo> 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> itemData = flattenItemData(loadStoryData(userId));
|
||||||
|
List<ItemData> 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<TimelineAnalyticsVo.LocationStats> topLocations = buildTopLocations(inYear, 1);
|
||||||
|
report.setTopLocation(topLocations.isEmpty() ? "-" : topLocations.get(0).getLocation());
|
||||||
|
|
||||||
|
List<TimelineAnalyticsVo.TagStats> 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> 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> itemData = flattenItemData(loadStoryData(userId));
|
||||||
|
return buildActivitySummary(itemData).currentStreak();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimelineAnalyticsVo getTimeDistribution(String userId) {
|
||||||
|
log.info("Get time distribution: userId={}", userId);
|
||||||
|
List<ItemData> 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<String, Object> 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<StoryData> loadStoryData(String userId) {
|
||||||
|
List<StoryDetailVo> stories = storyMapper.selectByOwnerId(userId);
|
||||||
|
if (stories == null || stories.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StoryData> data = new ArrayList<>();
|
||||||
|
for (StoryDetailVo story : stories) {
|
||||||
|
data.add(new StoryData(story, loadItemsForStory(story.getInstanceId())));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<StoryItemVo> loadItemsForStory(String storyInstanceId) {
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("storyInstanceId", storyInstanceId);
|
||||||
|
List<StoryItemVo> items = storyItemMapper.selectStoryItemByStoryInstanceId(params);
|
||||||
|
return items == null ? Collections.emptyList() : items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ItemData> flattenItemData(List<StoryData> storyData) {
|
||||||
|
List<ItemData> 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> storyData = loadStoryData(userId);
|
||||||
|
List<ItemData> itemData = flattenItemData(storyData);
|
||||||
|
Map<String, TimelineArchiveBucketVo> locationBuckets = new LinkedHashMap<>();
|
||||||
|
Map<String, TimelineArchiveBucketVo> tagBuckets = new LinkedHashMap<>();
|
||||||
|
List<TimelineArchiveMomentVo> 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<String> 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<String> tags = inferTags(item);
|
||||||
|
return new ItemData(story, item, images, imageCount, mediaCount, tags, eventTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> safeImages(List<String> 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<String> inferTags(StoryItemVo item) {
|
||||||
|
Set<String> 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<String, TimelineArchiveBucketVo> 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<TimelineArchiveBucketVo> sortArchiveBuckets(Map<String, TimelineArchiveBucketVo> 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<TimelineArchiveBucketVo> limitBuckets(List<TimelineArchiveBucketVo> 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<TimelineArchiveBucketVo> 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<TimelineAnalyticsVo.MonthlyStats> buildMonthlyTrend(List<ItemData> itemData, int year) {
|
||||||
|
List<TimelineAnalyticsVo.MonthlyStats> monthlyTrend = new ArrayList<>();
|
||||||
for (int month = 1; month <= 12; month++) {
|
for (int month = 1; month <= 12; month++) {
|
||||||
TimelineAnalyticsVo.MonthlyStats stats = new TimelineAnalyticsVo.MonthlyStats();
|
TimelineAnalyticsVo.MonthlyStats stats = new TimelineAnalyticsVo.MonthlyStats();
|
||||||
stats.setMonth(String.format("%d-%02d", year, month));
|
stats.setMonth(String.format("%d-%02d", year, month));
|
||||||
@@ -102,83 +541,213 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
|||||||
monthlyTrend.add(stats);
|
monthlyTrend.add(stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
vo.setMonthlyTrend(monthlyTrend);
|
for (ItemData item : itemData) {
|
||||||
return vo;
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
return monthlyTrend;
|
||||||
public TimelineAnalyticsVo getTopLocations(String userId, int limit) {
|
|
||||||
log.info("获取热门地点: userId={}, limit={}", userId, limit);
|
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
|
||||||
vo.setTopLocations(new ArrayList<>());
|
|
||||||
return vo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private List<TimelineAnalyticsVo.LocationStats> buildTopLocations(List<ItemData> itemData, int limit) {
|
||||||
public TimelineAnalyticsVo getTopTags(String userId, int limit) {
|
Map<String, Long> counts = new HashMap<>();
|
||||||
log.info("获取热门标签: userId={}, limit={}", userId, limit);
|
long total = 0L;
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
|
||||||
vo.setTopTags(new ArrayList<>());
|
for (ItemData item : itemData) {
|
||||||
return vo;
|
String location = item.item().getLocation();
|
||||||
|
if (!StringUtils.hasText(location)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
counts.merge(location, 1L, Long::sum);
|
||||||
|
total++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
List<TimelineAnalyticsVo.LocationStats> result = new ArrayList<>();
|
||||||
public TimelineAnalyticsVo.YearlyReport generateYearlyReport(String userId, int year) {
|
final long totalCount = total;
|
||||||
log.info("生成年度报告: userId={}, year={}", userId, year);
|
counts.entrySet().stream()
|
||||||
TimelineAnalyticsVo.YearlyReport report = new TimelineAnalyticsVo.YearlyReport();
|
.sorted((left, right) -> Long.compare(right.getValue(), left.getValue()))
|
||||||
report.setYear(year);
|
.limit(limit)
|
||||||
report.setTotalMoments(0L);
|
.forEach(entry -> {
|
||||||
report.setTotalMedia(0L);
|
TimelineAnalyticsVo.LocationStats stats = new TimelineAnalyticsVo.LocationStats();
|
||||||
report.setMostActiveMonth("-");
|
stats.setLocation(entry.getKey());
|
||||||
report.setMostActiveDay("-");
|
stats.setCount(entry.getValue());
|
||||||
report.setTopLocation("-");
|
stats.setPercentage(totalCount == 0 ? 0D : roundPercentage(entry.getValue(), totalCount));
|
||||||
report.setTopTag("-");
|
result.add(stats);
|
||||||
report.setMonthlyBreakdown(new ArrayList<>());
|
});
|
||||||
return report;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private List<TimelineAnalyticsVo.TagStats> buildTopTags(List<ItemData> itemData, int limit) {
|
||||||
public TimelineAnalyticsVo getActivityStats(String userId) {
|
Map<String, Long> counts = new HashMap<>();
|
||||||
log.info("获取活跃度统计: userId={}", userId);
|
long total = 0L;
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
|
||||||
vo.setConsecutiveDays(0);
|
for (ItemData item : itemData) {
|
||||||
vo.setMaxConsecutiveDays(0);
|
for (String tag : item.tags()) {
|
||||||
vo.setLastActiveDate(LocalDate.now().format(DateTimeFormatter.ISO_DATE));
|
counts.merge(tag, 1L, Long::sum);
|
||||||
return vo;
|
total++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
List<TimelineAnalyticsVo.TagStats> result = new ArrayList<>();
|
||||||
public int calculateConsecutiveDays(String userId) {
|
final long totalCount = total;
|
||||||
log.info("计算连续记录天数: userId={}", userId);
|
counts.entrySet().stream()
|
||||||
return 0;
|
.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
|
private Map<Integer, Long> buildWeeklyDistribution(List<ItemData> itemData) {
|
||||||
public TimelineAnalyticsVo getTimeDistribution(String userId) {
|
Map<Integer, Long> weeklyDistribution = new LinkedHashMap<>();
|
||||||
log.info("获取时间分布: userId={}", userId);
|
|
||||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
|
||||||
|
|
||||||
// 初始化周分布
|
|
||||||
Map<Integer, Long> weeklyDistribution = new HashMap<>();
|
|
||||||
for (int i = 1; i <= 7; i++) {
|
for (int i = 1; i <= 7; i++) {
|
||||||
weeklyDistribution.put(i, 0L);
|
weeklyDistribution.put(i, 0L);
|
||||||
}
|
}
|
||||||
vo.setWeeklyDistribution(weeklyDistribution);
|
|
||||||
|
|
||||||
// 初始化小时分布
|
for (ItemData item : itemData) {
|
||||||
Map<Integer, Long> hourlyDistribution = new HashMap<>();
|
if (item.eventTime() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int day = item.eventTime().getDayOfWeek().getValue();
|
||||||
|
weeklyDistribution.put(day, weeklyDistribution.get(day) + 1);
|
||||||
|
}
|
||||||
|
return weeklyDistribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, Long> buildHourlyDistribution(List<ItemData> itemData) {
|
||||||
|
Map<Integer, Long> hourlyDistribution = new LinkedHashMap<>();
|
||||||
for (int i = 0; i < 24; i++) {
|
for (int i = 0; i < 24; i++) {
|
||||||
hourlyDistribution.put(i, 0L);
|
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
|
private ActivitySummary buildActivitySummary(List<ItemData> itemData) {
|
||||||
public byte[] exportStats(String userId, String format) {
|
Set<LocalDate> distinctDates = new LinkedHashSet<>();
|
||||||
log.info("导出统计数据: userId={}, format={}", userId, format);
|
for (ItemData item : itemData) {
|
||||||
// 返回空数据
|
if (item.eventTime() != null) {
|
||||||
return new byte[0];
|
distinctDates.add(item.eventTime().toLocalDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocalDate> 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<LocalDate> 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> 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<TimelineAnalyticsVo.MonthlyStats> 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> itemData) {
|
||||||
|
Map<String, Long> 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<StoryItemVo> items) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ItemData(
|
||||||
|
StoryDetailVo story,
|
||||||
|
StoryItemVo item,
|
||||||
|
List<String> images,
|
||||||
|
long imageCount,
|
||||||
|
long mediaCount,
|
||||||
|
List<String> tags,
|
||||||
|
LocalDateTime eventTime) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ArchiveDataset(
|
||||||
|
int storyCount,
|
||||||
|
int shareableStoryCount,
|
||||||
|
int videoMomentCount,
|
||||||
|
List<TimelineArchiveBucketVo> locationBuckets,
|
||||||
|
List<TimelineArchiveBucketVo> tagBuckets,
|
||||||
|
List<TimelineArchiveMomentVo> moments) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ActivitySummary(String lastActiveDate, int currentStreak, int maxStreak) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,42 @@ package com.timeline.story.service.impl;
|
|||||||
|
|
||||||
import com.github.pagehelper.PageHelper;
|
import com.github.pagehelper.PageHelper;
|
||||||
import com.github.pagehelper.PageInfo;
|
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.IdUtils;
|
||||||
import com.timeline.common.utils.UserContextUtils;
|
import com.timeline.common.utils.UserContextUtils;
|
||||||
|
import com.timeline.story.dao.StoryMapper;
|
||||||
import com.timeline.story.dao.StoryItemMapper;
|
import com.timeline.story.dao.StoryItemMapper;
|
||||||
|
import com.timeline.story.dao.StoryShareMapper;
|
||||||
import com.timeline.story.entity.StoryItem;
|
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.service.StoryItemService;
|
||||||
|
import com.timeline.story.vo.StoryDetailVo;
|
||||||
import com.timeline.story.vo.StoryItemAddVo;
|
import com.timeline.story.vo.StoryItemAddVo;
|
||||||
import com.timeline.story.vo.StoryItemShareVo;
|
import com.timeline.story.vo.StoryItemShareVo;
|
||||||
import com.timeline.story.vo.StoryItemVo;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -29,15 +46,51 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private StoryItemMapper storyItemMapper;
|
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
|
@Override
|
||||||
public Map<String, Object> getItemsByMasterItem(StoryItemVo storyItemVo) {
|
public Map<String, Object> 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);
|
storyItemVo.getPageSize() != null ? storyItemVo.getPageSize() : 10);
|
||||||
|
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("storyInstanceId", storyItemVo.getStoryInstanceId());
|
params.put("storyInstanceId", storyItemVo.getStoryInstanceId());
|
||||||
if (storyItemVo.getAfterTime() != null) {
|
if (storyItemVo.getAfterTime() != null) {
|
||||||
params.put("afterTime", storyItemVo.getAfterTime());
|
params.put("afterTime", storyItemVo.getAfterTime());
|
||||||
}
|
}
|
||||||
|
if (storyItemVo.getBeforeTime() != null) {
|
||||||
|
params.put("beforeTime", storyItemVo.getBeforeTime());
|
||||||
|
}
|
||||||
|
|
||||||
List<StoryItemVo> list = storyItemMapper.selectStoryItemByStoryInstanceId(params);
|
List<StoryItemVo> list = storyItemMapper.selectStoryItemByStoryInstanceId(params);
|
||||||
PageInfo<StoryItemVo> pageInfo = new PageInfo<>(list);
|
PageInfo<StoryItemVo> pageInfo = new PageInfo<>(list);
|
||||||
@@ -61,7 +114,7 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
storyItemMapper.insert(storyItemAddVo);
|
storyItemMapper.insert(storyItemAddVo);
|
||||||
// Images handling to be implemented
|
// Images handling to be implemented separately.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -70,7 +123,7 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
storyItemAddVo.setUpdateId(UserContextUtils.getCurrentUserId());
|
storyItemAddVo.setUpdateId(UserContextUtils.getCurrentUserId());
|
||||||
storyItemAddVo.setUpdateTime(LocalDateTime.now());
|
storyItemAddVo.setUpdateTime(LocalDateTime.now());
|
||||||
storyItemMapper.update(storyItemAddVo);
|
storyItemMapper.update(storyItemAddVo);
|
||||||
// Images handling to be implemented
|
// Images handling to be implemented separately.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -86,7 +139,8 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getStoryItemImages(String itemId) {
|
public List<String> getStoryItemImages(String itemId) {
|
||||||
return storyItemMapper.selectImagesByItemId(itemId);
|
List<String> images = storyItemMapper.selectImagesByItemId(itemId);
|
||||||
|
return images == null ? Collections.emptyList() : images;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -94,12 +148,127 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
return storyItemMapper.countByStoryId(storyInstanceId);
|
return storyItemMapper.countByStoryId(storyInstanceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
@Override
|
||||||
* @Override
|
public StoryItemShareVo getItemByShareId(String shareId) {
|
||||||
* public StoryItemShareVo getItemByShareId(String shareId) {
|
StoryItemShareVo shareVo = storyItemMapper.selectByShareIdWithAuthor(shareId);
|
||||||
* return 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<String> 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
|
@Override
|
||||||
public Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize) {
|
public Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize) {
|
||||||
@@ -113,20 +282,6 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量更新时间线节点排序
|
|
||||||
*
|
|
||||||
* 实现思路:
|
|
||||||
* 1. 使用事务确保批量更新的原子性
|
|
||||||
* 2. 遍历排序数据,逐个更新节点的 sortOrder 字段
|
|
||||||
* 3. 同时更新 updateTime 时间戳
|
|
||||||
*
|
|
||||||
* 注意事项:
|
|
||||||
* - 该方法需要在事务中执行,确保数据一致性
|
|
||||||
* - 如果任一更新失败,整个事务将回滚
|
|
||||||
*
|
|
||||||
* @param items 排序数据列表
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void updateItemsOrder(List<Map<String, Object>> items) {
|
public void updateItemsOrder(List<Map<String, Object>> items) {
|
||||||
@@ -137,7 +292,6 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
String instanceId = (String) item.get("instanceId");
|
String instanceId = (String) item.get("instanceId");
|
||||||
Integer sortOrder = ((Number) item.get("sortOrder")).intValue();
|
Integer sortOrder = ((Number) item.get("sortOrder")).intValue();
|
||||||
|
|
||||||
// 构建更新参数
|
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("instanceId", instanceId);
|
params.put("instanceId", instanceId);
|
||||||
params.put("sortOrder", sortOrder);
|
params.put("sortOrder", sortOrder);
|
||||||
@@ -145,54 +299,27 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
params.put("updateTime", now);
|
params.put("updateTime", now);
|
||||||
|
|
||||||
storyItemMapper.updateOrder(params);
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void batchDeleteItems(List<String> instanceIds) {
|
public void batchDeleteItems(List<String> instanceIds) {
|
||||||
for (String instanceId : instanceIds) {
|
for (String instanceId : instanceIds) {
|
||||||
storyItemMapper.deleteByItemId(instanceId);
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void batchUpdateItemTime(List<String> instanceIds, String storyItemTime) {
|
public void batchUpdateItemTime(List<String> instanceIds, String storyItemTime) {
|
||||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
// 解析时间字符串为 LocalDateTime
|
|
||||||
LocalDateTime parsedTime = LocalDateTime.parse(storyItemTime.replace(" ", "T"));
|
LocalDateTime parsedTime = LocalDateTime.parse(storyItemTime.replace(" ", "T"));
|
||||||
|
|
||||||
for (String instanceId : instanceIds) {
|
for (String instanceId : instanceIds) {
|
||||||
@@ -203,9 +330,139 @@ public class StoryItemServiceImpl implements StoryItemService {
|
|||||||
params.put("updateTime", now);
|
params.put("updateTime", now);
|
||||||
|
|
||||||
storyItemMapper.updateItemTime(params);
|
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<String> 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<String> 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<String> featuredMomentIds, String heroMomentId) {
|
||||||
|
Set<String> 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<StoryShareMomentVo> loadFeaturedMoments(String storyId, List<String> featuredMomentIds) {
|
||||||
|
if (!StringUtils.hasText(storyId) || featuredMomentIds == null || featuredMomentIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StoryShareMomentVo> 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<String> 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,28 @@ package com.timeline.story.service.impl;
|
|||||||
import com.timeline.common.constants.CommonConstants;
|
import com.timeline.common.constants.CommonConstants;
|
||||||
import com.timeline.common.exception.CustomException;
|
import com.timeline.common.exception.CustomException;
|
||||||
import com.timeline.common.response.ResponseEnum;
|
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.Story;
|
||||||
import com.timeline.story.entity.StoryActivity;
|
import com.timeline.story.entity.StoryActivity;
|
||||||
import com.timeline.story.entity.StoryItem;
|
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.StoryItemService;
|
||||||
import com.timeline.story.service.StoryPermissionService;
|
import com.timeline.story.service.StoryPermissionService;
|
||||||
import com.timeline.story.service.StoryService;
|
import com.timeline.story.service.StoryService;
|
||||||
import com.timeline.story.mq.ActivityLogProducer;
|
|
||||||
import com.timeline.story.vo.StoryDetailVo;
|
import com.timeline.story.vo.StoryDetailVo;
|
||||||
import com.timeline.story.vo.StoryDetailWithItemsVo;
|
import com.timeline.story.vo.StoryDetailWithItemsVo;
|
||||||
import com.timeline.story.vo.StoryItemVo;
|
import com.timeline.story.vo.StoryItemVo;
|
||||||
import com.timeline.story.vo.StoryPermissionVo;
|
import com.timeline.story.vo.StoryPermissionVo;
|
||||||
import com.timeline.story.vo.StoryVo;
|
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.extern.slf4j.Slf4j;
|
||||||
import lombok.val;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
private String getCurrentUserId() {
|
private String getCurrentUserId() {
|
||||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||||
if (currentUserId == null || currentUserId.isEmpty()) {
|
if (currentUserId == null || currentUserId.isEmpty()) {
|
||||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份");
|
throw new CustomException(ResponseEnum.UNAUTHORIZED, "User context is missing");
|
||||||
}
|
}
|
||||||
return currentUserId;
|
return currentUserId;
|
||||||
}
|
}
|
||||||
@@ -57,10 +56,8 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void createStory(StoryVo storyVo) {
|
public void createStory(StoryVo storyVo) {
|
||||||
try {
|
try {
|
||||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
String currentUserId = getCurrentUserId();
|
||||||
if (currentUserId == null || currentUserId.isEmpty()) {
|
|
||||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份");
|
|
||||||
}
|
|
||||||
Story story = new Story();
|
Story story = new Story();
|
||||||
story.setOwnerId(currentUserId);
|
story.setOwnerId(currentUserId);
|
||||||
story.setUpdateId(currentUserId);
|
story.setUpdateId(currentUserId);
|
||||||
@@ -74,11 +71,10 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
story.setLogo(storyVo.getLogo());
|
story.setLogo(storyVo.getLogo());
|
||||||
story.setIsDelete(0);
|
story.setIsDelete(0);
|
||||||
storyMapper.insert(story);
|
storyMapper.insert(story);
|
||||||
// 自动添加创建者权限
|
|
||||||
StoryPermissionVo permissionVo = new StoryPermissionVo();
|
StoryPermissionVo permissionVo = new StoryPermissionVo();
|
||||||
permissionVo.setStoryInstanceId(story.getInstanceId());
|
permissionVo.setStoryInstanceId(story.getInstanceId());
|
||||||
permissionVo.setUserId(currentUserId);
|
permissionVo.setUserId(currentUserId);
|
||||||
// 创建者权限
|
|
||||||
permissionVo.setPermissionType(CommonConstants.STORY_PERMISSION_TYPE_OWNER);
|
permissionVo.setPermissionType(CommonConstants.STORY_PERMISSION_TYPE_OWNER);
|
||||||
storyPermissionService.createPermission(permissionVo);
|
storyPermissionService.createPermission(permissionVo);
|
||||||
|
|
||||||
@@ -88,9 +84,9 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
activity.setStoryInstanceId(story.getInstanceId());
|
activity.setStoryInstanceId(story.getInstanceId());
|
||||||
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_CREATE);
|
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_CREATE);
|
||||||
activityLogProducer.sendLog("story.activity.create", activity);
|
activityLogProducer.sendLog("story.activity.create", activity);
|
||||||
} catch (Exception e) {
|
} catch (Exception exception) {
|
||||||
log.error("创建故事失败", e);
|
log.error("Create story failed", exception);
|
||||||
throw new CustomException(500, "创建故事失败: " + e.toString());
|
throw new CustomException(500, "Create story failed: " + exception.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,17 +99,19 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
if (story == null) {
|
if (story == null) {
|
||||||
throw new CustomException(ResponseEnum.NOT_FOUND);
|
throw new CustomException(ResponseEnum.NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (!storyPermissionService.checkUserPermission(storyId, currentUserId,
|
if (!storyPermissionService.checkUserPermission(
|
||||||
|
storyId,
|
||||||
|
currentUserId,
|
||||||
CommonConstants.STORY_PERMISSION_TYPE_WRITE)) {
|
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.setTitle(storyVo.getTitle());
|
||||||
story.setDescription(storyVo.getDescription());
|
story.setDescription(storyVo.getDescription());
|
||||||
story.setStatus(storyVo.getStatus());
|
story.setStatus(storyVo.getStatus());
|
||||||
story.setStoryTime(storyVo.getStoryTime());
|
story.setStoryTime(storyVo.getStoryTime());
|
||||||
story.setUpdateTime(LocalDateTime.now());
|
story.setUpdateTime(LocalDateTime.now());
|
||||||
story.setLogo(storyVo.getLogo());
|
story.setLogo(storyVo.getLogo());
|
||||||
|
|
||||||
storyMapper.update(story);
|
storyMapper.update(story);
|
||||||
|
|
||||||
StoryActivity activity = new StoryActivity();
|
StoryActivity activity = new StoryActivity();
|
||||||
@@ -122,7 +120,6 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
activity.setStoryInstanceId(storyId);
|
activity.setStoryInstanceId(storyId);
|
||||||
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_UPDATE);
|
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_UPDATE);
|
||||||
activityLogProducer.sendLog("story.activity.update", activity);
|
activityLogProducer.sendLog("story.activity.update", activity);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -133,15 +130,16 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
if (story == null) {
|
if (story == null) {
|
||||||
throw new CustomException(ResponseEnum.NOT_FOUND);
|
throw new CustomException(ResponseEnum.NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (!storyPermissionService.checkUserPermission(storyId, currentUserId,
|
if (!storyPermissionService.checkUserPermission(
|
||||||
|
storyId,
|
||||||
|
currentUserId,
|
||||||
CommonConstants.STORY_PERMISSION_TYPE_ADMIN)) {
|
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);
|
storyMapper.deleteByInstanceId(storyId);
|
||||||
// delete permission
|
|
||||||
storyPermissionService.deletePermission(storyId);
|
storyPermissionService.deletePermission(storyId);
|
||||||
// delete activity
|
|
||||||
StoryActivity activity = new StoryActivity();
|
StoryActivity activity = new StoryActivity();
|
||||||
activity.setActorId(currentUserId);
|
activity.setActorId(currentUserId);
|
||||||
activity.setAction(CommonConstants.ACTION_TYPE_STORY_DELETE);
|
activity.setAction(CommonConstants.ACTION_TYPE_STORY_DELETE);
|
||||||
@@ -152,21 +150,32 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StoryDetailWithItemsVo getStoryByInstanceId(String storyId) {
|
public StoryDetailWithItemsVo getStoryByInstanceId(String storyId) {
|
||||||
val userId = getCurrentUserId();
|
String userId = getCurrentUserId();
|
||||||
StoryDetailVo story = storyMapper.selectByInstanceId(storyId, userId);
|
StoryDetailVo story = storyMapper.selectByInstanceId(storyId, userId);
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
throw new CustomException(ResponseEnum.NOT_FOUND);
|
throw new CustomException(ResponseEnum.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
StoryItemVo storyItemVo = new StoryItemVo();
|
StoryItemVo storyItemVo = new StoryItemVo();
|
||||||
storyItemVo.setStoryInstanceId(storyId);
|
storyItemVo.setStoryInstanceId(storyId);
|
||||||
storyItemVo.setCurrent(1);
|
storyItemVo.setCurrent(1);
|
||||||
storyItemVo.setPageSize(10);
|
storyItemVo.setPageSize(10);
|
||||||
Map itemsMap = storyItemService.getItemsByMasterItem(storyItemVo);
|
Map<String, Object> itemsMap = storyItemService.getItemsByMasterItem(storyItemVo);
|
||||||
List<StoryItem> items = (List<StoryItem>) itemsMap.get("list");
|
|
||||||
|
List<StoryItem> 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();
|
StoryDetailWithItemsVo result = new StoryDetailWithItemsVo();
|
||||||
result.setItems(items);
|
result.setItems(items);
|
||||||
result.setInstanceId(story.getInstanceId());
|
result.setInstanceId(story.getInstanceId());
|
||||||
|
result.setShareId(story.getShareId());
|
||||||
result.setOwnerId(story.getOwnerId());
|
result.setOwnerId(story.getOwnerId());
|
||||||
result.setUpdateId(story.getUpdateId());
|
result.setUpdateId(story.getUpdateId());
|
||||||
result.setTitle(story.getTitle());
|
result.setTitle(story.getTitle());
|
||||||
@@ -181,7 +190,6 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
result.setUpdateName(story.getUpdateName());
|
result.setUpdateName(story.getUpdateName());
|
||||||
result.setPermissionType(story.getPermissionType());
|
result.setPermissionType(story.getPermissionType());
|
||||||
result.setItemCount(story.getItemCount());
|
result.setItemCount(story.getItemCount());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +197,9 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
public List<StoryDetailVo> getStoriesByOwnerId(String ownerId) {
|
public List<StoryDetailVo> getStoriesByOwnerId(String ownerId) {
|
||||||
try {
|
try {
|
||||||
return storyMapper.selectByOwnerId(ownerId);
|
return storyMapper.selectByOwnerId(ownerId);
|
||||||
} catch (Exception e) {
|
} catch (Exception exception) {
|
||||||
log.error("查询用户故事列表失败", e);
|
log.error("Query user stories failed", exception);
|
||||||
throw new CustomException(500, "查询用户故事列表失败");
|
throw new CustomException(500, "Query user stories failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +208,9 @@ public class StoryServiceImpl implements StoryService {
|
|||||||
try {
|
try {
|
||||||
String currentUserId = getCurrentUserId();
|
String currentUserId = getCurrentUserId();
|
||||||
return storyMapper.selectByOwnerId(currentUserId);
|
return storyMapper.selectByOwnerId(currentUserId);
|
||||||
} catch (Exception e) {
|
} catch (Exception exception) {
|
||||||
log.error("查询用户故事列表失败", e);
|
log.error("Query stories failed", exception);
|
||||||
throw new CustomException(500, "查询用户故事列表失败");
|
throw new CustomException(500, "Query stories failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,22 @@ import com.timeline.story.entity.StoryItem;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class StoryItemShareVo extends StoryItem {
|
public class StoryItemShareVo extends StoryItem {
|
||||||
private String authorName;
|
private String authorName;
|
||||||
private String authorAvatar;
|
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<String> images;
|
||||||
|
private List<String> relatedImageInstanceIds;
|
||||||
|
private List<String> featuredMomentIds;
|
||||||
|
private List<StoryShareMomentVo> featuredMoments;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String> featuredMomentIds;
|
||||||
|
private Boolean published;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -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<String> images;
|
||||||
|
private List<String> relatedImageInstanceIds;
|
||||||
|
}
|
||||||
@@ -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<String> featuredMomentIds;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<TimelineArchiveMomentVo> moments;
|
||||||
|
}
|
||||||
@@ -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<String> tags;
|
||||||
|
private String coverInstanceId;
|
||||||
|
private String coverSrc;
|
||||||
|
private Integer mediaCount;
|
||||||
|
private Boolean hasVideo;
|
||||||
|
private Long sortValue;
|
||||||
|
}
|
||||||
@@ -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<TimelineArchiveBucketVo> locations;
|
||||||
|
private List<TimelineArchiveBucketVo> tags;
|
||||||
|
}
|
||||||
@@ -30,16 +30,26 @@
|
|||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
<select id="selectById" resultType="com.timeline.story.entity.StoryItem">
|
<select id="selectById" resultType="com.timeline.story.entity.StoryItem">
|
||||||
SELECT * FROM story_item WHERE instance_id = #{instanceId}
|
SELECT *
|
||||||
|
FROM story_item
|
||||||
|
WHERE instance_id = #{instanceId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectByMasterItem" resultType="com.timeline.story.entity.StoryItem">
|
<select id="selectByMasterItem" resultType="com.timeline.story.entity.StoryItem">
|
||||||
SELECT * FROM story_item WHERE master_item_id = #{masterItemId} AND is_delete = 0
|
SELECT *
|
||||||
|
FROM story_item
|
||||||
|
WHERE master_item_id = #{masterItemId}
|
||||||
|
AND is_delete = 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectImagesByItemId" resultType="java.lang.String">
|
<select id="selectImagesByItemId" resultType="java.lang.String">
|
||||||
SELECT sub_rela_id FROM common_relation WHERE rela_id = #{instanceId} AND rela_type = 5 AND is_delete = 0
|
SELECT sub_rela_id
|
||||||
|
FROM common_relation
|
||||||
|
WHERE rela_id = #{instanceId}
|
||||||
|
AND rela_type = 5
|
||||||
|
AND is_delete = 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectStoryItemByStoryInstanceId" resultType="com.timeline.story.vo.StoryItemVo">
|
<select id="selectStoryItemByStoryInstanceId" resultType="com.timeline.story.vo.StoryItemVo">
|
||||||
SELECT
|
SELECT
|
||||||
si.id,
|
si.id,
|
||||||
@@ -50,52 +60,71 @@
|
|||||||
si.story_instance_id,
|
si.story_instance_id,
|
||||||
si.master_item_id,
|
si.master_item_id,
|
||||||
si.is_delete,
|
si.is_delete,
|
||||||
si.story_item_time as story_item_time,
|
si.story_item_time AS story_item_time,
|
||||||
si.video_url,
|
si.video_url,
|
||||||
si.duration,
|
si.duration,
|
||||||
si.thumbnail_url,
|
si.thumbnail_url,
|
||||||
|
si.share_id,
|
||||||
si.create_id AS create_id,
|
si.create_id AS create_id,
|
||||||
si.create_time AS create_time,
|
si.create_time AS create_time,
|
||||||
u1.username AS create_name,
|
u1.username AS create_name,
|
||||||
si.update_id AS update_id,
|
si.update_id AS update_id,
|
||||||
si.update_time AS update_time,
|
si.update_time AS update_time,
|
||||||
u2.username AS update_name
|
u2.username AS update_name
|
||||||
FROM
|
FROM story_item si
|
||||||
story_item si
|
|
||||||
LEFT JOIN user u1 ON si.create_id = u1.user_id
|
LEFT JOIN user u1 ON si.create_id = u1.user_id
|
||||||
LEFT JOIN `user` u2 ON si.update_id = u2.user_id
|
LEFT JOIN user u2 ON si.update_id = u2.user_id
|
||||||
WHERE
|
WHERE si.story_instance_id = #{storyInstanceId}
|
||||||
story_instance_id = #{storyInstanceId}
|
AND si.is_delete = 0
|
||||||
AND is_delete = 0
|
|
||||||
<if test="afterTime != null">
|
<if test="afterTime != null">
|
||||||
AND story_item_time > #{afterTime}
|
AND si.story_item_time > #{afterTime}
|
||||||
</if>
|
</if>
|
||||||
ORDER BY
|
<if test="beforeTime != null">
|
||||||
story_item_time DESC
|
AND si.story_item_time < #{beforeTime}
|
||||||
|
</if>
|
||||||
|
ORDER BY si.story_item_time DESC, si.update_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="countByStoryId" resultType="int">
|
<select id="countByStoryId" resultType="int">
|
||||||
SELECT COUNT(*) FROM story_item WHERE story_instance_id = #{storyInstanceId} AND is_delete = 0
|
SELECT COUNT(*)
|
||||||
|
FROM story_item
|
||||||
|
WHERE story_instance_id = #{storyInstanceId}
|
||||||
|
AND is_delete = 0
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!--
|
|
||||||
<select id="selectByShareId" resultType="com.timeline.story.entity.StoryItem">
|
<select id="selectByShareId" resultType="com.timeline.story.entity.StoryItem">
|
||||||
SELECT * FROM story_item WHERE share_id = #{shareId} AND is_delete = 0
|
SELECT *
|
||||||
|
FROM story_item
|
||||||
|
WHERE share_id = #{shareId}
|
||||||
|
AND is_delete = 0
|
||||||
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectByShareIdWithAuthor" resultType="com.timeline.story.vo.StoryItemShareVo">
|
<select id="selectByShareIdWithAuthor" resultType="com.timeline.story.vo.StoryItemShareVo">
|
||||||
SELECT
|
SELECT
|
||||||
si.*,
|
si.*,
|
||||||
u.username AS authorName,
|
author.username AS author_name,
|
||||||
u.avatar AS authorAvatar
|
author.avatar AS author_avatar,
|
||||||
FROM
|
owner.username AS owner_name
|
||||||
story_item si
|
FROM story_item si
|
||||||
LEFT JOIN
|
LEFT JOIN story s ON si.story_instance_id = s.instance_id AND s.is_delete = 0
|
||||||
user u ON si.create_id = u.user_id
|
LEFT JOIN user author ON si.create_id = author.user_id AND author.is_deleted = 0
|
||||||
WHERE
|
LEFT JOIN user owner ON s.owner_id = owner.user_id AND owner.is_deleted = 0
|
||||||
si.share_id = #{shareId} AND si.is_delete = 0
|
WHERE si.share_id = #{shareId}
|
||||||
|
AND si.is_delete = 0
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPublishedByStoryId" resultType="com.timeline.story.entity.StoryItem">
|
||||||
|
SELECT *
|
||||||
|
FROM story_item
|
||||||
|
WHERE story_instance_id = #{storyInstanceId}
|
||||||
|
AND share_id IS NOT NULL
|
||||||
|
AND share_id != ''
|
||||||
|
AND is_delete = 0
|
||||||
|
ORDER BY update_time DESC, create_time DESC
|
||||||
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
-->
|
|
||||||
|
|
||||||
<select id="searchItems" resultType="com.timeline.story.vo.StoryItemVo">
|
<select id="searchItems" resultType="com.timeline.story.vo.StoryItemVo">
|
||||||
SELECT
|
SELECT
|
||||||
@@ -103,29 +132,38 @@
|
|||||||
si.story_instance_id,
|
si.story_instance_id,
|
||||||
si.title,
|
si.title,
|
||||||
si.description,
|
si.description,
|
||||||
si.story_item_time
|
si.story_item_time,
|
||||||
FROM
|
si.location,
|
||||||
story_item si
|
si.video_url,
|
||||||
WHERE
|
si.share_id
|
||||||
si.is_delete = 0
|
FROM story_item si
|
||||||
AND (si.title LIKE CONCAT('%', #{keyword}, '%') OR si.description LIKE CONCAT('%', #{keyword}, '%'))
|
WHERE si.is_delete = 0
|
||||||
ORDER BY
|
AND (
|
||||||
si.story_item_time DESC
|
si.title LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR si.description LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
)
|
||||||
|
ORDER BY si.story_item_time DESC, si.update_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!--
|
<update id="clearShareIdByStoryId">
|
||||||
更新节点排序值
|
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>
|
||||||
|
|
||||||
功能描述:
|
<update id="updateShareId">
|
||||||
根据拖拽排序结果更新节点的 sort_order 字段。
|
UPDATE story_item
|
||||||
同时更新 update_id 和 update_time 以记录修改信息。
|
SET share_id = #{shareId},
|
||||||
|
update_id = #{updateId},
|
||||||
|
update_time = #{updateTime}
|
||||||
|
WHERE instance_id = #{instanceId}
|
||||||
|
AND is_delete = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- instanceId: 节点唯一标识
|
|
||||||
- sortOrder: 新的排序值(数值越小越靠前)
|
|
||||||
- updateId: 操作用户ID
|
|
||||||
- updateTime: 更新时间
|
|
||||||
-->
|
|
||||||
<update id="updateOrder">
|
<update id="updateOrder">
|
||||||
UPDATE story_item
|
UPDATE story_item
|
||||||
SET sort_order = #{sortOrder},
|
SET sort_order = #{sortOrder},
|
||||||
@@ -134,19 +172,6 @@
|
|||||||
WHERE instance_id = #{instanceId}
|
WHERE instance_id = #{instanceId}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<!--
|
|
||||||
更新节点时间
|
|
||||||
|
|
||||||
功能描述:
|
|
||||||
批量修改节点时间时使用,更新 story_item_time 字段。
|
|
||||||
同时更新 update_id 和 update_time 以记录修改信息。
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- instanceId: 节点唯一标识
|
|
||||||
- storyItemTime: 新的时间值
|
|
||||||
- updateId: 操作用户ID
|
|
||||||
- updateTime: 更新时间
|
|
||||||
-->
|
|
||||||
<update id="updateItemTime">
|
<update id="updateItemTime">
|
||||||
UPDATE story_item
|
UPDATE story_item
|
||||||
SET story_item_time = #{storyItemTime},
|
SET story_item_time = #{storyItemTime},
|
||||||
|
|||||||
@@ -22,43 +22,74 @@
|
|||||||
</update>
|
</update>
|
||||||
|
|
||||||
<delete id="deleteByInstanceId">
|
<delete id="deleteByInstanceId">
|
||||||
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}
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
<select id="selectByInstanceId" resultType="com.timeline.story.vo.StoryDetailVo">
|
<select id="selectByInstanceId" resultType="com.timeline.story.vo.StoryDetailVo">
|
||||||
SELECT
|
SELECT
|
||||||
s.*,
|
s.*,
|
||||||
u1.username as owner_name,
|
(
|
||||||
u2.username as update_name,
|
SELECT si.share_id
|
||||||
sp.permission_type as permission_type,
|
FROM story_item si
|
||||||
(SELECT COUNT(*) FROM story_item si WHERE si.story_instance_id = s.instance_id AND si.is_delete = 0) as item_count
|
WHERE si.story_instance_id = s.instance_id
|
||||||
|
AND si.share_id IS NOT NULL
|
||||||
|
AND si.is_delete = 0
|
||||||
|
ORDER BY si.update_time DESC, si.create_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS share_id,
|
||||||
|
u1.username AS owner_name,
|
||||||
|
u2.username AS update_name,
|
||||||
|
sp.permission_type AS permission_type,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM story_item si
|
||||||
|
WHERE si.story_instance_id = s.instance_id
|
||||||
|
AND si.is_delete = 0
|
||||||
|
) AS item_count
|
||||||
FROM story s
|
FROM story s
|
||||||
|
|
||||||
LEFT JOIN user u1 ON s.owner_id = u1.user_id AND u1.is_deleted = 0
|
LEFT JOIN user u1 ON s.owner_id = u1.user_id AND u1.is_deleted = 0
|
||||||
LEFT JOIN user u2 ON s.update_id = u2.user_id AND u2.is_deleted = 0
|
LEFT JOIN user u2 ON s.update_id = u2.user_id AND u2.is_deleted = 0
|
||||||
LEFT JOIN story_permission sp on sp.story_instance_id = s.instance_id and sp.user_id = #{userId}
|
LEFT JOIN story_permission sp ON sp.story_instance_id = s.instance_id AND sp.user_id = #{userId}
|
||||||
|
|
||||||
WHERE s.instance_id = #{instanceId}
|
WHERE s.instance_id = #{instanceId}
|
||||||
|
AND s.is_delete = 0
|
||||||
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectByOwnerId" resultType="com.timeline.story.vo.StoryDetailVo">
|
<select id="selectByOwnerId" resultType="com.timeline.story.vo.StoryDetailVo">
|
||||||
SELECT
|
SELECT
|
||||||
s.*,
|
s.*,
|
||||||
|
(
|
||||||
|
SELECT si.share_id
|
||||||
|
FROM story_item si
|
||||||
|
WHERE si.story_instance_id = s.instance_id
|
||||||
|
AND si.share_id IS NOT NULL
|
||||||
|
AND si.is_delete = 0
|
||||||
|
ORDER BY si.update_time DESC, si.create_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS share_id,
|
||||||
u1.username AS owner_name,
|
u1.username AS owner_name,
|
||||||
u2.username AS update_name,
|
u2.username AS update_name,
|
||||||
sp.permission_type AS permission_type,
|
sp.permission_type AS permission_type,
|
||||||
( SELECT COUNT(*) FROM story_item si WHERE si.story_instance_id = s.instance_id AND si.is_delete = 0 ) AS item_count
|
(
|
||||||
FROM
|
SELECT COUNT(*)
|
||||||
story s
|
FROM story_item si
|
||||||
LEFT JOIN user u1 ON s.owner_id = u1.user_id
|
WHERE si.story_instance_id = s.instance_id
|
||||||
AND u1.is_deleted = 0
|
AND si.is_delete = 0
|
||||||
LEFT JOIN user u2 ON s.update_id = u2.user_id
|
) AS item_count
|
||||||
AND u2.is_deleted = 0
|
FROM story s
|
||||||
LEFT JOIN story_permission sp ON s.instance_id = sp.story_instance_id AND sp.user_id = #{owerId}
|
LEFT JOIN user u1 ON s.owner_id = u1.user_id AND u1.is_deleted = 0
|
||||||
WHERE
|
LEFT JOIN user u2 ON s.update_id = u2.user_id AND u2.is_deleted = 0
|
||||||
s.instance_id IN ( SELECT story_instance_id FROM story_permission WHERE user_id = #{owerId} )
|
LEFT JOIN story_permission sp ON s.instance_id = sp.story_instance_id AND sp.user_id = #{ownerId}
|
||||||
|
WHERE s.instance_id IN (
|
||||||
|
SELECT story_instance_id
|
||||||
|
FROM story_permission
|
||||||
|
WHERE user_id = #{ownerId}
|
||||||
|
)
|
||||||
AND s.is_delete = 0
|
AND s.is_delete = 0
|
||||||
|
ORDER BY s.update_time DESC, s.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<update id="touchUpdate">
|
<update id="touchUpdate">
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<mapper namespace="com.timeline.story.dao.StoryShareMapper">
|
||||||
|
|
||||||
|
<select id="selectActiveByStoryId" resultType="com.timeline.story.entity.StoryShare">
|
||||||
|
SELECT *
|
||||||
|
FROM story_share
|
||||||
|
WHERE story_instance_id = #{storyInstanceId}
|
||||||
|
AND is_delete = 0
|
||||||
|
ORDER BY update_time DESC, create_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectLatestByStoryId" resultType="com.timeline.story.entity.StoryShare">
|
||||||
|
SELECT *
|
||||||
|
FROM story_share
|
||||||
|
WHERE story_instance_id = #{storyInstanceId}
|
||||||
|
ORDER BY update_time DESC, create_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByShareId" resultType="com.timeline.story.entity.StoryShare">
|
||||||
|
SELECT *
|
||||||
|
FROM story_share
|
||||||
|
WHERE share_id = #{shareId}
|
||||||
|
AND is_delete = 0
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert">
|
||||||
|
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}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<update id="softDeleteByStoryId">
|
||||||
|
UPDATE story_share
|
||||||
|
SET is_delete = 1,
|
||||||
|
update_id = #{updateId},
|
||||||
|
update_time = #{updateTime}
|
||||||
|
WHERE story_instance_id = #{storyInstanceId}
|
||||||
|
AND is_delete = 0
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
Reference in New Issue
Block a user