Files
memory/server/internal/handler/post.go

512 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"database/sql"
"net/http"
"strconv"
"strings"
"memory/internal/config"
"memory/internal/middleware"
"memory/internal/model"
"github.com/gin-gonic/gin"
)
type PostHandler struct {
db *sql.DB
cfg *config.Config
}
func NewPostHandler(db *sql.DB, cfg *config.Config) *PostHandler {
return &PostHandler{db: db, cfg: cfg}
}
func (h *PostHandler) Create(c *gin.Context) {
var req model.CreatePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := middleware.GetUserID(c)
// 验证视频数量最多2个
videoCount := 0
for _, mediaURL := range req.MediaIDs {
if isVideoURL(mediaURL) {
videoCount++
}
}
if videoCount > 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": "最多只能上传2个视频"})
return
}
tx, err := h.db.Begin()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer tx.Rollback()
result, err := tx.Exec("INSERT INTO posts (user_id, content, visibility) VALUES (?, ?, ?)", userID, req.Content, req.Visibility)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create post"})
return
}
postID, _ := result.LastInsertId()
// 关联媒体文件
for i, mediaURL := range req.MediaIDs {
mediaType := "image"
if isVideoURL(mediaURL) {
mediaType = "video"
}
_, err := tx.Exec(
"INSERT INTO post_media (post_id, media_url, media_type, sort_order) VALUES (?, ?, ?, ?)",
postID, mediaURL, mediaType, i,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to attach media"})
return
}
}
// 处理音乐链接
if req.MusicURL != "" && IsQQMusicURL(req.MusicURL) {
parser := &QQMusicParser{}
musicInfo, err := parser.Parse(req.MusicURL)
if err == nil && musicInfo != nil {
_, err = tx.Exec(
"INSERT INTO post_music (post_id, title, artist, cover, share_url, platform) VALUES (?, ?, ?, ?, ?, ?)",
postID, musicInfo.Title, musicInfo.Artist, musicInfo.Cover, musicInfo.ShareURL, musicInfo.Platform,
)
if err != nil {
// 音乐解析失败不影响帖子创建,只记录日志
// log.Printf("Failed to save music info: %v", err)
}
}
}
if err := tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": postID})
}
func (h *PostHandler) List(c *gin.Context) {
userID := middleware.GetUserID(c)
isSuperAdmin, _ := c.Get("is_superadmin")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
offset := (page - 1) * pageSize
// 超管可以看到所有帖子,普通用户看不到超管的帖子
var rows *sql.Rows
var err error
if isSuperAdmin != nil && isSuperAdmin.(bool) {
// 超管:看到所有帖子
rows, err = h.db.Query(`
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at, COALESCE(p.visibility, 0),
u.id, u.username, u.nickname, u.avatar_url, COALESCE(u.is_superadmin, 0),
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as liked,
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count
FROM posts p
JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
`, userID, pageSize, offset)
} else {
// 普通用户:只能看到公开帖子 + 自己的私密帖子(不能看到别人的私密帖子)
rows, err = h.db.Query(`
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at, COALESCE(p.visibility, 0),
u.id, u.username, u.nickname, u.avatar_url, COALESCE(u.is_superadmin, 0),
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as liked,
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE (COALESCE(p.visibility, 0) = 0) OR (COALESCE(p.visibility, 0) = 1 AND p.user_id = ?)
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
`, userID, userID, pageSize, offset)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer rows.Close()
posts := []model.Post{}
for rows.Next() {
var post model.Post
var user model.User
var liked int
var updatedAt sql.NullTime
err := rows.Scan(
&post.ID, &post.UserID, &post.Content, &post.CreatedAt, &updatedAt, &post.Visibility,
&user.ID, &user.Username, &user.Nickname, &user.AvatarURL, &user.IsSuperAdmin,
&post.LikeCount, &liked, &post.CommentCount,
)
if err != nil {
continue
}
post.User = &user
post.Liked = liked > 0
if updatedAt.Valid {
post.UpdatedAt = &updatedAt.Time
}
// 获取媒体
post.Media = h.getPostMedia(post.ID)
// 获取表情反应
post.Reactions = h.getPostReactions(post.ID, userID)
// 获取音乐信息
post.Music = h.getPostMusic(post.ID)
posts = append(posts, post)
}
c.JSON(http.StatusOK, posts)
}
func (h *PostHandler) Get(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
var post model.Post
var user model.User
var liked int
var updatedAt sql.NullTime
err := h.db.QueryRow(`
SELECT p.id, p.user_id, p.content, p.created_at, p.updated_at, COALESCE(p.visibility, 0),
u.id, u.username, u.nickname, u.avatar_url, COALESCE(u.is_superadmin, 0),
(SELECT COUNT(*) FROM likes WHERE post_id = p.id) as like_count,
(SELECT COUNT(*) FROM likes WHERE post_id = p.id AND user_id = ?) as liked,
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.id = ?
`, userID, postID).Scan(
&post.ID, &post.UserID, &post.Content, &post.CreatedAt, &updatedAt, &post.Visibility,
&user.ID, &user.Username, &user.Nickname, &user.AvatarURL, &user.IsSuperAdmin,
&post.LikeCount, &liked, &post.CommentCount,
)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
post.User = &user
post.Liked = liked > 0
if updatedAt.Valid {
post.UpdatedAt = &updatedAt.Time
}
post.Media = h.getPostMedia(post.ID)
post.Reactions = h.getPostReactions(post.ID, userID)
post.Music = h.getPostMusic(post.ID)
c.JSON(http.StatusOK, post)
}
func (h *PostHandler) Update(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
isAdmin, _ := c.Get("is_admin")
isAdminBool := isAdmin != nil && isAdmin.(bool)
var req model.UpdatePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 验证视频数量最多2个
if req.MediaIDs != nil {
videoCount := 0
for _, mediaURL := range req.MediaIDs {
if isVideoURL(mediaURL) {
videoCount++
}
}
if videoCount > 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": "最多只能上传2个视频"})
return
}
}
// 检查权限
var postUserID int64
err := h.db.QueryRow("SELECT user_id FROM posts WHERE id = ?", postID).Scan(&postUserID)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
// 帖子作者或管理员可以编辑
if postUserID != userID && !isAdminBool {
c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
return
}
tx, err := h.db.Begin()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
return
}
defer tx.Rollback()
// 更新帖子内容
if req.Visibility != nil {
_, err = tx.Exec("UPDATE posts SET content = ?, visibility = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", req.Content, *req.Visibility, postID)
} else {
_, err = tx.Exec("UPDATE posts SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", req.Content, postID)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update post"})
return
}
// 如果提供了 media_ids更新媒体
if req.MediaIDs != nil {
// 删除旧的媒体
_, err = tx.Exec("DELETE FROM post_media WHERE post_id = ?", postID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update media"})
return
}
// 添加新的媒体
for i, mediaURL := range req.MediaIDs {
mediaType := "image"
if isVideoURL(mediaURL) {
mediaType = "video"
}
_, err := tx.Exec(
"INSERT INTO post_media (post_id, media_url, media_type, sort_order) VALUES (?, ?, ?, ?)",
postID, mediaURL, mediaType, i,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to attach media"})
return
}
}
}
// 处理音乐更新
if req.MusicURL != nil {
// 先删除旧的音乐
_, err = tx.Exec("DELETE FROM post_music WHERE post_id = ?", postID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update music"})
return
}
// 如果提供了新的音乐链接,解析并保存
if *req.MusicURL != "" && IsQQMusicURL(*req.MusicURL) {
parser := &QQMusicParser{}
musicInfo, err := parser.Parse(*req.MusicURL)
if err == nil && musicInfo != nil {
_, err = tx.Exec(
"INSERT INTO post_music (post_id, title, artist, cover, share_url, platform) VALUES (?, ?, ?, ?, ?, ?)",
postID, musicInfo.Title, musicInfo.Artist, musicInfo.Cover, musicInfo.ShareURL, musicInfo.Platform,
)
if err != nil {
// 音乐解析失败不影响帖子更新
}
}
}
}
if err := tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to commit"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "updated"})
}
func (h *PostHandler) Delete(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
isAdmin, _ := c.Get("is_admin")
isAdminBool := isAdmin != nil && isAdmin.(bool)
// 检查权限
var postUserID int64
err := h.db.QueryRow("SELECT user_id FROM posts WHERE id = ?", postID).Scan(&postUserID)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
return
}
// 帖子作者或管理员可以删除
if postUserID != userID && !isAdminBool {
c.JSON(http.StatusForbidden, gin.H{"error": "permission denied"})
return
}
_, err = h.db.Exec("DELETE FROM posts WHERE id = ?", postID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete post"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
func (h *PostHandler) Like(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
result, err := h.db.Exec("INSERT OR IGNORE INTO likes (post_id, user_id) VALUES (?, ?)", postID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to like"})
return
}
// 创建通知
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
var postOwnerID int64
h.db.QueryRow("SELECT user_id FROM posts WHERE id = ?", postID).Scan(&postOwnerID)
CreateNotification(h.db, postOwnerID, userID, "like", &postID, nil, "")
}
c.JSON(http.StatusOK, gin.H{"message": "liked"})
}
func (h *PostHandler) Unlike(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
_, err := h.db.Exec("DELETE FROM likes WHERE post_id = ? AND user_id = ?", postID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to unlike"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "unliked"})
}
func (h *PostHandler) AddReaction(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
var req model.AddReactionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.db.Exec(
"INSERT OR IGNORE INTO reactions (post_id, user_id, emoji) VALUES (?, ?, ?)",
postID, userID, req.Emoji,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add reaction"})
return
}
// 创建通知
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
var postOwnerID int64
h.db.QueryRow("SELECT user_id FROM posts WHERE id = ?", postID).Scan(&postOwnerID)
CreateNotification(h.db, postOwnerID, userID, "reaction", &postID, nil, req.Emoji)
}
c.JSON(http.StatusOK, gin.H{"message": "reaction added"})
}
func (h *PostHandler) RemoveReaction(c *gin.Context) {
postID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
userID := middleware.GetUserID(c)
emoji := c.Query("emoji")
_, err := h.db.Exec(
"DELETE FROM reactions WHERE post_id = ? AND user_id = ? AND emoji = ?",
postID, userID, emoji,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove reaction"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "reaction removed"})
}
func (h *PostHandler) getPostMedia(postID int64) []model.Media {
rows, err := h.db.Query(
"SELECT id, post_id, media_url, media_type, sort_order FROM post_media WHERE post_id = ? ORDER BY sort_order",
postID,
)
if err != nil {
return nil
}
defer rows.Close()
var media []model.Media
for rows.Next() {
var m model.Media
rows.Scan(&m.ID, &m.PostID, &m.MediaURL, &m.MediaType, &m.SortOrder)
media = append(media, m)
}
return media
}
func (h *PostHandler) getPostReactions(postID, userID int64) []model.ReactionGroup {
rows, err := h.db.Query(`
SELECT emoji, COUNT(*) as count,
SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END) as reacted
FROM reactions WHERE post_id = ?
GROUP BY emoji
ORDER BY count DESC
`, userID, postID)
if err != nil {
return nil
}
defer rows.Close()
var reactions []model.ReactionGroup
for rows.Next() {
var r model.ReactionGroup
var reacted int
rows.Scan(&r.Emoji, &r.Count, &reacted)
r.Reacted = reacted > 0
reactions = append(reactions, r)
}
return reactions
}
func (h *PostHandler) getPostMusic(postID int64) *model.MusicInfo {
var music model.MusicInfo
err := h.db.QueryRow(`
SELECT id, post_id, title, artist, cover, share_url, platform
FROM post_music WHERE post_id = ?
`, postID).Scan(&music.ID, &music.PostID, &music.Title, &music.Artist, &music.Cover, &music.ShareURL, &music.Platform)
if err != nil {
return nil
}
return &music
}
// isVideoURL 判断URL是否为视频文件
func isVideoURL(url string) bool {
return strings.HasSuffix(strings.ToLower(url), ".mp4") ||
strings.HasSuffix(strings.ToLower(url), ".mov")
}