diff --git a/deploy/update_story_share.sql b/deploy/update_story_share.sql index df8a6d2..ee4fabf 100644 --- a/deploy/update_story_share.sql +++ b/deploy/update_story_share.sql @@ -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, diff --git a/deploy/update_story_share_feedback.sql b/deploy/update_story_share_feedback.sql new file mode 100644 index 0000000..b9f9133 --- /dev/null +++ b/deploy/update_story_share_feedback.sql @@ -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; diff --git a/deploy/update_story_share_template.sql b/deploy/update_story_share_template.sql new file mode 100644 index 0000000..528b13d --- /dev/null +++ b/deploy/update_story_share_template.sql @@ -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; diff --git a/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java b/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java index 670543c..5a4c200 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java +++ b/timeline-story-service/src/main/java/com/timeline/story/controller/StoryPublicController.java @@ -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 getStoryItemByShareId(@PathVariable String shareId) { log.info("Get public story by shareId: {}", shareId); StoryItemShareVo storyItem = storyItemService.getItemByShareId(shareId); return ResponseEntity.success(storyItem); } -} \ No newline at end of file + + @GetMapping("/{shareId}/feedback") + public ResponseEntity 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 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 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)); + } +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareFeedbackMapper.java b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareFeedbackMapper.java new file mode 100644 index 0000000..9b08d21 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/dao/StoryShareFeedbackMapper.java @@ -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 selectRecentComments( + @Param("shareId") String shareId, + @Param("limit") int limit); +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java index 1f7df7e..179e025 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java +++ b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShare.java @@ -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; diff --git a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShareComment.java b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShareComment.java new file mode 100644 index 0000000..3fa95e4 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShareComment.java @@ -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; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShareMetric.java b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShareMetric.java new file mode 100644 index 0000000..8789168 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/entity/StoryShareMetric.java @@ -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; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/StoryShareFeedbackService.java b/timeline-story-service/src/main/java/com/timeline/story/service/StoryShareFeedbackService.java new file mode 100644 index 0000000..5399cd5 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/service/StoryShareFeedbackService.java @@ -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); +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java index 5749ffb..0cbcdbb 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/AnalyticsServiceImpl.java @@ -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 sortArchiveBuckets(Map bucketMap) { return bucketMap.values().stream() .sorted((left, right) -> { diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java index 9ae403b..ea32b7a 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryItemServiceImpl.java @@ -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 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 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); + } } diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java index e63f8fd..25c32d1 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryServiceImpl.java @@ -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"); } } -} \ No newline at end of file +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryShareFeedbackServiceImpl.java b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryShareFeedbackServiceImpl.java new file mode 100644 index 0000000..02242f6 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/service/impl/StoryShareFeedbackServiceImpl.java @@ -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 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; + } +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryDetailVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryDetailVo.java index f68acf2..cf9229e 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryDetailVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryDetailVo.java @@ -13,4 +13,7 @@ public class StoryDetailVo extends Story { // 新增字段:故事项数量 private Integer itemCount; private Integer permissionType; + private String publicShareId; + private Boolean shareConfigured; + private Boolean sharePublished; } diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java index 69ed745..6208d05 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryItemShareVo.java @@ -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 images; private List relatedImageInstanceIds; diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareCommentRequest.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareCommentRequest.java new file mode 100644 index 0000000..cb1fa30 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareCommentRequest.java @@ -0,0 +1,9 @@ +package com.timeline.story.vo; + +import lombok.Data; + +@Data +public class StoryShareCommentRequest { + private String visitorName; + private String content; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareCommentVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareCommentVo.java new file mode 100644 index 0000000..5647a79 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareCommentVo.java @@ -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; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java index bc697ef..903929c 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareConfigVo.java @@ -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 featuredMomentIds; private Boolean published; private LocalDateTime updatedAt; + private Long viewCount; + private Long commentCount; + private List recentComments; } diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareFeedbackVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareFeedbackVo.java new file mode 100644 index 0000000..a166190 --- /dev/null +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StoryShareFeedbackVo.java @@ -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 comments; +} diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java index 28763f2..1e12fe3 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishRequest.java @@ -11,5 +11,6 @@ public class StorySharePublishRequest { private String title; private String description; private String quote; + private String templateStyle; private List featuredMomentIds; } diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java index 85566a9..3b57e8d 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/StorySharePublishVo.java @@ -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; } diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java index 1e2c04e..a7efdb0 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveBucketVo.java @@ -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; diff --git a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java index 5c8bae3..ba1caef 100644 --- a/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java +++ b/timeline-story-service/src/main/java/com/timeline/story/vo/TimelineArchiveMomentVo.java @@ -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; diff --git a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml index 59c46d1..c0c5fea 100644 --- a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml +++ b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryMapper.xml @@ -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} - \ No newline at end of file + diff --git a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareFeedbackMapper.xml b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareFeedbackMapper.xml new file mode 100644 index 0000000..49b1ced --- /dev/null +++ b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareFeedbackMapper.xml @@ -0,0 +1,68 @@ + + + + + + + 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) + + + + UPDATE story_share_metric + SET view_count = view_count + 1, + update_time = NOW() + WHERE share_id = #{shareId} + + + + UPDATE story_share_metric + SET comment_count = comment_count + 1, + update_time = NOW() + WHERE share_id = #{shareId} + + + + + + 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} + ) + + + + + diff --git a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml index 0944597..8b064f5 100644 --- a/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml +++ b/timeline-story-service/src/main/resources/com/timeline/story/dao/StoryShareMapper.xml @@ -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},