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:
@@ -4,6 +4,8 @@ import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.story.service.AnalyticsService;
|
||||
import com.timeline.story.vo.TimelineAnalyticsVo;
|
||||
import com.timeline.story.vo.TimelineArchiveExploreVo;
|
||||
import com.timeline.story.vo.TimelineArchiveSummaryVo;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -124,6 +126,33 @@ public class AnalyticsController {
|
||||
* @param year 年份(可选,默认去年)
|
||||
* @return 年度报告
|
||||
*/
|
||||
@GetMapping("/archive/summary")
|
||||
public ResponseEntity<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")
|
||||
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo.YearlyReport> getYearlyReport(
|
||||
@RequestParam(required = false) Integer year) {
|
||||
|
||||
@@ -5,12 +5,12 @@ import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.service.StoryItemService;
|
||||
import com.timeline.story.service.StoryService;
|
||||
import com.timeline.story.vo.StoryItemAddVo;
|
||||
import com.timeline.story.vo.StoryItemShareVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
import com.timeline.story.vo.StoryVo;
|
||||
|
||||
import com.timeline.story.vo.StoryShareConfigVo;
|
||||
import com.timeline.story.vo.StorySharePublishRequest;
|
||||
import com.timeline.story.vo.StorySharePublishVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||
@@ -29,129 +29,126 @@ public class StoryItemController {
|
||||
private StoryItemService storyItemService;
|
||||
|
||||
@PostMapping()
|
||||
public ResponseEntity<String> createItem(@RequestParam("storyItem") String storyItemVoString,
|
||||
public ResponseEntity<String> createItem(
|
||||
@RequestParam("storyItem") String storyItemVoString,
|
||||
@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);
|
||||
return ResponseEntity.success("StoryItem 创建成功");
|
||||
return ResponseEntity.success("StoryItem created");
|
||||
}
|
||||
|
||||
@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) {
|
||||
log.info("更新 StoryItem: {}", storyItemVoString);
|
||||
log.info("Update StoryItem: {}", storyItemVoString);
|
||||
storyItemService.updateItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
|
||||
return ResponseEntity.success("StoryItem 更新成功");
|
||||
return ResponseEntity.success("StoryItem updated");
|
||||
}
|
||||
|
||||
@DeleteMapping("/{itemId}")
|
||||
public ResponseEntity<String> deleteItem(@PathVariable String itemId) {
|
||||
log.info("删除 StoryItem: {}", itemId);
|
||||
log.info("Delete StoryItem: {}", itemId);
|
||||
storyItemService.deleteItem(itemId);
|
||||
return ResponseEntity.success("StoryItem 删除成功");
|
||||
return ResponseEntity.success("StoryItem deleted");
|
||||
}
|
||||
|
||||
@GetMapping("/{itemId}")
|
||||
public ResponseEntity<StoryItem> getItemById(@PathVariable String itemId) {
|
||||
log.info("获取 StoryItem 详情: {}", itemId);
|
||||
log.info("Get StoryItem detail: {}", itemId);
|
||||
StoryItem item = storyItemService.getItemById(itemId);
|
||||
return ResponseEntity.success(item);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<Map> getItemsByMasterItem(@SpringQueryMap StoryItemVo storyItemVo) {
|
||||
log.info("查询 StoryItem 列表,storyInstanceId: {}, current: {}, pageSize:{}", storyItemVo.getStoryInstanceId(),
|
||||
storyItemVo.getCurrent(), storyItemVo.getPageSize());
|
||||
log.info(
|
||||
"Query StoryItem list: storyInstanceId={}, current={}, pageSize={}",
|
||||
storyItemVo.getStoryInstanceId(),
|
||||
storyItemVo.getCurrent(),
|
||||
storyItemVo.getPageSize());
|
||||
Map items = storyItemService.getItemsByMasterItem(storyItemVo);
|
||||
return ResponseEntity.success(items);
|
||||
}
|
||||
|
||||
@GetMapping("/images/{itemId}")
|
||||
public ResponseEntity<List<String>> getStoryItemImages(@PathVariable String itemId) {
|
||||
log.info("获取 StoryItem 图片列表,itemId: {}", itemId);
|
||||
log.info("Get StoryItem images: itemId={}", itemId);
|
||||
List<String> images = storyItemService.getStoryItemImages(itemId);
|
||||
return ResponseEntity.success(images);
|
||||
}
|
||||
|
||||
@GetMapping("/count/{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);
|
||||
return ResponseEntity.success(count);
|
||||
}
|
||||
|
||||
/*
|
||||
* @GetMapping("/public/story/item/{shareId}")
|
||||
* public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable
|
||||
* String shareId) {
|
||||
* log.info("获取分享的 StoryItem,shareId: {}", shareId);
|
||||
* StoryItemShareVo item = storyItemService.getItemByShareId(shareId);
|
||||
* return ResponseEntity.success(item);
|
||||
* }
|
||||
*/
|
||||
@GetMapping("/public/story/item/{shareId}")
|
||||
public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable String shareId) {
|
||||
log.info("Get public StoryItem by shareId: {}", shareId);
|
||||
StoryItemShareVo item = storyItemService.getItemByShareId(shareId);
|
||||
return ResponseEntity.success(item);
|
||||
}
|
||||
|
||||
@GetMapping("/share/{storyId}")
|
||||
public ResponseEntity<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")
|
||||
public ResponseEntity<Map> searchItems(@RequestParam String keyword,
|
||||
public ResponseEntity<Map> searchItems(
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
log.info("搜索 StoryItem,keyword: {}, pageNum: {}, pageSize: {}", keyword, pageNum, pageSize);
|
||||
log.info("Search StoryItem: keyword={}, pageNum={}, pageSize={}", keyword, pageNum, pageSize);
|
||||
Map result = storyItemService.searchItems(keyword, pageNum, pageSize);
|
||||
return ResponseEntity.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新时间线节点排序
|
||||
*
|
||||
* 功能描述:
|
||||
* 接收前端拖拽排序后的节点顺序,批量更新各节点的 sortOrder 字段。
|
||||
* 用于保存用户手动调整的时间线节点顺序。
|
||||
*
|
||||
* @param request 包含 items 数组,每个元素有 instanceId 和 sortOrder
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PutMapping("/order")
|
||||
public ResponseEntity<String> updateItemsOrder(@RequestBody Map<String, List<Map<String, Object>>> request) {
|
||||
List<Map<String, Object>> items = request.get("items");
|
||||
if (items == null || items.isEmpty()) {
|
||||
return ResponseEntity.error("排序数据不能为空");
|
||||
return ResponseEntity.error("Sort payload cannot be empty");
|
||||
}
|
||||
log.info("批量更新 StoryItem 排序,共 {} 项", items.size());
|
||||
log.info("Update StoryItem order: {} items", items.size());
|
||||
storyItemService.updateItemsOrder(items);
|
||||
return ResponseEntity.success("排序更新成功");
|
||||
return ResponseEntity.success("Order updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除时间线节点
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据节点ID列表批量软删除时间线节点。
|
||||
* 软删除仅标记 is_delete 字段,数据可恢复。
|
||||
*
|
||||
* @param request 包含 instanceIds 数组
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/batch-delete")
|
||||
public ResponseEntity<String> batchDeleteItems(@RequestBody Map<String, List<String>> request) {
|
||||
List<String> instanceIds = request.get("instanceIds");
|
||||
if (instanceIds == null || instanceIds.isEmpty()) {
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "删除列表不能为空");
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "Delete list cannot be empty");
|
||||
}
|
||||
log.info("批量删除 StoryItem,共 {} 项", instanceIds.size());
|
||||
log.info("Batch delete StoryItem: {} items", instanceIds.size());
|
||||
storyItemService.batchDeleteItems(instanceIds);
|
||||
return ResponseEntity.success("批量删除成功");
|
||||
return ResponseEntity.success("Batch delete completed");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改时间线节点时间
|
||||
*
|
||||
* 功能描述:
|
||||
* 批量修改多个节点的 storyItemTime 字段。
|
||||
* 用于批量调整节点的时间信息。
|
||||
*
|
||||
* @param request 包含 instanceIds 数组和 storyItemTime 字符串
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PutMapping("/batch-time")
|
||||
public ResponseEntity<String> batchUpdateItemTime(@RequestBody Map<String, Object> request) {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -159,14 +156,14 @@ public class StoryItemController {
|
||||
String storyItemTime = (String) request.get("storyItemTime");
|
||||
|
||||
if (instanceIds == null || instanceIds.isEmpty()) {
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "修改列表不能为空");
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "Update list cannot be empty");
|
||||
}
|
||||
if (storyItemTime == null || storyItemTime.isEmpty()) {
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "时间不能为空");
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "Time cannot be empty");
|
||||
}
|
||||
|
||||
log.info("批量修改 StoryItem 时间,共 {} 项,新时间: {}", instanceIds.size(), storyItemTime);
|
||||
log.info("Batch update StoryItem time: {} items, newTime={}", instanceIds.size(), storyItemTime);
|
||||
storyItemService.batchUpdateItemTime(instanceIds, storyItemTime);
|
||||
return ResponseEntity.success("批量修改时间成功");
|
||||
return ResponseEntity.success("Batch time update completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.timeline.story.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.service.StoryItemService;
|
||||
import com.timeline.story.vo.StoryItemShareVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -18,13 +18,10 @@ public class StoryPublicController {
|
||||
@Autowired
|
||||
private StoryItemService storyItemService;
|
||||
|
||||
/*
|
||||
* @GetMapping("/{shareId}")
|
||||
* public ResponseEntity<StoryItem> getStoryItemByShareId(@PathVariable String
|
||||
* shareId) {
|
||||
* log.info("根据 shareId 获取 StoryItem: {}", shareId);
|
||||
* StoryItem storyItem = storyItemService.getItemByShareId(shareId);
|
||||
* return ResponseEntity.success(storyItem);
|
||||
* }
|
||||
*/
|
||||
}
|
||||
@GetMapping("/{shareId}")
|
||||
public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable String shareId) {
|
||||
log.info("Get public story by shareId: {}", shareId);
|
||||
StoryItemShareVo 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.vo.StoryItemShareVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -16,35 +16,38 @@ public interface StoryItemMapper {
|
||||
|
||||
void update(StoryItem storyItem);
|
||||
|
||||
void deleteByItemId(String itemId);
|
||||
void deleteByItemId(@Param("instanceId") String itemId);
|
||||
|
||||
StoryItem selectById(String itemId);
|
||||
StoryItem selectById(@Param("instanceId") String itemId);
|
||||
|
||||
List<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);
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* 更新节点排序值
|
||||
*
|
||||
* @param params 包含 instanceId, sortOrder, updateId, updateTime
|
||||
*/
|
||||
void updateOrder(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 更新节点时间
|
||||
*
|
||||
* @param params 包含 instanceId, storyItemTime, updateId, updateTime
|
||||
*/
|
||||
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.vo.StoryDetailVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface StoryMapper {
|
||||
void insert(Story story);
|
||||
|
||||
void update(Story story);
|
||||
void deleteByInstanceId(String instanceId);
|
||||
StoryDetailVo selectByInstanceId(String instanceId, String userId);
|
||||
List<StoryDetailVo> selectByOwnerId(String ownerId);
|
||||
void touchUpdate(String instanceId, String updateId);
|
||||
}
|
||||
|
||||
void deleteByInstanceId(@Param("instanceId") String instanceId);
|
||||
|
||||
StoryDetailVo selectByInstanceId(@Param("instanceId") String instanceId, @Param("userId") String userId);
|
||||
|
||||
List<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
|
||||
public class Story {
|
||||
private String instanceId;
|
||||
private String shareId;
|
||||
private String title;
|
||||
private String description;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@@ -17,9 +18,9 @@ public class Story {
|
||||
private LocalDate storyTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private String updateId;
|
||||
private String updateId;
|
||||
private Integer isDelete;
|
||||
private String ownerId;
|
||||
private String status;
|
||||
private String logo;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public class StoryItem {
|
||||
private String instanceId;
|
||||
private String storyInstanceId;
|
||||
private String masterItemId;
|
||||
private String shareId;
|
||||
private String title;
|
||||
private String description;
|
||||
private String location;
|
||||
@@ -22,9 +23,5 @@ public class StoryItem {
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
private Integer isDelete;
|
||||
/**
|
||||
* 排序值 - 用于拖拽排序
|
||||
* 数值越小越靠前,默认为0
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import com.timeline.story.vo.TimelineAnalyticsVo;
|
||||
import com.timeline.story.vo.TimelineArchiveExploreVo;
|
||||
import com.timeline.story.vo.TimelineArchiveSummaryVo;
|
||||
|
||||
/**
|
||||
* AnalyticsService - 数据分析服务接口
|
||||
@@ -63,6 +65,10 @@ public interface AnalyticsService {
|
||||
*/
|
||||
TimelineAnalyticsVo getTopTags(String userId, int limit);
|
||||
|
||||
TimelineArchiveSummaryVo getArchiveSummary(String userId, int locationLimit, int tagLimit);
|
||||
|
||||
TimelineArchiveExploreVo getArchiveExplore(String userId, String type, String value, int limit);
|
||||
|
||||
/**
|
||||
* 生成年度报告
|
||||
*
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.vo.StoryItemAddVo;
|
||||
import com.timeline.story.vo.StoryItemShareVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
import com.timeline.story.vo.StoryShareConfigVo;
|
||||
import com.timeline.story.vo.StorySharePublishRequest;
|
||||
import com.timeline.story.vo.StorySharePublishVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,40 +27,19 @@ public interface StoryItemService {
|
||||
|
||||
Integer getStoryItemCount(String storyInstanceId);
|
||||
|
||||
// StoryItemShareVo getItemByShareId(String shareId);
|
||||
StoryItemShareVo getItemByShareId(String shareId);
|
||||
|
||||
StoryShareConfigVo getStoryShareConfig(String storyId);
|
||||
|
||||
StorySharePublishVo publishStoryShare(StorySharePublishRequest request);
|
||||
|
||||
void unpublishStoryShare(String storyId);
|
||||
|
||||
Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 批量更新时间线节点排序
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据前端传递的排序数据,批量更新各节点的 sortOrder 字段。
|
||||
* 该方法支持事务处理,确保所有排序更新原子性完成。
|
||||
*
|
||||
* @param items 排序数据列表,每个元素包含 instanceId 和 sortOrder
|
||||
*/
|
||||
void updateItemsOrder(List<Map<String, Object>> items);
|
||||
|
||||
/**
|
||||
* 批量删除时间线节点
|
||||
*
|
||||
* 功能描述:
|
||||
* 根据节点ID列表批量软删除时间线节点。
|
||||
* 软删除仅标记 is_delete 字段,数据可恢复。
|
||||
*
|
||||
* @param instanceIds 要删除的节点ID列表
|
||||
*/
|
||||
void batchDeleteItems(List<String> instanceIds);
|
||||
|
||||
/**
|
||||
* 批量修改时间线节点时间
|
||||
*
|
||||
* 功能描述:
|
||||
* 批量修改多个节点的 storyItemTime 字段。
|
||||
*
|
||||
* @param instanceIds 要修改的节点ID列表
|
||||
* @param storyItemTime 新的时间值
|
||||
*/
|
||||
void batchUpdateItemTime(List<String> instanceIds, String storyItemTime);
|
||||
}
|
||||
|
||||
@@ -1,99 +1,538 @@
|
||||
package com.timeline.story.service.impl;
|
||||
|
||||
import com.timeline.story.dao.StoryMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.timeline.story.dao.StoryItemMapper;
|
||||
import com.timeline.story.dao.StoryMapper;
|
||||
import com.timeline.story.service.AnalyticsService;
|
||||
import com.timeline.story.vo.StoryDetailVo;
|
||||
import com.timeline.story.vo.TimelineArchiveBucketVo;
|
||||
import com.timeline.story.vo.TimelineArchiveExploreVo;
|
||||
import com.timeline.story.vo.TimelineArchiveMomentVo;
|
||||
import com.timeline.story.vo.TimelineArchiveSummaryVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
import com.timeline.story.vo.TimelineAnalyticsVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Year;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* AnalyticsServiceImpl - 数据分析服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
|
||||
private static final Pattern HASHTAG_PATTERN = Pattern.compile("#([\\p{L}\\p{N}_-]+)");
|
||||
private static final String[] TAG_COLORS = {
|
||||
"blue", "green", "gold", "magenta", "purple", "cyan", "orange", "volcano"
|
||||
};
|
||||
|
||||
@Autowired
|
||||
private StoryMapper storyMapper;
|
||||
|
||||
@Autowired
|
||||
private StoryItemMapper storyItemMapper;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getOverallStats(String userId) {
|
||||
log.info("获取用户总体统计: userId={}", userId);
|
||||
log.info("Get overall timeline stats: userId={}", userId);
|
||||
List<StoryData> storyData = loadStoryData(userId);
|
||||
List<ItemData> itemData = flattenItemData(storyData);
|
||||
|
||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||
|
||||
// 基础统计数据 - 使用现有方法查询
|
||||
List<StoryDetailVo> stories = storyMapper.selectByOwnerId(userId);
|
||||
vo.setTotalStories((long) (stories != null ? stories.size() : 0));
|
||||
|
||||
// 统计所有故事中的时刻数量
|
||||
long totalMoments = 0;
|
||||
long imageCount = 0;
|
||||
long videoCount = 0;
|
||||
|
||||
if (stories != null) {
|
||||
for (StoryDetailVo story : stories) {
|
||||
Map<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.setTotalStories((long) storyData.size());
|
||||
vo.setTotalMoments((long) itemData.size());
|
||||
vo.setTotalMedia(itemData.stream().mapToLong(ItemData::mediaCount).sum());
|
||||
vo.setImageCount(itemData.stream().mapToLong(ItemData::imageCount).sum());
|
||||
vo.setVideoCount(itemData.stream().mapToLong(item -> hasVideo(item.item()) ? 1 : 0).sum());
|
||||
vo.setCollaborationCount(
|
||||
storyData.stream().filter(data -> data.story().getPermissionType() != null && data.story().getPermissionType() > 1).count());
|
||||
vo.setTotalComments(0L);
|
||||
vo.setTotalLikes(0L);
|
||||
vo.setTotalFavorites(0L);
|
||||
vo.setConsecutiveDays(0);
|
||||
vo.setMaxConsecutiveDays(0);
|
||||
vo.setMonthlyTrend(buildMonthlyTrend(itemData, Year.now().getValue()));
|
||||
vo.setTopLocations(buildTopLocations(itemData, 10));
|
||||
vo.setTopTags(buildTopTags(itemData, 10));
|
||||
vo.setWeeklyDistribution(buildWeeklyDistribution(itemData));
|
||||
vo.setHourlyDistribution(buildHourlyDistribution(itemData));
|
||||
|
||||
ActivitySummary activitySummary = buildActivitySummary(itemData);
|
||||
vo.setLastActiveDate(activitySummary.lastActiveDate());
|
||||
vo.setConsecutiveDays(activitySummary.currentStreak());
|
||||
vo.setMaxConsecutiveDays(activitySummary.maxStreak());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getStoryStats(String storyInstanceId) {
|
||||
log.info("获取故事统计: storyInstanceId={}", storyInstanceId);
|
||||
log.info("Get story stats: storyInstanceId={}", storyInstanceId);
|
||||
StoryDetailVo story = storyMapper.selectByInstanceId(storyInstanceId, null);
|
||||
List<StoryItemVo> items = loadItemsForStory(storyInstanceId);
|
||||
List<ItemData> itemData = new ArrayList<>();
|
||||
for (StoryItemVo item : items) {
|
||||
itemData.add(buildItemData(story, item));
|
||||
}
|
||||
|
||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||
// TODO: 实现故事统计逻辑
|
||||
vo.setTotalMoments(0L);
|
||||
vo.setTotalMedia(0L);
|
||||
vo.setTotalStories(1L);
|
||||
vo.setTotalMoments((long) itemData.size());
|
||||
vo.setTotalMedia(itemData.stream().mapToLong(ItemData::mediaCount).sum());
|
||||
vo.setImageCount(itemData.stream().mapToLong(ItemData::imageCount).sum());
|
||||
vo.setVideoCount(itemData.stream().mapToLong(item -> hasVideo(item.item()) ? 1 : 0).sum());
|
||||
vo.setTopLocations(buildTopLocations(itemData, 5));
|
||||
vo.setTopTags(buildTopTags(itemData, 5));
|
||||
vo.setWeeklyDistribution(buildWeeklyDistribution(itemData));
|
||||
vo.setHourlyDistribution(buildHourlyDistribution(itemData));
|
||||
vo.setMonthlyTrend(buildMonthlyTrend(itemData, resolveReportYear(itemData, Year.now().getValue())));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getMonthlyTrend(String userId, int year) {
|
||||
log.info("获取月度趋势: userId={}, year={}", userId, year);
|
||||
log.info("Get monthly trend: userId={}, year={}", userId, year);
|
||||
List<ItemData> itemData = flattenItemData(loadStoryData(userId));
|
||||
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++) {
|
||||
TimelineAnalyticsVo.MonthlyStats stats = new TimelineAnalyticsVo.MonthlyStats();
|
||||
stats.setMonth(String.format("%d-%02d", year, month));
|
||||
@@ -102,83 +541,213 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
monthlyTrend.add(stats);
|
||||
}
|
||||
|
||||
vo.setMonthlyTrend(monthlyTrend);
|
||||
return vo;
|
||||
for (ItemData item : itemData) {
|
||||
if (item.eventTime() == null || item.eventTime().getYear() != year) {
|
||||
continue;
|
||||
}
|
||||
TimelineAnalyticsVo.MonthlyStats stats = monthlyTrend.get(item.eventTime().getMonthValue() - 1);
|
||||
stats.setCount(stats.getCount() + 1);
|
||||
stats.setMediaCount(stats.getMediaCount() + item.mediaCount());
|
||||
}
|
||||
|
||||
return monthlyTrend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getTopLocations(String userId, int limit) {
|
||||
log.info("获取热门地点: userId={}, limit={}", userId, limit);
|
||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||
vo.setTopLocations(new ArrayList<>());
|
||||
return vo;
|
||||
private List<TimelineAnalyticsVo.LocationStats> buildTopLocations(List<ItemData> itemData, int limit) {
|
||||
Map<String, Long> counts = new HashMap<>();
|
||||
long total = 0L;
|
||||
|
||||
for (ItemData item : itemData) {
|
||||
String location = item.item().getLocation();
|
||||
if (!StringUtils.hasText(location)) {
|
||||
continue;
|
||||
}
|
||||
counts.merge(location, 1L, Long::sum);
|
||||
total++;
|
||||
}
|
||||
|
||||
List<TimelineAnalyticsVo.LocationStats> result = new ArrayList<>();
|
||||
final long totalCount = total;
|
||||
counts.entrySet().stream()
|
||||
.sorted((left, right) -> Long.compare(right.getValue(), left.getValue()))
|
||||
.limit(limit)
|
||||
.forEach(entry -> {
|
||||
TimelineAnalyticsVo.LocationStats stats = new TimelineAnalyticsVo.LocationStats();
|
||||
stats.setLocation(entry.getKey());
|
||||
stats.setCount(entry.getValue());
|
||||
stats.setPercentage(totalCount == 0 ? 0D : roundPercentage(entry.getValue(), totalCount));
|
||||
result.add(stats);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getTopTags(String userId, int limit) {
|
||||
log.info("获取热门标签: userId={}, limit={}", userId, limit);
|
||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||
vo.setTopTags(new ArrayList<>());
|
||||
return vo;
|
||||
private List<TimelineAnalyticsVo.TagStats> buildTopTags(List<ItemData> itemData, int limit) {
|
||||
Map<String, Long> counts = new HashMap<>();
|
||||
long total = 0L;
|
||||
|
||||
for (ItemData item : itemData) {
|
||||
for (String tag : item.tags()) {
|
||||
counts.merge(tag, 1L, Long::sum);
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
List<TimelineAnalyticsVo.TagStats> result = new ArrayList<>();
|
||||
final long totalCount = total;
|
||||
counts.entrySet().stream()
|
||||
.sorted((left, right) -> Long.compare(right.getValue(), left.getValue()))
|
||||
.limit(limit)
|
||||
.forEach(entry -> {
|
||||
TimelineAnalyticsVo.TagStats stats = new TimelineAnalyticsVo.TagStats();
|
||||
stats.setTag(entry.getKey());
|
||||
stats.setColor(TAG_COLORS[Math.abs(entry.getKey().hashCode()) % TAG_COLORS.length]);
|
||||
stats.setCount(entry.getValue());
|
||||
stats.setPercentage(totalCount == 0 ? 0D : roundPercentage(entry.getValue(), totalCount));
|
||||
result.add(stats);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo.YearlyReport generateYearlyReport(String userId, int year) {
|
||||
log.info("生成年度报告: userId={}, year={}", userId, year);
|
||||
TimelineAnalyticsVo.YearlyReport report = new TimelineAnalyticsVo.YearlyReport();
|
||||
report.setYear(year);
|
||||
report.setTotalMoments(0L);
|
||||
report.setTotalMedia(0L);
|
||||
report.setMostActiveMonth("-");
|
||||
report.setMostActiveDay("-");
|
||||
report.setTopLocation("-");
|
||||
report.setTopTag("-");
|
||||
report.setMonthlyBreakdown(new ArrayList<>());
|
||||
return report;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getActivityStats(String userId) {
|
||||
log.info("获取活跃度统计: userId={}", userId);
|
||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||
vo.setConsecutiveDays(0);
|
||||
vo.setMaxConsecutiveDays(0);
|
||||
vo.setLastActiveDate(LocalDate.now().format(DateTimeFormatter.ISO_DATE));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculateConsecutiveDays(String userId) {
|
||||
log.info("计算连续记录天数: userId={}", userId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimelineAnalyticsVo getTimeDistribution(String userId) {
|
||||
log.info("获取时间分布: userId={}", userId);
|
||||
TimelineAnalyticsVo vo = new TimelineAnalyticsVo();
|
||||
|
||||
// 初始化周分布
|
||||
Map<Integer, Long> weeklyDistribution = new HashMap<>();
|
||||
private Map<Integer, Long> buildWeeklyDistribution(List<ItemData> itemData) {
|
||||
Map<Integer, Long> weeklyDistribution = new LinkedHashMap<>();
|
||||
for (int i = 1; i <= 7; i++) {
|
||||
weeklyDistribution.put(i, 0L);
|
||||
}
|
||||
vo.setWeeklyDistribution(weeklyDistribution);
|
||||
|
||||
// 初始化小时分布
|
||||
Map<Integer, Long> hourlyDistribution = new HashMap<>();
|
||||
for (ItemData item : itemData) {
|
||||
if (item.eventTime() == null) {
|
||||
continue;
|
||||
}
|
||||
int day = item.eventTime().getDayOfWeek().getValue();
|
||||
weeklyDistribution.put(day, weeklyDistribution.get(day) + 1);
|
||||
}
|
||||
return weeklyDistribution;
|
||||
}
|
||||
|
||||
private Map<Integer, Long> buildHourlyDistribution(List<ItemData> itemData) {
|
||||
Map<Integer, Long> hourlyDistribution = new LinkedHashMap<>();
|
||||
for (int i = 0; i < 24; i++) {
|
||||
hourlyDistribution.put(i, 0L);
|
||||
}
|
||||
vo.setHourlyDistribution(hourlyDistribution);
|
||||
|
||||
return vo;
|
||||
for (ItemData item : itemData) {
|
||||
if (item.eventTime() == null) {
|
||||
continue;
|
||||
}
|
||||
int hour = item.eventTime().getHour();
|
||||
hourlyDistribution.put(hour, hourlyDistribution.get(hour) + 1);
|
||||
}
|
||||
return hourlyDistribution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] exportStats(String userId, String format) {
|
||||
log.info("导出统计数据: userId={}, format={}", userId, format);
|
||||
// 返回空数据
|
||||
return new byte[0];
|
||||
private ActivitySummary buildActivitySummary(List<ItemData> itemData) {
|
||||
Set<LocalDate> distinctDates = new LinkedHashSet<>();
|
||||
for (ItemData item : itemData) {
|
||||
if (item.eventTime() != null) {
|
||||
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.PageInfo;
|
||||
import com.timeline.common.constants.CommonConstants;
|
||||
import com.timeline.common.exception.CustomException;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.common.utils.IdUtils;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.story.dao.StoryMapper;
|
||||
import com.timeline.story.dao.StoryItemMapper;
|
||||
import com.timeline.story.dao.StoryShareMapper;
|
||||
import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.entity.StoryShare;
|
||||
import com.timeline.story.service.StoryPermissionService;
|
||||
import com.timeline.story.service.StoryItemService;
|
||||
import com.timeline.story.vo.StoryDetailVo;
|
||||
import com.timeline.story.vo.StoryItemAddVo;
|
||||
import com.timeline.story.vo.StoryItemShareVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
import com.timeline.story.vo.StoryShareConfigVo;
|
||||
import com.timeline.story.vo.StoryShareMomentVo;
|
||||
import com.timeline.story.vo.StorySharePublishRequest;
|
||||
import com.timeline.story.vo.StorySharePublishVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -29,15 +46,51 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
@Autowired
|
||||
private StoryItemMapper storyItemMapper;
|
||||
|
||||
@Autowired
|
||||
private StoryMapper storyMapper;
|
||||
|
||||
@Autowired
|
||||
private StoryPermissionService storyPermissionService;
|
||||
|
||||
@Autowired
|
||||
private StoryShareMapper storyShareMapper;
|
||||
|
||||
private String getCurrentUserId() {
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (!StringUtils.hasText(currentUserId)) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "User context is missing");
|
||||
}
|
||||
return currentUserId;
|
||||
}
|
||||
|
||||
private StoryDetailVo getAccessibleStory(String storyId, String currentUserId) {
|
||||
StoryDetailVo story = storyMapper.selectByInstanceId(storyId, currentUserId);
|
||||
if (story == null) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND, "Story not found");
|
||||
}
|
||||
if (!storyPermissionService.checkUserPermission(
|
||||
storyId,
|
||||
currentUserId,
|
||||
CommonConstants.STORY_PERMISSION_TYPE_WRITE)) {
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "No permission to publish this story");
|
||||
}
|
||||
return story;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<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);
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("storyInstanceId", storyItemVo.getStoryInstanceId());
|
||||
if (storyItemVo.getAfterTime() != null) {
|
||||
params.put("afterTime", storyItemVo.getAfterTime());
|
||||
}
|
||||
if (storyItemVo.getBeforeTime() != null) {
|
||||
params.put("beforeTime", storyItemVo.getBeforeTime());
|
||||
}
|
||||
|
||||
List<StoryItemVo> list = storyItemMapper.selectStoryItemByStoryInstanceId(params);
|
||||
PageInfo<StoryItemVo> pageInfo = new PageInfo<>(list);
|
||||
@@ -61,7 +114,7 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
}
|
||||
|
||||
storyItemMapper.insert(storyItemAddVo);
|
||||
// Images handling to be implemented
|
||||
// Images handling to be implemented separately.
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,7 +123,7 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
storyItemAddVo.setUpdateId(UserContextUtils.getCurrentUserId());
|
||||
storyItemAddVo.setUpdateTime(LocalDateTime.now());
|
||||
storyItemMapper.update(storyItemAddVo);
|
||||
// Images handling to be implemented
|
||||
// Images handling to be implemented separately.
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,7 +139,8 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
|
||||
@Override
|
||||
public List<String> getStoryItemImages(String itemId) {
|
||||
return storyItemMapper.selectImagesByItemId(itemId);
|
||||
List<String> images = storyItemMapper.selectImagesByItemId(itemId);
|
||||
return images == null ? Collections.emptyList() : images;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,12 +148,127 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
return storyItemMapper.countByStoryId(storyInstanceId);
|
||||
}
|
||||
|
||||
/*
|
||||
* @Override
|
||||
* public StoryItemShareVo getItemByShareId(String shareId) {
|
||||
* return storyItemMapper.selectByShareIdWithAuthor(shareId);
|
||||
* }
|
||||
*/
|
||||
@Override
|
||||
public StoryItemShareVo getItemByShareId(String shareId) {
|
||||
StoryItemShareVo shareVo = storyItemMapper.selectByShareIdWithAuthor(shareId);
|
||||
if (shareVo == null) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND, "Public share not found");
|
||||
}
|
||||
|
||||
hydrateShareItem(shareVo);
|
||||
|
||||
StoryShare storyShare = storyShareMapper.selectByShareId(shareId);
|
||||
if (storyShare != null) {
|
||||
shareVo.setShareTitle(trimToNull(storyShare.getShareTitle()));
|
||||
shareVo.setShareDescription(trimToNull(storyShare.getShareDescription()));
|
||||
shareVo.setShareQuote(trimToNull(storyShare.getShareQuote()));
|
||||
shareVo.setHeroMomentId(storyShare.getHeroMomentId());
|
||||
List<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
|
||||
public Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize) {
|
||||
@@ -113,20 +282,6 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新时间线节点排序
|
||||
*
|
||||
* 实现思路:
|
||||
* 1. 使用事务确保批量更新的原子性
|
||||
* 2. 遍历排序数据,逐个更新节点的 sortOrder 字段
|
||||
* 3. 同时更新 updateTime 时间戳
|
||||
*
|
||||
* 注意事项:
|
||||
* - 该方法需要在事务中执行,确保数据一致性
|
||||
* - 如果任一更新失败,整个事务将回滚
|
||||
*
|
||||
* @param items 排序数据列表
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateItemsOrder(List<Map<String, Object>> items) {
|
||||
@@ -137,7 +292,6 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
String instanceId = (String) item.get("instanceId");
|
||||
Integer sortOrder = ((Number) item.get("sortOrder")).intValue();
|
||||
|
||||
// 构建更新参数
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("instanceId", instanceId);
|
||||
params.put("sortOrder", sortOrder);
|
||||
@@ -145,54 +299,27 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
params.put("updateTime", now);
|
||||
|
||||
storyItemMapper.updateOrder(params);
|
||||
log.debug("更新节点排序: instanceId={}, sortOrder={}", instanceId, sortOrder);
|
||||
log.debug("Updated story item order: instanceId={}, sortOrder={}", instanceId, sortOrder);
|
||||
}
|
||||
|
||||
log.info("批量更新排序完成,共更新 {} 个节点", items.size());
|
||||
log.info("Updated order for {} story items", items.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除时间线节点
|
||||
*
|
||||
* 实现思路:
|
||||
* 1. 使用事务确保批量删除的原子性
|
||||
* 2. 遍历节点ID列表,逐个执行软删除
|
||||
* 3. 软删除仅标记 is_delete = 1,数据可恢复
|
||||
*
|
||||
* 注意事项:
|
||||
* - 该方法需要在事务中执行
|
||||
* - 如果任一删除失败,整个事务将回滚
|
||||
*
|
||||
* @param instanceIds 要删除的节点ID列表
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchDeleteItems(List<String> instanceIds) {
|
||||
for (String instanceId : instanceIds) {
|
||||
storyItemMapper.deleteByItemId(instanceId);
|
||||
log.debug("软删除节点: instanceId={}", instanceId);
|
||||
log.debug("Soft deleted story item: {}", instanceId);
|
||||
}
|
||||
log.info("批量删除完成,共删除 {} 个节点", instanceIds.size());
|
||||
log.info("Soft deleted {} story items", instanceIds.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量修改时间线节点时间
|
||||
*
|
||||
* 实现思路:
|
||||
* 1. 使用事务确保批量修改的原子性
|
||||
* 2. 遍历节点ID列表,逐个更新时间字段
|
||||
* 3. 同时更新 updateTime 时间戳
|
||||
*
|
||||
* @param instanceIds 要修改的节点ID列表
|
||||
* @param storyItemTime 新的时间值
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchUpdateItemTime(List<String> instanceIds, String storyItemTime) {
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 解析时间字符串为 LocalDateTime
|
||||
LocalDateTime parsedTime = LocalDateTime.parse(storyItemTime.replace(" ", "T"));
|
||||
|
||||
for (String instanceId : instanceIds) {
|
||||
@@ -203,9 +330,139 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
params.put("updateTime", now);
|
||||
|
||||
storyItemMapper.updateItemTime(params);
|
||||
log.debug("更新节点时间: instanceId={}, storyItemTime={}", instanceId, storyItemTime);
|
||||
log.debug("Updated story item time: instanceId={}, storyItemTime={}", instanceId, storyItemTime);
|
||||
}
|
||||
|
||||
log.info("批量修改时间完成,共修改 {} 个节点", instanceIds.size());
|
||||
log.info("Updated time for {} story items", instanceIds.size());
|
||||
}
|
||||
|
||||
private void hydrateShareItem(StoryItemShareVo shareVo) {
|
||||
List<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.exception.CustomException;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.common.utils.IdUtils;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.story.dao.StoryMapper;
|
||||
import com.timeline.story.entity.Story;
|
||||
import com.timeline.story.entity.StoryActivity;
|
||||
import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.dao.StoryMapper;
|
||||
import com.timeline.story.mq.ActivityLogProducer;
|
||||
import com.timeline.story.service.StoryItemService;
|
||||
import com.timeline.story.service.StoryPermissionService;
|
||||
import com.timeline.story.service.StoryService;
|
||||
import com.timeline.story.mq.ActivityLogProducer;
|
||||
import com.timeline.story.vo.StoryDetailVo;
|
||||
import com.timeline.story.vo.StoryDetailWithItemsVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
import com.timeline.story.vo.StoryPermissionVo;
|
||||
import com.timeline.story.vo.StoryVo;
|
||||
import com.timeline.common.utils.IdUtils;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import lombok.val;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -48,7 +47,7 @@ public class StoryServiceImpl implements StoryService {
|
||||
private String getCurrentUserId() {
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId == null || currentUserId.isEmpty()) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份");
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "User context is missing");
|
||||
}
|
||||
return currentUserId;
|
||||
}
|
||||
@@ -57,10 +56,8 @@ public class StoryServiceImpl implements StoryService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createStory(StoryVo storyVo) {
|
||||
try {
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId == null || currentUserId.isEmpty()) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份");
|
||||
}
|
||||
String currentUserId = getCurrentUserId();
|
||||
|
||||
Story story = new Story();
|
||||
story.setOwnerId(currentUserId);
|
||||
story.setUpdateId(currentUserId);
|
||||
@@ -74,11 +71,10 @@ public class StoryServiceImpl implements StoryService {
|
||||
story.setLogo(storyVo.getLogo());
|
||||
story.setIsDelete(0);
|
||||
storyMapper.insert(story);
|
||||
// 自动添加创建者权限
|
||||
|
||||
StoryPermissionVo permissionVo = new StoryPermissionVo();
|
||||
permissionVo.setStoryInstanceId(story.getInstanceId());
|
||||
permissionVo.setUserId(currentUserId);
|
||||
// 创建者权限
|
||||
permissionVo.setPermissionType(CommonConstants.STORY_PERMISSION_TYPE_OWNER);
|
||||
storyPermissionService.createPermission(permissionVo);
|
||||
|
||||
@@ -88,9 +84,9 @@ public class StoryServiceImpl implements StoryService {
|
||||
activity.setStoryInstanceId(story.getInstanceId());
|
||||
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_CREATE);
|
||||
activityLogProducer.sendLog("story.activity.create", activity);
|
||||
} catch (Exception e) {
|
||||
log.error("创建故事失败", e);
|
||||
throw new CustomException(500, "创建故事失败: " + e.toString());
|
||||
} catch (Exception exception) {
|
||||
log.error("Create story failed", exception);
|
||||
throw new CustomException(500, "Create story failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,17 +99,19 @@ public class StoryServiceImpl implements StoryService {
|
||||
if (story == null) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND);
|
||||
}
|
||||
if (!storyPermissionService.checkUserPermission(storyId, currentUserId,
|
||||
if (!storyPermissionService.checkUserPermission(
|
||||
storyId,
|
||||
currentUserId,
|
||||
CommonConstants.STORY_PERMISSION_TYPE_WRITE)) {
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "无权限修改故事");
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "No permission to update story");
|
||||
}
|
||||
|
||||
story.setTitle(storyVo.getTitle());
|
||||
story.setDescription(storyVo.getDescription());
|
||||
story.setStatus(storyVo.getStatus());
|
||||
story.setStoryTime(storyVo.getStoryTime());
|
||||
story.setUpdateTime(LocalDateTime.now());
|
||||
story.setLogo(storyVo.getLogo());
|
||||
|
||||
storyMapper.update(story);
|
||||
|
||||
StoryActivity activity = new StoryActivity();
|
||||
@@ -122,7 +120,6 @@ public class StoryServiceImpl implements StoryService {
|
||||
activity.setStoryInstanceId(storyId);
|
||||
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_UPDATE);
|
||||
activityLogProducer.sendLog("story.activity.update", activity);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,15 +130,16 @@ public class StoryServiceImpl implements StoryService {
|
||||
if (story == null) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND);
|
||||
}
|
||||
if (!storyPermissionService.checkUserPermission(storyId, currentUserId,
|
||||
if (!storyPermissionService.checkUserPermission(
|
||||
storyId,
|
||||
currentUserId,
|
||||
CommonConstants.STORY_PERMISSION_TYPE_ADMIN)) {
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "无权限删除故事");
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "No permission to delete story");
|
||||
}
|
||||
// delete story
|
||||
|
||||
storyMapper.deleteByInstanceId(storyId);
|
||||
// delete permission
|
||||
storyPermissionService.deletePermission(storyId);
|
||||
// delete activity
|
||||
|
||||
StoryActivity activity = new StoryActivity();
|
||||
activity.setActorId(currentUserId);
|
||||
activity.setAction(CommonConstants.ACTION_TYPE_STORY_DELETE);
|
||||
@@ -152,21 +150,32 @@ public class StoryServiceImpl implements StoryService {
|
||||
|
||||
@Override
|
||||
public StoryDetailWithItemsVo getStoryByInstanceId(String storyId) {
|
||||
val userId = getCurrentUserId();
|
||||
String userId = getCurrentUserId();
|
||||
StoryDetailVo story = storyMapper.selectByInstanceId(storyId, userId);
|
||||
if (story == null) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND);
|
||||
}
|
||||
|
||||
StoryItemVo storyItemVo = new StoryItemVo();
|
||||
storyItemVo.setStoryInstanceId(storyId);
|
||||
storyItemVo.setCurrent(1);
|
||||
storyItemVo.setPageSize(10);
|
||||
Map itemsMap = storyItemService.getItemsByMasterItem(storyItemVo);
|
||||
List<StoryItem> items = (List<StoryItem>) itemsMap.get("list");
|
||||
Map<String, Object> itemsMap = storyItemService.getItemsByMasterItem(storyItemVo);
|
||||
|
||||
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();
|
||||
result.setItems(items);
|
||||
result.setInstanceId(story.getInstanceId());
|
||||
result.setShareId(story.getShareId());
|
||||
result.setOwnerId(story.getOwnerId());
|
||||
result.setUpdateId(story.getUpdateId());
|
||||
result.setTitle(story.getTitle());
|
||||
@@ -181,7 +190,6 @@ public class StoryServiceImpl implements StoryService {
|
||||
result.setUpdateName(story.getUpdateName());
|
||||
result.setPermissionType(story.getPermissionType());
|
||||
result.setItemCount(story.getItemCount());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -189,9 +197,9 @@ public class StoryServiceImpl implements StoryService {
|
||||
public List<StoryDetailVo> getStoriesByOwnerId(String ownerId) {
|
||||
try {
|
||||
return storyMapper.selectByOwnerId(ownerId);
|
||||
} catch (Exception e) {
|
||||
log.error("查询用户故事列表失败", e);
|
||||
throw new CustomException(500, "查询用户故事列表失败");
|
||||
} catch (Exception exception) {
|
||||
log.error("Query user stories failed", exception);
|
||||
throw new CustomException(500, "Query user stories failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,9 +208,9 @@ public class StoryServiceImpl implements StoryService {
|
||||
try {
|
||||
String currentUserId = getCurrentUserId();
|
||||
return storyMapper.selectByOwnerId(currentUserId);
|
||||
} catch (Exception e) {
|
||||
log.error("查询用户故事列表失败", e);
|
||||
throw new CustomException(500, "查询用户故事列表失败");
|
||||
} catch (Exception exception) {
|
||||
log.error("Query stories failed", exception);
|
||||
throw new CustomException(500, "Query stories failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,22 @@ import com.timeline.story.entity.StoryItem;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class StoryItemShareVo extends StoryItem {
|
||||
private String authorName;
|
||||
private String authorAvatar;
|
||||
private String ownerName;
|
||||
private String coverInstanceId;
|
||||
private String thumbnailInstanceId;
|
||||
private String shareTitle;
|
||||
private String shareDescription;
|
||||
private String shareQuote;
|
||||
private String heroMomentId;
|
||||
private List<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;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<insert id="insert">
|
||||
INSERT INTO story_item (instance_id, master_item_id, description, location, title, create_id, story_instance_id, is_delete, story_item_time, update_id, video_url, duration, thumbnail_url)
|
||||
VALUES (#{instanceId}, #{masterItemId}, #{description}, #{location}, #{title},#{createId}, #{storyInstanceId}, #{isDelete}, #{storyItemTime}, #{updateId}, #{videoUrl}, #{duration}, #{thumbnailUrl})
|
||||
VALUES (#{instanceId}, #{masterItemId}, #{description}, #{location}, #{title}, #{createId}, #{storyInstanceId}, #{isDelete}, #{storyItemTime}, #{updateId}, #{videoUrl}, #{duration}, #{thumbnailUrl})
|
||||
</insert>
|
||||
|
||||
<update id="update">
|
||||
@@ -30,16 +30,26 @@
|
||||
</delete>
|
||||
|
||||
<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 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 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 id="selectStoryItemByStoryInstanceId" resultType="com.timeline.story.vo.StoryItemVo">
|
||||
SELECT
|
||||
si.id,
|
||||
@@ -50,52 +60,71 @@
|
||||
si.story_instance_id,
|
||||
si.master_item_id,
|
||||
si.is_delete,
|
||||
si.story_item_time as story_item_time,
|
||||
si.story_item_time AS story_item_time,
|
||||
si.video_url,
|
||||
si.duration,
|
||||
si.thumbnail_url,
|
||||
si.share_id,
|
||||
si.create_id AS create_id,
|
||||
si.create_time AS create_time,
|
||||
u1.username AS create_name,
|
||||
si.update_id AS update_id,
|
||||
si.update_time AS update_time,
|
||||
u2.username AS update_name
|
||||
FROM
|
||||
story_item si
|
||||
LEFT JOIN user u1 ON si.create_id = u1.user_id
|
||||
LEFT JOIN `user` u2 ON si.update_id = u2.user_id
|
||||
WHERE
|
||||
story_instance_id = #{storyInstanceId}
|
||||
AND is_delete = 0
|
||||
<if test="afterTime != null">
|
||||
AND story_item_time > #{afterTime}
|
||||
</if>
|
||||
ORDER BY
|
||||
story_item_time DESC
|
||||
FROM story_item si
|
||||
LEFT JOIN user u1 ON si.create_id = u1.user_id
|
||||
LEFT JOIN user u2 ON si.update_id = u2.user_id
|
||||
WHERE si.story_instance_id = #{storyInstanceId}
|
||||
AND si.is_delete = 0
|
||||
<if test="afterTime != null">
|
||||
AND si.story_item_time > #{afterTime}
|
||||
</if>
|
||||
<if test="beforeTime != null">
|
||||
AND si.story_item_time < #{beforeTime}
|
||||
</if>
|
||||
ORDER BY si.story_item_time DESC, si.update_time DESC
|
||||
</select>
|
||||
|
||||
<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 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 id="selectByShareIdWithAuthor" resultType="com.timeline.story.vo.StoryItemShareVo">
|
||||
SELECT
|
||||
si.*,
|
||||
u.username AS authorName,
|
||||
u.avatar AS authorAvatar
|
||||
FROM
|
||||
story_item si
|
||||
LEFT JOIN
|
||||
user u ON si.create_id = u.user_id
|
||||
WHERE
|
||||
si.share_id = #{shareId} AND si.is_delete = 0
|
||||
author.username AS author_name,
|
||||
author.avatar AS author_avatar,
|
||||
owner.username AS owner_name
|
||||
FROM story_item si
|
||||
LEFT JOIN story s ON si.story_instance_id = s.instance_id AND s.is_delete = 0
|
||||
LEFT JOIN user author ON si.create_id = author.user_id AND author.is_deleted = 0
|
||||
LEFT JOIN user owner ON s.owner_id = owner.user_id AND owner.is_deleted = 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 id="searchItems" resultType="com.timeline.story.vo.StoryItemVo">
|
||||
SELECT
|
||||
@@ -103,29 +132,38 @@
|
||||
si.story_instance_id,
|
||||
si.title,
|
||||
si.description,
|
||||
si.story_item_time
|
||||
FROM
|
||||
story_item si
|
||||
WHERE
|
||||
si.is_delete = 0
|
||||
AND (si.title LIKE CONCAT('%', #{keyword}, '%') OR si.description LIKE CONCAT('%', #{keyword}, '%'))
|
||||
ORDER BY
|
||||
si.story_item_time DESC
|
||||
si.story_item_time,
|
||||
si.location,
|
||||
si.video_url,
|
||||
si.share_id
|
||||
FROM story_item si
|
||||
WHERE si.is_delete = 0
|
||||
AND (
|
||||
si.title LIKE CONCAT('%', #{keyword}, '%')
|
||||
OR si.description LIKE CONCAT('%', #{keyword}, '%')
|
||||
)
|
||||
ORDER BY si.story_item_time DESC, si.update_time DESC
|
||||
</select>
|
||||
|
||||
<!--
|
||||
更新节点排序值
|
||||
|
||||
功能描述:
|
||||
根据拖拽排序结果更新节点的 sort_order 字段。
|
||||
同时更新 update_id 和 update_time 以记录修改信息。
|
||||
|
||||
参数说明:
|
||||
- instanceId: 节点唯一标识
|
||||
- sortOrder: 新的排序值(数值越小越靠前)
|
||||
- updateId: 操作用户ID
|
||||
- updateTime: 更新时间
|
||||
-->
|
||||
<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">
|
||||
UPDATE story_item
|
||||
SET share_id = #{shareId},
|
||||
update_id = #{updateId},
|
||||
update_time = #{updateTime}
|
||||
WHERE instance_id = #{instanceId}
|
||||
AND is_delete = 0
|
||||
</update>
|
||||
|
||||
<update id="updateOrder">
|
||||
UPDATE story_item
|
||||
SET sort_order = #{sortOrder},
|
||||
@@ -134,19 +172,6 @@
|
||||
WHERE instance_id = #{instanceId}
|
||||
</update>
|
||||
|
||||
<!--
|
||||
更新节点时间
|
||||
|
||||
功能描述:
|
||||
批量修改节点时间时使用,更新 story_item_time 字段。
|
||||
同时更新 update_id 和 update_time 以记录修改信息。
|
||||
|
||||
参数说明:
|
||||
- instanceId: 节点唯一标识
|
||||
- storyItemTime: 新的时间值
|
||||
- updateId: 操作用户ID
|
||||
- updateTime: 更新时间
|
||||
-->
|
||||
<update id="updateItemTime">
|
||||
UPDATE story_item
|
||||
SET story_item_time = #{storyItemTime},
|
||||
|
||||
@@ -22,43 +22,74 @@
|
||||
</update>
|
||||
|
||||
<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>
|
||||
|
||||
<select id="selectByInstanceId" resultType="com.timeline.story.vo.StoryDetailVo">
|
||||
SELECT
|
||||
s.*,
|
||||
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
|
||||
|
||||
(
|
||||
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,
|
||||
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
|
||||
|
||||
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 story_permission sp on sp.story_instance_id = s.instance_id and sp.user_id = #{userId}
|
||||
|
||||
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 story_permission sp ON sp.story_instance_id = s.instance_id AND sp.user_id = #{userId}
|
||||
WHERE s.instance_id = #{instanceId}
|
||||
AND s.is_delete = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectByOwnerId" resultType="com.timeline.story.vo.StoryDetailVo">
|
||||
SELECT
|
||||
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,
|
||||
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
|
||||
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 story_permission sp ON s.instance_id = sp.story_instance_id AND sp.user_id = #{owerId}
|
||||
WHERE
|
||||
s.instance_id IN ( SELECT story_instance_id FROM story_permission WHERE user_id = #{owerId} )
|
||||
AND s.is_delete = 0
|
||||
(
|
||||
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
|
||||
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 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
|
||||
ORDER BY s.update_time DESC, s.create_time DESC
|
||||
</select>
|
||||
|
||||
<update id="touchUpdate">
|
||||
@@ -68,4 +99,4 @@
|
||||
WHERE instance_id = #{instanceId}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
</mapper>
|
||||
@@ -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