feat: 记录删除与筛选、用户侧内容删除、AI 聊天历史
修复雪花 ID 精度丢失(真 bug) - bottom-sheet 的 postId 声明为 Number,而 ID 是 18 位雪花字符串, 超出 JS 安全整数范围:339346584095428608 会被转成 ...600, 评论弹层实际查的是不存在的帖子。改为 String 记录 - 后端 DELETE /records/:id 早已就绪但前端从未调用,记错了删不掉。 时间轴支持长按删除 - 时间轴加类型筛选(体重/便便/饮食/异常/用药/疫苗/消费/照片), 后端 ?type= 本就支持 用户侧内容删除(UGC 合规:用户应能删除自己发布的内容) - 新增 DELETE /posts/:id 与 DELETE /comments/:id,仅限本人, 越权返回 40300;帖子软删除,评论删除同步递减 comment_count - 评论列表返回 is_self;社区长按自己的帖子、长按自己的评论可删 AI 聊天历史 - ai_messages 一直在写但没有查询接口,每次打开弹层都是空白。 新增 GET /api/ai/messages,弹层打开时接在开场白后加载 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ func (h *Handler) ListComments(c *gin.Context) {
|
||||
_ = c.ShouldBindQuery(&req)
|
||||
req.Normalize()
|
||||
|
||||
comments, total, err := h.svc.ListComments(idParam(c, "id"), req.Offset(), req.Limit())
|
||||
comments, total, err := h.svc.ListComments(middleware.UserID(c), idParam(c, "id"), req.Offset(), req.Limit())
|
||||
if err != nil {
|
||||
response.FailErr(c, err)
|
||||
return
|
||||
@@ -152,3 +152,21 @@ func (h *Handler) UnfollowUser(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, gin.H{"followed": false})
|
||||
}
|
||||
|
||||
// DeleteOwnPost DELETE /api/posts/:id (仅限本人)
|
||||
func (h *Handler) DeleteOwnPost(c *gin.Context) {
|
||||
if err := h.svc.DeleteOwnPost(middleware.UserID(c), idParam(c, "id")); err != nil {
|
||||
respondErr(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
// DeleteOwnComment DELETE /api/comments/:id (仅限本人)
|
||||
func (h *Handler) DeleteOwnComment(c *gin.Context) {
|
||||
if err := h.svc.DeleteOwnComment(middleware.UserID(c), idParam(c, "id")); err != nil {
|
||||
respondErr(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
@@ -27,5 +27,9 @@ func respondErr(c *gin.Context, err error) {
|
||||
response.Fail(c, errcode.ErrNotFound, "")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrForbidden) {
|
||||
response.Fail(c, errcode.ErrForbidden, err.Error())
|
||||
return
|
||||
}
|
||||
response.FailErr(c, err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -173,3 +174,17 @@ func (h *Handler) AdminSetFeedbackHandled(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ListAIMessages GET /api/ai/messages?session=&limit=
|
||||
func (h *Handler) ListAIMessages(c *gin.Context) {
|
||||
limit := 0
|
||||
if v := c.Query("limit"); v != "" {
|
||||
limit, _ = strconv.Atoi(v)
|
||||
}
|
||||
msgs, err := h.svc.ListAIMessages(middleware.UserID(c), c.Query("session"), limit)
|
||||
if err != nil {
|
||||
response.FailErr(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, msgs)
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ type Comment struct {
|
||||
AuthorName string `gorm:"size:64" json:"author_name"`
|
||||
Content string `gorm:"size:512" json:"content"`
|
||||
Status string `gorm:"size:16;default:published" json:"status"`
|
||||
|
||||
IsSelf bool `gorm:"-" json:"is_self"` // 计算字段:是不是我自己发的
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage
|
||||
g.DELETE("/posts/:id/like", h.UnlikePost)
|
||||
g.GET("/posts/:id/comments", h.ListComments)
|
||||
g.POST("/posts/:id/comments", h.CreateComment)
|
||||
g.DELETE("/posts/:id", h.DeleteOwnPost)
|
||||
g.DELETE("/comments/:id", h.DeleteOwnComment)
|
||||
|
||||
g.GET("/articles", h.ListArticles)
|
||||
g.POST("/feedback", h.CreateFeedback)
|
||||
@@ -103,6 +105,7 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage
|
||||
g.POST("/pro/activate", h.ActivatePro)
|
||||
|
||||
g.POST("/ai/chat", h.AIChat)
|
||||
g.GET("/ai/messages", h.ListAIMessages)
|
||||
g.POST("/pets/:id/ai/assess-symptom", h.AssessSymptom)
|
||||
g.POST("/upload", h.Upload)
|
||||
}
|
||||
|
||||
@@ -29,3 +29,23 @@ func (s *Service) AIChat(userID string, petID *string, session, text string) (st
|
||||
func (s *Service) Upload(objectName string, reader io.Reader, size int64, contentType string) (string, error) {
|
||||
return s.storage.Upload(objectName, reader, size, contentType)
|
||||
}
|
||||
|
||||
// ListAIMessages 取当前用户的聊天历史(按会话,默认最近 N 条,返回时间正序)
|
||||
func (s *Service) ListAIMessages(userID, session string, limit int) ([]model.AIMessage, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 30
|
||||
}
|
||||
q := s.db.Where("user_id = ?", userID)
|
||||
if session != "" {
|
||||
q = q.Where("session = ?", session)
|
||||
}
|
||||
var msgs []model.AIMessage
|
||||
if err := q.Order("id desc").Limit(limit).Find(&msgs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 反转成时间正序,前端直接按顺序渲染
|
||||
for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
|
||||
msgs[i], msgs[j] = msgs[j], msgs[i]
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func (s *Service) postLikeCount(postID string) (int, error) {
|
||||
}
|
||||
|
||||
// ListComments 评论分页
|
||||
func (s *Service) ListComments(postID string, offset, limit int) ([]model.Comment, int64, error) {
|
||||
func (s *Service) ListComments(userID, postID string, offset, limit int) ([]model.Comment, int64, error) {
|
||||
q := s.db.Model(&model.Comment{}).Where("post_id = ? AND status = ?", postID, "published")
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
@@ -202,6 +202,9 @@ func (s *Service) ListComments(postID string, offset, limit int) ([]model.Commen
|
||||
if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&comments).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for i := range comments {
|
||||
comments[i].IsSelf = comments[i].UserID == userID
|
||||
}
|
||||
return comments, total, nil
|
||||
}
|
||||
|
||||
@@ -227,3 +230,38 @@ func (s *Service) CreateComment(userID, postID string, content string) (*model.C
|
||||
}
|
||||
return &comment, nil
|
||||
}
|
||||
|
||||
// DeleteOwnPost 用户删除自己的帖子(软删除,保留数据可追溯)
|
||||
func (s *Service) DeleteOwnPost(userID, postID string) error {
|
||||
var p model.Post
|
||||
if err := s.db.Where("id = ?", postID).First(&p).Error; err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
if p.UserID != userID {
|
||||
return ErrForbidden
|
||||
}
|
||||
return s.db.Model(&model.Post{}).Where("id = ?", postID).
|
||||
Update("status", model.PostDeleted).Error
|
||||
}
|
||||
|
||||
// DeleteOwnComment 用户删除自己的评论,并同步帖子评论数
|
||||
func (s *Service) DeleteOwnComment(userID, commentID string) error {
|
||||
var c model.Comment
|
||||
if err := s.db.Where("id = ?", commentID).First(&c).Error; err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
if c.UserID != userID {
|
||||
return ErrForbidden
|
||||
}
|
||||
if c.Status == "deleted" {
|
||||
return nil // 幂等
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", commentID).
|
||||
Update("status", "deleted").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Post{}).Where("id = ? AND comment_count > 0", c.PostID).
|
||||
UpdateColumn("comment_count", gorm.Expr("comment_count - 1")).Error
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ var ErrNotFound = errors.New("not found")
|
||||
// 避免「点一下白拿会员」。
|
||||
var ErrPayNotReady = errors.New("支付功能即将开放,敬请期待")
|
||||
|
||||
// ErrForbidden 越权操作(如删除别人的内容)
|
||||
var ErrForbidden = errors.New("无权操作")
|
||||
|
||||
// Service 业务逻辑聚合,方法按领域分散在各文件
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
|
||||
Reference in New Issue
Block a user