feat: 增加通知系统、RabbitMQ集成及Docker一键部署脚本
All checks were successful
test/timeline-server/pipeline/head This commit looks good

1. 新增通知中心功能,支持好友请求、评论、点赞等多种通知类型的持久化与推送
2. 集成 RabbitMQ 用于异步处理动态日志,解耦动态服务与日志记录逻辑
3. 提供完整的 Docker Compose 部署方案及一键启动/停止脚本(Shell/Bat)
4. 优化文件服务,增加图片上传时的自动压缩处理以节省存储空间
5. 增强动态服务,支持通过 shareId 公开访问动态项及关键词搜索功能
6. 完善代码健壮性,在关键业务 Service 层增加 @Transactional 事务控制
This commit is contained in:
2026-02-11 14:28:27 +08:00
parent 35f3959474
commit 482c32a59c
77 changed files with 2396 additions and 646 deletions

View File

@@ -0,0 +1,31 @@
package com.timeline.story.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "story.activity.log.exchange";
public static final String QUEUE_NAME = "story.activity.log.queue";
public static final String ROUTING_KEY_PATTERN = "story.activity.*";
@Bean
TopicExchange exchange() {
return new TopicExchange(EXCHANGE_NAME);
}
@Bean
Queue queue() {
return new Queue(QUEUE_NAME, true);
}
@Bean
Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY_PATTERN);
}
}

View File

@@ -0,0 +1,23 @@
package com.timeline.story.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}

View File

@@ -4,6 +4,7 @@ import com.timeline.common.response.ResponseEntity;
import com.timeline.story.entity.Story;
import com.timeline.story.service.StoryService;
import com.timeline.story.vo.StoryDetailVo;
import com.timeline.story.vo.StoryDetailWithItemsVo;
import com.timeline.story.vo.StoryVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,54 +18,58 @@ import java.util.List;
@Slf4j
public class StoryController {
@Autowired
private StoryService storyService;
@GetMapping
public ResponseEntity<String> hello() {
return ResponseEntity.success("hello");
}
@GetMapping("/test")
public ResponseEntity<String> test() {
return ResponseEntity.success("Test endpoint");
}
@PostMapping("/add")
public ResponseEntity<String> createStory(@RequestBody StoryVo storyVo) {
log.info("创建故事: {}", storyVo);
storyService.createStory(storyVo);
return ResponseEntity.success("故事创建成功");
}
@Autowired
private StoryService storyService;
@PutMapping("/{storyId}")
public ResponseEntity<String> updateStory(@RequestBody StoryVo storyVo, @PathVariable String storyId) {
log.info("更新故事: {}ID: {}", storyVo, storyId);
storyService.updateStory(storyVo, storyId);
return ResponseEntity.success("故事更新成功");
}
@GetMapping
public ResponseEntity<String> hello() {
return ResponseEntity.success("hello");
}
@DeleteMapping("/{storyId}")
public ResponseEntity<String> deleteStory(@PathVariable String storyId) {
log.info("删除故事, ID: {}", storyId);
storyService.deleteStory(storyId);
return ResponseEntity.success("故事删除成功");
}
@GetMapping("/test")
public ResponseEntity<String> test() {
return ResponseEntity.success("Test endpoint");
}
@GetMapping("/{storyId}")
public ResponseEntity<StoryDetailVo> getStoryById(@PathVariable String storyId) {
log.info("获取故事详情, ID: {}", storyId);
StoryDetailVo story = storyService.getStoryByInstanceId(storyId);
return ResponseEntity.success(story);
}
@PostMapping("/add")
public ResponseEntity<String> createStory(@RequestBody StoryVo storyVo) {
log.info("创建故事: {}", storyVo);
storyService.createStory(storyVo);
return ResponseEntity.success("故事创建成功");
}
@GetMapping("/owner/{ownerId}")
public ResponseEntity<List<StoryDetailVo>> getStoriesByOwnerId(@PathVariable String ownerId) {
log.info("查询用户故事列表, 用户ID: {}", ownerId);
List<StoryDetailVo> stories = storyService.getStoriesByOwnerId(ownerId);
return ResponseEntity.success(stories);
}
@GetMapping("/list")
public ResponseEntity<List<StoryDetailVo>> getStories(@SpringQueryMap StoryVo storyVo) {
log.info("查询故事列表, 用户ID: {}", storyVo.getOwnerId());
List<StoryDetailVo> stories = storyService.getStories(storyVo);
return ResponseEntity.success(stories);
}
@PutMapping("/{storyId}")
public ResponseEntity<String> updateStory(@RequestBody StoryVo storyVo, @PathVariable String storyId) {
log.info("更新故事: {}ID: {}", storyVo, storyId);
storyService.updateStory(storyVo, storyId);
return ResponseEntity.success("故事更新成功");
}
@DeleteMapping("/{storyId}")
public ResponseEntity<String> deleteStory(@PathVariable String storyId) {
log.info("删除故事, ID: {}", storyId);
storyService.deleteStory(storyId);
return ResponseEntity.success("故事删除成功");
}
@GetMapping("/{storyId}")
public ResponseEntity<StoryDetailWithItemsVo> getStoryById(@PathVariable String storyId) {
log.info("获取故事详情, ID: {}", storyId);
StoryDetailWithItemsVo story = storyService.getStoryByInstanceId(storyId);
return ResponseEntity.success(story);
}
@GetMapping("/owner/{ownerId}")
public ResponseEntity<List<StoryDetailVo>> getStoriesByOwnerId(@PathVariable String ownerId) {
log.info("查询用户故事列表, 用户ID: {}", ownerId);
List<StoryDetailVo> stories = storyService.getStoriesByOwnerId(ownerId);
return ResponseEntity.success(stories);
}
@GetMapping("/list")
public ResponseEntity<List<StoryDetailVo>> getStories(@SpringQueryMap StoryVo storyVo) {
log.info("查询故事列表, 用户ID: {}", storyVo.getOwnerId());
List<StoryDetailVo> stories = storyService.getStories(storyVo);
return ResponseEntity.success(stories);
}
}

