feat: 新增协作邀请功能与标签管理
Some checks failed
test/timeline-server/pipeline/head Something is wrong with the build of this commit

新增故事协作邀请功能,包括邀请状态字段和相关接口
添加标签管理功能,支持时间线节点的标签分类
实现智能填充服务,从图片EXIF提取时间和地点信息
优化Docker镜像使用Alpine基础镜像减少体积
新增批量操作功能,包括排序、删除和时间修改
扩展通知系统支持协作邀请相关消息
添加评论和提醒功能相关实体和服务接口
This commit is contained in:
2026-02-24 10:32:35 +08:00
parent d645164daa
commit 40412f6f67
49 changed files with 3816 additions and 15 deletions

View File

@@ -0,0 +1,195 @@
package com.timeline.story.controller;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.utils.UserContextUtils;
import com.timeline.story.service.AnalyticsService;
import com.timeline.story.vo.TimelineAnalyticsVo;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* AnalyticsController - 数据分析控制器
*
* 功能描述:
* 提供时间线数据分析的 REST API 接口。
*
* API 列表:
* - GET /story/analytics/overview: 获取总体统计
* - GET /story/analytics/story/{storyId}: 获取故事统计
* - GET /story/analytics/monthly-trend: 获取月度趋势
* - GET /story/analytics/top-locations: 获取热门地点
* - GET /story/analytics/top-tags: 获取热门标签
* - GET /story/analytics/yearly-report: 获取年度报告
* - GET /story/analytics/activity: 获取活跃度统计
* - GET /story/analytics/export: 导出统计数据
*
* @author Timeline Team
* @date 2024
*/
@Slf4j
@RestController
@RequestMapping("/story/analytics")
public class AnalyticsController {
@Autowired
private AnalyticsService analyticsService;
/**
* 获取用户时间线总体统计
*
* @return 统计数据
*/
@GetMapping("/overview")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getOverview() {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取用户总体统计: userId={}", userId);
TimelineAnalyticsVo stats = analyticsService.getOverallStats(userId);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 获取故事统计数据
*
* @param storyInstanceId 故事ID
* @return 统计数据
*/
@GetMapping("/story/{storyInstanceId}")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getStoryStats(
@PathVariable String storyInstanceId) {
log.info("获取故事统计: storyId={}", storyInstanceId);
TimelineAnalyticsVo stats = analyticsService.getStoryStats(storyInstanceId);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 获取月度趋势数据
*
* @param year 年份(可选,默认当前年)
* @return 月度趋势
*/
@GetMapping("/monthly-trend")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getMonthlyTrend(
@RequestParam(required = false) Integer year) {
String userId = UserContextUtils.getCurrentUserId();
if (year == null) {
year = java.time.Year.now().getValue();
}
log.info("获取月度趋势: userId={}, year={}", userId, year);
TimelineAnalyticsVo stats = analyticsService.getMonthlyTrend(userId, year);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 获取热门地点
*
* @param limit 返回数量默认10
* @return 热门地点列表
*/
@GetMapping("/top-locations")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getTopLocations(
@RequestParam(defaultValue = "10") int limit) {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取热门地点: userId={}, limit={}", userId, limit);
TimelineAnalyticsVo stats = analyticsService.getTopLocations(userId, limit);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 获取热门标签
*
* @param limit 返回数量默认10
* @return 热门标签列表
*/
@GetMapping("/top-tags")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getTopTags(
@RequestParam(defaultValue = "10") int limit) {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取热门标签: userId={}, limit={}", userId, limit);
TimelineAnalyticsVo stats = analyticsService.getTopTags(userId, limit);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 获取年度报告
*
* @param year 年份(可选,默认去年)
* @return 年度报告
*/
@GetMapping("/yearly-report")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo.YearlyReport> getYearlyReport(
@RequestParam(required = false) Integer year) {
String userId = UserContextUtils.getCurrentUserId();
if (year == null) {
year = java.time.Year.now().getValue() - 1;
}
log.info("获取年度报告: userId={}, year={}", userId, year);
TimelineAnalyticsVo.YearlyReport report = analyticsService.generateYearlyReport(userId, year);
return com.timeline.common.response.ResponseEntity.success(report);
}
/**
* 获取活跃度统计
*
* @return 活跃度数据
*/
@GetMapping("/activity")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getActivity() {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取活跃度统计: userId={}", userId);
TimelineAnalyticsVo stats = analyticsService.getActivityStats(userId);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 获取时间分布
*
* @return 时间分布数据
*/
@GetMapping("/time-distribution")
public com.timeline.common.response.ResponseEntity<TimelineAnalyticsVo> getTimeDistribution() {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取时间分布: userId={}", userId);
TimelineAnalyticsVo stats = analyticsService.getTimeDistribution(userId);
return com.timeline.common.response.ResponseEntity.success(stats);
}
/**
* 导出统计数据
*
* @param format 导出格式 (json/csv)
* @param response HTTP 响应
*/
@GetMapping("/export")
public void exportStats(
@RequestParam(defaultValue = "json") String format,
HttpServletResponse response) {
String userId = UserContextUtils.getCurrentUserId();
log.info("导出统计数据: userId={}, format={}", userId, format);
try {
byte[] data = analyticsService.exportStats(userId, format);
String fileName = "timeline_stats_" + java.time.LocalDate.now() + "." + format;
response.setContentType(format.equals("csv") ? "text/csv" : "application/json");
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.getOutputStream().write(data);
response.getOutputStream().flush();
} catch (Exception e) {
log.error("导出统计数据失败", e);
}
}
}

View File

@@ -0,0 +1,207 @@
package com.timeline.story.controller;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.utils.UserContextUtils;
import com.timeline.story.entity.StoryComment;
import com.timeline.story.service.CommentService;
import com.timeline.story.vo.CommentVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* CommentController - 评论控制器
*
* 功能描述:
* 提供评论相关的 REST API 接口。
*
* API 列表:
* - POST /story/comment: 发表评论
* - POST /story/comment/{parentId}/reply: 回复评论
* - DELETE /story/comment/{commentId}: 删除评论
* - GET /story/comment/{commentId}: 获取评论详情
* - GET /story/comment/list/{storyItemId}: 获取节点的评论列表
* - GET /story/comment/{commentId}/replies: 获取评论的回复列表
* - POST /story/comment/{commentId}/like: 点赞评论
* - DELETE /story/comment/{commentId}/like: 取消点赞
*
* @author Timeline Team
* @date 2024
*/
@Slf4j
@RestController
@RequestMapping("/story/comment")
public class CommentController {
@Autowired
private CommentService commentService;
/**
* 发表评论
*
* @param commentVo 评论内容
* @return 创建的评论
*/
@PostMapping
public ResponseEntity<CommentVo> createComment(@RequestBody CommentVo commentVo) {
String userId = UserContextUtils.getCurrentUserId();
log.info("用户 {} 发表评论: storyItemId={}", userId, commentVo.getStoryItemId());
StoryComment comment = commentService.createComment(commentVo);
return ResponseEntity.success(convertToVo(comment));
}
/**
* 回复评论
*
* @param parentId 父评论ID
* @param commentVo 回复内容
* @return 创建的回复
*/
@PostMapping("/{parentId}/reply")
public ResponseEntity<CommentVo> replyComment(
@PathVariable String parentId,
@RequestBody CommentVo commentVo) {
String userId = UserContextUtils.getCurrentUserId();
log.info("用户 {} 回复评论: parentId={}", userId, parentId);
StoryComment comment = commentService.replyComment(parentId, commentVo);
return ResponseEntity.success(convertToVo(comment));
}
/**
* 删除评论
*
* @param commentInstanceId 评论ID
* @return 操作结果
*/
@DeleteMapping("/{commentInstanceId}")
public ResponseEntity<String> deleteComment(@PathVariable String commentInstanceId) {
String userId = UserContextUtils.getCurrentUserId();
log.info("用户 {} 删除评论: {}", userId, commentInstanceId);
commentService.deleteComment(commentInstanceId);
return ResponseEntity.success("评论已删除");
}
/**
* 获取评论详情
*
* @param commentInstanceId 评论ID
* @return 评论信息
*/
@GetMapping("/{commentInstanceId}")
public ResponseEntity<CommentVo> getComment(@PathVariable String commentInstanceId) {
StoryComment comment = commentService.getCommentById(commentInstanceId);
return ResponseEntity.success(convertToVo(comment));
}
/**
* 获取节点的评论列表
*
* @param storyItemId 节点ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 评论列表
*/
@GetMapping("/list/{storyItemId}")
public ResponseEntity<List<CommentVo>> getComments(
@PathVariable String storyItemId,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
log.info("获取节点评论列表: storyItemId={}", storyItemId);
List<CommentVo> comments = commentService.getCommentsByStoryItem(storyItemId, pageNum, pageSize);
return ResponseEntity.success(comments);
}
/**
* 获取评论的回复列表
*
* @param parentId 父评论ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 回复列表
*/
@GetMapping("/{parentId}/replies")
public ResponseEntity<List<CommentVo>> getReplies(
@PathVariable String parentId,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "5") int pageSize) {
log.info("获取评论回复列表: parentId={}", parentId);
List<CommentVo> replies = commentService.getRepliesByComment(parentId, pageNum, pageSize);
return ResponseEntity.success(replies);
}
/**
* 点赞评论
*
* @param commentInstanceId 评论ID
* @return 操作结果
*/
@PostMapping("/{commentInstanceId}/like")
public ResponseEntity<String> likeComment(@PathVariable String commentInstanceId) {
String userId = UserContextUtils.getCurrentUserId();
log.info("用户 {} 点赞评论: {}", userId, commentInstanceId);
commentService.likeComment(commentInstanceId, userId);
return ResponseEntity.success("点赞成功");
}
/**
* 取消点赞
*
* @param commentInstanceId 评论ID
* @return 操作结果
*/
@DeleteMapping("/{commentInstanceId}/like")
public ResponseEntity<String> unlikeComment(@PathVariable String commentInstanceId) {
String userId = UserContextUtils.getCurrentUserId();
log.info("用户 {} 取消点赞评论: {}", userId, commentInstanceId);
commentService.unlikeComment(commentInstanceId, userId);
return ResponseEntity.success("取消点赞成功");
}
/**
* 获取用户的评论列表
*
* @param pageNum 页码
* @param pageSize 每页数量
* @return 评论列表
*/
@GetMapping("/my")
public ResponseEntity<List<CommentVo>> getMyComments(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
String userId = UserContextUtils.getCurrentUserId();
List<CommentVo> comments = commentService.getCommentsByUser(userId, pageNum, pageSize);
return ResponseEntity.success(comments);
}
/**
* 转换为 VO
*/
private CommentVo convertToVo(StoryComment comment) {
if (comment == null) return null;
CommentVo vo = new CommentVo();
vo.setInstanceId(comment.getInstanceId());
vo.setStoryItemId(comment.getStoryItemId());
vo.setUserId(comment.getUserId());
vo.setParentId(comment.getParentId());
vo.setReplyToUserId(comment.getReplyToUserId());
vo.setContent(comment.getContent());
vo.setLikeCount(comment.getLikeCount());
vo.setCreateTime(comment.getCreateTime());
vo.setUpdateTime(comment.getUpdateTime());
return vo;
}
}

View File

@@ -0,0 +1,171 @@
package com.timeline.story.controller;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.utils.UserContextUtils;
import com.timeline.story.entity.Notification;
import com.timeline.story.service.NotificationService;
import com.timeline.story.vo.NotificationVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* NotificationController - 消息通知控制器
*
* 功能描述:
* 提供消息通知相关的 REST API 接口。
*
* API 列表:
* - GET /story/notification/list: 获取通知列表
* - GET /story/notification/unread: 获取未读通知列表
* - GET /story/notification/unread-count: 获取未读数量
* - PUT /story/notification/{notificationId}/read: 标记已读
* - PUT /story/notification/read-all: 全部标记已读
* - DELETE /story/notification/{notificationId}: 删除通知
* - DELETE /story/notification/clear: 清空所有通知
*
* @author Timeline Team
* @date 2024
*/
@Slf4j
@RestController
@RequestMapping("/story/notification")
public class NotificationController {
@Autowired
private NotificationService notificationService;
/**
* 获取通知列表
*
* @param type 消息类型(可选)
* @param pageNum 页码
* @param pageSize 每页数量
* @return 通知列表
*/
@GetMapping("/list")
public ResponseEntity<List<NotificationVo>> getNotifications(
@RequestParam(required = false) String type,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取用户通知列表: userId={}, type={}", userId, type);
List<NotificationVo> notifications = notificationService.getNotifications(userId, type, pageNum, pageSize);
return ResponseEntity.success(notifications);
}
/**
* 获取未读通知列表
*
* @param pageNum 页码
* @param pageSize 每页数量
* @return 未读通知列表
*/
@GetMapping("/unread")
public ResponseEntity<List<NotificationVo>> getUnreadNotifications(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取用户未读通知: userId={}", userId);
List<NotificationVo> notifications = notificationService.getUnreadNotifications(userId, pageNum, pageSize);
return ResponseEntity.success(notifications);
}
/**
* 获取未读通知数量
*
* @return 未读数量
*/
@GetMapping("/unread-count")
public ResponseEntity<Map<String, Object>> getUnreadCount() {
String userId = UserContextUtils.getCurrentUserId();
int totalCount = notificationService.getUnreadCount(userId);
Map<String, Integer> countByType = notificationService.getUnreadCountByType(userId);
return ResponseEntity.success(Map.of(
"total", totalCount,
"byType", countByType
));
}
/**
* 标记单条通知为已读
*
* @param notificationInstanceId 通知ID
* @return 操作结果
*/
@PutMapping("/{notificationInstanceId}/read")
public ResponseEntity<String> markAsRead(@PathVariable String notificationInstanceId) {
String userId = UserContextUtils.getCurrentUserId();
log.info("标记通知已读: userId={}, notificationId={}", userId, notificationInstanceId);
notificationService.markAsRead(notificationInstanceId, userId);
return ResponseEntity.success("已标记为已读");
}
/**
* 标记所有通知为已读
*
* @return 操作结果
*/
@PutMapping("/read-all")
public ResponseEntity<String> markAllAsRead() {
String userId = UserContextUtils.getCurrentUserId();
log.info("标记所有通知已读: userId={}", userId);
notificationService.markAllAsRead(userId);
return ResponseEntity.success("已全部标记为已读");
}
/**
* 按类型标记所有通知为已读
*
* @param type 消息类型
* @return 操作结果
*/
@PutMapping("/read-all/{type}")
public ResponseEntity<String> markAllAsReadByType(@PathVariable String type) {
String userId = UserContextUtils.getCurrentUserId();
log.info("按类型标记通知已读: userId={}, type={}", userId, type);
notificationService.markAllAsReadByType(userId, type);
return ResponseEntity.success("已标记为已读");
}
/**
* 删除通知
*
* @param notificationInstanceId 通知ID
* @return 操作结果
*/
@DeleteMapping("/{notificationInstanceId}")
public ResponseEntity<String> deleteNotification(@PathVariable String notificationInstanceId) {
String userId = UserContextUtils.getCurrentUserId();
log.info("删除通知: userId={}, notificationId={}", userId, notificationInstanceId);
notificationService.deleteNotification(notificationInstanceId, userId);
return ResponseEntity.success("通知已删除");
}
/**
* 清空所有通知
*
* @return 操作结果
*/
@DeleteMapping("/clear")
public ResponseEntity<String> clearAllNotifications() {
String userId = UserContextUtils.getCurrentUserId();
log.info("清空所有通知: userId={}", userId);
notificationService.clearAllNotifications(userId);
return ResponseEntity.success("通知已清空");
}
}

View File

@@ -0,0 +1,207 @@
package com.timeline.story.controller;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.utils.UserContextUtils;
import com.timeline.story.entity.Reminder;
import com.timeline.story.service.ReminderService;
import com.timeline.story.vo.ReminderVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* ReminderController - 提醒控制器
*
* 功能描述:
* 提供提醒管理的 REST API 接口。
*
* API 列表:
* - POST /story/reminder: 创建提醒
* - PUT /story/reminder/{reminderId}: 更新提醒
* - DELETE /story/reminder/{reminderId}: 删除提醒
* - GET /story/reminder/list: 获取提醒列表
* - PUT /story/reminder/{reminderId}/toggle: 启用/禁用提醒
* - POST /story/reminder/memory: 生成回忆提醒
*
* @author Timeline Team
* @date 2024
*/
@Slf4j
@RestController
@RequestMapping("/story/reminder")
public class ReminderController {
@Autowired
private ReminderService reminderService;
/**
* 创建提醒
*
* @param reminderVo 提醒信息
* @return 创建的提醒
*/
@PostMapping
public ResponseEntity<ReminderVo> createReminder(@RequestBody ReminderVo reminderVo) {
String userId = UserContextUtils.getCurrentUserId();
log.info("创建提醒: userId={}, type={}", userId, reminderVo.getType());
Reminder reminder = reminderService.createReminder(reminderVo);
return ResponseEntity.success(convertToVo(reminder));
}
/**
* 更新提醒
*
* @param reminderInstanceId 提醒ID
* @param reminderVo 提醒信息
* @return 更新后的提醒
*/
@PutMapping("/{reminderInstanceId}")
public ResponseEntity<ReminderVo> updateReminder(
@PathVariable String reminderInstanceId,
@RequestBody ReminderVo reminderVo) {
log.info("更新提醒: reminderId={}", reminderInstanceId);
Reminder reminder = reminderService.updateReminder(reminderInstanceId, reminderVo);
return ResponseEntity.success(convertToVo(reminder));
}
/**
* 删除提醒
*
* @param reminderInstanceId 提醒ID
* @return 操作结果
*/
@DeleteMapping("/{reminderInstanceId}")
public ResponseEntity<String> deleteReminder(@PathVariable String reminderInstanceId) {
log.info("删除提醒: reminderId={}", reminderInstanceId);
reminderService.deleteReminder(reminderInstanceId);
return ResponseEntity.success("提醒已删除");
}
/**
* 获取提醒详情
*
* @param reminderInstanceId 提醒ID
* @return 提醒信息
*/
@GetMapping("/{reminderInstanceId}")
public ResponseEntity<ReminderVo> getReminder(@PathVariable String reminderInstanceId) {
Reminder reminder = reminderService.getReminderById(reminderInstanceId);
return ResponseEntity.success(convertToVo(reminder));
}
/**
* 获取用户的提醒列表
*
* @param type 提醒类型(可选)
* @return 提醒列表
*/
@GetMapping("/list")
public ResponseEntity<List<ReminderVo>> getReminders(
@RequestParam(required = false) String type) {
String userId = UserContextUtils.getCurrentUserId();
log.info("获取提醒列表: userId={}, type={}", userId, type);
List<Reminder> reminders = reminderService.getRemindersByUser(userId, type);
List<ReminderVo> voList = reminders.stream()
.map(this::convertToVo)
.toList();
return ResponseEntity.success(voList);
}
/**
* 启用/禁用提醒
*
* @param reminderInstanceId 提醒ID
* @param enabled 是否启用
* @return 操作结果
*/
@PutMapping("/{reminderInstanceId}/toggle")
public ResponseEntity<String> toggleReminder(
@PathVariable String reminderInstanceId,
@RequestParam boolean enabled) {
log.info("切换提醒状态: reminderId={}, enabled={}", reminderInstanceId, enabled);
reminderService.toggleReminder(reminderInstanceId, enabled);
return ResponseEntity.success(enabled ? "提醒已启用" : "提醒已禁用");
}
/**
* 生成回忆提醒
*
* 功能描述:
* 检查用户是否有"去年的今天"的记录,
* 如果有则生成回忆提醒。
*
* @return 操作结果
*/
@PostMapping("/memory")
public ResponseEntity<String> generateMemoryReminders() {
String userId = UserContextUtils.getCurrentUserId();
log.info("生成回忆提醒: userId={}", userId);
reminderService.generateMemoryReminders(userId);
return ResponseEntity.success("回忆提醒已生成");
}
/**
* 转换为 VO
*/
private ReminderVo convertToVo(Reminder reminder) {
if (reminder == null) return null;
ReminderVo vo = new ReminderVo();
vo.setInstanceId(reminder.getInstanceId());
vo.setType(reminder.getType());
vo.setTitle(reminder.getTitle());
vo.setContent(reminder.getContent());
vo.setStoryInstanceId(reminder.getStoryInstanceId());
vo.setStoryItemId(reminder.getStoryItemId());
vo.setRemindTime(reminder.getRemindTime());
vo.setRepeatType(reminder.getRepeatType());
vo.setIsSent(reminder.getIsSent() == 1);
vo.setSentTime(reminder.getSentTime());
vo.setIsEnabled(reminder.getIsEnabled() == 1);
vo.setCreateTime(reminder.getCreateTime());
// 设置类型描述
vo.setTypeDesc(getTypeDesc(reminder.getType()));
vo.setRepeatTypeDesc(getRepeatTypeDesc(reminder.getRepeatType()));
return vo;
}
/**
* 获取类型描述
*/
private String getTypeDesc(String type) {
return switch (type) {
case "RECORD" -> "记录提醒";
case "MEMORY" -> "回忆提醒";
case "ANNIVERSARY" -> "纪念日提醒";
case "CUSTOM" -> "自定义提醒";
default -> "未知类型";
};
}
/**
* 获取重复类型描述
*/
private String getRepeatTypeDesc(String repeatType) {
return switch (repeatType) {
case "NONE" -> "不重复";
case "DAILY" -> "每天";
case "WEEKLY" -> "每周";
case "MONTHLY" -> "每月";
case "YEARLY" -> "每年";
default -> "未知";
};
}
}

View File

@@ -2,6 +2,7 @@ package com.timeline.story.controller;
import com.alibaba.fastjson.JSONObject;
import com.timeline.common.response.ResponseEntity;
import com.timeline.common.response.ResponseEnum;
import com.timeline.story.entity.StoryItem;
import com.timeline.story.service.StoryItemService;
import com.timeline.story.service.StoryService;
@@ -98,4 +99,74 @@ public class StoryItemController {
Map result = storyItemService.searchItems(keyword, pageNum, pageSize);
return ResponseEntity.success(result);
}
/**
* 批量更新时间线节点排序
*
* 功能描述:
* 接收前端拖拽排序后的节点顺序,批量更新各节点的 sortOrder 字段。
* 用于保存用户手动调整的时间线节点顺序。
*
* @param request 包含 items 数组,每个元素有 instanceId 和 sortOrder
* @return 操作结果
*/
@PutMapping("/order")
public ResponseEntity<String> updateItemsOrder(@RequestBody Map<String, List<Map<String, Object>>> request) {
List<Map<String, Object>> items = request.get("items");
if (items == null || items.isEmpty()) {
return ResponseEntity.error("排序数据不能为空");
}
log.info("批量更新 StoryItem 排序,共 {} 项", items.size());
storyItemService.updateItemsOrder(items);
return ResponseEntity.success("排序更新成功");
}
/**
* 批量删除时间线节点
*
* 功能描述:
* 根据节点ID列表批量软删除时间线节点。
* 软删除仅标记 is_delete 字段,数据可恢复。
*
* @param request 包含 instanceIds 数组
* @return 操作结果
*/
@PostMapping("/batch-delete")
public ResponseEntity<String> batchDeleteItems(@RequestBody Map<String, List<String>> request) {
List<String> instanceIds = request.get("instanceIds");
if (instanceIds == null || instanceIds.isEmpty()) {
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "删除列表不能为空");
}
log.info("批量删除 StoryItem共 {} 项", instanceIds.size());
storyItemService.batchDeleteItems(instanceIds);
return ResponseEntity.success("批量删除成功");
}
/**
* 批量修改时间线节点时间
*
* 功能描述:
* 批量修改多个节点的 storyItemTime 字段。
* 用于批量调整节点的时间信息。
*
* @param request 包含 instanceIds 数组和 storyItemTime 字符串
* @return 操作结果
*/
@PutMapping("/batch-time")
public ResponseEntity<String> batchUpdateItemTime(@RequestBody Map<String, Object> request) {
@SuppressWarnings("unchecked")
List<String> instanceIds = (List<String>) request.get("instanceIds");
String storyItemTime = (String) request.get("storyItemTime");
if (instanceIds == null || instanceIds.isEmpty()) {
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "修改列表不能为空");
}
if (storyItemTime == null || storyItemTime.isEmpty()) {
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "时间不能为空");
}
log.info("批量修改 StoryItem 时间,共 {} 项,新时间: {}", instanceIds.size(), storyItemTime);
storyItemService.batchUpdateItemTime(instanceIds, storyItemTime);
return ResponseEntity.success("批量修改时间成功");
}
}

View File

@@ -12,7 +12,6 @@ import java.util.List;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@RestController
@RequestMapping("/story/permission")
@Slf4j
@@ -34,6 +33,7 @@ public class StoryPermissionController {
storyPermissionService.updatePermission(permissionVo);
return ResponseEntity.success("权限更新成功");
}
@PostMapping("/authorize")
public ResponseEntity<String> authorizePermission(@RequestBody StoryPermissionVo permissionVo) {
log.info("授权权限: {}", permissionVo);
@@ -76,7 +76,29 @@ public class StoryPermissionController {
@RequestParam Integer requiredPermissionType) {
log.info("检查用户权限: storyInstanceId={}, userId={}, requiredPermissionType={}",
storyInstanceId, userId, requiredPermissionType);
boolean hasPermission = storyPermissionService.checkUserPermission(storyInstanceId, userId, requiredPermissionType);
boolean hasPermission = storyPermissionService.checkUserPermission(storyInstanceId, userId,
requiredPermissionType);
return ResponseEntity.success(hasPermission);
}
@PostMapping("/invite")
public ResponseEntity<String> inviteUser(@RequestBody StoryPermissionVo permissionVo) {
log.info("邀请用户协作: {}", permissionVo);
storyPermissionService.inviteUser(permissionVo);
return ResponseEntity.success("邀请发送成功");
}
@PutMapping("/invite/{inviteId}/accept")
public ResponseEntity<String> acceptInvite(@PathVariable String inviteId) {
log.info("接受邀请: {}", inviteId);
storyPermissionService.acceptInvite(inviteId);
return ResponseEntity.success("邀请已接受");
}
@PutMapping("/invite/{inviteId}/reject")
public ResponseEntity<String> rejectInvite(@PathVariable String inviteId) {
log.info("拒绝邀请: {}", inviteId);
storyPermissionService.rejectInvite(inviteId);
return ResponseEntity.success("邀请已拒绝");
}
}

View File

@@ -33,4 +33,18 @@ public interface StoryItemMapper {
// StoryItemShareVo selectByShareIdWithAuthor(String shareId);
List<StoryItemVo> searchItems(@Param("keyword") String keyword);
/**
* 更新节点排序值
*
* @param params 包含 instanceId, sortOrder, updateId, updateTime
*/
void updateOrder(Map<String, Object> params);
/**
* 更新节点时间
*
* @param params 包含 instanceId, storyItemTime, updateId, updateTime
*/
void updateItemTime(Map<String, Object> params);
}

View File

@@ -8,11 +8,20 @@ import java.util.List;
@Mapper
public interface StoryPermissionMapper {
void insert(StoryPermission permission);
void update(StoryPermission permission);
void updateInviteStatus(String permissionId, Integer inviteStatus);
void deleteByPermissionId(String permissionId);
StoryPermission selectByPermissionId(String permissionId);
List<StoryPermission> selectByStoryInstanceId(String storyInstanceId);
List<StoryPermission> selectByUserId(String userId);
StoryPermission selectByStoryAndUser(String storyInstanceId, String userId);
List<StoryPermission> selectByStoryAndPermissionType(String storyInstanceId, Integer permissionType);
}

View File

@@ -0,0 +1,101 @@
package com.timeline.story.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Notification - 消息通知实体类
*
* 功能描述:
* 定义系统消息通知的数据结构。
* 支持多种消息类型:评论、点赞、@提及、邀请、系统通知。
*
* 消息类型说明:
* - COMMENT: 评论通知
* - LIKE: 点赞通知
* - MENTION: @提及通知
* - INVITE: 协作邀请通知
* - SYSTEM: 系统通知
*
* @author Timeline Team
* @date 2024
*/
@Data
public class Notification {
/**
* 主键ID
*/
private Long id;
/**
* 消息唯一标识
*/
private String instanceId;
/**
* 接收用户ID
*/
private String userId;
/**
* 消息类型
* COMMENT/LIKE/MENTION/INVITE/SYSTEM
*/
private String type;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 关联ID评论ID/节点ID等
*/
private String relatedId;
/**
* 关联类型
* STORY_ITEM/COMMENT/STORY
*/
private String relatedType;
/**
* 发送者ID
*/
private String senderId;
/**
* 发送者名称
*/
private String senderName;
/**
* 发送者头像
*/
private String senderAvatar;
/**
* 是否已读
*/
private Integer isRead;
/**
* 阅读时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime readTime;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,106 @@
package com.timeline.story.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Reminder - 提醒实体类
*
* 功能描述:
* 定义用户提醒的数据结构,支持多种提醒类型。
*
* 提醒类型:
* - RECORD: 记录提醒(每日记录提醒)
* - MEMORY: 回忆提醒(去年的今天)
* - ANNIVERSARY: 纪念日提醒
* - CUSTOM: 自定义提醒
*
* @author Timeline Team
* @date 2024
*/
@Data
public class Reminder {
/**
* 主键ID
*/
private Long id;
/**
* 提醒唯一标识
*/
private String instanceId;
/**
* 用户ID
*/
private String userId;
/**
* 提醒类型
* RECORD/MEMORY/ANNIVERSARY/CUSTOM
*/
private String type;
/**
* 提醒标题
*/
private String title;
/**
* 提醒内容
*/
private String content;
/**
* 关联的故事ID可选
*/
private String storyInstanceId;
/**
* 关联的节点ID可选
*/
private String storyItemId;
/**
* 提醒时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime remindTime;
/**
* 重复类型
* NONE/DAILY/WEEKLY/MONTHLY/YEARLY
*/
private String repeatType;
/**
* 是否已发送
*/
private Integer isSent;
/**
* 发送时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime sentTime;
/**
* 是否启用
*/
private Integer isEnabled;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,84 @@
package com.timeline.story.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* StoryComment - 评论实体类
*
* 功能描述:
* 定义时间线节点评论的数据结构。
* 支持多级评论(回复功能)。
*
* 字段说明:
* - instanceId: 评论唯一标识
* - storyItemId: 关联的时间线节点ID
* - userId: 评论用户ID
* - parentId: 父评论ID用于回复
* - content: 评论内容
* - likeCount: 点赞数
*
* @author Timeline Team
* @date 2024
*/
@Data
public class StoryComment {
/**
* 主键ID
*/
private Long id;
/**
* 评论唯一标识
*/
private String instanceId;
/**
* 时间线节点ID
*/
private String storyItemId;
/**
* 评论用户ID
*/
private String userId;
/**
* 父评论ID用于回复功能
*/
private String parentId;
/**
* 回复的用户ID
*/
private String replyToUserId;
/**
* 评论内容
*/
private String content;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 创建时间
*/
@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;
}

View File

@@ -22,4 +22,9 @@ public class StoryItem {
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer isDelete;
/**
* 排序值 - 用于拖拽排序
* 数值越小越靠前默认为0
*/
private Integer sortOrder;
}

View File

@@ -0,0 +1,44 @@
package com.timeline.story.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* StoryItemTag - 时间线节点标签关联实体
*
* 功能描述:
* 定义时间线节点与标签的多对多关联关系。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class StoryItemTag {
/**
* 主键ID
*/
private Long id;
/**
* 时间线节点ID
*/
private String storyItemId;
/**
* 标签ID
*/
private String tagId;
/**
* 创建者ID
*/
private String createId;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@@ -12,6 +12,7 @@ public class StoryPermission {
private String storyInstanceId;
private String userId;
private Integer permissionType; // 1-创建者2-仅查看3-可新增4-可管理
private Integer inviteStatus; // 0-待处理, 1-已接受, 2-已拒绝
private Integer isDeleted;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

View File

@@ -0,0 +1,66 @@
package com.timeline.story.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Tag - 标签实体类
*
* 功能描述:
* 定义标签的数据结构,用于时间线节点的分类和检索。
*
* 字段说明:
* - instanceId: 标签唯一标识
* - name: 标签名称
* - color: 标签颜色(十六进制)
* - ownerId: 创建者ID
*
* @author Timeline Team
* @date 2024
*/
@Data
public class Tag {
/**
* 主键ID
*/
private Long id;
/**
* 标签唯一标识
*/
private String instanceId;
/**
* 标签名称
*/
private String name;
/**
* 标签颜色(十六进制,如 #1890ff
*/
private String color;
/**
* 创建者ID
*/
private String ownerId;
/**
* 创建时间
*/
@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;
}

View File

@@ -0,0 +1,107 @@
package com.timeline.story.service;
import com.timeline.story.vo.TimelineAnalyticsVo;
/**
* AnalyticsService - 数据分析服务接口
*
* 功能描述:
* 提供时间线数据的统计和分析功能。
*
* 功能列表:
* - 总体统计
* - 时间分布分析
* - 地点分布分析
* - 标签分布分析
* - 年度报告生成
*
* @author Timeline Team
* @date 2024
*/
public interface AnalyticsService {
/**
* 获取用户时间线总体统计
*
* @param userId 用户ID
* @return 统计数据
*/
TimelineAnalyticsVo getOverallStats(String userId);
/**
* 获取故事统计数据
*
* @param storyInstanceId 故事ID
* @return 统计数据
*/
TimelineAnalyticsVo getStoryStats(String storyInstanceId);
/**
* 获取月度趋势数据
*
* @param userId 用户ID
* @param year 年份
* @return 月度趋势
*/
TimelineAnalyticsVo getMonthlyTrend(String userId, int year);
/**
* 获取热门地点
*
* @param userId 用户ID
* @param limit 返回数量
* @return 热门地点列表
*/
TimelineAnalyticsVo getTopLocations(String userId, int limit);
/**
* 获取热门标签
*
* @param userId 用户ID
* @param limit 返回数量
* @return 热门标签列表
*/
TimelineAnalyticsVo getTopTags(String userId, int limit);
/**
* 生成年度报告
*
* @param userId 用户ID
* @param year 年份
* @return 年度报告数据
*/
TimelineAnalyticsVo.YearlyReport generateYearlyReport(String userId, int year);
/**
* 获取活跃度统计
*
* @param userId 用户ID
* @return 活跃度数据
*/
TimelineAnalyticsVo getActivityStats(String userId);
/**
* 计算连续记录天数
*
* @param userId 用户ID
* @return 连续天数
*/
int calculateConsecutiveDays(String userId);
/**
* 获取记录时间分布
*
* @param userId 用户ID
* @return 时间分布数据
*/
TimelineAnalyticsVo getTimeDistribution(String userId);
/**
* 导出统计数据
*
* @param userId 用户ID
* @param format 导出格式 (json/csv)
* @return 导出数据
*/
byte[] exportStats(String userId, String format);
}

View File

@@ -0,0 +1,123 @@
package com.timeline.story.service;
import com.timeline.story.entity.StoryComment;
import com.timeline.story.vo.CommentVo;
import java.util.List;
/**
* CommentService - 评论服务接口
*
* 功能描述:
* 提供评论的 CRUD 操作和互动功能。
*
* 功能列表:
* - 发表评论
* - 回复评论
* - 删除评论
* - 点赞评论
* - 获取评论列表
* - 获取回复列表
*
* @author Timeline Team
* @date 2024
*/
public interface CommentService {
/**
* 发表评论
*
* @param commentVo 评论信息
* @return 创建的评论
*/
StoryComment createComment(CommentVo commentVo);
/**
* 回复评论
*
* @param parentId 父评论ID
* @param commentVo 回复内容
* @return 创建的回复
*/
StoryComment replyComment(String parentId, CommentVo commentVo);
/**
* 删除评论
* 软删除,保留数据
*
* @param commentInstanceId 评论ID
*/
void deleteComment(String commentInstanceId);
/**
* 获取评论详情
*
* @param commentInstanceId 评论ID
* @return 评论信息
*/
StoryComment getCommentById(String commentInstanceId);
/**
* 获取节点的评论列表
* 分页获取,按时间倒序
*
* @param storyItemId 节点ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 评论列表
*/
List<CommentVo> getCommentsByStoryItem(String storyItemId, int pageNum, int pageSize);
/**
* 获取评论的回复列表
*
* @param parentId 父评论ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 回复列表
*/
List<CommentVo> getRepliesByComment(String parentId, int pageNum, int pageSize);
/**
* 获取节点的评论数量
*
* @param storyItemId 节点ID
* @return 评论数量
*/
int getCommentCountByStoryItem(String storyItemId);
/**
* 点赞评论
*
* @param commentInstanceId 评论ID
* @param userId 用户ID
*/
void likeComment(String commentInstanceId, String userId);
/**
* 取消点赞评论
*
* @param commentInstanceId 评论ID
* @param userId 用户ID
*/
void unlikeComment(String commentInstanceId, String userId);
/**
* 检查用户是否已点赞
*
* @param commentInstanceId 评论ID
* @param userId 用户ID
* @return 是否已点赞
*/
boolean hasLiked(String commentInstanceId, String userId);
/**
* 获取用户的评论列表
*
* @param userId 用户ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 评论列表
*/
List<CommentVo> getCommentsByUser(String userId, int pageNum, int pageSize);
}

View File

@@ -0,0 +1,156 @@
package com.timeline.story.service;
import com.timeline.story.entity.Notification;
import com.timeline.story.vo.NotificationVo;
import java.util.List;
/**
* NotificationService - 消息通知服务接口
*
* 功能描述:
* 提供消息通知的管理和推送功能。
*
* 功能列表:
* - 创建通知
* - 获取用户通知列表
* - 标记已读
* - 批量标记已读
* - 删除通知
* - 获取未读数量
* - 实时推送通知
*
* @author Timeline Team
* @date 2024
*/
public interface NotificationService {
/**
* 创建评论通知
*
* @param storyItemId 节点ID
* @param commentId 评论ID
* @param senderId 评论者ID
*/
void createCommentNotification(String storyItemId, String commentId, String senderId);
/**
* 创建点赞通知
*
* @param storyItemId 节点ID
* @param senderId 点赞者ID
*/
void createLikeNotification(String storyItemId, String senderId);
/**
* 创建@提及通知
*
* @param toUserId 被提及的用户ID
* @param relatedType 关联类型
* @param relatedId 关联ID
* @param senderId 提及者ID
*/
void createMentionNotification(String toUserId, String relatedType, String relatedId, String senderId);
/**
* 创建协作邀请通知
*
* @param storyId 故事ID
* @param toUserId 被邀请者ID
* @param senderId 邀请者ID
*/
void createInviteNotification(String storyId, String toUserId, String senderId);
/**
* 创建系统通知
*
* @param userId 用户ID
* @param title 标题
* @param content 内容
*/
void createSystemNotification(String userId, String title, String content);
/**
* 获取用户的通知列表
*
* @param userId 用户ID
* @param type 消息类型(可选)
* @param pageNum 页码
* @param pageSize 每页数量
* @return 通知列表
*/
List<NotificationVo> getNotifications(String userId, String type, int pageNum, int pageSize);
/**
* 获取未读通知列表
*
* @param userId 用户ID
* @param pageNum 页码
* @param pageSize 每页数量
* @return 未读通知列表
*/
List<NotificationVo> getUnreadNotifications(String userId, int pageNum, int pageSize);
/**
* 获取未读通知数量
*
* @param userId 用户ID
* @return 未读数量
*/
int getUnreadCount(String userId);
/**
* 按类型获取未读数量
*
* @param userId 用户ID
* @return 各类型的未读数量
*/
java.util.Map<String, Integer> getUnreadCountByType(String userId);
/**
* 标记单条通知为已读
*
* @param notificationInstanceId 通知ID
* @param userId 用户ID
*/
void markAsRead(String notificationInstanceId, String userId);
/**
* 标记所有通知为已读
*
* @param userId 用户ID
*/
void markAllAsRead(String userId);
/**
* 按类型标记所有通知为已读
*
* @param userId 用户ID
* @param type 消息类型
*/
void markAllAsReadByType(String userId, String type);
/**
* 删除通知
*
* @param notificationInstanceId 通知ID
* @param userId 用户ID
*/
void deleteNotification(String notificationInstanceId, String userId);
/**
* 清空所有通知
*
* @param userId 用户ID
*/
void clearAllNotifications(String userId);
/**
* 推送实时通知
* 通过 WebSocket 推送给在线用户
*
* @param userId 用户ID
* @param notification 通知内容
*/
void pushNotification(String userId, Notification notification);
}

View File

@@ -0,0 +1,124 @@
package com.timeline.story.service;
import com.timeline.story.entity.Reminder;
import com.timeline.story.vo.ReminderVo;
import java.util.List;
/**
* ReminderService - 提醒服务接口
*
* 功能描述:
* 提供提醒的管理和推送功能。
*
* 功能列表:
* - 创建提醒
* - 更新提醒
* - 删除提醒
* - 获取提醒列表
* - 发送提醒
* - 生成回忆提醒
*
* @author Timeline Team
* @date 2024
*/
public interface ReminderService {
/**
* 创建提醒
*
* @param reminderVo 提醒信息
* @return 创建的提醒
*/
Reminder createReminder(ReminderVo reminderVo);
/**
* 更新提醒
*
* @param reminderInstanceId 提醒ID
* @param reminderVo 提醒信息
* @return 更新后的提醒
*/
Reminder updateReminder(String reminderInstanceId, ReminderVo reminderVo);
/**
* 删除提醒
*
* @param reminderInstanceId 提醒ID
*/
void deleteReminder(String reminderInstanceId);
/**
* 获取提醒详情
*
* @param reminderInstanceId 提醒ID
* @return 提醒信息
*/
Reminder getReminderById(String reminderInstanceId);
/**
* 获取用户的提醒列表
*
* @param userId 用户ID
* @param type 提醒类型(可选)
* @return 提醒列表
*/
List<Reminder> getRemindersByUser(String userId, String type);
/**
* 获取待发送的提醒列表
*
* @return 待发送的提醒列表
*/
List<Reminder> getPendingReminders();
/**
* 发送提醒
*
* @param reminder 提醒信息
*/
void sendReminder(Reminder reminder);
/**
* 批量发送提醒
*
* @param reminders 提醒列表
*/
void sendReminders(List<Reminder> reminders);
/**
* 生成回忆提醒
*
* 功能描述:
* 检查用户是否有"去年的今天"的记录,
* 如果有则生成回忆提醒。
*
* @param userId 用户ID
*/
void generateMemoryReminders(String userId);
/**
* 启用/禁用提醒
*
* @param reminderInstanceId 提醒ID
* @param enabled 是否启用
*/
void toggleReminder(String reminderInstanceId, boolean enabled);
/**
* 标记提醒为已发送
*
* @param reminderInstanceId 提醒ID
*/
void markAsSent(String reminderInstanceId);
/**
* 处理重复提醒
*
* 功能描述:
* 对于重复类型的提醒,发送后自动创建下一次提醒。
*
* @param reminder 原提醒
*/
void handleRepeatReminder(Reminder reminder);
}

View File

@@ -27,4 +27,37 @@ public interface StoryItemService {
// StoryItemShareVo getItemByShareId(String shareId);
Map<String, Object> searchItems(String keyword, Integer pageNum, Integer pageSize);
/**
* 批量更新时间线节点排序
*
* 功能描述:
* 根据前端传递的排序数据,批量更新各节点的 sortOrder 字段。
* 该方法支持事务处理,确保所有排序更新原子性完成。
*
* @param items 排序数据列表,每个元素包含 instanceId 和 sortOrder
*/
void updateItemsOrder(List<Map<String, Object>> items);
/**
* 批量删除时间线节点
*
* 功能描述:
* 根据节点ID列表批量软删除时间线节点。
* 软删除仅标记 is_delete 字段,数据可恢复。
*
* @param instanceIds 要删除的节点ID列表
*/
void batchDeleteItems(List<String> instanceIds);
/**
* 批量修改时间线节点时间
*
* 功能描述:
* 批量修改多个节点的 storyItemTime 字段。
*
* @param instanceIds 要修改的节点ID列表
* @param storyItemTime 新的时间值
*/
void batchUpdateItemTime(List<String> instanceIds, String storyItemTime);
}

View File

@@ -7,12 +7,26 @@ import java.util.List;
public interface StoryPermissionService {
void createPermission(StoryPermissionVo permissionVo);
void updatePermission(StoryPermissionVo permissionVo);
void deletePermission(String permissionId);
StoryPermission getPermissionById(String permissionId);
List<StoryPermission> getPermissionsByStoryId(String storyInstanceId);
List<StoryPermission> getPermissionsByUserId(String userId);
StoryPermission getPermissionByStoryAndUser(String storyInstanceId, String userId);
boolean checkUserPermission(String storyInstanceId, String userId, Integer requiredPermissionType);
List<StoryPermission> getPermissionsByStoryAndType(String storyInstanceId, Integer permissionType);
void inviteUser(StoryPermissionVo permissionVo);
void acceptInvite(String permissionId);
void rejectInvite(String permissionId);
}

View File

@@ -0,0 +1,135 @@
package com.timeline.story.service;
import com.timeline.story.entity.Tag;
import com.timeline.story.vo.TagVo;
import java.util.List;
/**
* TagService - 标签服务接口
*
* 功能描述:
* 提供标签的 CRUD 操作和节点标签关联管理。
*
* 功能列表:
* - 创建标签
* - 更新标签
* - 删除标签
* - 获取用户标签列表
* - 为节点添加标签
* - 移除节点标签
* - 获取节点的标签列表
*
* @author Timeline Team
* @date 2024
*/
public interface TagService {
/**
* 创建标签
*
* @param tagVo 标签信息
* @return 创建的标签
*/
Tag createTag(TagVo tagVo);
/**
* 更新标签
*
* @param tagInstanceId 标签ID
* @param tagVo 标签信息
* @return 更新后的标签
*/
Tag updateTag(String tagInstanceId, TagVo tagVo);
/**
* 删除标签
* 同时删除所有关联关系
*
* @param tagInstanceId 标签ID
*/
void deleteTag(String tagInstanceId);
/**
* 获取标签详情
*
* @param tagInstanceId 标签ID
* @return 标签信息
*/
Tag getTagById(String tagInstanceId);
/**
* 获取用户的所有标签
*
* @param userId 用户ID
* @return 标签列表
*/
List<Tag> getTagsByUserId(String userId);
/**
* 搜索标签
* 根据名称模糊搜索
*
* @param keyword 关键词
* @param userId 用户ID
* @return 匹配的标签列表
*/
List<Tag> searchTags(String keyword, String userId);
/**
* 为节点添加标签
*
* @param storyItemId 节点ID
* @param tagInstanceId 标签ID
*/
void addTagToStoryItem(String storyItemId, String tagInstanceId);
/**
* 为节点批量添加标签
*
* @param storyItemId 节点ID
* @param tagInstanceIds 标签ID列表
*/
void addTagsToStoryItem(String storyItemId, List<String> tagInstanceIds);
/**
* 移除节点标签
*
* @param storyItemId 节点ID
* @param tagInstanceId 标签ID
*/
void removeTagFromStoryItem(String storyItemId, String tagInstanceId);
/**
* 移除节点的所有标签
*
* @param storyItemId 节点ID
*/
void removeAllTagsFromStoryItem(String storyItemId);
/**
* 获取节点的标签列表
*
* @param storyItemId 节点ID
* @return 标签列表
*/
List<Tag> getTagsByStoryItemId(String storyItemId);
/**
* 获取标签下的节点数量
*
* @param tagInstanceId 标签ID
* @return 节点数量
*/
int getStoryItemCountByTag(String tagInstanceId);
/**
* 获取热门标签
* 按使用次数排序
*
* @param userId 用户ID
* @param limit 返回数量
* @return 热门标签列表
*/
List<Tag> getHotTags(String userId, int limit);
}

View File

@@ -112,4 +112,100 @@ public class StoryItemServiceImpl implements StoryItemService {
result.put("total", pageInfo.getTotal());
return result;
}
/**
* 批量更新时间线节点排序
*
* 实现思路:
* 1. 使用事务确保批量更新的原子性
* 2. 遍历排序数据,逐个更新节点的 sortOrder 字段
* 3. 同时更新 updateTime 时间戳
*
* 注意事项:
* - 该方法需要在事务中执行,确保数据一致性
* - 如果任一更新失败,整个事务将回滚
*
* @param items 排序数据列表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateItemsOrder(List<Map<String, Object>> items) {
String currentUserId = UserContextUtils.getCurrentUserId();
LocalDateTime now = LocalDateTime.now();
for (Map<String, Object> item : items) {
String instanceId = (String) item.get("instanceId");
Integer sortOrder = ((Number) item.get("sortOrder")).intValue();
// 构建更新参数
Map<String, Object> params = new HashMap<>();
params.put("instanceId", instanceId);
params.put("sortOrder", sortOrder);
params.put("updateId", currentUserId);
params.put("updateTime", now);
storyItemMapper.updateOrder(params);
log.debug("更新节点排序: instanceId={}, sortOrder={}", instanceId, sortOrder);
}
log.info("批量更新排序完成,共更新 {} 个节点", items.size());
}
/**
* 批量删除时间线节点
*
* 实现思路:
* 1. 使用事务确保批量删除的原子性
* 2. 遍历节点ID列表逐个执行软删除
* 3. 软删除仅标记 is_delete = 1数据可恢复
*
* 注意事项:
* - 该方法需要在事务中执行
* - 如果任一删除失败,整个事务将回滚
*
* @param instanceIds 要删除的节点ID列表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDeleteItems(List<String> instanceIds) {
for (String instanceId : instanceIds) {
storyItemMapper.deleteByItemId(instanceId);
log.debug("软删除节点: instanceId={}", instanceId);
}
log.info("批量删除完成,共删除 {} 个节点", instanceIds.size());
}
/**
* 批量修改时间线节点时间
*
* 实现思路:
* 1. 使用事务确保批量修改的原子性
* 2. 遍历节点ID列表逐个更新时间字段
* 3. 同时更新 updateTime 时间戳
*
* @param instanceIds 要修改的节点ID列表
* @param storyItemTime 新的时间值
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void batchUpdateItemTime(List<String> instanceIds, String storyItemTime) {
String currentUserId = UserContextUtils.getCurrentUserId();
LocalDateTime now = LocalDateTime.now();
// 解析时间字符串为 LocalDateTime
LocalDateTime parsedTime = LocalDateTime.parse(storyItemTime.replace(" ", "T"));
for (String instanceId : instanceIds) {
Map<String, Object> params = new HashMap<>();
params.put("instanceId", instanceId);
params.put("storyItemTime", parsedTime);
params.put("updateId", currentUserId);
params.put("updateTime", now);
storyItemMapper.updateItemTime(params);
log.debug("更新节点时间: instanceId={}, storyItemTime={}", instanceId, storyItemTime);
}
log.info("批量修改时间完成,共修改 {} 个节点", instanceIds.size());
}
}

View File

@@ -29,6 +29,7 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
private StoryPermissionMapper storyPermissionMapper;
@Autowired
private UserServiceClient userServiceClient;
private String getCurrentUserId() {
String uid = UserContextUtils.getCurrentUserId();
if (uid == null) {
@@ -45,10 +46,13 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
if (currentUserId.equals(permissionVo.getUserId()) && permissionVo.getPermissionType() != 1) {
throw new CustomException(ResponseEnum.BAD_REQUEST, "不能授权给自己");
}
StoryPermission selectByStoryAndUser = storyPermissionMapper.selectByStoryAndUser(permissionVo.getStoryInstanceId(), permissionVo.getUserId());;
StoryPermission selectByStoryAndUser = storyPermissionMapper
.selectByStoryAndUser(permissionVo.getStoryInstanceId(), permissionVo.getUserId());
if (selectByStoryAndUser != null) {
log.info("用户已有该故事权限,更新当前权限为:{}", permissionVo.getPermissionType());
selectByStoryAndUser.setPermissionType(permissionVo.getPermissionType());;
selectByStoryAndUser.setPermissionType(permissionVo.getPermissionType());
selectByStoryAndUser.setUpdateTime(LocalDateTime.now());
storyPermissionMapper.update(selectByStoryAndUser);
return;
@@ -61,19 +65,20 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
} else if (response.getData() == null) {
throw new CustomException(ResponseEnum.BAD_REQUEST, "用户不存在");
}
log.info("新建故事{} 授权 {} 给 {}", permissionVo.getStoryInstanceId(), permissionVo.getPermissionType(), permissionVo.getUserId());
log.info("新建故事{} 授权 {} 给 {}", permissionVo.getStoryInstanceId(), permissionVo.getPermissionType(),
permissionVo.getUserId());
StoryPermission permission = new StoryPermission();
BeanUtils.copyProperties(permissionVo, permission);
permission.setPermissionId(IdUtils.randomUuidUpper());
permission.setCreateTime(LocalDateTime.now());
permission.setUpdateTime(LocalDateTime.now());
permission.setIsDeleted(CommonConstants.NOT_DELETED);
permission.setInviteStatus(1); // Direct creation is accepted
storyPermissionMapper.insert(permission);
} catch(CustomException e) {
} catch (CustomException e) {
throw e;
}
catch (Exception e) {
} catch (Exception e) {
log.error("创建权限失败", e);
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "创建权限失败");
}
@@ -142,6 +147,11 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
return false;
}
// Check invite status: Must be ACCEPTED (1)
if (permission.getInviteStatus() != null && permission.getInviteStatus() != 1) {
return false;
}
// 权限类型数字越小权限越高
return permission.getPermissionType() <= requiredPermissionType;
}
@@ -150,4 +160,87 @@ public class StoryPermissionServiceImpl implements StoryPermissionService {
public List<StoryPermission> getPermissionsByStoryAndType(String storyInstanceId, Integer permissionType) {
return storyPermissionMapper.selectByStoryAndPermissionType(storyInstanceId, permissionType);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void inviteUser(StoryPermissionVo permissionVo) {
try {
// Check if user exists (Feign)
ResponseEntity<Map> response = userServiceClient.getUserByUserId(permissionVo.getUserId());
if (response.getCode() != 200 || response.getData() == null) {
throw new CustomException(ResponseEnum.BAD_REQUEST, "用户不存在");
}
StoryPermission existing = storyPermissionMapper.selectByStoryAndUser(permissionVo.getStoryInstanceId(),
permissionVo.getUserId());
if (existing != null) {
if (existing.getInviteStatus() != null && existing.getInviteStatus() == 1) {
throw new CustomException(ResponseEnum.BAD_REQUEST, "用户已经是协作者");
}
// Update existing to PENDING
existing.setPermissionType(permissionVo.getPermissionType());
existing.setInviteStatus(0);
storyPermissionMapper.update(existing);
storyPermissionMapper.updateInviteStatus(existing.getPermissionId(), 0);
} else {
StoryPermission permission = new StoryPermission();
BeanUtils.copyProperties(permissionVo, permission);
permission.setPermissionId(IdUtils.randomUuidUpper());
permission.setCreateTime(LocalDateTime.now());
permission.setUpdateTime(LocalDateTime.now());
permission.setIsDeleted(CommonConstants.NOT_DELETED);
permission.setInviteStatus(0); // PENDING
storyPermissionMapper.insert(permission);
}
// TODO: Send notification
} catch (CustomException e) {
throw e;
} catch (Exception e) {
log.error("邀请用户失败", e);
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "邀请用户失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void acceptInvite(String permissionId) {
try {
StoryPermission permission = storyPermissionMapper.selectByPermissionId(permissionId);
if (permission == null) {
throw new CustomException(ResponseEnum.NOT_FOUND, "邀请不存在");
}
// Check if current user is the invitee
String currentUserId = getCurrentUserId();
if (!permission.getUserId().equals(currentUserId)) {
throw new CustomException(ResponseEnum.FORBIDDEN, "无权操作此邀请");
}
storyPermissionMapper.updateInviteStatus(permissionId, 1); // ACCEPTED
} catch (CustomException e) {
throw e;
} catch (Exception e) {
log.error("接受邀请失败", e);
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "接受邀请失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void rejectInvite(String permissionId) {
try {
StoryPermission permission = storyPermissionMapper.selectByPermissionId(permissionId);
if (permission == null) {
throw new CustomException(ResponseEnum.NOT_FOUND, "邀请不存在");
}
String currentUserId = getCurrentUserId();
if (!permission.getUserId().equals(currentUserId)) {
throw new CustomException(ResponseEnum.FORBIDDEN, "无权操作此邀请");
}
storyPermissionMapper.updateInviteStatus(permissionId, 2); // REJECTED
} catch (CustomException e) {
throw e;
} catch (Exception e) {
log.error("拒绝邀请失败", e);
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "拒绝邀请失败");
}
}
}

View File

@@ -0,0 +1,95 @@
package com.timeline.story.vo;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* CommentVo - 评论值对象
*
* 功能描述:
* 封装评论信息,包含用户信息和回复列表。
* 用于前端展示评论树结构。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class CommentVo {
/**
* 评论ID
*/
private String instanceId;
/**
* 时间线节点ID
*/
private String storyItemId;
/**
* 评论用户ID
*/
private String userId;
/**
* 评论用户名
*/
private String userName;
/**
* 评论用户头像
*/
private String userAvatar;
/**
* 父评论ID
*/
private String parentId;
/**
* 回复的用户ID
*/
private String replyToUserId;
/**
* 回复的用户名
*/
private String replyToUserName;
/**
* 评论内容
*/
private String content;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 当前用户是否已点赞
*/
private Boolean hasLiked;
/**
* 回复数量
*/
private Integer replyCount;
/**
* 回复列表(子评论)
*/
private List<CommentVo> replies;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,88 @@
package com.timeline.story.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* NotificationVo - 消息通知值对象
*
* 功能描述:
* 封装消息通知信息,用于前端展示。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class NotificationVo {
/**
* 通知ID
*/
private String instanceId;
/**
* 消息类型
*/
private String type;
/**
* 消息类型描述
*/
private String typeDesc;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 关联ID
*/
private String relatedId;
/**
* 关联类型
*/
private String relatedType;
/**
* 发送者ID
*/
private String senderId;
/**
* 发送者名称
*/
private String senderName;
/**
* 发送者头像
*/
private String senderAvatar;
/**
* 是否已读
*/
private Boolean isRead;
/**
* 阅读时间
*/
private LocalDateTime readTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 相对时间描述刚刚、5分钟前
*/
private String timeAgo;
}

View File

@@ -0,0 +1,95 @@
package com.timeline.story.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* ReminderVo - 提醒值对象
*
* 功能描述:
* 封装提醒信息,用于前端展示和创建。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class ReminderVo {
/**
* 提醒ID
*/
private String instanceId;
/**
* 提醒类型
* RECORD/MEMORY/ANNIVERSARY/CUSTOM
*/
private String type;
/**
* 提醒类型描述
*/
private String typeDesc;
/**
* 提醒标题
*/
private String title;
/**
* 提醒内容
*/
private String content;
/**
* 关联的故事ID
*/
private String storyInstanceId;
/**
* 关联的故事标题
*/
private String storyTitle;
/**
* 关联的节点ID
*/
private String storyItemId;
/**
* 提醒时间
*/
private LocalDateTime remindTime;
/**
* 重复类型
* NONE/DAILY/WEEKLY/MONTHLY/YEARLY
*/
private String repeatType;
/**
* 重复类型描述
*/
private String repeatTypeDesc;
/**
* 是否已发送
*/
private Boolean isSent;
/**
* 发送时间
*/
private LocalDateTime sentTime;
/**
* 是否启用
*/
private Boolean isEnabled;
/**
* 创建时间
*/
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,120 @@
package com.timeline.story.vo;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* SearchResultVo - 搜索结果值对象
*
* 功能描述:
* 封装搜索结果数据,包含分页信息和高亮内容。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class SearchResultVo {
/**
* 搜索结果列表
*/
private List<SearchItemVo> list;
/**
* 总数量
*/
private long total;
/**
* 当前页码
*/
private int pageNum;
/**
* 每页数量
*/
private int pageSize;
/**
* 搜索耗时(毫秒)
*/
private long took;
/**
* 是否有更多结果
*/
private boolean hasMore;
/**
* 搜索项值对象
*/
@Data
public static class SearchItemVo {
/**
* 节点ID
*/
private String instanceId;
/**
* 所属故事ID
*/
private String storyInstanceId;
/**
* 故事标题
*/
private String storyTitle;
/**
* 节点标题
*/
private String title;
/**
* 高亮标题(包含 <em> 标签)
*/
private String highlightTitle;
/**
* 节点描述
*/
private String description;
/**
* 高亮描述
*/
private String highlightDescription;
/**
* 时间
*/
private LocalDateTime storyItemTime;
/**
* 地点
*/
private String location;
/**
* 标签列表
*/
private List<String> tags;
/**
* 创建者名称
*/
private String createName;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 相关性得分
*/
private float score;
}
}

View File

@@ -10,4 +10,5 @@ public class StoryPermissionVo {
private String storyInstanceId;
private String userId;
private Integer permissionType; // 1-创建者2-仅查看3-可新增4-可管理
private Integer inviteStatus; // 0-待处理, 1-已接受, 2-已拒绝
}

View File

@@ -0,0 +1,53 @@
package com.timeline.story.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* TagVo - 标签值对象
*
* 功能描述:
* 封装标签信息,用于前端展示和创建。
*
* @author Timeline Team
* @date 2024
*/
@Data
public class TagVo {
/**
* 标签ID
*/
private String instanceId;
/**
* 标签名称
*/
private String name;
/**
* 标签颜色
*/
private String color;
/**
* 创建者ID
*/
private String ownerId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 使用次数
*/
private Integer usageCount;
}

View File

@@ -0,0 +1,168 @@
package com.timeline.story.vo;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* TimelineAnalyticsVo - 时间线数据分析值对象
*
* 功能描述:
* 封装用户时间线的统计数据,用于数据分析面板展示。
*
* 数据维度:
* - 总体统计:时刻数、媒体数、协作数
* - 时间分布:按年/月/周统计
* - 地点分布:热门地点
* - 标签分布:热门标签
* - 活跃度:记录频率
*
* @author Timeline Team
* @date 2024
*/
@Data
public class TimelineAnalyticsVo {
/**
* 总时刻数
*/
private Long totalMoments;
/**
* 总媒体文件数
*/
private Long totalMedia;
/**
* 图片数量
*/
private Long imageCount;
/**
* 视频数量
*/
private Long videoCount;
/**
* 总故事数
*/
private Long totalStories;
/**
* 协作故事数
*/
private Long collaborationCount;
/**
* 总评论数
*/
private Long totalComments;
/**
* 总点赞数
*/
private Long totalLikes;
/**
* 总收藏数
*/
private Long totalFavorites;
/**
* 月度记录趋势
* key: 月份 (yyyy-MM)
* value: 记录数量
*/
private List<MonthlyStats> monthlyTrend;
/**
* 周记录分布
* key: 星期 (1-7)
* value: 记录数量
*/
private Map<Integer, Long> weeklyDistribution;
/**
* 小时分布
* key: 小时 (0-23)
* value: 记录数量
*/
private Map<Integer, Long> hourlyDistribution;
/**
* 热门地点 Top 10
*/
private List<LocationStats> topLocations;
/**
* 热门标签 Top 10
*/
private List<TagStats> topTags;
/**
* 最近活跃日期
*/
private String lastActiveDate;
/**
* 连续记录天数
*/
private Integer consecutiveDays;
/**
* 最长连续记录天数
*/
private Integer maxConsecutiveDays;
/**
* 年度报告数据
*/
private YearlyReport yearlyReport;
/**
* 月度统计
*/
@Data
public static class MonthlyStats {
private String month;
private Long count;
private Long mediaCount;
}
/**
* 地点统计
*/
@Data
public static class LocationStats {
private String location;
private Long count;
private Double percentage;
}
/**
* 标签统计
*/
@Data
public static class TagStats {
private String tag;
private String color;
private Long count;
private Double percentage;
}
/**
* 年度报告
*/
@Data
public static class YearlyReport {
private Integer year;
private Long totalMoments;
private Long totalMedia;
private String mostActiveMonth;
private String mostActiveDay;
private String topLocation;
private String topTag;
private List<MonthlyStats> monthlyBreakdown;
}
}

View File

@@ -112,4 +112,46 @@
ORDER BY
si.story_item_time DESC
</select>
<!--
更新节点排序值
功能描述:
根据拖拽排序结果更新节点的 sort_order 字段。
同时更新 update_id 和 update_time 以记录修改信息。
参数说明:
- instanceId: 节点唯一标识
- sortOrder: 新的排序值(数值越小越靠前)
- updateId: 操作用户ID
- updateTime: 更新时间
-->
<update id="updateOrder">
UPDATE story_item
SET sort_order = #{sortOrder},
update_id = #{updateId},
update_time = #{updateTime}
WHERE instance_id = #{instanceId}
</update>
<!--
更新节点时间
功能描述:
批量修改节点时间时使用,更新 story_item_time 字段。
同时更新 update_id 和 update_time 以记录修改信息。
参数说明:
- instanceId: 节点唯一标识
- storyItemTime: 新的时间值
- updateId: 操作用户ID
- updateTime: 更新时间
-->
<update id="updateItemTime">
UPDATE story_item
SET story_item_time = #{storyItemTime},
update_id = #{updateId},
update_time = #{updateTime}
WHERE instance_id = #{instanceId}
</update>
</mapper>

View File

@@ -5,8 +5,8 @@
<mapper namespace="com.timeline.story.dao.StoryPermissionMapper">
<insert id="insert">
INSERT INTO story_permission (permission_id, story_instance_id, user_id, permission_type)
VALUES (#{permissionId}, #{storyInstanceId}, #{userId}, #{permissionType})
INSERT INTO story_permission (permission_id, story_instance_id, user_id, permission_type, invite_status)
VALUES (#{permissionId}, #{storyInstanceId}, #{userId}, #{permissionType}, #{inviteStatus})
</insert>
<update id="update">
UPDATE story_permission
@@ -15,6 +15,13 @@
WHERE permission_id = #{permissionId} AND is_deleted = 0
</update>
<update id="updateInviteStatus">
UPDATE story_permission
SET invite_status = #{inviteStatus},
update_time = NOW()
WHERE permission_id = #{permissionId} AND is_deleted = 0
</update>
<update id="deleteByPermissionId">
UPDATE story_permission
SET is_deleted = 1