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
实现故事分享的评论和浏览统计功能,包括: 1. 新增StoryShareFeedbackService接口及相关实现 2. 添加StoryShareComment、StoryShareMetric实体类 3. 创建story_share_comment和story_share_metric表 4. 在StoryDetailVo中增加shareConfigured和sharePublished字段 5. 扩展StoryShareConfigVo包含反馈统计数据 6. 新增公共API端点处理反馈请求 7. 实现模板样式选择功能
This commit is contained in:
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS `story_share` (
|
||||
`share_title` varchar(255) DEFAULT NULL COMMENT 'Curated public title',
|
||||
`share_description` text COMMENT 'Curated public description',
|
||||
`share_quote` text COMMENT 'Curated story note',
|
||||
`template_style` varchar(32) DEFAULT 'editorial' COMMENT 'Public template style',
|
||||
`featured_moment_ids` text COMMENT 'Comma separated featured moment IDs',
|
||||
`create_id` varchar(32) DEFAULT NULL,
|
||||
`update_id` varchar(32) DEFAULT NULL,
|
||||
|
||||
24
deploy/update_story_share_feedback.sql
Normal file
24
deploy/update_story_share_feedback.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE IF NOT EXISTS `story_share_metric` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`share_id` varchar(64) NOT NULL,
|
||||
`view_count` bigint NOT NULL DEFAULT '0',
|
||||
`comment_count` bigint NOT NULL DEFAULT '0',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_story_share_metric_share_id` (`share_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `story_share_comment` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` varchar(64) NOT NULL,
|
||||
`share_id` varchar(64) NOT NULL,
|
||||
`visitor_name` varchar(40) NOT NULL,
|
||||
`content` varchar(280) NOT NULL,
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_delete` tinyint(1) DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_story_share_comment_instance_id` (`instance_id`),
|
||||
KEY `idx_story_share_comment_share_id` (`share_id`, `is_delete`, `create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
24
deploy/update_story_share_template.sql
Normal file
24
deploy/update_story_share_template.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
SET @story_share_table_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'story_share'
|
||||
);
|
||||
|
||||
SET @story_share_template_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'story_share'
|
||||
AND COLUMN_NAME = 'template_style'
|
||||
);
|
||||
|
||||
SET @story_share_template_stmt = IF(
|
||||
@story_share_table_exists = 1 AND @story_share_template_exists = 0,
|
||||
"ALTER TABLE story_share ADD COLUMN template_style varchar(32) DEFAULT 'editorial' COMMENT 'Public template style' AFTER share_quote",
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE story_share_template_stmt FROM @story_share_template_stmt;
|
||||
EXECUTE story_share_template_stmt;
|
||||
DEALLOCATE PREPARE story_share_template_stmt;
|
||||
@@ -2,12 +2,18 @@ package com.timeline.story.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.story.service.StoryItemService;
|
||||
import com.timeline.story.service.StoryShareFeedbackService;
|
||||
import com.timeline.story.vo.StoryItemShareVo;
|
||||
import com.timeline.story.vo.StoryShareCommentRequest;
|
||||
import com.timeline.story.vo.StoryShareFeedbackVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@@ -18,10 +24,38 @@ public class StoryPublicController {
|
||||
@Autowired
|
||||
private StoryItemService storyItemService;
|
||||
|
||||
@Autowired
|
||||
private StoryShareFeedbackService storyShareFeedbackService;
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{shareId}/feedback")
|
||||
public ResponseEntity<StoryShareFeedbackVo> getShareFeedback(
|
||||
@PathVariable String shareId,
|
||||
@RequestParam(defaultValue = "8") int limit) {
|
||||
log.info("Get public story feedback: shareId={}, limit={}", shareId, limit);
|
||||
return ResponseEntity.success(storyShareFeedbackService.getFeedback(shareId, limit));
|
||||
}
|
||||
|
||||
@PostMapping("/{shareId}/feedback/view")
|
||||
public ResponseEntity<StoryShareFeedbackVo> recordShareView(
|
||||
@PathVariable String shareId,
|
||||
@RequestParam(defaultValue = "8") int limit) {
|
||||
log.info("Record public story view: shareId={}, limit={}", shareId, limit);
|
||||
return ResponseEntity.success(storyShareFeedbackService.recordView(shareId, limit));
|
||||
}
|
||||
|
||||
@PostMapping("/{shareId}/feedback/comment")
|
||||
public ResponseEntity<StoryShareFeedbackVo> createShareComment(
|
||||
@PathVariable String shareId,
|
||||
@RequestBody StoryShareCommentRequest request,
|
||||
@RequestParam(defaultValue = "8") int limit) {
|
||||
log.info("Create public story comment: shareId={}, limit={}", shareId, limit);
|
||||
return ResponseEntity.success(storyShareFeedbackService.createComment(shareId, request, limit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.timeline.story.dao;
|
||||
|
||||
import com.timeline.story.entity.StoryShareComment;
|
||||
import com.timeline.story.entity.StoryShareMetric;
|
||||
import com.timeline.story.vo.StoryShareCommentVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface StoryShareFeedbackMapper {
|
||||
void ensureMetricRow(@Param("shareId") String shareId);
|
||||
|
||||
void incrementViewCount(@Param("shareId") String shareId);
|
||||
|
||||
void incrementCommentCount(@Param("shareId") String shareId);
|
||||
|
||||
StoryShareMetric selectMetricByShareId(@Param("shareId") String shareId);
|
||||
|
||||
void insertComment(StoryShareComment comment);
|
||||
|
||||
List<StoryShareCommentVo> selectRecentComments(
|
||||
@Param("shareId") String shareId,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public class StoryShare {
|
||||
private String shareTitle;
|
||||
private String shareDescription;
|
||||
private String shareQuote;
|
||||
private String templateStyle;
|
||||
private String featuredMomentIds;
|
||||
private String createId;
|
||||
private String updateId;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.timeline.story.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class StoryShareComment {
|
||||
private Long id;
|
||||
private String instanceId;
|
||||
private String shareId;
|
||||
private String visitorName;
|
||||
private String content;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Integer isDelete;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.timeline.story.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class StoryShareMetric {
|
||||
private Long id;
|
||||
private String shareId;
|
||||
private Long viewCount;
|
||||
private Long commentCount;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.timeline.story.service;
|
||||
|
||||
import com.timeline.story.vo.StoryShareCommentRequest;
|
||||
import com.timeline.story.vo.StoryShareFeedbackVo;
|
||||
|
||||
public interface StoryShareFeedbackService {
|
||||
StoryShareFeedbackVo getFeedback(String shareId, int limit);
|
||||
|
||||
StoryShareFeedbackVo recordView(String shareId, int limit);
|
||||
|
||||
StoryShareFeedbackVo createComment(String shareId, StoryShareCommentRequest request, int limit);
|
||||
}
|
||||
@@ -314,7 +314,7 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
|
||||
return new ArchiveDataset(
|
||||
storyData.size(),
|
||||
(int) storyData.stream().filter(data -> StringUtils.hasText(data.story().getShareId())).count(),
|
||||
(int) storyData.stream().filter(data -> isShareReady(data.story())).count(),
|
||||
(int) itemData.stream().filter(data -> hasVideo(data.item())).count(),
|
||||
sortArchiveBuckets(locationBuckets),
|
||||
sortArchiveBuckets(tagBuckets),
|
||||
@@ -405,7 +405,9 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
? item.getInstanceId()
|
||||
: (story.getInstanceId() + "-" + Math.abs((item.getTitle() == null ? "" : item.getTitle()).hashCode())));
|
||||
moment.setStoryInstanceId(story.getInstanceId());
|
||||
moment.setStoryShareId(story.getShareId());
|
||||
moment.setStoryShareId(resolvePublicShareId(story));
|
||||
moment.setShareConfigured(isShareReady(story));
|
||||
moment.setSharePublished(isSharePublished(story));
|
||||
moment.setStoryTitle(StringUtils.hasText(story.getTitle()) ? story.getTitle() : "Untitled story");
|
||||
moment.setStoryTime(formatArchiveDateTime(itemData.eventTime()));
|
||||
moment.setItemInstanceId(item.getInstanceId());
|
||||
@@ -438,6 +440,8 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
bucket.setSubtitle(moment.getStoryTitle());
|
||||
bucket.setStoryInstanceId(moment.getStoryInstanceId());
|
||||
bucket.setStoryShareId(moment.getStoryShareId());
|
||||
bucket.setShareConfigured(moment.getShareConfigured());
|
||||
bucket.setSharePublished(moment.getSharePublished());
|
||||
bucket.setCoverInstanceId(moment.getCoverInstanceId());
|
||||
bucket.setCoverSrc(moment.getCoverSrc());
|
||||
bucket.setSampleMomentTitle(moment.getItemTitle());
|
||||
@@ -451,7 +455,9 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
if (momentSortValue >= bucketSortValue) {
|
||||
bucket.setSubtitle(moment.getStoryTitle());
|
||||
bucket.setStoryInstanceId(story.getInstanceId());
|
||||
bucket.setStoryShareId(story.getShareId());
|
||||
bucket.setStoryShareId(resolvePublicShareId(story));
|
||||
bucket.setShareConfigured(isShareReady(story));
|
||||
bucket.setSharePublished(isSharePublished(story));
|
||||
bucket.setCoverInstanceId(moment.getCoverInstanceId());
|
||||
bucket.setCoverSrc(moment.getCoverSrc());
|
||||
bucket.setSampleMomentTitle(moment.getItemTitle());
|
||||
@@ -459,6 +465,21 @@ public class AnalyticsServiceImpl implements AnalyticsService {
|
||||
}
|
||||
}
|
||||
|
||||
private String resolvePublicShareId(StoryDetailVo story) {
|
||||
if (story == null) {
|
||||
return null;
|
||||
}
|
||||
return StringUtils.hasText(story.getPublicShareId()) ? story.getPublicShareId() : story.getShareId();
|
||||
}
|
||||
|
||||
private boolean isSharePublished(StoryDetailVo story) {
|
||||
return story != null && (Boolean.TRUE.equals(story.getSharePublished()) || StringUtils.hasText(resolvePublicShareId(story)));
|
||||
}
|
||||
|
||||
private boolean isShareReady(StoryDetailVo story) {
|
||||
return story != null && (Boolean.TRUE.equals(story.getShareConfigured()) || isSharePublished(story));
|
||||
}
|
||||
|
||||
private List<TimelineArchiveBucketVo> sortArchiveBuckets(Map<String, TimelineArchiveBucketVo> bucketMap) {
|
||||
return bucketMap.values().stream()
|
||||
.sorted((left, right) -> {
|
||||
|
||||
@@ -9,14 +9,17 @@ 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.StoryShareFeedbackMapper;
|
||||
import com.timeline.story.dao.StoryShareMapper;
|
||||
import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.entity.StoryShare;
|
||||
import com.timeline.story.entity.StoryShareMetric;
|
||||
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.StoryShareCommentVo;
|
||||
import com.timeline.story.vo.StoryItemVo;
|
||||
import com.timeline.story.vo.StoryShareConfigVo;
|
||||
import com.timeline.story.vo.StoryShareMomentVo;
|
||||
@@ -43,6 +46,8 @@ import java.util.Set;
|
||||
@Service
|
||||
public class StoryItemServiceImpl implements StoryItemService {
|
||||
|
||||
private static final String DEFAULT_TEMPLATE_STYLE = "editorial";
|
||||
|
||||
@Autowired
|
||||
private StoryItemMapper storyItemMapper;
|
||||
|
||||
@@ -55,6 +60,9 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
@Autowired
|
||||
private StoryShareMapper storyShareMapper;
|
||||
|
||||
@Autowired
|
||||
private StoryShareFeedbackMapper storyShareFeedbackMapper;
|
||||
|
||||
private String getCurrentUserId() {
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (!StringUtils.hasText(currentUserId)) {
|
||||
@@ -162,11 +170,15 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
shareVo.setShareTitle(trimToNull(storyShare.getShareTitle()));
|
||||
shareVo.setShareDescription(trimToNull(storyShare.getShareDescription()));
|
||||
shareVo.setShareQuote(trimToNull(storyShare.getShareQuote()));
|
||||
shareVo.setTemplateStyle(normalizeTemplateStyle(storyShare.getTemplateStyle()));
|
||||
shareVo.setHeroMomentId(storyShare.getHeroMomentId());
|
||||
List<String> featuredMomentIds = parseFeaturedMomentIds(storyShare.getFeaturedMomentIds());
|
||||
shareVo.setFeaturedMomentIds(featuredMomentIds);
|
||||
shareVo.setFeaturedMoments(loadFeaturedMoments(shareVo.getStoryInstanceId(), featuredMomentIds));
|
||||
}
|
||||
if (!StringUtils.hasText(shareVo.getTemplateStyle())) {
|
||||
shareVo.setTemplateStyle(DEFAULT_TEMPLATE_STYLE);
|
||||
}
|
||||
|
||||
if ((shareVo.getFeaturedMoments() == null || shareVo.getFeaturedMoments().isEmpty())
|
||||
&& StringUtils.hasText(shareVo.getInstanceId())) {
|
||||
@@ -187,21 +199,31 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
StoryDetailVo story = getAccessibleStory(storyId, currentUserId);
|
||||
StoryShare storyShare = storyShareMapper.selectLatestByStoryId(storyId);
|
||||
StoryItem publishedItem = storyItemMapper.selectPublishedByStoryId(storyId);
|
||||
boolean published = StringUtils.hasText(story.getShareId());
|
||||
String publicShareId = published ? story.getShareId() : null;
|
||||
String persistedShareId = StringUtils.hasText(publicShareId)
|
||||
? publicShareId
|
||||
: storyShare != null && StringUtils.hasText(storyShare.getShareId())
|
||||
? storyShare.getShareId()
|
||||
: publishedItem == null ? null : publishedItem.getShareId();
|
||||
|
||||
StoryShareConfigVo config = new StoryShareConfigVo();
|
||||
config.setStoryId(storyId);
|
||||
config.setShareId(StringUtils.hasText(story.getShareId()) ? story.getShareId() : storyShare == null ? null : storyShare.getShareId());
|
||||
config.setShareId(persistedShareId);
|
||||
config.setPublicShareId(publicShareId);
|
||||
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.setTemplateStyle(storyShare == null ? DEFAULT_TEMPLATE_STYLE : normalizeTemplateStyle(storyShare.getTemplateStyle()));
|
||||
config.setFeaturedMomentIds(storyShare == null
|
||||
? Collections.emptyList()
|
||||
: parseFeaturedMomentIds(storyShare.getFeaturedMomentIds()));
|
||||
config.setPublished(StringUtils.hasText(story.getShareId()));
|
||||
config.setPublished(published);
|
||||
config.setUpdatedAt(storyShare == null ? null : storyShare.getUpdateTime());
|
||||
hydrateShareFeedback(config, persistedShareId);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -249,6 +271,7 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
result.setStoryId(story.getInstanceId());
|
||||
result.setShareId(shareId);
|
||||
result.setHeroMomentId(heroItem.getInstanceId());
|
||||
result.setTemplateStyle(normalizeTemplateStyle(request.getTemplateStyle()));
|
||||
result.setPublicPath("/share/" + shareId);
|
||||
result.setPublishedAt(now);
|
||||
return result;
|
||||
@@ -266,7 +289,6 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
storyItemMapper.clearShareIdByStoryId(storyId, currentUserId, now);
|
||||
storyShareMapper.softDeleteByStoryId(storyId, currentUserId, now);
|
||||
storyMapper.touchUpdate(storyId, currentUserId);
|
||||
}
|
||||
|
||||
@@ -392,6 +414,7 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
storyShare.setShareTitle(trimToNull(request.getTitle()));
|
||||
storyShare.setShareDescription(trimToNull(request.getDescription()));
|
||||
storyShare.setShareQuote(trimToNull(request.getQuote()));
|
||||
storyShare.setTemplateStyle(normalizeTemplateStyle(request.getTemplateStyle()));
|
||||
storyShare.setFeaturedMomentIds(serializeFeaturedMomentIds(request.getFeaturedMomentIds(), heroMomentId));
|
||||
storyShare.setUpdateId(currentUserId);
|
||||
storyShare.setUpdateTime(now);
|
||||
@@ -465,4 +488,32 @@ public class StoryItemServiceImpl implements StoryItemService {
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String normalizeTemplateStyle(String templateStyle) {
|
||||
if (!StringUtils.hasText(templateStyle)) {
|
||||
return DEFAULT_TEMPLATE_STYLE;
|
||||
}
|
||||
String normalized = templateStyle.trim().toLowerCase();
|
||||
return switch (normalized) {
|
||||
case "cinematic", "scrapbook", "editorial" -> normalized;
|
||||
default -> DEFAULT_TEMPLATE_STYLE;
|
||||
};
|
||||
}
|
||||
|
||||
private void hydrateShareFeedback(StoryShareConfigVo config, String shareId) {
|
||||
if (config == null || !StringUtils.hasText(shareId)) {
|
||||
if (config != null) {
|
||||
config.setViewCount(0L);
|
||||
config.setCommentCount(0L);
|
||||
config.setRecentComments(Collections.emptyList());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
StoryShareMetric metric = storyShareFeedbackMapper.selectMetricByShareId(shareId);
|
||||
List<StoryShareCommentVo> recentComments = storyShareFeedbackMapper.selectRecentComments(shareId, 5);
|
||||
config.setViewCount(metric == null || metric.getViewCount() == null ? 0L : metric.getViewCount());
|
||||
config.setCommentCount(metric == null || metric.getCommentCount() == null ? 0L : metric.getCommentCount());
|
||||
config.setRecentComments(recentComments == null ? Collections.emptyList() : recentComments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ public class StoryServiceImpl implements StoryService {
|
||||
result.setItems(items);
|
||||
result.setInstanceId(story.getInstanceId());
|
||||
result.setShareId(story.getShareId());
|
||||
result.setPublicShareId(story.getPublicShareId());
|
||||
result.setOwnerId(story.getOwnerId());
|
||||
result.setUpdateId(story.getUpdateId());
|
||||
result.setTitle(story.getTitle());
|
||||
@@ -190,6 +191,8 @@ public class StoryServiceImpl implements StoryService {
|
||||
result.setUpdateName(story.getUpdateName());
|
||||
result.setPermissionType(story.getPermissionType());
|
||||
result.setItemCount(story.getItemCount());
|
||||
result.setShareConfigured(story.getShareConfigured());
|
||||
result.setSharePublished(story.getSharePublished());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -213,4 +216,4 @@ public class StoryServiceImpl implements StoryService {
|
||||
throw new CustomException(500, "Query stories failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.timeline.story.service.impl;
|
||||
|
||||
import com.timeline.common.exception.CustomException;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.common.utils.IdUtils;
|
||||
import com.timeline.story.dao.StoryItemMapper;
|
||||
import com.timeline.story.dao.StoryShareFeedbackMapper;
|
||||
import com.timeline.story.entity.StoryItem;
|
||||
import com.timeline.story.entity.StoryShareComment;
|
||||
import com.timeline.story.entity.StoryShareMetric;
|
||||
import com.timeline.story.service.StoryShareFeedbackService;
|
||||
import com.timeline.story.vo.StoryShareCommentRequest;
|
||||
import com.timeline.story.vo.StoryShareCommentVo;
|
||||
import com.timeline.story.vo.StoryShareFeedbackVo;
|
||||
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 java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class StoryShareFeedbackServiceImpl implements StoryShareFeedbackService {
|
||||
|
||||
private static final int DEFAULT_LIMIT = 8;
|
||||
private static final int MAX_LIMIT = 20;
|
||||
private static final int MAX_VISITOR_NAME_LENGTH = 40;
|
||||
private static final int MAX_COMMENT_LENGTH = 280;
|
||||
|
||||
@Autowired
|
||||
private StoryItemMapper storyItemMapper;
|
||||
|
||||
@Autowired
|
||||
private StoryShareFeedbackMapper storyShareFeedbackMapper;
|
||||
|
||||
@Override
|
||||
public StoryShareFeedbackVo getFeedback(String shareId, int limit) {
|
||||
validatePublicShare(shareId);
|
||||
storyShareFeedbackMapper.ensureMetricRow(shareId);
|
||||
return buildFeedbackVo(shareId, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public StoryShareFeedbackVo recordView(String shareId, int limit) {
|
||||
validatePublicShare(shareId);
|
||||
storyShareFeedbackMapper.ensureMetricRow(shareId);
|
||||
storyShareFeedbackMapper.incrementViewCount(shareId);
|
||||
return buildFeedbackVo(shareId, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public StoryShareFeedbackVo createComment(String shareId, StoryShareCommentRequest request, int limit) {
|
||||
validatePublicShare(shareId);
|
||||
|
||||
String visitorName = normalizeVisitorName(request == null ? null : request.getVisitorName());
|
||||
String content = normalizeContent(request == null ? null : request.getContent());
|
||||
|
||||
StoryShareComment comment = new StoryShareComment();
|
||||
comment.setInstanceId(IdUtils.randomUuid());
|
||||
comment.setShareId(shareId);
|
||||
comment.setVisitorName(visitorName);
|
||||
comment.setContent(content);
|
||||
comment.setCreateTime(LocalDateTime.now());
|
||||
comment.setUpdateTime(comment.getCreateTime());
|
||||
comment.setIsDelete(0);
|
||||
|
||||
storyShareFeedbackMapper.ensureMetricRow(shareId);
|
||||
storyShareFeedbackMapper.insertComment(comment);
|
||||
storyShareFeedbackMapper.incrementCommentCount(shareId);
|
||||
return buildFeedbackVo(shareId, limit);
|
||||
}
|
||||
|
||||
private StoryShareFeedbackVo buildFeedbackVo(String shareId, int limit) {
|
||||
StoryShareMetric metric = storyShareFeedbackMapper.selectMetricByShareId(shareId);
|
||||
List<StoryShareCommentVo> comments = storyShareFeedbackMapper.selectRecentComments(shareId, normalizeLimit(limit));
|
||||
|
||||
StoryShareFeedbackVo feedback = new StoryShareFeedbackVo();
|
||||
feedback.setShareId(shareId);
|
||||
feedback.setViewCount(metric == null || metric.getViewCount() == null ? 0L : metric.getViewCount());
|
||||
feedback.setCommentCount(metric == null || metric.getCommentCount() == null ? 0L : metric.getCommentCount());
|
||||
feedback.setComments(comments == null ? Collections.emptyList() : comments);
|
||||
return feedback;
|
||||
}
|
||||
|
||||
private void validatePublicShare(String shareId) {
|
||||
if (!StringUtils.hasText(shareId)) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "Share id is required");
|
||||
}
|
||||
|
||||
StoryItem item = storyItemMapper.selectByShareId(shareId);
|
||||
if (item == null || !StringUtils.hasText(item.getShareId())) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND, "Public share not found");
|
||||
}
|
||||
}
|
||||
|
||||
private int normalizeLimit(int limit) {
|
||||
if (limit <= 0) {
|
||||
return DEFAULT_LIMIT;
|
||||
}
|
||||
return Math.min(limit, MAX_LIMIT);
|
||||
}
|
||||
|
||||
private String normalizeVisitorName(String visitorName) {
|
||||
String normalized = StringUtils.hasText(visitorName) ? visitorName.trim() : "Guest";
|
||||
if (normalized.length() > MAX_VISITOR_NAME_LENGTH) {
|
||||
normalized = normalized.substring(0, MAX_VISITOR_NAME_LENGTH);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizeContent(String content) {
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "Comment content is required");
|
||||
}
|
||||
|
||||
String normalized = content.trim();
|
||||
if (normalized.length() > MAX_COMMENT_LENGTH) {
|
||||
normalized = normalized.substring(0, MAX_COMMENT_LENGTH);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,7 @@ public class StoryDetailVo extends Story {
|
||||
// 新增字段:故事项数量
|
||||
private Integer itemCount;
|
||||
private Integer permissionType;
|
||||
private String publicShareId;
|
||||
private Boolean shareConfigured;
|
||||
private Boolean sharePublished;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public class StoryItemShareVo extends StoryItem {
|
||||
private String shareTitle;
|
||||
private String shareDescription;
|
||||
private String shareQuote;
|
||||
private String templateStyle;
|
||||
private String heroMomentId;
|
||||
private List<String> images;
|
||||
private List<String> relatedImageInstanceIds;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.timeline.story.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StoryShareCommentRequest {
|
||||
private String visitorName;
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.timeline.story.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class StoryShareCommentVo {
|
||||
private String instanceId;
|
||||
private String shareId;
|
||||
private String visitorName;
|
||||
private String content;
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -8,12 +8,17 @@ import java.util.List;
|
||||
@Data
|
||||
public class StoryShareConfigVo {
|
||||
private String shareId;
|
||||
private String publicShareId;
|
||||
private String storyId;
|
||||
private String heroMomentId;
|
||||
private String title;
|
||||
private String description;
|
||||
private String quote;
|
||||
private String templateStyle;
|
||||
private List<String> featuredMomentIds;
|
||||
private Boolean published;
|
||||
private LocalDateTime updatedAt;
|
||||
private Long viewCount;
|
||||
private Long commentCount;
|
||||
private List<StoryShareCommentVo> recentComments;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.timeline.story.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class StoryShareFeedbackVo {
|
||||
private String shareId;
|
||||
private Long viewCount;
|
||||
private Long commentCount;
|
||||
private List<StoryShareCommentVo> comments;
|
||||
}
|
||||
@@ -11,5 +11,6 @@ public class StorySharePublishRequest {
|
||||
private String title;
|
||||
private String description;
|
||||
private String quote;
|
||||
private String templateStyle;
|
||||
private List<String> featuredMomentIds;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ public class StorySharePublishVo {
|
||||
private String storyId;
|
||||
private String shareId;
|
||||
private String heroMomentId;
|
||||
private String templateStyle;
|
||||
private String publicPath;
|
||||
private LocalDateTime publishedAt;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ public class TimelineArchiveBucketVo {
|
||||
private String subtitle;
|
||||
private String storyInstanceId;
|
||||
private String storyShareId;
|
||||
private Boolean shareConfigured;
|
||||
private Boolean sharePublished;
|
||||
private String coverInstanceId;
|
||||
private String coverSrc;
|
||||
private String sampleMomentTitle;
|
||||
|
||||
@@ -9,6 +9,8 @@ public class TimelineArchiveMomentVo {
|
||||
private String key;
|
||||
private String storyInstanceId;
|
||||
private String storyShareId;
|
||||
private Boolean shareConfigured;
|
||||
private Boolean sharePublished;
|
||||
private String storyTitle;
|
||||
private String storyTime;
|
||||
private String itemInstanceId;
|
||||
|
||||
@@ -40,6 +40,40 @@
|
||||
ORDER BY si.update_time DESC, si.create_time DESC
|
||||
LIMIT 1
|
||||
) AS share_id,
|
||||
(
|
||||
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 public_share_id,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM story_item si
|
||||
WHERE si.story_instance_id = s.instance_id
|
||||
AND si.share_id IS NOT NULL
|
||||
AND si.is_delete = 0
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS share_published,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM story_share ss
|
||||
WHERE ss.story_instance_id = s.instance_id
|
||||
AND ss.is_delete = 0
|
||||
) OR EXISTS (
|
||||
SELECT 1
|
||||
FROM story_item si
|
||||
WHERE si.story_instance_id = s.instance_id
|
||||
AND si.share_id IS NOT NULL
|
||||
AND si.is_delete = 0
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS share_configured,
|
||||
u1.username AS owner_name,
|
||||
u2.username AS update_name,
|
||||
sp.permission_type AS permission_type,
|
||||
@@ -70,6 +104,40 @@
|
||||
ORDER BY si.update_time DESC, si.create_time DESC
|
||||
LIMIT 1
|
||||
) AS share_id,
|
||||
(
|
||||
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 public_share_id,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM story_item si
|
||||
WHERE si.story_instance_id = s.instance_id
|
||||
AND si.share_id IS NOT NULL
|
||||
AND si.is_delete = 0
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS share_published,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM story_share ss
|
||||
WHERE ss.story_instance_id = s.instance_id
|
||||
AND ss.is_delete = 0
|
||||
) OR EXISTS (
|
||||
SELECT 1
|
||||
FROM story_item si
|
||||
WHERE si.story_instance_id = s.instance_id
|
||||
AND si.share_id IS NOT NULL
|
||||
AND si.is_delete = 0
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END AS share_configured,
|
||||
u1.username AS owner_name,
|
||||
u2.username AS update_name,
|
||||
sp.permission_type AS permission_type,
|
||||
@@ -99,4 +167,4 @@
|
||||
WHERE instance_id = #{instanceId}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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.StoryShareFeedbackMapper">
|
||||
|
||||
<insert id="ensureMetricRow">
|
||||
INSERT INTO story_share_metric (share_id, view_count, comment_count, create_time, update_time)
|
||||
VALUES (#{shareId}, 0, 0, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE update_time = VALUES(update_time)
|
||||
</insert>
|
||||
|
||||
<update id="incrementViewCount">
|
||||
UPDATE story_share_metric
|
||||
SET view_count = view_count + 1,
|
||||
update_time = NOW()
|
||||
WHERE share_id = #{shareId}
|
||||
</update>
|
||||
|
||||
<update id="incrementCommentCount">
|
||||
UPDATE story_share_metric
|
||||
SET comment_count = comment_count + 1,
|
||||
update_time = NOW()
|
||||
WHERE share_id = #{shareId}
|
||||
</update>
|
||||
|
||||
<select id="selectMetricByShareId" resultType="com.timeline.story.entity.StoryShareMetric">
|
||||
SELECT *
|
||||
FROM story_share_metric
|
||||
WHERE share_id = #{shareId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<insert id="insertComment">
|
||||
INSERT INTO story_share_comment (
|
||||
instance_id,
|
||||
share_id,
|
||||
visitor_name,
|
||||
content,
|
||||
create_time,
|
||||
update_time,
|
||||
is_delete
|
||||
) VALUES (
|
||||
#{instanceId},
|
||||
#{shareId},
|
||||
#{visitorName},
|
||||
#{content},
|
||||
#{createTime},
|
||||
#{updateTime},
|
||||
#{isDelete}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="selectRecentComments" resultType="com.timeline.story.vo.StoryShareCommentVo">
|
||||
SELECT
|
||||
instance_id,
|
||||
share_id,
|
||||
visitor_name,
|
||||
content,
|
||||
create_time
|
||||
FROM story_share_comment
|
||||
WHERE share_id = #{shareId}
|
||||
AND is_delete = 0
|
||||
ORDER BY create_time DESC, id DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -37,6 +37,7 @@
|
||||
share_title,
|
||||
share_description,
|
||||
share_quote,
|
||||
template_style,
|
||||
featured_moment_ids,
|
||||
create_id,
|
||||
update_id,
|
||||
@@ -50,6 +51,7 @@
|
||||
#{shareTitle},
|
||||
#{shareDescription},
|
||||
#{shareQuote},
|
||||
#{templateStyle},
|
||||
#{featuredMomentIds},
|
||||
#{createId},
|
||||
#{updateId},
|
||||
@@ -66,6 +68,7 @@
|
||||
share_title = #{shareTitle},
|
||||
share_description = #{shareDescription},
|
||||
share_quote = #{shareQuote},
|
||||
template_style = #{templateStyle},
|
||||
featured_moment_ids = #{featuredMomentIds},
|
||||
update_id = #{updateId},
|
||||
update_time = #{updateTime},
|
||||
|
||||
Reference in New Issue
Block a user