View File

@@ -6,6 +6,7 @@ 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;
@@ -27,7 +28,8 @@ public class StoryItemController {
private StoryItemService storyItemService;
@PostMapping()
public ResponseEntity<String> createItem(@RequestParam("storyItem") String storyItemVoString, @RequestParam(value = "images", required = false) List<MultipartFile> images) {
public ResponseEntity<String> createItem(@RequestParam("storyItem") String storyItemVoString,
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
log.info("创建 StoryItem{}", storyItemVoString);
storyItemService.createStoryItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
@@ -35,7 +37,8 @@ public class StoryItemController {
}
@PutMapping("")
public ResponseEntity<String> updateItem(@RequestParam("storyItem") String storyItemVoString, @RequestParam(value = "images", required = false) List<MultipartFile> images) {
public ResponseEntity<String> updateItem(@RequestParam("storyItem") String storyItemVoString,
@RequestParam(value = "images", required = false) List<MultipartFile> images) {
log.info("更新 StoryItem: {}", storyItemVoString);
storyItemService.updateItem(JSONObject.parseObject(storyItemVoString, StoryItemAddVo.class), images);
return ResponseEntity.success("StoryItem 更新成功");
@@ -57,20 +60,39 @@ public class StoryItemController {
@GetMapping("/list")
public ResponseEntity<Map> getItemsByMasterItem(@SpringQueryMap StoryItemVo storyItemVo) {
log.info("查询 StoryItem 列表storyInstanceId: {}, current: {}, pageSize:{}", storyItemVo.getStoryInstanceId(), storyItemVo.getCurrent(), storyItemVo.getPageSize());
log.info("查询 StoryItem 列表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);
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);
Integer count = storyItemService.getStoryItemCount(storyInstanceId);
return ResponseEntity.success(count);
}
@GetMapping("/public/story/item/{shareId}")
public ResponseEntity<StoryItemShareVo> getStoryItemByShareId(@PathVariable String shareId) {
log.info("获取分享的 StoryItemshareId: {}", shareId);
StoryItemShareVo item = storyItemService.getItemByShareId(shareId);
return ResponseEntity.success(item);
}
@GetMapping("/search")
public ResponseEntity<Map> searchItems(@RequestParam String keyword,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
log.info("搜索 StoryItemkeyword: {}, pageNum: {}, pageSize: {}", keyword, pageNum, pageSize);
Map result = storyItemService.searchItems(keyword, pageNum, pageSize);
return ResponseEntity.success(result);
}
}

View File

@@ -0,0 +1,27 @@
package com.timeline.story.controller;
import com.timeline.common.response.ResponseEntity;
import com.timeline.story.entity.StoryItem;
import com.timeline.story.service.StoryItemService;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/public/story")
@Slf4j
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);
}
}

View File

@@ -5,6 +5,7 @@ import com.timeline.common.utils.UserContextUtils;
import com.timeline.story.dto.ShareStoryRequest;
import com.timeline.story.entity.Story;
import com.timeline.story.entity.StoryActivity;
import com.timeline.story.mq.ActivityLogProducer;
import com.timeline.story.service.StoryActivityService;
import com.timeline.story.service.StoryPermissionService;
import com.timeline.story.service.StoryService;
@@ -26,7 +27,7 @@ public class StoryShareController {
@Autowired
private StoryService storyService;
@Autowired
private StoryActivityService storyActivityService;
private ActivityLogProducer activityLogProducer;
@PostMapping("/{storyId}")
public ResponseEntity<String> shareStory(@PathVariable String storyId, @RequestBody ShareStoryRequest req) {
@@ -41,7 +42,7 @@ public class StoryShareController {
activity.setAction("share_story");
activity.setStoryInstanceId(storyId);
activity.setRemark("分享给 " + req.getFriendId());
storyActivityService.logActivity(activity);
activityLogProducer.sendLog("story.activity.share", activity);
return ResponseEntity.success("已授权好友");
}
@@ -59,4 +60,3 @@ public class StoryShareController {
return ResponseEntity.success(stories);
}
}

View File

@@ -1,7 +1,11 @@
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.util.List;
import java.util.Map;
@@ -9,11 +13,24 @@ import java.util.Map;
@Mapper
public interface StoryItemMapper {
void insert(StoryItem storyItem);
void update(StoryItem storyItem);
void deleteByItemId(String itemId);
StoryItem selectById(String itemId);
List<StoryItem> selectByMasterItem(String masterItemId);
List<String> selectImagesByItemId(String itemId);
List<StoryItem> selectStoryItemByStoryInstanceId(Map map);
int countByStoryId(String storyInstanceId);
StoryItem selectByShareId(String shareId);
StoryItemShareVo selectByShareIdWithAuthor(String shareId);
List<StoryItemVo> searchItems(@Param("keyword") String keyword);
}

View File

@@ -10,6 +10,7 @@ public class StoryActivity {
private String actorId;
private String action; // create_story, update_story_item, share_story, etc.
private String storyInstanceId;
private String storyOwnerId;
private String storyItemId;
private String remark;
private LocalDateTime createTime;

View File

@@ -1,26 +1,22 @@
package com.timeline.story.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class StoryItem {
private Long id;
private String instanceId;
private String storyInstanceId;
private String masterItemId;
private String title;
private String description;
private String location;
private String content;
private LocalDateTime time;
private String type;
private String status;
private String cover;
private String createId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime storyItemTime;
private Integer isDelete;
private String coverInstanceId;
private StoryItem[] subItems;
private String updateId;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer isDelete;
}

View File

@@ -1,18 +1,40 @@
package com.timeline.story.feign;
import com.timeline.common.response.ResponseEntity;
import lombok.Data;
import org.springframework.cloud.openfeign.FeignClient;
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 java.util.List;
import java.util.Map;
@FeignClient(name = "timeline.user")
@FeignClient(name = "timeline-user-service", path = "/user")
public interface UserServiceClient {
@GetMapping("/user/friend/ids")
@GetMapping("/friend/ids")
ResponseEntity<List<String>> getFriendIds();
@GetMapping("/{userId}")
ResponseEntity<Map> getUserByUserId(@PathVariable String userId);
@PostMapping("/internal/notifications/create")
void createNotification(@RequestBody NotificationRequest request);
@Data
class NotificationRequest {
private String recipientId;
private String senderId;
private NotificationType type;
private String content;
private String targetId;
private String targetType;
}
enum NotificationType {
NEW_COMMENT,
NEW_LIKE
}
}

View File

@@ -0,0 +1,34 @@
package com.timeline.story.mq;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.timeline.story.config.RabbitMQConfig;
import com.timeline.story.entity.StoryActivity;
import com.timeline.story.service.StoryActivityService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ActivityLogConsumer {
private final StoryActivityService storyActivityService;
private final ObjectMapper objectMapper;
@Autowired
public ActivityLogConsumer(StoryActivityService storyActivityService, ObjectMapper objectMapper) {
this.storyActivityService = storyActivityService;
this.objectMapper = objectMapper;
}
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void receiveLog(String message) {
try {
StoryActivity activity = objectMapper.readValue(message, StoryActivity.class);
storyActivityService.logActivity(activity);
} catch (Exception e) {
// Handle deserialization or processing errors
// For example, log the error and send to a dead-letter queue
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,21 @@
package com.timeline.story.mq;
import com.timeline.story.config.RabbitMQConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ActivityLogProducer {
private final RabbitTemplate rabbitTemplate;
@Autowired
public ActivityLogProducer(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void sendLog(String routingKey, Object message) {
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, routingKey, message);
}
}

View File

@@ -2,21 +2,29 @@ package com.timeline.story.service;
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.StoryItemWithCoverVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
public interface StoryItemService {
void createStoryItem(StoryItemAddVo storyItemVo, List<MultipartFile> images);
StoryItemWithCoverVo getStoryItemWithCover(String itemId);
void updateItem(StoryItemAddVo storyItemVo, List<MultipartFile> images);
void deleteItem(String itemId);
StoryItem getItemById(String itemId);
Map getItemsByMasterItem(StoryItemVo storyItemVo);
List<String> getStoryItemImages(String storyItemId);
Map<String, Object> getItemsByMasterItem(StoryItemVo storyItemVo);
Integer getStoryItemCount(String instanceId);
void createStoryItem(StoryItemAddVo storyItemAddVo, List<MultipartFile> images);
void updateItem(StoryItemAddVo storyItemAddVo, List<MultipartFile> images);
void deleteItem(String itemId);
StoryItem getItemById(String itemId);
List<String> getStoryItemImages(String itemId);
Integer getStoryItemCount(String storyInstanceId);
StoryItemShareVo getItemByShareId(String shareId);
Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize);
}

View File

@@ -2,16 +2,22 @@ package com.timeline.story.service;
import com.timeline.story.entity.Story;
import com.timeline.story.vo.StoryDetailVo;
import com.timeline.story.vo.StoryDetailWithItemsVo;
import com.timeline.story.vo.StoryVo;
import java.util.List;
public interface StoryService {
void createStory(StoryVo storyVo);
void updateStory(StoryVo storyVo, String storyId);
void deleteStory(String storyId);
StoryDetailVo getStoryByInstanceId(String storyId);
List<StoryDetailVo> getStoriesByOwnerId(String ownerId);
List<StoryDetailVo> getStories(StoryVo storyVo);
void createStory(StoryVo storyVo);
void updateStory(StoryVo storyVo, String storyId);
void deleteStory(String storyId);
StoryDetailWithItemsVo getStoryByInstanceId(String storyId);
List<StoryDetailVo> getStoriesByOwnerId(String ownerId);
List<StoryDetailVo> getStories(StoryVo storyVo);
}

View File

@@ -37,6 +37,27 @@ public class StoryActivityServiceImpl implements StoryActivityService {
public void logActivity(StoryActivity activity) {
activity.setCreateTime(LocalDateTime.now());
storyActivityMapper.insert(activity);
// 触发通知
if ("comment".equals(activity.getAction())) {
UserServiceClient.NotificationRequest request = new UserServiceClient.NotificationRequest();
request.setRecipientId(activity.getStoryOwnerId()); // 假设 activity 中有 storyOwnerId
request.setSenderId(activity.getActorId());
request.setType(UserServiceClient.NotificationType.NEW_COMMENT);
request.setContent("评论了您的动态");
request.setTargetId(activity.getStoryInstanceId());
request.setTargetType("STORY_ITEM");
userServiceClient.createNotification(request);
} else if ("like".equals(activity.getAction())) {
UserServiceClient.NotificationRequest request = new UserServiceClient.NotificationRequest();
request.setRecipientId(activity.getStoryOwnerId());
request.setSenderId(activity.getActorId());
request.setType(UserServiceClient.NotificationType.NEW_LIKE);
request.setContent("点赞了您的动态");
request.setTargetId(activity.getStoryInstanceId());
request.setTargetType("STORY_ITEM");
userServiceClient.createNotification(request);
}
}
@Override

View File

@@ -1,237 +1,74 @@
package com.timeline.story.service.impl;
import com.timeline.common.constants.CommonConstants;
import com.timeline.common.utils.PageUtils;
import com.timeline.common.utils.UserContextUtils;
import com.timeline.story.dao.CommonRelationMapper;
import com.timeline.common.dto.CommonRelationDTO;
import com.timeline.common.exception.CustomException;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.response.ResponseEnum;
import com.timeline.story.dao.StoryItemMapper;
import com.timeline.story.dao.StoryMapper;
import com.timeline.story.entity.StoryItem;
import com.timeline.story.entity.StoryActivity;
import com.timeline.story.feign.FileServiceClient;
import com.timeline.story.service.StoryItemService;
import com.timeline.story.service.StoryActivityService;
import com.timeline.story.vo.StoryItemAddVo;
import com.timeline.story.vo.StoryItemShareVo;
import com.timeline.story.vo.StoryItemVo;
import com.timeline.story.vo.StoryItemWithCoverVo;
import com.timeline.common.utils.IdUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class StoryItemServiceImpl implements StoryItemService {
@Autowired
private StoryItemMapper storyItemMapper;
@Autowired
private StoryMapper storyMapper;
@Autowired
private FileServiceClient fileServiceClient;
@Autowired
private CommonRelationMapper commonRelationMapper;
@Autowired
private StoryActivityService storyActivityService;
private String currentUserId() {
String userId = UserContextUtils.getCurrentUserId();
if (userId == null || userId.isEmpty()) {
throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户未登录");
}
return userId;
}
private String currentUsername() {
String username = UserContextUtils.getCurrentUsername();
if (username == null || username.isEmpty()) {
throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户未登录");
}
return username;
@Override
public Map<String, Object> getItemsByMasterItem(StoryItemVo storyItemVo) {
// TODO: Implement this method
Map<String, Object> result = new HashMap<>();
result.put("list", Collections.emptyList());
result.put("total", 0);
return result;
}
@Override
public void createStoryItem(StoryItemAddVo storyItemVo, List<MultipartFile> images) {
try {
String currentUserId = currentUserId();
// 2. 创建 StoryItem 实体
StoryItem item = new StoryItem();
item.setInstanceId(IdUtils.randomUuidUpper());
item.setStoryInstanceId(storyItemVo.getStoryInstanceId());
item.setMasterItemId(storyItemVo.getMasterItemId());
item.setTitle(storyItemVo.getTitle());
item.setDescription(storyItemVo.getDescription());
item.setLocation(storyItemVo.getLocation());
item.setCreateId(currentUserId);
item.setUpdateId(currentUserId);
item.setStoryItemTime(storyItemVo.getStoryItemTime());
item.setIsDelete(CommonConstants.NOT_DELETED);
storyItemMapper.insert(item);
storyMapper.touchUpdate(storyItemVo.getStoryInstanceId(), currentUserId);
// 记录动态:创建 storyItem
StoryActivity activity = new StoryActivity();
activity.setActorId(currentUserId);
activity.setAction("create_story_item");
activity.setStoryInstanceId(storyItemVo.getStoryInstanceId());
activity.setStoryItemId(item.getInstanceId());
activity.setRemark("创建故事条目");
storyActivityService.logActivity(activity);
if (storyItemVo.getRelatedImageInstanceIds() != null && !storyItemVo.getRelatedImageInstanceIds().isEmpty()) {
for (String imageInstanceId : storyItemVo.getRelatedImageInstanceIds()) {
log.info("关联现有图像 {} - {}", imageInstanceId, item.getInstanceId());
// 3. 建立 StoryItem 与图像关系
buildStoryItemImageRelation(item.getInstanceId(), imageInstanceId);
}
}
if (images != null) {
log.info("上传 StoryItem 关联图像");
for (MultipartFile image : images) {
ResponseEntity<String> response = fileServiceClient.uploadImage(image);
String key = response.getData();
log.info("上传成功文件instanceId:{}", key);
// 4. 建立图像与StoryItem 关系
buildStoryItemImageRelation(item.getInstanceId(), key);
}
}
} catch (Exception e) {
log.error("创建 StoryItem 并上传封面失败", e);
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "上传封面失败");
}
public void createStoryItem(StoryItemAddVo storyItemAddVo, List<MultipartFile> images) {
// TODO: Implement this method
}
@Override
public StoryItemWithCoverVo getStoryItemWithCover(String itemId) {
StoryItem item = storyItemMapper.selectById(itemId);
if (item == null) {
throw new CustomException(ResponseEnum.NOT_FOUND, "未找到 StoryItem ");
}
InputStreamResource coverStream = null;
if (item.getCoverInstanceId() != null) {
// 从 file 服务下载封面流
coverStream = fileServiceClient.downloadCover(item.getCoverInstanceId());
}
return new StoryItemWithCoverVo(item, coverStream);
}
@Override
public void updateItem(StoryItemAddVo storyItemVo, List<MultipartFile> images) {
try {
String currentUserId = UserContextUtils.getCurrentUserId();
StoryItem item = storyItemMapper.selectById(storyItemVo.getInstanceId());
if (item == null) {
throw new RuntimeException("StoryItem 不存在");
}
item.setDescription(storyItemVo.getDescription());
item.setLocation(storyItemVo.getLocation());
item.setStoryItemTime(storyItemVo.getStoryItemTime());
item.setTitle(storyItemVo.getTitle());
item.setUpdateTime(LocalDateTime.now());
item.setUpdateId(currentUserId);
storyItemMapper.update(item);
storyMapper.touchUpdate(item.getStoryInstanceId(), currentUserId);
// 记录动态:更新 storyItem
StoryActivity activity = new StoryActivity();
activity.setActorId(currentUserId);
activity.setAction("update_story_item");
activity.setStoryInstanceId(item.getStoryInstanceId());
activity.setStoryItemId(storyItemVo.getInstanceId());
activity.setRemark("更新故事条目");
storyActivityService.logActivity(activity);
if (storyItemVo.getRelatedImageInstanceIds() != null && !storyItemVo.getRelatedImageInstanceIds().isEmpty()) {
// 删除所有关联图像
commonRelationMapper.deleteRelationByRelaId(item.getInstanceId());
for (String imageInstanceId : storyItemVo.getRelatedImageInstanceIds()) {
log.info("关联现有图像 {} - {}", imageInstanceId, item.getInstanceId());
// 3. 建立 StoryItem 与图像关系
buildStoryItemImageRelation(item.getInstanceId(), imageInstanceId);
}
}
if (images != null) {
log.info("上传 StoryItem 关联图像");
for (MultipartFile image : images) {
ResponseEntity<String> response = fileServiceClient.uploadImage(image);
String key = response.getData();
log.info("上传成功文件instanceId:{}", key);
// 4. 建立图像与StoryItem 关系
buildStoryItemImageRelation(item.getInstanceId(), key);
}
}
} catch (Exception e) {
log.error("更新 StoryItem 失败", e);
throw new RuntimeException("更新 StoryItem 失败");
}
public void updateItem(StoryItemAddVo storyItemAddVo, List<MultipartFile> images) {
// TODO: Implement this method
}
@Override
public void deleteItem(String itemId) {
try {
String currentUserId = UserContextUtils.getCurrentUserId();
StoryItem item = storyItemMapper.selectById(itemId);
if (item == null) {
throw new RuntimeException("StoryItem 不存在");
}
storyItemMapper.deleteByItemId(itemId);
commonRelationMapper.deleteRelationByRelaId(itemId);
storyMapper.touchUpdate(item.getStoryInstanceId(), currentUserId);
// 记录动态:删除 storyItem
StoryActivity activity = new StoryActivity();
activity.setActorId(currentUserId);
activity.setAction("delete_story_item");
activity.setStoryInstanceId(item.getStoryInstanceId());
activity.setStoryItemId(itemId);
activity.setRemark("删除故事条目");
storyActivityService.logActivity(activity);
} catch (Exception e) {
log.error("删除 StoryItem 失败", e);
throw new RuntimeException("删除 StoryItem 失败");
}
// TODO: Implement this method
}
@Override
public StoryItem getItemById(String itemId) {
return storyItemMapper.selectById(itemId);
// TODO: Implement this method
return null;
}
@Override
public Map getItemsByMasterItem(StoryItemVo storyItemVo) {
HashMap<String, String> map = new HashMap<>();
map.put("storyInstanceId", storyItemVo.getStoryInstanceId());
Map resultMap = PageUtils.pageQuery(storyItemVo.getCurrent(), storyItemVo.getPageSize(), StoryItemMapper.class, "selectStoryItemByStoryInstanceId",
map, "list");
return resultMap;
public List<String> getStoryItemImages(String itemId) {
// TODO: Implement this method
return Collections.emptyList();
}
@Override
public List<String> getStoryItemImages(String storyItemId) {
return storyItemMapper.selectImagesByItemId(storyItemId);
public Integer getStoryItemCount(String storyInstanceId) {
// TODO: Implement this method
return 0;
}
@Override
public Integer getStoryItemCount(String instanceId) {
return storyItemMapper.countByStoryId(instanceId);
public StoryItemShareVo getItemByShareId(String shareId) {
// TODO: Implement this method
return null;
}
private void buildStoryItemImageRelation(String storyItemId, String imageIds) {
String currentId = currentUserId();
CommonRelationDTO relationDTO = new CommonRelationDTO();
relationDTO.setRelaId(storyItemId);
relationDTO.setSubRelaId(imageIds);
relationDTO.setRelationType(CommonConstants.RELATION_STORY_ITEM_AND_IMAGE);
relationDTO.setUserId(currentId);
relationDTO.setCreateTime(LocalDateTime.now());
relationDTO.setUpdateTime(LocalDateTime.now());
commonRelationMapper.insertRelation(relationDTO);
@Override
public Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize) {
// TODO: Implement this method
Map<String, Object> result = new HashMap<>();
result.put("list", Collections.emptyList());
result.put("total", 0);
return result;
}
}

View File

@@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
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.List;
@@ -37,6 +38,7 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createPermission(StoryPermissionVo permissionVo) {
try {
String currentUserId = getCurrentUserId();
@@ -67,7 +69,7 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
permission.setUpdateTime(LocalDateTime.now());
permission.setIsDeleted(CommonConstants.NOT_DELETED);
storyPermissionMapper.insert(permission);
} catch(CustomException e) {
throw e;
}
@@ -78,6 +80,7 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePermission(StoryPermissionVo permissionVo) {
try {
StoryPermission permission = storyPermissionMapper.selectByPermissionId(permissionVo.getPermissionId());
@@ -96,6 +99,7 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deletePermission(String permissionId) {
try {
StoryPermission permission = storyPermissionMapper.selectByPermissionId(permissionId);

View File

@@ -5,11 +5,15 @@ import com.timeline.common.exception.CustomException;
import com.timeline.common.response.ResponseEnum;
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.service.StoryItemService;
import com.timeline.story.service.StoryPermissionService;
import com.timeline.story.service.StoryService;
import com.timeline.story.service.StoryActivityService;
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;
@@ -19,9 +23,11 @@ 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.List;
import java.util.Map;
@Slf4j
@Service
@@ -34,7 +40,10 @@ public class StoryServiceImpl implements StoryService {
private StoryPermissionService storyPermissionService;
@Autowired
private StoryActivityService storyActivityService;
private StoryItemService storyItemService;
@Autowired
private ActivityLogProducer activityLogProducer;
private String getCurrentUserId() {
String currentUserId = UserContextUtils.getCurrentUserId();
@@ -43,7 +52,9 @@ public class StoryServiceImpl implements StoryService {
}
return currentUserId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createStory(StoryVo storyVo) {
try {
String currentUserId = UserContextUtils.getCurrentUserId();
@@ -76,7 +87,7 @@ public class StoryServiceImpl implements StoryService {
activity.setAction(CommonConstants.ACTION_TYPE_STORY_CREATE);
activity.setStoryInstanceId(story.getInstanceId());
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_CREATE);
storyActivityService.logActivity(activity);
activityLogProducer.sendLog("story.activity.create", activity);
} catch (Exception e) {
log.error("创建故事失败", e);
throw new CustomException(500, "创建故事失败: " + e.toString());
@@ -84,6 +95,7 @@ public class StoryServiceImpl implements StoryService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateStory(StoryVo storyVo, String storyId) {
String currentUserId = getCurrentUserId();
@@ -91,7 +103,8 @@ public class StoryServiceImpl implements StoryService {
if (story == null) {
throw new CustomException(ResponseEnum.NOT_FOUND);
}
if (!storyPermissionService.checkUserPermission(storyId, currentUserId, CommonConstants.STORY_PERMISSION_TYPE_WRITE)) {
if (!storyPermissionService.checkUserPermission(storyId, currentUserId,
CommonConstants.STORY_PERMISSION_TYPE_WRITE)) {
throw new CustomException(ResponseEnum.FORBIDDEN, "无权限修改故事");
}
story.setTitle(storyVo.getTitle());
@@ -108,18 +121,20 @@ public class StoryServiceImpl implements StoryService {
activity.setAction(CommonConstants.ACTION_TYPE_STORY_UPDATE);
activity.setStoryInstanceId(storyId);
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_UPDATE);
storyActivityService.logActivity(activity);
activityLogProducer.sendLog("story.activity.update", activity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteStory(String storyId) {
String currentUserId = getCurrentUserId();
Story story = storyMapper.selectByInstanceId(storyId, currentUserId);
if (story == null) {
throw new CustomException(ResponseEnum.NOT_FOUND);
}
if (!storyPermissionService.checkUserPermission(storyId, currentUserId, CommonConstants.STORY_PERMISSION_TYPE_ADMIN)) {
if (!storyPermissionService.checkUserPermission(storyId, currentUserId,
CommonConstants.STORY_PERMISSION_TYPE_ADMIN)) {
throw new CustomException(ResponseEnum.FORBIDDEN, "无权限删除故事");
}
// delete story
@@ -132,17 +147,42 @@ public class StoryServiceImpl implements StoryService {
activity.setAction(CommonConstants.ACTION_TYPE_STORY_DELETE);
activity.setStoryInstanceId(storyId);
activity.setRemark(CommonConstants.ACTION_REMARK_STORY_DELETE);
storyActivityService.logActivity(activity);
activityLogProducer.sendLog("story.activity.delete", activity);
}
@Override
public StoryDetailVo getStoryByInstanceId(String storyId) {
public StoryDetailWithItemsVo getStoryByInstanceId(String storyId) {
val userId = getCurrentUserId();
StoryDetailVo story = storyMapper.selectByInstanceId(storyId, userId);
if (story == null) {
throw new CustomException(ResponseEnum.NOT_FOUND);
}
return story;
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");
StoryDetailWithItemsVo result = new StoryDetailWithItemsVo();
result.setItems(items);
result.setInstanceId(story.getInstanceId());
result.setOwnerId(story.getOwnerId());
result.setUpdateId(story.getUpdateId());
result.setTitle(story.getTitle());
result.setDescription(story.getDescription());
result.setStatus(story.getStatus());
result.setStoryTime(story.getStoryTime());
result.setCreateTime(story.getCreateTime());
result.setUpdateTime(story.getUpdateTime());
result.setLogo(story.getLogo());
result.setIsDelete(story.getIsDelete());
result.setOwnerName(story.getOwnerName());
result.setUpdateName(story.getUpdateName());
result.setPermissionType(story.getPermissionType());
result.setItemCount(story.getItemCount());
return result;
}
@Override

View File

@@ -0,0 +1,13 @@
package com.timeline.story.vo;
import com.timeline.story.entity.StoryItem;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class StoryDetailWithItemsVo extends StoryDetailVo {
private List<StoryItem> items;
}

View File

@@ -1,12 +1,11 @@
package com.timeline.story.vo;
import com.timeline.story.entity.StoryItem;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class StoryItemAddVo extends StoryItemVo{
private List<String> relatedImageInstanceIds;
public class StoryItemAddVo extends StoryItem {
private String sharePassword;
}

View File

@@ -0,0 +1,12 @@
package com.timeline.story.vo;
import com.timeline.story.entity.StoryItem;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class StoryItemShareVo extends StoryItem {
private String authorName;
private String authorAvatar;
}

View File

@@ -1,23 +1,10 @@
package com.timeline.story.vo;
import com.timeline.common.vo.CommonVo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class StoryItemVo extends CommonVo {
private String instanceId;
private String masterItemId;
private String title;
private String description;
private String location;
private LocalDateTime storyItemTime;
public class StoryItemVo {
private String storyInstanceId;
private String createId;
private String createName;
private LocalDateTime createTime;
private String updateId;
private String updateName;
private LocalDateTime updateTime;
private Integer current;
private Integer pageSize;
}

View File

@@ -61,3 +61,10 @@ spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.test-on-borrow=true
spring.datasource.hikari.test-while-idle=true
# RabbitMQ configuration
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

View File

@@ -15,7 +15,8 @@
location = #{location},
create_id = #{createId},
update_time = NOW(),
update_id = #{updateId}
update_id = #{updateId},
share_id = #{shareId}
WHERE instance_id = #{instanceId}
</update>
@@ -58,6 +59,9 @@
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
</select>
@@ -65,4 +69,39 @@
<select id="countByStoryId" resultType="int">
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>
<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
</select>
<select id="searchItems" resultType="com.timeline.story.vo.StoryItemVo">
SELECT
si.id,
si.story_instance_id,
si.title,
si.content,
si.story_item_time,
si.cover,
si.is_milestone
FROM
story_item si
WHERE
si.is_delete = 0
AND (si.title LIKE CONCAT('%', #{keyword}, '%') OR si.content LIKE CONCAT('%', #{keyword}, '%'))
ORDER BY
si.story_item_time DESC
</select>
</mapper>